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:
TypedDictTaxonomy 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
.gztransparently.- 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_bondplus inferred inter-residue bonds).- extra_fieldslist[str] or None
Additional
atom_siteannotations to carry onto the array.
- Returns:
- struc.AtomArray
The single model, with a
selected_altloc_idannotation.
- 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_idannotation 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’saltloc="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_polysequence column; the default is the canonical one-letter code,pdbx_seq_one_letter_codekeeps 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_idcontains author chain IDs and therefore cannot be used for reconstructed biological assemblies, whose chain IDs are<instance>.<label_asym_id>. The stable mapping isstruct_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_sitecategory).
- 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 ascendingsym_id; one is dropped when more thanclash_ratioof its atoms lie withinclash_distanceof 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 thesym_idandlabel_asym_idannotations.
- plinder.data.annotations.cif_utils.build_biounit(cif_file: CIFFile, assembly_id: str) AtomArray[source]#
Build a biological assembly with stable
instance.asymchain 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_idenumerates 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 distinctres_idvalues, 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);Nonefor?/., 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 everypH <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_growrows.Per row:
pH, elsepdbx_pH_range, else pH mentions inpdbx_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 thecomp_idset 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
Nonewhen 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=Trueor caller-set). Bonds missing from a residue are filled from the CCD; a multi-atom input still bond-less after that raisesValueError. 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_heavybefore conversion, so the sanitized molecule, ring info included, is returned as is.connect_via_residue_namesis 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_connpartner order.auth_seqisauth_seq_idwith insertion code (?when unknown),comp_idislabel_comp_id(entity_poly_seq.mon_idfor polymers),asymislabel_asym_id,label_seqislabel_seq_id(.for non-polymer and branched residues) andatomislabel_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_conninto a list of connection dicts.- Parameters:
- blockpdbx.CIFBlock
Source mmCIF data block.
- Returns:
- list[dict[str, str]]
One dict per connection (empty when
_struct_connor 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.bondsis replaced with the pruned graph. A no-op when there are no bonds.
Notes
get_assembly/get_structurewithinclude_bonds=Truecan emit chemically impossible bonds:a residue-ambiguity clash, where a
res_idcollision makes biotite cross-link atoms of two different residues. Branched sugars are the usual cause (undefinedlabel_seq_id);_branched_residue_numbering()prevents that upstream, so this stays a backstop for any other collision.inter-residue inference, where
connect_via_residue_namesbonds consecutive-but-spatially-distant residues (peptideC-N/ nucleicO3'-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:
ValueErrorRaised 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_siteas 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_bondrows 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_bondis keyed by comp_id so all instances must share atom naming for biotite to apply the single bond definition correctly).
Notes
Mutates
cif_fileby appending bond entries for unknown ligands using the provided SMILES templates. Known CCD compounds are skipped and existing_chem_comp_bondrows are preserved. Seeassign_bond_orders_from_smiles()for the atom-order assumption and theforce_substructure_matchopt-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 tooutput_path(or overwritescif_pathwhenoutput_pathisNone). Callers that already hold apdbx.CIFFileshould useenrich_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
ValueErrorat the first mismatch.Set
force_substructure_match=Trueto 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.