Skip to content

Expanding support for python modules - #18

Merged
ncborcherding merged 11 commits into
develfrom
scxpand
Aug 10, 2026
Merged

Expanding support for python modules#18
ncborcherding merged 11 commits into
develfrom
scxpand

Conversation

@ncborcherding

Copy link
Copy Markdown
Member

Summary

Adds three Python tool integrations to immLynx and fixes a basilisk environment bug that was silently dropping a version constraint.

Closes #6 without a GPU dependency

XT-neighbor is CUDA C++, not a Python package. It requires an NVIDIA GPU and CUDA 11.0+ on Linux, has no PyPI release, and must be compiled with nvcc. Bioconductor builders have no NVIDIA GPUs, so it cannot be built or tested there.

Two things settle the direction. The upstream readme redirects users to SymScan for actual use. And the underlying algorithm, symmetric deletion lookup, already ships inside pyrepseq, which immLynx already installs. Same research group authored both.

runSymdelNeighbors() wraps pyrepseq.nn.symdel in the existing immLynxEnv. No new environment, no new dependency. Measured single core:

Sequences max_edits Time
200,000 1 3.15 s
50,000 2 6.15 s

It searches over unique sequences and maps results back to cells. return_object = FALSE returns a neighbor edge list, TRUE adds a per-cell symdel_degree column to colData.

Closes #8, the blocker was resolved upstream

Issue #8 describes DeepTCR 2.1.1. Upstream is now 2.1.29 and every pin the issue names has moved:

Issue states Ships in 2.1.29
numpy==1.18.0 numpy==1.23.5
matplotlib==3.3.3 matplotlib==3.7.2
h5py==2.10.0 h5py==3.8.0
llvmlite==0.36.0, numba==0.53.1 numba>=0.60.0

The reported NameError: name 'CCompiler' is not defined came from numpy 1.18.0 failing against modern setuptools. That cause is gone.

runDeepTCR() trains DeepTCR_U on unique sequences and writes features to a dimensional reduction, mirroring runEmbeddings(). DeepTCR's Load_Data does not merge identical amino acid sequences, so training on unique sequences avoids weighting the loss toward expanded clones.

Environment notes

The obvious python=3.11 plus pip = c("DeepTCR==2.1.29") does not work. Three failures surfaced while building it, each fixed and re-verified by training a real VAE:

  1. Python 3.11 fails to build. DeepTCR pins biopython==1.76, which ships no wheel past cp38 and none for macOS arm64, so it compiles from source. Its C extension assigns to Py_TYPE(), which CPython 3.11 made a hard error.
  2. macOS aborts at import. DeepTCR's macOS requirements pull tensorflow-metal==0.8.0, built against TF 2.11. Under TF 2.12 the process dies with Abort trap: 6 and platform is already registered with name: "METAL". Sourcing TensorFlow from conda-forge avoids the plugin.
  3. conda-forge biopython is too new. Its oldest osx-arm64 build is 1.78, which removed Bio.Alphabet. DeepTCR still imports it, so biopython stays on pip.

--no-deps applies to the whole pip step since conda satisfies every dependency. It also keeps DeepTCR's jupyterlab and notebook requirements out of the environment, which it declares but never imports.

Fixes a silent version-constraint drop

scanpyExportEnv declared anndata>=0.8. basilisk::setupBasiliskEnv() passes the pip vector to system2(), which pastes arguments into one string and runs it through /bin/sh without quoting. The shell parsed anndata>=0.8 as the word anndata, a > redirect, and the target =0.8:

BEFORE: ARGV: ['-m','pip','install','--no-user','anndata','scanpy','muon','scirpy']
        FILES CREATED: =0.8
AFTER:  ARGV: ['-m','pip','install','--no-user','anndata==0.11.4','scanpy','muon','scirpy']
        FILES CREATED: (none)

Two failures at once. The version floor was silently dropped, and pip's stdout was written to a file named =0.8 in the working directory. Now pinned to anndata==0.11.4, the version pip already resolved to.

tests/testthat/test-zz-basilisk-specs.R guards against recurrence. It enumerates BasiliskEnvironment objects from the namespace by class, so new environments are covered automatically. pip specs must contain no shell metacharacter. conda specs get a looser rule that permits version bounds like pytorch-cpu>=2.5, since reticulate quotes those arguments, but rejects whitespace and command characters.

Warning cleanup

Seurat::as.SingleCellExperiment() warns once per empty layer when converting a counts-only object. .coerceToSCE() now muffles only messages matching ^Layer '.*' is empty and lets everything else through.

Testing

  • devtools::test(): 0 failures, 0 warnings
  • R CMD check: 0 errors, 0 warnings
  • BiocCheck: 0 errors, 0 warnings
  • spelling::spell_check_package(): clean

Remaining R CMD check notes are Non-staged installation, which StagedInstall: no requires for basilisk, and a .git note that only appears when checking a worktree source directory.

Both vignettes updated. The symdel section executes at build time. The DeepTCR section is eval=FALSE with the reason stated, since building a TensorFlow environment during a Bioconductor vignette build would be hostile.

@ncborcherding

Copy link
Copy Markdown
Member Author

Every immLynx wrapper now accepts a SingleCellExperiment or a Seurat object and returns whichever class it was given. Previously seven of them rejected Seurat outright with Input must be a SingleCellExperiment object.

This also fixes two CI failures in the symdel and basilisk spec tests.

Why the change was small

Two things were already in place.

immApex::getIR() dispatches on both classes through .is_seurat_or_se_object(). Verified that it returns identical results for a Seurat and a SingleCellExperiment carrying the same data, so the extraction layer needed no changes at all.

runScXpand() had already solved the write-back problem with .writeCellColumn() and .writeObjMetadata(). Those are now promoted to R/utils.R and shared, with the duplicates removed from runScXpand.R so there is a single definition.

Only the input guards and the write-back paths were actually broken.

New shared accessors in R/utils.R

Helper Purpose
.isSCObject() TRUE for SingleCellExperiment or Seurat
.assertSCObject() Shared guard, names both accepted classes in the error
.writeCellColumn() Per-cell column to colData or meta.data
.writeReduction() Reduction to reducedDim or a DimReduc
.writeObjMetadata() Summary to metadata() or @misc

.writeReduction() is the only genuinely new logic. Seurat validates reduction keys, requiring them to start with a letter, contain only alphanumerics, and end in an underscore. The helper sanitizes the key rather than letting CreateDimReducObject() throw on something like tcr_esm.

Changes per function

Seven guards replaced with .assertSCObject(): runClustTCR, runOLGA, runEmbeddings, runTCRdist, runSoNNia, runSymdelNeighbors, runDeepTCR.

runMetaclonotypist had a subtler bug. It tested is(input, "SingleCellExperiment") and fell through to a data.frame branch otherwise, so a Seurat object failed with a misleading Input data.frame must contain 'barcode' and 'cdr3_aa' columns. It now uses .isSCObject().

Four write-back sites assumed SingleCellExperiment:

  • runEmbeddings wrote reducedDim() and colData() directly
  • runDeepTCR wrote reducedDim() directly
  • runSymdelNeighbors wrote colData() directly
  • runTCRdist wrote S4Vectors::metadata() directly, and now reports the correct location per class (metadata(obj)$tcrdist or obj@misc$tcrdist)

One behavior change to be aware of

runClustTCR and runOLGA already branched on class internally, but their local .add_metadata helpers left names on the vector stored in colData while the Seurat path did not. A parity test caught the mismatch. Both now delegate to the shared writer, so columns from those two functions no longer carry names. Values are unchanged.

@ncborcherding
ncborcherding merged commit 87c1e5f into devel Aug 10, 2026
4 checks passed
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.12472% with 394 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
R/runScXpand.R 56.60% 266 Missing ⚠️
R/calculate_helpers.R 31.08% 51 Missing ⚠️
R/runDeepTCR.R 20.00% 44 Missing ⚠️
R/utils.R 56.33% 31 Missing ⚠️
R/runSymdelNeighbors.R 96.42% 2 Missing ⚠️
Files with missing lines Coverage Δ
R/exportToScanpy.R 88.09% <100.00%> (+21.59%) ⬆️
R/runClustTCR.R 94.62% <100.00%> (+5.14%) ⬆️
R/runEmbeddings.R 98.73% <100.00%> (-0.04%) ⬇️
R/runMetaclonotypist.R 93.75% <100.00%> (ø)
R/runOLGA.R 88.67% <100.00%> (+3.38%) ⬆️
R/runSoNNia.R 10.25% <100.00%> (-4.38%) ⬇️
R/runTCRdist.R 86.90% <100.00%> (+0.48%) ⬆️
R/runSymdelNeighbors.R 96.42% <96.42%> (ø)
R/utils.R 87.41% <56.33%> (-9.90%) ⬇️
R/runDeepTCR.R 20.00% <20.00%> (ø)
... and 2 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DeepTCR Integration - Dependency Conflict Build XT-neighbor Support

1 participant