feat: async execution support for Metabolism - #616
Conversation
Add start/wait/watch support for large ligand batches, replacing the hard 250-ligand cap on run() with a workflow-routing threshold at 30 ligands (matching platform preflight routing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Prepared proteins already on UFA were re-uploaded via biotite on sync(lazy=True), which dropped REMARK 99 and overwrote the stamped file. Skip upload when remote_path is set, and re-stamp after PDB rewrites.
Protein.show() was forcing a PDB state dump, which fails for mmCIF structures with residue names longer than 3 characters. Fall back to CIF and teach Mol* HTML to load mmcif by path suffix.
Protein.sync(lazy=True) was skipping id resolution whenever remote_path was already set (not just when id was already set), so SystemPrep.sync_inputs() got a protein with id=None after a lazy sync and raised. Lazy now only short-circuits on an existing id; the upload-skip for an already-set remote_path stays separate. Also quiets a SonarCloud path-injection hotspot on stamp_prepared_protein_pdb's write, matching the existing NOSONAR convention for this kind of caller-supplied local path.
The new "Removing unsupported ligands" snippet in ligands.md used a
plain python fence, so pytest-markdown-docs ran it and failed on the
placeholder path/to/ligands.csv. Mark it {.python notest} like the
other placeholder-path examples in this repo (pocketfinder.md,
patent.md).
SonarCloud's path-injection annotation targets the write_bytes call's start line; the ruff-formatted multi-line call put the NOSONAR comment on the closing-paren line instead, so the suppression didn't take. Keep the call on one line so the comment lands where Sonar looks.
There was a problem hiding this comment.
Pull request overview
This PR extends the Drug Discovery CLI SDK to support asynchronous execution for Metabolism (with a workflow-routing threshold), while also improving structure handling (Prepared Protein stamp preservation and mmCIF support) and tightening related tooling/docs/tests.
Changes:
- Add async
start()support forMetabolism, and gaterun()to only allow batches smaller thanMETABOLISM_WORKFLOW_LIGAND_THRESHOLD(30). - Preserve the Prepared Protein
REMARK 99 DO_PREPAREDstamp throughProtein.to_pdb()rewrites and avoid overwriting stamped remote objects duringProtein.register()/sync(). - Add mmCIF writing/fallback paths for visualization (
Protein.to_cif(),_dump_state()choosing PDB vs CIF) and update Mol* HTML rendering to choose the correct format.
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| zensical.toml | Adds docs nav entry for the Prepared Protein stamp reference page. |
| tests/test_protein.py | Adds coverage for _dump_state() PDB vs CIF behavior and to_cif(). |
| tests/test_prepared_protein_stamp.py | Adds unit tests for stamp detection/preservation and sync/register behaviors with remote-only proteins. |
| tests/test_pocket_finder.py | Verifies PocketFinder payload omits null protein IDs and supports file-path-only proteins from DTOs. |
| tests/test_molstar_html.py | Adds tests for Mol* structure-format mapping and CIF handling in rendered HTML. |
| tests/test_metabolism_unit.py | Updates unit tests for new threshold behavior and default execution naming/payload. |
| tests/test_metabolism_local.py | Adds mock-server integration tests for Metabolism.start() and async polling behavior. |
| tests/test_ligand_set.py | Adds tests for LigandSet.remove_unsupported() and improved sync error messaging. |
| tests/mock_server/server.py | Tunes mock execution duration for metabolism to support async local tests. |
| tests/mock_server/routers/tools.py | Adds async DTO support and result injection for metabolism executions. |
| src/viz/molstar_html.py | Adds _molstar_structure_format() and uses it so Mol* loads PDB vs mmCIF correctly. |
| src/utils/constants.py | Introduces PREPARED_PROTEIN_STAMP_LINE and replaces metabolism cap with workflow threshold constant. |
| src/platform/constants.py | Bumps pocket-finder tool version. |
| src/drug_discovery/structures/protein.py | Adds PDB-compat detection, to_cif(), stamp preservation in to_pdb(), and upload-skipping behavior when remote_path is already set. |
| src/drug_discovery/structures/prepared_protein_stamp.py | New helper module for detecting/stamping REMARK 99 DO_PREPARED. |
| src/drug_discovery/structures/ligand.py | Adds LigandSet.remove_unsupported() and improves sync error guidance. |
| src/drug_discovery/pocket_finder.py | Uses _protein_tool_input() so payload omits null IDs; DTO rehydration accepts file-path-only proteins. |
| src/drug_discovery/metabolism.py | Adds async execution support, naming, and threshold gating for sync run(). |
| src/drug_discovery/constants.py | Adds a dedicated CIF dump path constant. |
| src/drug_discovery/class-design.md | Updates design notes to reflect Metabolism’s dual-mode execution. |
| docs/notebooks/clean/metabolism.ipynb | Updates notebook narrative/examples for sync vs async Metabolism usage. |
| docs/dd/tools/proteinprep.md | Documents Prepared Protein stamp implications and preservation guidance. |
| docs/dd/tools/metabolism.md | Adds guidance for large-batch async execution. |
| docs/dd/ref/prepared_protein_stamp.md | New reference page for stamp helpers. |
| docs/dd/ref/metabolism.md | Updates reference text for threshold and async path. |
| docs/dd/how-to/proteins.md | Documents to_cif() and notes show() CIF fallback. |
| docs/dd/how-to/ligands.md | Documents remove_unsupported() and updates Metabolism guidance for large batches. |
| CONTEXT.md | Updates project context to reflect workflow threshold and async usage for Metabolism. |
| .gitignore | Ignores untitled notebooks. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Metabolism._start_impl discarded approve_amount instead of forwarding it, so start(quote=True) silently ran the real (billed) execution instead of quoting. Metabolism has no quote/billing path by design (see CONTEXT.md), so _make_payload and _start_impl now raise a clear ValueError when approve_amount is not None, matching the Execution base class's _make_payload(*, approve_amount, sync) contract that Docking/Admet already follow. Also fixes "Site of Metabolism for 1 ligands" -> "1 ligand".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 29 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
tests/test_protein.py:38
- The
tmp_pathfixture is accepted but never used in this test, which can trigger unused-argument linting and makes the intent unclear. If you don't need it, drop it; if you want the fixture for future expansion, prefix it with_to mark it intentionally unused.
def test_dump_state_writes_cif_when_resnames_exceed_pdb_limit(tmp_path: Path) -> None:
tests/test_metabolism_local.py:213
- The timeout loop tracks
elapsed += poll_interval, which doesn’t include time spent injob.sync()and can exceedtimeout_secondsunder slow responses. Usingtime.monotonic()(orjob.wait(timeout=..., poll_interval=...)) makes the timeout behavior accurate and less flaky.
elapsed = 0.0
while elapsed < timeout_seconds:
job.sync()
if job.status in TERMINAL_STATES:
break
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 30 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tests/test_protein.py:38
- The
tmp_pathfixture is declared but never used in this test, which adds noise and may trigger unused-argument linting depending on the repo’s ruff settings. Remove the parameter (or use it to isolate the dump path if that was the intent).
def test_dump_state_writes_cif_when_resnames_exceed_pdb_limit(tmp_path: Path) -> None:
Summary
start()/wait()/watch()support toMetabolism(viaAsyncExecutableMixinandNotebookWatchMixin) for large ligand batchesrun()with a workflow-routing threshold (METABOLISM_WORKFLOW_LIGAND_THRESHOLD = 30) matching platform preflight routing;run()now rejects batches at or above the threshold and directs callers tostart()Builds on #615 (already merged).
Merge-ready status
Auto-updated — cycle 5 (final), last updated: 2026-08-28T23:40:00Z
main56f0a4cbd0b8a7level-1-tests (3.13, prod)Pocket Finder platform-state failure ignored, not required)bd0b8a7— 1 comment, addressed & resolvedMerge-ready. ✅
Recent activity
Protein.sync(lazy=True)regression: it was skipping id resolution wheneverremote_pathwas already set, not just whenidwas already set, which brokeSystemPrep.sync_inputs()(CI failuretest_sysprep_lv2[backend_only]).pytest-markdown-docsfailure: the new "Removing unsupported ligands" snippet inligands.mdused a plain fence and got executed against a placeholder path — marked{.python notest}like sibling examples.stamp_prepared_protein_pdb's write with a# NOSONARcomment, then fixed its placement (ruff had split the call across lines, moving the comment off the flagged line) — Quality Gate now passes.level-1-tests (3.13, prod)fails on an unrelated, pre-existing "Pocket finder tool not registered" platform-state issue; not a required check, not blocking.Metabolism.start(quote=True)silently ran the real execution instead of quoting, since_start_impldiscardedapprove_amount. Fixed by forwarding it into_make_payload, which now matches theExecutionbase contract and raises when Metabolism (which has no billing/quote path) is asked to quote. Also fixed "1 ligands" → "1 ligand" pluralization.pocket_findertool_version pin bump ("1"→"2") as undocumented scope creep — confirmed intentional; replied and resolved.Test plan
uv run pytest tests/test_metabolism_local.py tests/test_metabolism_unit.py --env local— 25 passeduv run coverage run -m pytest --env local— 1148 passed, 32 skipped, 4 xpassed (full suite, after fix)