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
Closed
Conversation
This was referenced Aug 13, 2026
Closed
`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
force-pushed
the
mr/fix-env-drift-and-smoke-test
branch
from
August 14, 2026 15:38
6382ab4 to
707e4f6
Compare
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>
Contributor
Author
|
Closing in favour of #108, which is this branch cut down after #103 landed. Dropped from this PR:
Carried over to #108: the conda cache |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
matplotlibandsetuptoolsinenvs/analysis.ymlenvs/analysis.ymlpins its seven direct dependencies but nothing transitive, so a fresh solve installs versions incompatible with its pinnednumpy=1.23.5, and two rules fail:setuptools >= 81removespkg_resources, whichumap-learn 0.5.3imports at module scope. The matplotlib failure is an ABI break rather than a dependency conflict: the solver installs a numpy-2-built matplotlib alongsidenumpy=1.23.5without 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 inplotting.ymlandcartography_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-keysfrom the conda env cacherestore-keysbegan withsnakemake-conda-while the key begins withconda-, 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-keysmatch still leavescache-hitfalse, so the env creation step runs regardless, and that step begins withrm -rf .snakemake/conda. Making the prefix match would therefore download a stale multi-gigabyte cache on every change toenvs/*.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-testentry pointThe
run_blastrule callsblastp -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_blastnow copies a canned results file whenPROTEINCARTOGRAPHY_BLAST_STUB_RESULTS_FILEPATHis 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 patchedblast_utils.run_blastand so requiredrun_blast.pyto import thetestspackage — the same class of problem #100 fixes, since importingtests.mockscallsfind_repo_dirpath().make smoke-testruns 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
blastpcalls that exit with a status of zeroWhen the remote server refuses to queue a request,
blastp -remotereports the error on stderr, writes an empty results file, and still exits zero:run_blast.pybranched onresult.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" fromextract_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
mambainenvs/cartography_test.ymlmambawas left unpinned and now resolves to mamba 2.x, which removed themamba env createCLI that snakemake 7.25.3 invokes.--conda-frontend mambatherefore fails with aCreateCondaEnvironmentExceptionand no output from the frontend.Pinned to
1.4.2, matchingcartography_tidy.yml. CI does not hit this becausetest.ymlforces 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 formatunder 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
NameErrorinplot_interactiveandplot_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 thatsnakefmt --checknow 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
pythonin the environments that left it unpinnedenvs/analysis.yml,envs/plotting.ymlandenvs/pandas.ymldeclare nopythonat 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 on3.9.16: solved as they were,analysis.ymlgave python 3.11 andplotting.ymlgave 3.10.20.This is what put
analysis.ymlon a python new enough to resolve thesetuptools >= 81that removespkg_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
analysisenvironment importsmatplotlib.pyplot,pkg_resourcesandumap— 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)final_resultsoutputs producedruff check,ruff format --check, andsnakefmt --check: clean, using CI's pinned versions (ruff 0.1.6, snakefmt 0.8.5) under Python 3.9pip 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, andshell=Truewere all ruled out by probing NCBI'sRTOEestimate against a control. This PR deliberately keepsblastp -remoteand only makes its failures visible (4).