Skip to content

Per-copy ligand binding-site annotation, auxiliary small-molecule catalogue, and unified curation gating - #7

Merged
iskoldt-X merged 36 commits into
protwis:mainfrom
iskoldt-X:prompt-routing
Jul 21, 2026
Merged

Per-copy ligand binding-site annotation, auxiliary small-molecule catalogue, and unified curation gating#7
iskoldt-X merged 36 commits into
protwis:mainfrom
iskoldt-X:prompt-routing

Conversation

@iskoldt-X

@iskoldt-X iskoldt-X commented Jun 29, 2026

Copy link
Copy Markdown
Member

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:

  1. A single ligand compound can occupy different sites in different copies (e.g. an orthosteric copy and an allosteric copy of the same molecule). Annotating one site/role per compound cannot represent this, and it made per-residue CSV output ambiguous. This PR keys binding-site and role annotation to each copy's author identifier (auth_asym_id:auth_seq_id).
  2. Ions, cofactors, glycans, detergents, and matrix lipids were being dropped silently or awkwardly mixed into the ligand table. They are now catalogued explicitly on their own lane, so nothing modelled is lost and ligands.csv holds 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:

  • detect carries each ligand copy's author identifier through the per-copy site evidence and labels it in the detector-evidence block (67134d3, src/gpcr_tools/detector/ligands.py); polymer residues are excluded from ligand-copy selection (2be90eb).
  • annotate adds a per-PDB ligand_copies array schema whose copy_id is pinned by enum to exactly that structure's copies, and reuses the compound-level site_ref/role enums so the two answer spaces cannot drift (68f9945; build_ligand_copies_schema in src/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).
  • aggregate votes per-copy site/role keyed by copy identifier, on 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).
  • CSV makes ligands.csv Residue_seq_id self-describing — each copy's residues prefixed with its author chain (auth_asym_id:auth_seq_id), comma-joined and aligned copy-for-copy with label_asym_id (2f2574b; src/gpcr_tools/csv_generator/csv_writer.py).
  • curate makes the per-copy ligand table reviewable (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_rows in csv_writer.py). Columns: PDB, ChainID, Name (chemical-component code), Type (Ion / Lipid / Detergent / Other, from AUX_TYPE_MAP in config.py), Function (Cofactor or 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, so ligands.csv and auxiliary_small_molecules.csv never 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.md step 10): the model judges each metal from the paper and defaults it to role = Cofactor / is_functional_ligand = false unless 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). See config.py; final wording refined in 61eb96c.

4. Unified curation gating + decision brief — three new UI-free modules in src/gpcr_tools/validator/, depending only on config:

  • 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 ranked DecisionItem per 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. an active state 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.csv renames 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 (f1116d4 adds FMN/NI/NH4/SCN/UNX; d1c43e0 drops PLM, adds UNL).

7. Sequential batch submissionannotate --batch --sequential submits 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-paths pre-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 a site_ref reference doc (2544d33, 4f5c518).

Testing

  • Suite grew from 1,427 to 1,736 test functions (69 → 75 test files). New suites: 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 fixtures controversy_default_forks.json and decision_brief_signals.json. Large expansions to test_aggregator_runner.py (+1,314), test_csv_writer.py (+906), test_annotator_runner.py (+678), test_oligomer.py (+476), and test_voting.py (+342).
  • CI (.github/workflows/ci.yml) runs on every push/PR to main: 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-accept verifying host-visible CSV/audit/state outputs).
  • Release CI (.github/workflows/docker-publish.yml) re-runs the smoke gate then builds and pushes a multi-arch (linux/amd64,linux/arm64) image to GHCR on v* tags.
  • Live-network integration tests remain gated behind GPCR_RUN_LIVE_TESTS=1; the default suite mocks external APIs.

Breaking changes / migration notes

Downstream consumers of the CSV outputs should note:

  1. g_proteins.csv: Alpha_UniProtAlpha_identity (csv_writer.py, was Alpha_UniProt at the base). Two columns added: Alpha_alpha5_identity (α5-helix functional coupling) and Alpha_backbone (modelled scaffold).
  2. ligands.csv: Name semantics changedName is 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 in Title (csv_writer.py; at the base Name held the descriptive name). A new Residue_seq_id column lists each modelled copy's residues.
  3. New file auxiliary_small_molecules.csv — ingestion tooling that scans output/csv/ should expect it. Molecules that previously might have surfaced in ligands.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; --force recomputes.

iskoldt-X and others added 30 commits June 23, 2026 09:01
…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]>
iskoldt-X and others added 4 commits July 11, 2026 15:24
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]>
@iskoldt-X iskoldt-X changed the title TBD Per-copy ligand binding-site annotation, auxiliary small-molecule catalogue, and unified curation gating Jul 21, 2026
@iskoldt-X
iskoldt-X requested a review from Copilot July 21, 2026 14:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.csv and route non-functional/auxiliary molecules (including model-judged auxiliary and mechanically stripped molecules) out of ligands.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.

Comment thread src/gpcr_tools/csv_generator/ui.py
Comment thread src/gpcr_tools/detector/ligands.py Outdated
Comment on lines +82 to +84
"""One advisory signal naming the non-polymer copies that sit on a G-protein /
transducer chain rather than on the receptor.

Comment on lines +47 to +50
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.
Comment on lines +118 to +121
summary=(
f"Copies on a G-protein / transducer chain (not the receptor): "
f"{', '.join(copies)}. Judge each as the transducer's cofactor (role Cofactor)."
),
iskoldt-X and others added 2 commits July 21, 2026 19:37
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
@iskoldt-X
iskoldt-X marked this pull request as ready for review July 21, 2026 17:13
@iskoldt-X
iskoldt-X merged commit 912e3f5 into protwis:main Jul 21, 2026
5 checks passed
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.

2 participants