plinder.data.annotations.cif_utils#

mmCIF I/O utilities using biotite.

Generic helpers for reading CIF blocks, extracting scalar values and category rows, plus ligand bond-order detection and assignment from SMILES templates.

class plinder.data.annotations.cif_utils.EntryTaxonomy[source]#

Bases: TypedDict

Taxonomy fields extracted from deposited entry metadata.

source_taxonomy_ids: list[int]#
source_organism_names: list[str]#
host_taxonomy_ids: list[int]#
host_organism_names: list[str]#
plinder.data.annotations.cif_utils.select_assembly_ids(available: Iterable[str], selected: Iterable[str] | None) → list[str][source]#

Return a validated assembly subset in caller-provided order.

plinder.data.annotations.cif_utils.read_mmcif_file(mmcif_filename: Path | str) → CIFFile[source]#

Read an mmCIF file and add unambiguous optional atom-site defaults.

Handles .gz transparently.

Parameters:
mmcif_filenamePath or str

Path to the mmCIF file (optionally gzip-compressed).

Returns:
pdbx.CIFFile

Parsed CIF file with the optional atom-site defaults filled in.

plinder.data.annotations.cif_utils.check_custom_mmcif_fields(block: CIFBlock, *, source: Path, structure_mode: str, require_label_ids: bool = False) → None[source]#

Check fields required to read custom coordinates and assemblies.

Model number and insertion code are optional for a single-model custom structure and receive unambiguous defaults. Deposited-PDB mode additionally requires the assembly operation tables. Other deposition metadata (authors, citations, experimental details, and validation categories) is not required.

plinder.data.annotations.cif_utils.read_mmcif_container(mmcif_filename: Path) → CIFBlock[source]#

Parse an mmCIF file and return its first data block.

Parameters:
mmcif_filenamePath

Path to the mmCIF file.

Returns:
pdbx.CIFBlock

The first data block.

plinder.data.annotations.cif_utils.get_structure_with_altloc(cif_file: CIFFile | CIFBlock, *, model: int = 1, use_author_fields: bool = False, include_bonds: bool = False, extra_fields: list[str] | None = None) → AtomArray[source]#

Load one model using its deposited-first alternate conformers.

Parameters:
cif_filepdbx.CIFFile or pdbx.CIFBlock

Source mmCIF.

modelint, default=1

Model number to load.

use_author_fieldsbool, default=False

Use author chain/residue numbering instead of the label fields.

include_bondsbool, default=False

Read the bond graph (_chem_comp_bond plus inferred inter-residue bonds).

extra_fieldslist[str] or None

Additional atom_site annotations to carry onto the array.

Returns:
struc.AtomArray

The single model, with a selected_altloc_id annotation.

Raises:
TypeError

If loading a single model does not return an AtomArray.

Notes

Biotite only recognizes alphabetic alternate-location IDs while filtering. Non-alphabetic source IDs are therefore mapped temporarily, then restored on the returned selected_altloc_id annotation so validation can select the exact same deposited conformer.

TODO: discuss switching to highest-occupancy selection (altloc="occupancy") instead of deposited-first. Occupancy is the crystallographer’s quantitative statement of the dominant conformer, so it may be more principled — but it is a deliberate deposited-first choice today, and the switch would change coordinates (hence bonds/interactions/validation) for altloc-heavy entries, can stitch per-residue occupancy picks across correlated altloc states, and biotite’s altloc="occupancy" raises when a CIF has no occupancy column (predicted/custom CIFs) so it must fall back to "first" there.

plinder.data.annotations.cif_utils.get_unit_cell_with_altloc(cif_file: CIFFile, *, model: int = 1, use_author_fields: bool = False) → AtomArray[source]#

Build a unit cell using normalized deposited-first altlocs.

Parameters:
cif_filepdbx.CIFFile

Source mmCIF.

modelint, default=1

Model number to expand.

use_author_fieldsbool, default=False

Use author chain/residue numbering instead of the label fields.

Returns:
struc.AtomArray

The unit-cell atoms.

Raises:
TypeError

If loading the unit cell does not return an AtomArray.

plinder.data.annotations.cif_utils.get_label_asym_sequences(block: CIFBlock, column: str = 'pdbx_seq_one_letter_code_can') → dict[str, str][source]#

Extract polymer sequences keyed by label asym ID.

Parameters:
blockpdbx.CIFBlock

Source mmCIF data block.

columnstr

entity_poly sequence column; the default is the canonical one-letter code, pdbx_seq_one_letter_code keeps modified residues as (CCD).

Returns:
dict[str, str]

Sequence keyed by label_asym_id (empty when the required categories are absent).

Notes

entity_poly.pdbx_strand_id contains author chain IDs and therefore cannot be used for reconstructed biological assemblies, whose chain IDs are <instance>.<label_asym_id>. The stable mapping is struct_asym.id -> struct_asym.entity_id -> entity_poly.entity_id.

plinder.data.annotations.cif_utils.get_entry_taxonomy(block: CIFBlock) → EntryTaxonomy[source]#

Extract distinct source and expression-host organisms for an entry.

plinder.data.annotations.cif_utils.get_mmcif_revision(block: CIFBlock) → tuple[int, int][source]#

Return the latest structure-model major/minor revision in an mmCIF.

Parameters:
blockpdbx.CIFBlock

Source mmCIF data block.

Returns:
tuple[int, int]

The (major, minor) revision of the latest structure-model entry.

Raises:
ValueError

If the revision-history category or its columns are missing, or it has no structure-model revisions.

plinder.data.annotations.cif_utils.get_model_count(cif_file: CIFFile) → int[source]#

Return the number of models in a CIF (1 if no model column present).

Parameters:
cif_filepdbx.CIFFile

Source mmCIF.

Returns:
int

Number of models (0 if there is no atom_site category).

plinder.data.annotations.cif_utils.get_legacy_chain_instance_mapping(cif_file: CIFFile | CIFBlock, assembly_id: str) → dict[str, str][source]#

Map canonical per-asym instance IDs to historical global-copy IDs.

The canonical Biotite representation numbers transformed copies separately for each label asym ID. Earlier Plinder releases used OpenStructure, which numbered every generated transformation globally across assembly rows. This mapping preserves those historical identifiers as lookup aliases.

plinder.data.annotations.cif_utils.drop_self_clashing_symmetry_copies(assembly: AtomArray, *, clash_distance: float = 1.0, clash_ratio: float = 0.5, context: str = 'assembly') → AtomArray[source]#

Drop coincident symmetry copies and severely overlapping protein chains.

Chains on a symmetry axis can expand into coincident copies of one label_asym_id. Copies are visited in ascending sym_id; one is dropped when more than clash_ratio of its atoms lie within clash_distance of an already kept copy. Waters are untouched. Protein chains from different source asym IDs are also compared by their C-alpha atoms: a chain is removed if at least five and more than 10%, or at least twenty regardless of chain length, lie within 2 Å of previously retained protein chains. Such C-alpha distances are nonphysical for separate chains. Requires the sym_id and label_asym_id annotations.

plinder.data.annotations.cif_utils.build_biounit(cif_file: CIFFile, assembly_id: str) → AtomArray[source]#

Build a biological assembly with stable instance.asym chain IDs.

Parameters:
cif_filepdbx.CIFFile

Source mmCIF to build the assembly from.

assembly_idstr

Identifier of the biological assembly to construct.

Returns:
struc.AtomArray

Heavy-atom assembly with <instance>.<label_asym_id> chain IDs and an intra/inter-residue bond graph.

Raises:
ValueError

If biotite returns no bonds despite include_bonds=True.

Notes

Biotite’s sym_id enumerates transformed copies independently for each source asym chain. Using it avoids assuming that an assembly consists of complete, contiguous ASU-sized blocks, which is false when operators apply to only a subset of chains.

Branched entities are renumbered (_branched_residue_numbering()) before building so biotite gives their residues distinct res_id values, and any non-physical bonds biotite’s inference still emits are then dropped (remove_nonphysical_bonds()). Copies generated on top of each other by a symmetry axis are reduced to one (drop_self_clashing_symmetry_copies()).

plinder.data.annotations.cif_utils.parse_ph_range(text: str | None) → tuple[float, float] | None[source]#

Parse "7.5", "7.0-8.0", "pH8.0" or "6.5 to 7.5" into (low, high); None for ?/., free text, or values outside 0-14.

plinder.data.annotations.cif_utils.find_ph_mentions(text: str | None) → tuple[float, float] | None[source]#

Return (low, high) over every pH <value>[-<value>] mention in text.

plinder.data.annotations.cif_utils.get_entry_ph_range(block: CIFBlock) → tuple[float | None, float | None][source]#

Crystallization pH range over exptl_crystal_grow rows.

Per row: pH, else pdbx_pH_range, else pH mentions in pdbx_details. Returns (None, None) when nothing parses.

plinder.data.annotations.cif_utils.get_ligand_of_interest(block: CIFBlock) → tuple[bool | None, frozenset[str] | None][source]#

Return has_ligand_of_interest (Y/N -> bool, else None) and the comp_id set flagged SUBJECT OF INVESTIGATION (None when absent).

plinder.data.annotations.cif_utils.get_entry_info(data: CIFBlock) → dict[str, str | None][source]#

Get entry-level information from a CIF block.

Parameters:
datapdbx.CIFBlock

Source mmCIF data block.

Returns:
dict[str, str | None]

Entry-level metadata (oligomeric state, determination method, keywords, pH, resolution); each value is None when absent. Resolution uses refinement metadata first, then EM reconstruction metadata.

plinder.data.annotations.cif_utils.get_chain_external_mappings(data: CIFBlock) → dict[str, dict[str, dict[str, list[tuple[str, str] | None]]]][source]#

Get additional metadata directory from nextgen mmcif.

Parameters:
datapdbx.CIFBlock

Source mmCIF data block.

Returns:
dict[str, dict[str, dict[str, list[tuple[str, str] | None]]]]

Per-chain SIFTS / UniProt / BIRD mappings keyed by asym_id.

plinder.data.annotations.cif_utils.atoms_to_rdkit_mol(atoms: AtomArray, assign_stereo: bool = True) → Mol[source]#

Convert a biotite AtomArray to a sanitized RDKit Mol.

Parameters:
atomsAtomArray

Atoms with bonds (include_bonds=True or caller-set). Bonds missing from a residue are filled from the CCD; a multi-atom input still bond-less after that raises ValueError. Single atoms (ions) need none.

assign_stereobool

If True, perceive stereo from the coordinates via AssignStereochemistryFrom3D (tetrahedral R/S and double-bond E/Z).

Returns:
Chem.Mol

Sanitized RDKit molecule with 3D coordinates and PDB atom info, heavy atoms only.

Raises:
ValueError

If a multi-atom input has no bonds (even after CCD fill), or conversion fails.

Notes

Missing intra-residue bonds are recovered from the bundled CCD first, then stereo (atom R/S and double-bond E/Z) is optionally assigned from the 3D coordinates. H and D are dropped by filter_heavy before conversion, so the sanitized molecule, ring info included, is returned as is. connect_via_residue_names is deliberately not used to derive bonds — it silently drops inter-residue bonds for non-standard residues.

plinder.data.annotations.cif_utils.residue_address(auth_seq: str, comp_id: str, asym_id: str, label_seq: str | int, atom_name: str | None = None) → str[source]#

{auth_seq}:{comp_id}:{asym}:{label_seq}[:{atom}] in _struct_conn partner order.

auth_seq is auth_seq_id with insertion code (? when unknown), comp_id is label_comp_id (entity_poly_seq.mon_id for polymers), asym is label_asym_id, label_seq is label_seq_id (. for non-polymer and branched residues) and atom is label_atom_id. Shared by covalent links, modified residues and unresolved atoms.

plinder.data.annotations.cif_utils.parse_struct_conn(block: CIFBlock) → list[dict[str, str]][source]#

Parse _struct_conn into a list of connection dicts.

Parameters:
blockpdbx.CIFBlock

Source mmCIF data block.

Returns:
list[dict[str, str]]

One dict per connection (empty when _struct_conn or a required column is absent).

plinder.data.annotations.cif_utils.remove_nonphysical_bonds(atoms: AtomArray) → None[source]#

Drop bonds too long to be covalent, in-place, logging each removal.

Parameters:
atomsstruc.AtomArray

Bonded structure cleaned in-place; atoms.bonds is replaced with the pruned graph. A no-op when there are no bonds.

Notes

get_assembly/get_structure with include_bonds=True can emit chemically impossible bonds:

  • a residue-ambiguity clash, where a res_id collision makes biotite cross-link atoms of two different residues. Branched sugars are the usual cause (undefined label_seq_id); _branched_residue_numbering() prevents that upstream, so this stays a backstop for any other collision.

  • inter-residue inference, where connect_via_residue_names bonds consecutive-but-spatially-distant residues (peptide C-N / nucleic O3'-P) without checking the distance.

Both are far longer than any real bond, so we drop any bond exceeding the element pair’s distance threshold (_max_bond_length()). Real bonds (<=~2.4 Å, disulfides and carborane cages included) are kept.

exception plinder.data.annotations.cif_utils.MissingBondOrderError[source]#

Bases: ValueError

Raised when a CIF file has ligands with unresolvable bond orders.

plinder.data.annotations.cif_utils.get_unknown_ligand_ids(cif_input: CIFFile | Path | str) → set[str][source]#

Return HETATM comp_ids not in CCD and missing _chem_comp_bond.

Parameters:
cif_inputCIFFile, Path, or str

Biotite CIFFile or path to an mmCIF file.

Returns:
set[str]

Component IDs requiring user-supplied SMILES.

plinder.data.annotations.cif_utils.check_cif_bond_orders(cif_input: CIFFile | Path | str) → None[source]#

Raise if any ligand has unresolvable bond orders.

Parameters:
cif_inputCIFFile, Path, or str

Biotite CIFFile or path to an mmCIF file.

Raises:
MissingBondOrderError

If any ligand is unknown to CCD and has no _chem_comp_bond.

plinder.data.annotations.cif_utils.enrich_cif_with_ccd_bonds(cif_file: CIFFile, ligand_ccd_codes: dict[str, str]) → dict[str, str][source]#

Assign custom component bonds from Chemical Component Dictionary entries.

The input mapping uses the custom component ID in _atom_site as its key and a reference CCD code as its value, for example {"LIG": "ATP"}. Atom names are matched first. If they differ, provisional connectivity is inferred from the supplied coordinates and graph-matched to the CCD template. The function refuses partial or ambiguous mappings instead of assigning bonds from element order alone.

Returns:
dict[str, str]

Canonical CCD SMILES keyed by the custom component ID.

plinder.data.annotations.cif_utils.enrich_cif_with_smiles_bonds(cif_file: CIFFile, ligand_smiles: dict[str, str], force_substructure_match: bool = False) → None[source]#

Add _chem_comp_bond rows to a CIFFile in-memory.

Parameters:
cif_filepdbx.CIFFile

CIF object to mutate in place.

ligand_smilesdict[str, str]

Mapping of component ID (e.g. LIG) to SMILES.

force_substructure_matchbool, default=False

If True, skip the positional element check entirely and assign bonds via RDKit substructure matching instead.

Raises:
MissingBondOrderError

If unknown ligands remain without SMILES.

ValueError

If SMILES is invalid, atom counts differ, element order does not match (default path), sanitize / template matching fails (substructure path — this path fails loudly rather than risk emitting chemically wrong bond orders), or multiple instances of the same comp_id disagree on heavy-atom naming/order (mmCIF _chem_comp_bond is keyed by comp_id so all instances must share atom naming for biotite to apply the single bond definition correctly).

Notes

Mutates cif_file by appending bond entries for unknown ligands using the provided SMILES templates. Known CCD compounds are skipped and existing _chem_comp_bond rows are preserved. See assign_bond_orders_from_smiles() for the atom-order assumption and the force_substructure_match opt-in.

plinder.data.annotations.cif_utils.assign_bond_orders_from_smiles(cif_path: Path, ligand_smiles: dict[str, str], output_path: Path | None = None, force_substructure_match: bool = False) → Path[source]#

Disk-based wrapper around enrich_cif_with_smiles_bonds().

Parameters:
cif_pathPath

Input mmCIF file.

ligand_smilesdict[str, str]

Mapping of component ID (e.g. LIG) to SMILES.

output_pathPath | None

Output path. Defaults to overwriting cif_path.

force_substructure_matchbool, default=False

If True, skip the positional element check entirely and assign bonds via RDKit substructure matching instead. Use only when CIF atom order is not guaranteed to match SMILES order.

Returns:
Path

Path to the written CIF file.

Raises:
MissingBondOrderError, ValueError

Propagated from enrich_cif_with_smiles_bonds(). See that function’s docstring for the full list of failure modes.

Notes

Reads cif_path, enriches the CIF in memory, and writes the result to output_path (or overwrites cif_path when output_path is None). Callers that already hold a pdbx.CIFFile should use enrich_cif_with_smiles_bonds() directly to avoid the read/write round-trip.

Atom-order assumption. By default this assumes the CIF heavy-atom order matches the SMILES heavy-atom parse order — the convention produced by structure-prediction tools (e.g. Boltz, AlphaFold3, Chai-1). Under it the CIF atom -> SMILES atom mapping is the identity, so bond orders copy directly from the template; the function verifies this by comparing the element at each position and raises ValueError at the first mismatch.

Set force_substructure_match=True to fully replace the default path with RDKit substructure matching. This does NOT fall back on failure — it is the only method used when the flag is set. Slower, can be ambiguous for symmetric molecules, and should only be used for CIFs from tools that don’t preserve SMILES atom order.