Skip to content

Fix conda env dependency drift, add a blast stub and smoke test, and detect failed blastp calls - #101

Closed
mrubash1 wants to merge 7 commits into
Arcadia-Science:mainfrom
mrubash1:mr/fix-env-drift-and-smoke-test
Closed

Fix conda env dependency drift, add a blast stub and smoke test, and detect failed blastp calls#101
mrubash1 wants to merge 7 commits into
Arcadia-Science:mainfrom
mrubash1:mr/fix-env-drift-and-smoke-test

Conversation

@mrubash1

@mrubash1 mrubash1 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Follow-on to #100 (now merged). Seven fixes found while running the pipeline locally on macOS and on x86-64 Linux. None of them are platform-specific; each was reproduced on both.

1. Pin matplotlib and setuptools in envs/analysis.yml

envs/analysis.yml pins its seven direct dependencies but nothing transitive, so a fresh solve installs versions incompatible with its pinned numpy=1.23.5, and two rules fail:

rule leiden_clustering (scanpy -> matplotlib):
  ImportError: Matplotlib requires numpy>=1.25; you have 1.23.5

rule dim_reduction (umap-learn 0.5.3):
  ModuleNotFoundError: No module named 'pkg_resources'

setuptools >= 81 removes pkg_resources, which umap-learn 0.5.3 imports at module scope. The matplotlib failure is an ABI break rather than a dependency conflict: the solver installs a numpy-2-built matplotlib alongside numpy=1.23.5 without complaint, and it fails only when imported — which is why building the environment never caught it, and why an environment that pins numpy has to pin what is compiled against it. The matplotlib pin matches the one already in plotting.yml and cartography_pub.yml.

This is not platform-specific: a fresh solve produced matplotlib 3.11.0 on osx-64 and 3.9.4 on linux-64, and the pipeline failed identically on both.

2. Remove the restore-keys from the conda env cache

restore-keys began with snakemake-conda- while the key begins with conda-, and they are matched as a prefix of the key, so the fallback restore could never fire.

An earlier revision of this branch corrected the prefix. That was wrong, and the correction is reversed here: a restore-keys match still leaves cache-hit false, so the env creation step runs regardless, and that step begins with rm -rf .snakemake/conda. Making the prefix match would therefore download a stale multi-gigabyte cache on every change to envs/*.yml, only to delete it — strictly slower than a prefix that never matched. There is no path in which the restored files are used, so the keys are removed instead.

This cache is also why CI did not catch the drift in (1): the key includes hashFiles('envs/*.yml'), so while the env files are unchanged CI restores previously solved envs and never re-resolves them against the current package index. That is worth addressing separately — a scheduled job that re-solves from scratch would catch this class of drift before it reaches a PR.

3. Add a blast stub and a make smoke-test entry point

The run_blast rule calls blastp -remote, which queues on NCBI's public servers; NCBI's own time-to-completion estimate (RTOE) was observed at ~2.5 hours, which makes end-to-end verification impractical.

run_blast now copies a canned results file when PROTEINCARTOGRAPHY_BLAST_STUB_RESULTS_FILEPATH is set. An env variable is used rather than a CLI flag because snakemake rule environments inherit env variables from the calling process, so neither the Snakefile nor the config needs to change.

This replaces tests.mocks.mock_run_blast, which patched blast_utils.run_blast and so required run_blast.py to import the tests package — the same class of problem #100 fixes, since importing tests.mocks calls find_repo_dirpath().

make smoke-test runs the search-mode pipeline with the stub. That test also now asserts on snakemake's return value, which was previously discarded (so it could pass while the pipeline failed), and checks that outputs are non-empty rather than merely present.

4. Detect failed blastp calls that exit with a status of zero

When the remote server refuses to queue a request, blastp -remote reports the error on stderr, writes an empty results file, and still exits zero:

$ blastp -remote -db nr ... ; echo $?
Error: [blastp] bad_request: Could not queue request: DB operation failed.
0

run_blast.py branched on result.returncode == 0, so it treated this as success. The word-size backoff was never attempted — the situation it exists for — and the empty file surfaced later as a misleading "no hits were returned" from extract_blast_hits.py.

An empty results file is deliberately not treated as a failure, since a query that legitimately has no hits produces one too.

This does not make the remote call faster; it means a refused request is retried and then reported.

5. Pin mamba in envs/cartography_test.yml

mamba was left unpinned and now resolves to mamba 2.x, which removed the mamba env create CLI that snakemake 7.25.3 invokes. --conda-frontend mamba therefore fails with a CreateCondaEnvironmentException and no output from the frontend.

Pinned to 1.4.2, matching cartography_tidy.yml. CI does not hit this because test.yml forces the conda frontend, with a comment noting the mamba frontend "results in errors during env creation" — this is that error, and this is its cause. Anyone using the mamba frontend locally does hit it.

Same class of problem as (1): a dependency left unpinned in 2023 that resolves to an incompatible major version today.

6. Build the wildcard output paths without f-strings

Two rules built an output path from an f-string mixing an interpolated value with an escaped snakemake wildcard:

f"{ANALYSIS_NAME}_aggregated_features_{{plotting_mode}}.html"

The doubled braces render the literal {plotting_mode} that snakemake needs as a wildcard. That is easy to misread, and a formatter can silently change its meaning: make format under Python 3.12+ rewrites the doubled braces to single ones, turning the wildcard into an interpolation of a name that does not exist. (Python 3.12 changed f-string tokenization, PEP 701; the pinned snakefmt and black predate it.)

The resulting Snakefile would still pass lint and would fail at runtime with a NameError in plot_interactive and plot_cluster_distributions.

Concatenating a plain string avoids the escaping, produces a byte-identical path, and cannot be rewritten. Verified that the DAG resolves the same targets (..._aggregated_features_pca_umap.html, ..._P60709_distribution_analysis.svg) and that snakefmt --check now passes under both Python 3.9 (CI's version) and Python 3.12+, where it previously wanted to rewrite the file.

The doubled braces elsewhere in the Snakefile are unaffected: they appear in a plain (non-f) string and in a shell block, neither of which a formatter rewrites.

7. Pin python in the environments that left it unpinned

envs/analysis.yml, envs/plotting.yml and envs/pandas.yml declare no python at all, so each resolves to whatever the newest python conda-forge has builds for. They had drifted apart from each other and from the six environments that do pin it, which are all on 3.9.16: solved as they were, analysis.yml gave python 3.11 and plotting.yml gave 3.10.20.

This is what put analysis.yml on a python new enough to resolve the setuptools >= 81 that removes pkg_resources — the failure in (1). Pinning the interpreter is the more direct fix for that class of drift than pinning each package that reacts to it.

All three now solve at 3.9.16.

Verification

  • All three newly-pinned environments solve at python 3.9.16, checked from a snapshot of the env files. The analysis environment imports matplotlib.pyplot, pkg_resources and umap — the three imports that were failing.
  • make test: 11 passed (both pipeline integration tests plus the unit tests)
  • make smoke-test: 26 of 26 rules complete (~49 s with envs cached)
  • Cluster-mode demo end to end: 16 of 16 rules, all final_results outputs produced
  • ruff check, ruff format --check, and snakefmt --check: clean, using CI's pinned versions (ruff 0.1.6, snakefmt 0.8.5) under Python 3.9
  • Fix pip install . and importing package modules outside a git working tree #100's packaging tests independently confirmed passing on linux/amd64 (Modal)

Not included

A replacement for the remote BLAST interface is proposed separately in #102, which is stacked on this branch. Benchmarking found that the slowness tracks NCBI's global queue load rather than any of the pipeline's blastp arguments — -word_size, -outfmt, -max_target_seqs, and shell=True were all ruled out by probing NCBI's RTOE estimate against a control. This PR deliberately keeps blastp -remote and only makes its failures visible (4).

mrubash1 and others added 6 commits August 13, 2026 16:28
`envs/analysis.yml` pinned its seven direct dependencies but nothing
transitive, so resolving it today installs versions of matplotlib and
setuptools that are incompatible with its pinned numpy 1.23.5, and two
rules fail:

    rule leiden_clustering (scanpy -> matplotlib):
      ImportError: Matplotlib requires numpy>=1.25; you have 1.23.5

    rule dim_reduction (umap-learn 0.5.3):
      ModuleNotFoundError: No module named 'pkg_resources'

matplotlib >= 3.8 requires numpy >= 1.25, and setuptools >= 81 removes
`pkg_resources`, which umap-learn 0.5.3 imports at module scope. The
matplotlib pin matches the one already used in `plotting.yml` and
`cartography_pub.yml`.

This is not platform-specific: a fresh solve produced matplotlib 3.11.0
on osx-64 and 3.9.4 on linux-64, and the pipeline failed identically on
both. Verified by running the cluster-mode demo end to end, where all
rules now complete and every final_results output is produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLpJZQ4W4XsL9NUUjjsry9
`restore-keys` entries are matched as a prefix of `key`, but this one
began with "snakemake-conda-" while the key begins with "conda-", so it
could never match and the fallback restore never fired.

Note that this cache is also why CI did not catch the dependency drift
fixed in the previous commit: the key includes `hashFiles('envs/*.yml')`,
so as long as the env files are unchanged, CI restores a previously
solved set of conda envs and never re-resolves them against the current
package index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLpJZQ4W4XsL9NUUjjsry9
The `run_blast` rule calls `blastp -remote`, which queues on NCBI's
public servers. Those queues are frequently long (NCBI's own RTOE
estimate was observed at ~2.5 hours), so running the pipeline end to end
was impractical for a quick sanity check or for CI.

`run_blast` now copies a canned results file when the env variable
`PROTEINCARTOGRAPHY_BLAST_STUB_RESULTS_FILEPATH` is set. An env variable
is used, rather than a CLI flag, because snakemake rule environments
inherit their env variables from the calling process, so neither the
Snakefile nor the pipeline config needs to change.

This replaces `tests.mocks.mock_run_blast`, which patched
`blast_utils.run_blast` and so required `run_blast.py` to import the
`tests` package. That import is removed here, extending the fix in an
earlier commit: importing `tests.mocks` calls `find_repo_dirpath()`,
which made the module unusable outside a git working tree.

`make smoke-test` runs the search-mode pipeline test with the stub. That
test also now asserts on snakemake's return value, which was previously
discarded, so the test could pass even when the pipeline failed, and it
checks that the output files are non-empty rather than merely present.

Verified by running the smoke test: 26 of 26 rules complete, in about
two minutes once the conda envs exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLpJZQ4W4XsL9NUUjjsry9
When the remote server refuses to queue a request, `blastp -remote`
reports the error on stderr, writes an empty results file, and still
exits with a status of zero:

    $ blastp -remote -db nr ... ; echo $?
    Error: [blastp] bad_request: Could not queue request: DB operation
    failed.(ERR:-99  )((Severe Error) DB Put Request error:
    sp_NewRequestEx failed)
    0

`run_blast.py` branched on `result.returncode == 0`, so it treated these
failures as successes. The word-size backoff was therefore never
attempted, which is the situation it was added for, and the empty
results file was only reported later by `extract_blast_hits.py` as a
misleading "no hits were returned" error.

Failure is now determined by `blast_call_failed`, which also treats an
error on stderr as a failure. An empty results file is deliberately not
treated as a failure, because a query that legitimately has no hits
produces one too.

Note that this does not make the remote call any faster: the calls
observed hanging were queued by NCBI, whose own estimate of the time to
completion (`RTOE`) was ~2.5 hours at the time. It does mean that a
refused request is retried and then reported, rather than silently
producing an empty results file.

Also adds `pythonpath` to the pytest configuration, so that tests can
import the modules in the `ProteinCartography` package the same way the
snakemake rules do (as top-level modules).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLpJZQ4W4XsL9NUUjjsry9
`envs/cartography_test.yml` left `mamba` unpinned, so it now resolves to
mamba 2.x. Mamba 2 removed the `mamba env create` CLI that snakemake
7.25.3 invokes, so `--conda-frontend mamba` fails with a
`CreateCondaEnvironmentException` and no output from the frontend.

Pinned to 1.4.2, matching `cartography_tidy.yml`, which is the version
the pipeline has been run with successfully.

CI does not hit this, because `.github/workflows/test.yml` forces the
conda frontend (with a comment noting that the mamba frontend "results in
errors during env creation" — this is that error, and this is its cause).
Anyone running the mamba frontend locally does hit it.

This is the same class of problem as the unpinned transitive dependencies
fixed earlier in this branch: a dependency left unpinned in 2023 that
resolves to an incompatible major version today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLpJZQ4W4XsL9NUUjjsry9
Two rules built an output path from an f-string that mixed an
interpolated value with an escaped snakemake wildcard:

    f"{ANALYSIS_NAME}_aggregated_features_{{plotting_mode}}.html"

The doubled braces are there so that the f-string renders the literal
`{plotting_mode}` that snakemake needs as a wildcard. That is easy to
misread, and a formatter can silently change what it means: running
`make format` under Python 3.12 or later rewrites the doubled braces to
single ones, which turns the wildcard into an interpolation of a name
that does not exist. (Python 3.12 changed how f-strings are tokenized,
see PEP 701; the versions of snakefmt and black pinned here predate it.)

Concatenating a plain string avoids the escaping entirely, produces a
byte-identical path, and cannot be rewritten this way. Verified that the
DAG still resolves the same targets, with the wildcards expanding to
`..._aggregated_features_pca_umap.html` and
`..._P60709_distribution_analysis.svg` as before, and that
`snakefmt --check` now passes under both Python 3.9 (which CI uses) and
Python 3.12+, where it previously wanted to rewrite the file.

Note that the doubled braces elsewhere in the Snakefile are not affected:
they appear in a plain (non-f) string and in a shell block, neither of
which a formatter rewrites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLpJZQ4W4XsL9NUUjjsry9
…che restore-keys

Three environment files declared no python at all, so each resolved to whatever
the newest python conda-forge had builds for every other pin in the file. They
had already drifted apart from each other and from the six environments that do
pin it, which are all on 3.9.16: solved as they were, `analysis.yml` gave python
3.11 and `plotting.yml` gave 3.10.20. With this change all three solve at 3.9.16,
and the `analysis` environment imports matplotlib.pyplot, pkg_resources and umap
-- the three imports that were failing before the pins on this branch.

This is the same class of drift as the rest of this branch, and it is what put
`analysis.yml` on a python new enough to resolve a setuptools that removed
`pkg_resources`, which is the failure the `setuptools<81` pin here addresses.

Also remove the `restore-keys` from the conda env cache rather than correcting
their prefix, which is what this branch did previously. A `restore-keys` match
still leaves `cache-hit` false, so the env creation step runs regardless, and
that step begins by deleting `.snakemake/conda`. Making the prefix match would
therefore download a stale multi-gigabyte cache only to discard it -- strictly
slower than the broken prefix that never matched anything.

Finally, correct the reason given for the matplotlib pin. Recent matplotlib and
`numpy=1.23.5` are not incompatible to the solver, which installs them together
without complaint; matplotlib is built against numpy 2 and fails on import. The
distinction matters because it means the breakage cannot be caught when the
environment is built, only when it is used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLpJZQ4W4XsL9NUUjjsry9
@mrubash1
mrubash1 force-pushed the mr/fix-env-drift-and-smoke-test branch from 6382ab4 to 707e4f6 Compare August 14, 2026 15:38
ahmedhosny added a commit that referenced this pull request Aug 14, 2026
…s import.

Unpinned matplotlib now requires numpy>=1.25 while this env pins 1.23.5;
setuptools 81 dropped pkg_resources, which umap-learn still imports.
Same pins as #101.

Co-authored-by: Cursor <cursoragent@cursor.com>
ahmedhosny added a commit that referenced this pull request Aug 14, 2026
…103)

* Fix search-mode reliability for BLAST, UniProt, AFDB, and small maps

Replace hanging blastp -remote with NCBI WWW/QBlast by default, harden
UniProt idmapping/metadata and AlphaFold PDB downloads, and keep tiny
maps from crashing UMAP/Leiden. Proven while wiring ProteinCartography
into Arcadia Studio full cartography on Modal.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address review: AF isoform PDBs, t-SNE clamp, BLAST soft-fail alignment.

Query AlphaFold's prediction API first so isoform entries like Q9Y6V0-3
are not written as 404 error bodies. Stop scaling t-SNE perplexity for
every N<250. Share one PC_BLAST_SOFT_FAIL opt-in so an empty BLAST TSV
cannot succeed in run_blast and then abort extract_blast_hits.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Put ProteinCartography/ on pytest's pythonpath so CI can collect tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Pin matplotlib and setuptools in the analysis env so CI pipeline tests import.

Unpinned matplotlib now requires numpy>=1.25 while this env pins 1.23.5;
setuptools 81 dropped pkg_resources, which umap-learn still imports.
Same pins as #101.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
@mrubash1

Copy link
Copy Markdown
Contributor Author

Closing in favour of #108, which is this branch cut down after #103 landed.

Dropped from this PR:

  • the analysis.yml matplotlib and setuptools pins — Fix search-mode reliability for BLAST, UniProt, AFDB, and small maps #103 took them verbatim, so they are already on main
  • the blast stub and make smoke-testmain now has extended mocks plus test_pipeline_in_search_mode, which covers the same ground, and the stub was the piece that conflicted hardest with the rewritten blast_utils
  • the python=3.9.16 pins — CI is green on main without them, and they are the prime suspect for plotting.yml env creation going from ~74s to over 12 minutes without finishing. That is unconfirmed, so it belongs in its own PR where the CI timing is the evidence rather than bundled with unrelated fixes.

Carried over to #108: the conda cache restore-keys removal, the mamba pin, the Snakefile f-string fix, and the blastp exit-zero detection — all four verified as still needed against the current main.

@mrubash1 mrubash1 closed this Aug 17, 2026
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.

1 participant