Skip to content

Commit 9d4ea2c

Browse files
Error on multiple ligands per chain; preserve gaps in override mode
When allow_ligand_on_existing_chain is False, raise an error if multiple ligand residues share the same chain. Reset res_id min to 1 per chain, preserving relative gaps when ligands share a chain (override mode). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 96b2e5f commit 9d4ea2c

1 file changed

Lines changed: 24 additions & 16 deletions

File tree

models/rfd3/src/rfd3/inference/input_parsing.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -673,28 +673,36 @@ def _append_ligand(self, atom_array, atom_array_input_annotated):
673673
+ list(atom_array_input_annotated.get_annotation_categories())
674674
),
675675
)
676-
# Error if any ligand shares a chain ID with the already-built
677-
# atom array — chain ID is leaked to the model so collisions
678-
# represent a significant deviation from the expected convention.
676+
# Validate chain assignments — chain ID is leaked to the model
677+
# so collisions are a significant deviation from convention.
679678
ligand_chains = np.unique(ligand_array.chain_id)
680679
existing_chains = set(np.unique(atom_array.chain_id))
681680
overlapping = sorted(existing_chains & set(ligand_chains))
682-
if overlapping and not self.allow_ligand_on_existing_chain:
683-
raise ValueError(
684-
f"Ligand chain(s) {overlapping} overlap with existing "
685-
f"chain(s) {sorted(existing_chains)}. Place ligands on "
686-
f"separate chains or set 'allow_ligand_on_existing_chain: "
687-
f"true' to override this check."
688-
)
689-
# Reset ligand res_id to start from 1 per chain, matching the
690-
# convention AF3 uses in its output CIF files. Use dense
691-
# rank-based renumbering so gaps in the original numbering
692-
# (e.g. res_id 850, 900) become sequential (1, 2).
681+
if not self.allow_ligand_on_existing_chain:
682+
if overlapping:
683+
raise ValueError(
684+
f"Ligand chain(s) {overlapping} overlap with existing "
685+
f"chain(s) {sorted(existing_chains)}. Place ligands on "
686+
f"separate chains or set 'allow_ligand_on_existing_chain: "
687+
f"true' to override this check."
688+
)
689+
# Multiple ligands must each be on their own chain.
690+
for chain in ligand_chains:
691+
n_residues = len(
692+
np.unique(ligand_array.res_id[ligand_array.chain_id == chain])
693+
)
694+
if n_residues > 1:
695+
raise ValueError(
696+
f"Multiple ligand residues on chain {chain}. Each "
697+
f"ligand must be on its own chain, or set "
698+
f"'allow_ligand_on_existing_chain: true' to override."
699+
)
700+
# Reset ligand res_id to start from 1 per chain. When ligands
701+
# share a chain (override mode), preserve relative gaps.
693702
for chain in ligand_chains:
694703
mask = ligand_array.chain_id == chain
695704
chain_res_ids = ligand_array.res_id[mask]
696-
_, inverse = np.unique(chain_res_ids, return_inverse=True)
697-
ligand_array.res_id[mask] = inverse + 1
705+
ligand_array.res_id[mask] = chain_res_ids - np.min(chain_res_ids) + 1
698706
# Harmonize conditioning annotations before concatenation: biotite's
699707
# concatenate only preserves annotations present in ALL arrays (set
700708
# intersection), so mismatched optional conditioning annotations

0 commit comments

Comments
 (0)