Per-copy ligand binding-site annotation, auxiliary small-molecule catalogue, and unified curation gating - #7
Conversation
…ting G-protein alpha: record the alpha5-derived functional coupling identity and the modeled backbone scaffold as two separate fields, so an engineered mini-G chimera no longer exports only its scaffold identity. When the alpha5 resolves a unique subtype it is used; otherwise the model's family-matching slug is kept and a review conflict is raised on family disagreement. Record backbone provenance even when the entity carries no UniProt cross-reference, and reword the non-human-ortholog G-alpha alert (it is not a chimera). Batch annotation: retry a transient paper-PDF upload to the file API before dropping a structure, and add a run-manifest reconciliation that diffs every target against what actually ran, so a structure dropped on upload failure is reported instead of vanishing from the accounting. Ligands: drop excluded buffers, detergents and crystallization additives at the aggregation stage unless the model affirmatively judges the molecule functional, so a hallucinated additive never reaches curation or the exported CSV. Curation gating: give the run manifest and the non-interactive accept path the same gating decision the interactive curator already applies (oligomer cross-check alerts and vote-disagreement controversies) through one shared helper, so the three can never disagree; the non-interactive path now skips a gated structure for review instead of accepting it. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ef reference doc The GPCRdb generic-numbering/segment table maintenance script tripped ruff (multiple statements per line, a bare except, percent-format, combined imports); rewrite it lint- and format-clean with identical behavior and output. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Resolve the corpus root and output path from environment variables instead of a hard-coded home directory, and add a pre-commit hook that rejects committing absolute home-directory paths. An optional git-ignored denylist blocks extra machine-specific tokens per checkout. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Use the team spelling 'G protein' (no hyphen) standalone and 'G protein-coupled/-coupling' for the compound modifier, across comments, docstrings, and the README. Runtime output strings, model-facing prompt/schema text, and test fixtures are intentionally left for a separate pass. Also ignore the local conventions reference file so it stays out of the public tree.
…ting Use the agreed spelling 'G protein' (and 'G protein-coupling' for the compound) in CLI help, reports, curator warnings, the two alert-prefix tags, and oligomer reasons. Harden the signaling-partner warning matchers to also accept the spaced spelling (additive: the underscore and hyphenated forms are kept) and add a regression test pinning that routing. Test assertions updated in lockstep.
Spelling-only change of 'G-protein' to 'G protein' in the annotation prompt, tool-schema descriptions, and the detector evidence shown to the model. No instruction, enum value, or meaning changed; golden-snapshot tests updated in lockstep.
…identity Header-only rename of two g_proteins.csv columns: Alpha_UniProt -> Alpha_identity and Alpha_functional_coupling -> Alpha_alpha5_identity (Alpha_backbone unchanged). Column values, order, count, and the internal data model are unchanged; only the CSV header strings and their writer/test references move. Note: g_proteins.csv files written before this commit carry the old headers and will no longer match the schema, so the writer's schema pre-flight will refuse to append to them -- delete and regenerate those run outputs.
…fusion/binder names, and G-protein subunits - ligands.csv: Name now carries the PDBe chemical-component code (falling back to the descriptive name when none exists); Title keeps the full name; new Residue_seq_id column emits each modelled copy's residue number, aligned copy-for-copy with label_asym_id. - structures.csv Partner_UniProt: gate the column to genuine >=4-annotated-TM receptor protomers so peptide ligands, secreted agonists and single-pass co-receptors that carry a GPCRdb slug are no longer written as receptor partners; skip the gate when TM data is unavailable; alert on eviction. - auxiliary-protein names: normalize cytochrome-b562 crystallization-fusion spellings to BRIL (fusion-typed only); correct antibody binders the model named after their antigen (e.g. anti-BRIL Fab) from the RCSB chain description, antigen-agnostic and advisory. - G-protein subunits: detect subunit fragments misfiled into ligands/auxiliary proteins and relocate the unambiguous ones into the G-protein record routed to alpha/beta/gamma, gating each for curator confirmation; gate-in-place the no-slug and cross-role cases; catch a misfiled G-protein peptide in the ligand bucket regardless of role. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…unit chain ids, preserve antibody clone tags - relocate_misfiled_g_protein_fragments: when a recovered G-protein fragment has the same subunit identity as an already-populated column but a different chain, merge the chain into the existing chain_id (e.g. 'A' + 'B' -> 'A, B') instead of dropping it; genuine identity conflicts still gate in place. - correct_binder_names: preserve a genuine trailing clone tag (e.g. an antibody's '(P2C2)') when canonicalizing a binder name, while still dropping a redundant antigen restatement. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…with its author chain (auth_asym_id:auth_seq_id) Multi-copy ligands (e.g. calcium on two chains) had repeating bare residue numbers not attributable to a chain without cross-referencing label_asym_id. Each token is now <auth_asym_id>:<auth_seq_id>, taken per-copy from the nonpolymer instance list (not the AI chain_id), preserving 1:1 order with label_asym_id; verified unique per component across the corpus. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Per-copy ligand geometry selected copies by residue name alone, with no polymer gate. When a ligand's component code collides with a standard amino acid (e.g. free glutamate, GLU), backbone residues were swept in as spurious copies, inflating the copy count and polluting the per-copy geometry evidence (facing, membrane depth, contacts). Gate all five copy-selection sites with is_protein_atom so only non-polymer residues count, keeping the three index-aligned per-copy fact lists consistent. Adds regression tests, including an alignment guard that fails if any single gate is dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…_seq_id) through per-copy site evidence and label it in the detector-evidence block Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ies) Add a per-PDB ligand_copies array to the annotation tool so the model records site_ref/role/confidence for each physical ligand copy, keyed by a fixed auth_asym_id:auth_seq_id enum. The copy roster is sourced from non-polymer instance metadata (all functional-candidate copies, incidental lipids included); the prompt lists one line per copy with its geometry facts or a sparse-contact note. The existing ligands array is unchanged; ligand_copies is additive and built per request in both single and batch paths. Site/role enums are reused from the ligands item so they cannot drift; the array has no fixed-length constraint. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ade single-run; drop on batch mismatch) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Group ligand_copies across runs by copy identifier (not component+site), so one physical copy is one vote group; reuse the existing majority + near-tie + low-confidence review path. Robust when a run omits the array (branch on the first non-empty run, not run 0). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A compound at multiple sites now lists only the copies voted to that site in each row (residue and chain columns stay 1:1). Copies with no determined site are surfaced with a marker, never dropped; falls back to the prior all-copies listing when per-copy votes are absent. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ixes Add 'annotate --batch --sequential' to submit one shard of PDBs per batch job with at most one job in flight, keeping large corpora under the provider's enqueued-token limit. Also: - retry batch-job creation on transient errors (mirrors the upload retry) - honor an explicit --targets / PDB argument in sequential mode - self-heal a stranded downloaded-but-unrecovered job on --check-batch - invalidate upload-cache entries when their remote file is deleted - file-lock shard submission to prevent concurrent double-submit - guard shard size against the per-job request cap Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- ligands: emit the authoritative structure-derived PubChem CID in the PubChemID column when available, falling back to the model-reported value - gating: treat purely lexical vote ties as advisory (do not gate) — ligand/auxiliary-protein name spelling variants, and PubChem-ID ties that are blank or backed by an authoritative CID; genuine field disagreements still gate - validation: flag an active-state structure bound only to an inverse agonist with no transducer, as a prompt to confirm the receptor state Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ts; add read-only decision brief At a contested review leaf, no default is offered when the field carries the structure's identity or biology (functional/oligomeric state, ligand role and molecular type, receptor/G-protein identity, chimeric flag, coupling family) and the runs genuinely disagree -- the best run differs from the majority vote, or the top-two vote margin is a near-tie. A bare Enter can then no longer silently commit a value at a real disagreement. Display-string fields (names, PubChem ids) keep their default. The default kwarg is omitted (never None) and a non-string prompt result can no longer reach .lower(). Add gpcr_tools.validator.decisions: enumerates per-signal decisions (validation criticals, oligomer findings, and every fork in the raw voting log, ranked by severity) independently of the whole-structure gated boolean, reconciling the gating=None flag conservatively (gates unless explicitly False). The curate flow renders a read-only decision brief per gated structure. Tests and fixtures use controversy records extracted from real voting logs. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Re-derive each keyed small-molecule ligand row from the aggregated per-copy site attribution, so a physical copy is placed on the binding-site row the runs agreed on rather than inheriting one outlier run's copy list -- which could inflate one row's copy count or drop a row and strand its copies onto a surviving sibling row. Runs as the final aggregation step: after discrepancy detection (so review gating is computed and preserved against the original best run) and after the excluded-buffer prune (the post-prune list is the chemistry-and-existence source). Per-site decisions (role, is_functional, keep/drop) follow the in-memory majority vote per component:site, not the selected run; chemistry is joined by component id from the post-prune array, and a component with no surviving row is not rebuilt; chain_id is re-derived from the physical copies. csv_writer now lets a copy voted to a dropped row's site follow that row out of the table instead of flooding a surviving sibling. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The per-site rebuild loop reused the name 'site' -- bound to a guaranteed-present per-copy token in an earlier loop -- for a dict lookup that can return None, so mypy flagged a str | None -> str assignment. Rename the lookup result to 'voted_site' to keep its Optional type distinct. Behavior is unchanged; the None case was already guarded. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add five non-functional entities to LIGAND_EXCLUDE_LIST so they no longer surface as ligand rows: FMN (flavoprotein-fusion redox cofactor, sibling of FAD), NI (His-tag/IMAC nickel, sibling of ZN/MN/FE), NH4 and SCN (crystallization salts), and UNX (unidentified atom/ion with no chemical identity). Removes 17 spurious ligand rows across the corpus with no impact on any functional ligand. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ite row A physical ligand copy whose per-copy vote gives no determinable binding site was parked on the compound's first surviving row with a ' (?)' mark on its residue token -- overloading the residue column (whose chain:residue is always known) with a caveat that really concerns the SITE. Instead, gather such copies into a clean row with site 'unknown' and unmarked residue tokens, so the uncertainty lives honestly in the Site column. A compound judged non-functional at every real site (no surviving row) builds no such row -- its undetermined copies drop with it, like any non-ligand. The residue marker is removed entirely. The row-keep predicate is the single source of truth for the 'has a surviving row' guard; it and sanitize_value move to config so the aggregator and CSV writer share them without a reverse package dependency. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Drop PLM (palmitic acid): it was the only compound on both LIGAND_EXCLUDE_LIST and INCIDENTAL_CANDIDATES. That double-listing let a palmitate the model judged functional be rescued from buffer-pruning yet ship to CSV with empty SMILES / InChIKey / PubChem / is_endogenous, because the chemistry fetch, index and fill paths all skip exclude-list members. Dropping it makes palmitate behave like cholesterol (incidental-only): chemistry fetched and filled, role model-judged, functional copies kept with chemistry, non-functional copies dropped via the is_functional=False path. A test invariant now asserts the incidental and exclude sets stay disjoint. Note the intended parity: multi-copy palmitate now routes to human review like multi-copy cholesterol already does. Add UNL (unknown-ligand placeholder): a sibling of UNX with no chemical identity, so it can never be a cataloged ligand row. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The interactive review used None both as a legitimate JSON null leaf value and as the 'q' quit signal. Accepting a field whose value was null returned None, which the recursion read as 'quit', collapsing the whole structure's review to None; the curate loop then turned that into a KeyboardInterrupt, so the structure was silently dropped from the CSV with no error or traceback. Route quit through a dedicated ReviewAbortedError exception instead of a bare return None: raise it at the four quit sites, delete the five None-propagation checks (a real null value now flows through as data), and catch it in the curate loop alongside KeyboardInterrupt. This also closes the same latent trap in fix-issues mode. Return annotations tightened accordingly. Add regression tests (accepting a null leaf preserves None instead of aborting; quitting raises ReviewAbortedError) and update the one test that encoded the old return-None-on-quit contract. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The audit directory mkdir ran outside the try/except, so a read-only mount or a plain file sitting at the output path would raise on the first accept/edit and crash the entire curation session. Move the mkdir inside the existing try so any filesystem error soft-fails (logged, then continue) just as the write already did, and broaden the message accordingly. Add a regression test that makes the audit directory un-creatable and asserts the logger does not raise. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Now that quitting a review raises an exception, review always returns a dict (or unwinds), and each of the accept/review/fix modes assigns the final data, so the None initialization and the always-true 'is not None' guard were dead code. Convert the mode dispatch to if/elif/elif and drop the guard; behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A per-copy binding-site or role disagreement could gate a structure (disabling one-click accept-all and forcing human review), yet the review walker never descended into the per-copy ligand table, leaving the curator gated with no way to resolve it. Add that table to the reviewable top-level blocks so its contested per-copy site assignments surface as editable controversies, and a curator's edit flows through to the CSV's per-site residue partitioning. Tests cover reachability of a contested per-copy site_ref end to end, that a null soft field is accepted without aborting the review, that a clean block ships verbatim and auto-accepts in fix mode, that editing a copy's site_ref moves its residue token to the correct site row, and that editing a per-copy role leaves the CSV role column unchanged (the role column is sourced from the compound-level ligand). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A per-copy role disagreement in the per-copy ligand table could gate a structure (disabling one-click accept-all), but the CSV role column is sourced from the compound-level ligand and the per-copy role is never read into any output, so resolving such a disagreement changes nothing shipped. Mark per-copy role disagreements advisory (still surfaced for review, no longer blocking), mirroring the existing name and PubChem-id carve-outs, in both the differing and near-tie branches. Per-copy binding-site disagreements still gate (they do affect residue partitioning), and compound-level role disagreements still gate. Takes effect on re-aggregation. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Most structures carry a per-copy ligand table, and it is derived from the compound-level ligands rather than annotated directly, so prompting the curator to accept it on every clean structure is needless friction. Pass a clean per-copy ligand block (no controversy and no validation alert) through review silently; the primary annotation blocks keep their accept prompt unchanged. A contested per-copy block still enters review as before. Because explicitly skipping the block discards its per-copy site partitioning (the CSV then over-lists a compound's residues across its site rows), an explicit skip now asks for confirmation. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Molecules present in the structure but not receptor ligands -- ions, bound cofactors, glycans, detergents, matrix lipids, and a transducer's own nucleotides -- were previously either dropped by the buffer strip or mislabelled as functional ligands. They are now catalogued in a new auxiliary_small_molecules.csv, located using the same per-copy columns as ligands.csv (ChainID, label_asym_id, Residue_seq_id). - Free the counter-ion metals (Na/Mg/Zn/Mn/Ca) from the hard exclude list so the model sees them and judges whether each is functional or structural. - Route every small molecule through a single three-way classifier: functional to ligands.csv, auxiliary to auxiliary_small_molecules.csv, ghost/apo dropped. A real-modality guard prevents a stray non-functional verdict from deleting the receptor's own drug. - Flag non-polymer copies that sit on a G-protein or arrestin transducer chain (identified from the structure, not the model's name) and advise the model, per copy, to type them as the transducer's cofactor, so the same component split across a transducer chain and the receptor is distinguished. Co-Authored-By: Claude <[email protected]>
Structural metals dominate; the Mg-as-required-co-agonist example was steering the model to over-label structural Mg as a co-agonist. Metals still fall under the general 'functional only on an explicit paper mechanism' rule, matching Zn/Mn which carry no worked example. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…f, and g_proteins columns; refresh test count Co-Authored-By: Claude Opus 4.8 <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR substantially extends the GPCR annotation pipeline to (1) annotate ligand binding site/role per modelled copy rather than per compound, (2) introduce a dedicated CSV output lane for non-functional/auxiliary small molecules, and (3) unify “needs human review” gating into a single shared implementation used consistently by curation UI, auto-accept, and run-manifest reporting. It also standardizes “G protein” terminology across prompts/UI/tests, adds a decision-brief UI, hardens interactive review control-flow, and introduces sequential Batch API submission.
Changes:
- Thread per-copy ligand identity (
auth_asym_id:auth_seq_id) through detection → annotation schema → aggregation/voting → CSV → review UI. - Add
auxiliary_small_molecules.csvand route non-functional/auxiliary molecules (including model-judged auxiliary and mechanically stripped molecules) out ofligands.csv. - Centralize gating logic (
validator/gating.py) and surface all gating reasons consistently (curator UI, auto-accept skip, manifest quality split), plus decision-brief rendering.
Reviewed changes
Copilot reviewed 77 out of 78 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_validator_gating.py | Adds unit coverage for shared gating predicate and oligomer warning promotion. |
| tests/unit/test_validator_consistency.py | Adds unit coverage for state/ligand consistency advisories. |
| tests/unit/test_validation_display.py | Updates validation display matching for “G protein” spelling and adds matcher regression test. |
| tests/unit/test_schema_contract.py | Updates schema wording assertion to “g protein”. |
| tests/unit/test_review_engine.py | Adds extensive tests for ReviewAbortedError control-flow and ligand_copies review behavior. |
| tests/unit/test_reports.py | Adds reconciliation + all-sources gating coverage to run manifest tests. |
| tests/unit/test_ne3_logic.py | Adds tests for TM-based non-receptor partner eviction and updates wording. |
| tests/unit/test_missed_polymers.py | Updates terminology in doc/comments. |
| tests/unit/test_ligand_validator.py | Updates G protein peptide misfiling behavior + tests; adjusts excluded-buffer example. |
| tests/unit/test_geometry.py | Adds tests ensuring ligand-copy selection skips polymer residues and carries copy IDs. |
| tests/unit/test_dual_role_detector.py | Adjusts candidate selection tests; adds transducer-copy detector tests; adds ion skip test. |
| tests/unit/test_detector.py | Updates top-level test docstring wording. |
| tests/unit/test_decisions.py | Adds comprehensive tests for decision enumeration from raw voting logs and severity ordering. |
| tests/unit/test_decision_brief_ui.py | Adds tests for decision-brief rendering and read-only behavior. |
| tests/unit/test_coupling.py | Updates wording in module docstring. |
| tests/unit/test_controversy_default_suppression.py | Adds tests for default suppression logic in controversy prompts. |
| tests/unit/test_config.py | Adds aux-lane config tests and ligand_copies identity keying tests. |
| tests/unit/test_chimera.py | Updates terminology in doc/comments. |
| tests/unit/test_binder_name_correction.py | Adds tests for binder-name correction from RCSB descriptions and advisory routing. |
| tests/unit/test_audit.py | Adds regression test ensuring audit-dir creation failures soft-fail. |
| tests/unit/test_annotator_prompt_builder.py | Adds tests for LIGAND COPIES prompt block injection and incidental candidate behavior. |
| tests/unit/test_annotator_post_processor.py | Updates aux-name normalization tests and adds BRIL vote-group merge test. |
| tests/unit/test_aggregate_retry_unavailable.py | Updates expected wording for chimera warning in retry selector. |
| tests/integration/test_real_pdb_review_engine.py | Updates integration behavior: quit now raises ReviewAbortedError. |
| tests/integration/test_real_pdb_pipeline.py | Updates expected CSV outputs/counts to include auxiliary_small_molecules lane. |
| tests/integration/test_dual_role_live.py | Updates wording in test comments. |
| tests/integration/test_curate_cli.py | Adds integration test ensuring auto-accept skips gated PDBs. |
| tests/integration/test_csv_pipeline.py | Updates integration expectations for ligands and g_proteins column rename. |
| tests/integration/test_aggregate_pipeline.py | Adds ordering/invariance test for discrepancy detection vs per-copy rebuild. |
| tests/fixtures/decision_brief_signals.json | Adds fixture data for decision brief / decision enumeration tests. |
| tests/fixtures/controversy_default_forks.json | Adds fixture vote-fork records for default-suppression tests. |
| src/gpcr_tools/validator/membrane.py | Ensures ligand-facing calculations ignore polymer residues with name collisions. |
| src/gpcr_tools/validator/ligand_validator.py | Refactors G protein fragment detection and broadens misfiled-fragment warning conditions. |
| src/gpcr_tools/validator/geometry.py | Adds polymer-residue exclusion for name collisions; enriches per-copy contact returns with copy identity. |
| src/gpcr_tools/validator/gating.py | Introduces unified gating implementation (validation + oligomer + voting controversies). |
| src/gpcr_tools/validator/consistency.py | Introduces cross-field advisory (active-state + inverse agonist + no G protein). |
| src/gpcr_tools/validator/chimera.py | Updates terminology and extends alpha5-mimetic detection documentation. |
| src/gpcr_tools/reports.py | Adds reconciliation section and expands quality gating to include oligomer + voting sources. |
| src/gpcr_tools/detector/stage.py | Runs new transducer-copy detector in detect stage. |
| src/gpcr_tools/detector/site_ref.py | Threads copy_id through site-ref evidence and excludes polymer residues from atom lists. |
| src/gpcr_tools/detector/signals.py | Adds SIGNAL_TRANSDUCER_COPY constant. |
| src/gpcr_tools/detector/ligands.py | Expands incidental-candidate framing and adds detect_transducer_copies. |
| src/gpcr_tools/detector/gprotein.py | Terminology normalization in detector docstring/API. |
| src/gpcr_tools/detector/geometry.py | Skips single-atom ions from dual-role geometry candidate selection. |
| src/gpcr_tools/detector/coupling.py | Terminology normalization in coupling protomer detector. |
| src/gpcr_tools/data/prompts/v5.md | Updates prompt terminology and adds explicit metals/ions handling rule. |
| src/gpcr_tools/csv_generator/validation_display.py | Delegates oligomer warning injection to shared gating module; expands signaling-partners matcher to “g protein”. |
| src/gpcr_tools/csv_generator/ui.py | Adds read-only decision-brief UI rendering. |
| src/gpcr_tools/csv_generator/review_engine.py | Introduces default suppression for semantic vote forks; replaces quit sentinel with ReviewAbortedError; silences clean ligand_copies prompting. |
| src/gpcr_tools/csv_generator/logic.py | Adds TM-based non-receptor partner eviction with curator alert. |
| src/gpcr_tools/csv_generator/exceptions.py | Adds ReviewAbortedError exception type. |
| src/gpcr_tools/csv_generator/audit.py | Makes audit-dir creation failure soft-fail within try/except. |
| src/gpcr_tools/csv_generator/app.py | Renders decision brief; handles ReviewAbortedError; auto-accept now skips gated PDBs. |
| src/gpcr_tools/annotator/schema.py | Adds per-PDB ligand_copies schema builder and updates role-check schema description. |
| src/gpcr_tools/annotator/prompt_builder.py | Injects LIGAND COPIES block into prompts when candidates exist. |
| src/gpcr_tools/annotator/post_processor.py | Gates BRIL name collapse on fusion-typed entries and preserves binder names. |
| src/gpcr_tools/aggregator/voting.py | Hardens majority-vote list detection; marks some controversies advisory-only; adds per-copy low-confidence flags. |
| src/gpcr_tools/main.py | Adds annotate --batch --sequential CLI option and validates flag combinations. |
| scripts/check_no_local_paths.py | Adds pre-commit hook script to reject developer-local absolute paths. |
| scripts/build_site_ref_table.py | Makes site-ref table build script portable and env-driven. |
| scripts/.local-denylist.example | Adds example denylist file for local path hook. |
| README.md | Updates documentation for new outputs, terminology, and sequential batch mode. |
| .pre-commit-config.yaml | Adds local pre-commit hook for no-local-paths. |
| .gitignore | Ignores scripts/.local-denylist and adds CONVENTIONS.md ignore entry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| """One advisory signal naming the non-polymer copies that sit on a G-protein / | ||
| transducer chain rather than on the receptor. | ||
|
|
| A modelled G protein is deliberately treated as settling the state: a | ||
| G-protein-coupled complex carrying an inverse agonist is real published | ||
| biology, not something to flag. Antagonist / NAM ligands are excluded so a | ||
| blocked-but-active structure does not raise a false advisory. |
| summary=( | ||
| f"Copies on a G-protein / transducer chain (not the receptor): " | ||
| f"{', '.join(copies)}. Judge each as the transducer's cofactor (role Cofactor)." | ||
| ), |
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
…ings, and comments Co-Authored-By: Claude Opus 4.8 <[email protected]>
Summary
This PR extends the annotation pipeline from one binding-site/role per ligand compound to one per modelled copy, adds a dedicated output lane for small molecules that are present in a structure but are not functional receptor ligands, and consolidates the "does this structure need a human?" decision into a single shared subsystem used identically by the interactive curator, the non-interactive pass, and the run-manifest report. It also normalizes G-protein terminology across all model-facing and user-facing surfaces, corrects several CSV identity/output defects reported during expert review, and adds a sequential Batch-API submission mode for large corpora.
The pipeline shape is unchanged —
fetch → fetch-papers → detect → annotate → aggregate → curate— but per-copy ligand identity now threads through every stage, and the curation gate now has one authoritative implementation.Why
Two limitations motivated the bulk of the work:
auth_asym_id:auth_seq_id).ligands.csvholds only functional receptor ligands.A third theme is operational consolidation: three separate places used to decide, slightly differently, whether a structure was "clean". They now call one module, so the curator, the auto-accept path, and reporting can never disagree.
Major components and how they fit together
1. Per-copy ligand binding-site / role (
ligand_copies) — threads through four stages:67134d3,src/gpcr_tools/detector/ligands.py); polymer residues are excluded from ligand-copy selection (2be90eb).ligand_copiesarray schema whosecopy_idis pinned by enum to exactly that structure's copies, and reuses the compound-levelsite_ref/roleenums so the two answer spaces cannot drift (68f9945;build_ligand_copies_schemainsrc/gpcr_tools/annotator/schema.py). Per-copy coverage is validated every run: retry-then-degrade in single-run mode, drop on batch mismatch (701db8d,src/gpcr_tools/annotator/runner.py).ligand_copies[<copy_id>].<field>paths (21751b7,src/gpcr_tools/aggregator/voting.py), rebuilds the small-molecule ligand rows from the per-copy site votes (3fb45c7), and gives a site-undetermined copy an explicit unknown-site row rather than dropping it (ca5af6a).ligands.csvResidue_seq_idself-describing — each copy's residues prefixed with its author chain (auth_asym_id:auth_seq_id), comma-joined and aligned copy-for-copy withlabel_asym_id(2f2574b;src/gpcr_tools/csv_generator/csv_writer.py).28840e9); per-copy role disagreements are advisory, not gating (836cad5); a clean per-copy block passes review without prompting (e42a757).2. Auxiliary small-molecule catalogue (
auxiliary_small_molecules.csv) — new output lane (1b85b37;_auxiliary_small_molecule_rowsincsv_writer.py). Columns:PDB, ChainID, Name(chemical-component code),Type(Ion/Lipid/Detergent/Other, fromAUX_TYPE_MAPinconfig.py),Function(Cofactoror blank),label_asym_id, Residue_seq_id. Two lanes feed it: molecules the model judged auxiliary (a cofactor, or a detector-flagged incidental candidate judged non-functional —ligand_routed_to_aux,config.py), and mechanical molecules stripped before the model ever saw them, enumerated from the structure's nonpolymer roster (MECHANICAL_AUX,config.py). Each molecule lives in exactly one file, soligands.csvandauxiliary_small_molecules.csvnever double-count a copy.3. Metals as candidate ligands — the counter-ion metals (Ca, Na, Mg, Zn, Mn) are freed to the model's auxiliary lane instead of being hard-excluded, with a new prompt rule (
v5.mdstep 10): the model judges each metal from the paper and defaults it torole = Cofactor/is_functional_ligand = falseunless the paper's mechanism/mutagenesis shows it acts at this receptor's own site (e.g. Ca²⁺ at the calcium-sensing receptor, Na⁺ in the Class A 2.50 pocket). Seeconfig.py; final wording refined in61eb96c.4. Unified curation gating + decision brief — three new UI-free modules in
src/gpcr_tools/validator/, depending only onconfig:gating.py(new) — the single source of truth for whether a PDB is gated.is_pdb_gated()returns True if any of: a validation-log critical warning / algorithmic conflict, a gating oligomer finding, or a gating vote controversy. The interactive curator, the auto-accept pass, and the read-only run manifest all call it, so all three reach the same verdict (gating.py).decisions.py(new) — enumerates one rankedDecisionItemper signal (each validation finding, each gating oligomer finding, and every vote fork read straight from the raw voting log), powering a read-only decision brief so a curator sees the complete picture, not only what tripped the boolean gate (1857543).consistency.py(new) — cross-field advisories that ask a curator to confirm rather than assert an error (e.g. anactivestate annotated alongside an inverse-agonist ligand with no G protein modelled).Curation was also hardened: genuine identity disagreements no longer pre-select a default, forcing an explicit choice (
1857543); a null field value is no longer conflated with the review quit signal (661aab4); audit-directory creation soft-fails so curation never crashes on it (eb5bf5b).5. G-protein terminology + column rename — "G-protein" → "G protein" across the model-facing prompt/schema (
ff40d22), user-visible output and review routing (f074d34), comments/docs (78bb707), and README.g_proteins.csvrenames the alpha column and adds two (32e0f64;csv_writer.py) — see Breaking changes.6. CSV identity/output fixes — six curator-flagged defects fixed across ligands, partners, fusion/binder names, and G-protein subunits (
6c0dea3); G-protein subunit recovery and binder renaming refined to merge same-subunit chain IDs and preserve antibody clone tags (21ea1b6); ligand exclude-list corrected (f1116d4adds FMN/NI/NH4/SCN/UNX;d1c43e0drops PLM, adds UNL).7. Sequential batch submission —
annotate --batch --sequentialsubmits one shard per invocation and refuses to submit while a prior batch job is still in flight, so repeated (cron or manual) runs advance the corpus one shard at a time without overrunning the provider's enqueued-token limit (6fef06e;src/gpcr_tools/__main__.py).8. Repo hygiene / tooling — new
no-local-pathspre-commit hook that rejects developer-local absolute paths from the tree (.pre-commit-config.yaml,scripts/check_no_local_paths.py); the generic-numbering / site-ref table build script made portable and guarded against developer-local paths, plus asite_refreference doc (2544d33,4f5c518).Testing
test_validator_gating.py,test_decisions.py,test_validator_consistency.py,test_controversy_default_suppression.py,test_binder_name_correction.py,test_decision_brief_ui.py, plus new fixturescontroversy_default_forks.jsonanddecision_brief_signals.json. Large expansions totest_aggregator_runner.py(+1,314),test_csv_writer.py(+906),test_annotator_runner.py(+678),test_oligomer.py(+476), andtest_voting.py(+342)..github/workflows/ci.yml) runs on every push/PR tomain: Ruff lint + format check, mypy (ignore_missing_imports = false), pytest with coverage across Python 3.11 and 3.12, and Docker smoke tests (baked code version,init-workspace,curate --help,curate --auto-acceptverifying host-visible CSV/audit/state outputs)..github/workflows/docker-publish.yml) re-runs the smoke gate then builds and pushes a multi-arch (linux/amd64,linux/arm64) image to GHCR onv*tags.GPCR_RUN_LIVE_TESTS=1; the default suite mocks external APIs.Breaking changes / migration notes
Downstream consumers of the CSV outputs should note:
g_proteins.csv:Alpha_UniProt→Alpha_identity(csv_writer.py, wasAlpha_UniProtat the base). Two columns added:Alpha_alpha5_identity(α5-helix functional coupling) andAlpha_backbone(modelled scaffold).ligands.csv:Namesemantics changed —Nameis now the PDBe chemical-component code (e.g.RET,U0G), falling back to the descriptive name only when no component code exists; the full descriptive name is now inTitle(csv_writer.py; at the baseNameheld the descriptive name). A newResidue_seq_idcolumn lists each modelled copy's residues.auxiliary_small_molecules.csv— ingestion tooling that scansoutput/csv/should expect it. Molecules that previously might have surfaced inligands.csv(ions, cofactors, glycans, detergents, matrix lipids, model-judged-non-functional molecules) now appear here instead.No new dependencies, environment variables, or workspace-contract changes. Re-running any stage remains idempotent;
--forcerecomputes.