AlphaFold3 / flax_model /alphafold3 /constants /chemical_components.py
wuxing0105's picture
Add files using upload-large-folder tool
40d185e verified
Raw
History Blame Contribute Delete
6.15 kB
"""Chemical Components found in PDB (CCD) constants."""
from collections.abc import ItemsView, Iterator, KeysView, Mapping, Sequence, ValuesView
import dataclasses
import functools
import os
import pickle
from flax_model.alphafold3.common import resources
from flax_model.alphafold3.cpp import cif_dict
_CCD_PICKLE_FILE = resources.filename(
resources.ROOT / 'constants/converters/ccd.pickle'
)
@functools.cache
def _load_ccd_pickle_cached(
path: os.PathLike[str],
) -> dict[str, Mapping[str, Sequence[str]]]:
"""Loads the CCD pickle file and caches it so that it is only loaded once."""
with open(path, 'rb') as f:
return pickle.loads(f.read())
class Ccd(Mapping[str, Mapping[str, Sequence[str]]]):
"""Chemical Components found in PDB (CCD) constants.
See https://academic.oup.com/bioinformatics/article/31/8/1274/212200 for CCD
CIF format documentation.
Wraps the dict to prevent accidental mutation.
"""
__slots__ = ('_dict', '_ccd_pickle_path')
def __init__(
self,
ccd_pickle_path: os.PathLike[str] | None = None,
user_ccd: str | None = None,
):
"""Initialises the chemical components dictionary.
Args:
ccd_pickle_path: Path to the CCD pickle file. If None, uses the default
CCD pickle file included in the source code.
user_ccd: A string containing the user-provided CCD. This has to conform
to the same format as the CCD, see https://www.wwpdb.org/data/ccd. If
provided, takes precedence over the CCD for the the same key. This can
be used to override specific entries in the CCD if desired.
"""
self._ccd_pickle_path = ccd_pickle_path or _CCD_PICKLE_FILE
self._dict = _load_ccd_pickle_cached(self._ccd_pickle_path)
if user_ccd is not None:
if not user_ccd:
raise ValueError('User CCD cannot be an empty string.')
user_ccd_cifs = {
key: {k: tuple(v) for k, v in value.items()}
for key, value in cif_dict.parse_multi_data_cif(user_ccd).items()
}
self._dict.update(user_ccd_cifs)
def __getitem__(self, key: str) -> Mapping[str, Sequence[str]]:
return self._dict[key]
def __contains__(self, key: str) -> bool:
return key in self._dict
def __iter__(self) -> Iterator[str]:
return self._dict.__iter__()
def __len__(self) -> int:
return len(self._dict)
def __hash__(self) -> int:
return id(self) # Ok since this is immutable.
def get(
self, key: str, default: None | Mapping[str, Sequence[str]] = None
) -> Mapping[str, Sequence[str]] | None:
return self._dict.get(key, default)
def items(self) -> ItemsView[str, Mapping[str, Sequence[str]]]:
return self._dict.items()
def values(self) -> ValuesView[Mapping[str, Sequence[str]]]:
return self._dict.values()
def keys(self) -> KeysView[str]:
return self._dict.keys()
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class ComponentInfo:
name: str
type: str
pdbx_synonyms: str
formula: str
formula_weight: str
mon_nstd_parent_comp_id: str
mon_nstd_flag: str
pdbx_smiles: str
def mmcif_to_info(mmcif: Mapping[str, Sequence[str]]) -> ComponentInfo:
"""Converts CCD mmCIFs to component info. Missing fields are left empty."""
names = mmcif['_chem_comp.name']
types = mmcif['_chem_comp.type']
mon_nstd_parent_comp_ids = mmcif['_chem_comp.mon_nstd_parent_comp_id']
pdbx_synonyms = mmcif['_chem_comp.pdbx_synonyms']
formulas = mmcif['_chem_comp.formula']
formula_weights = mmcif['_chem_comp.formula_weight']
def front_or_empty(values: Sequence[str]) -> str:
return values[0] if values else ''
type_ = front_or_empty(types)
mon_nstd_parent_comp_id = front_or_empty(mon_nstd_parent_comp_ids)
if type_.lower() == 'non-polymer':
# Unset for non-polymers, e.g. water or ions.
mon_nstd_flag = '.'
elif mon_nstd_parent_comp_id == '?':
# A standard component - it doesn't have a standard parent, e.g. MET.
mon_nstd_flag = 'y'
else:
# A non-standard component, e.g. MSE.
mon_nstd_flag = 'n'
# Default SMILES is the canonical SMILES, but we fall back to the SMILES if a
# canonical SMILES is not available. Of canonical SMILES, we prefer ones from
# the OpenEye OEToolkits program.
canonical_pdbx_smiles = ''
fallback_pdbx_smiles = ''
descriptor_types = mmcif.get('_pdbx_chem_comp_descriptor.type', [])
descriptors = mmcif.get('_pdbx_chem_comp_descriptor.descriptor', [])
programs = mmcif.get('_pdbx_chem_comp_descriptor.program', [])
for descriptor_type, descriptor, program in zip(
descriptor_types, descriptors, programs
):
if descriptor_type == 'SMILES_CANONICAL':
if (not canonical_pdbx_smiles) or program == 'OpenEye OEToolkits':
canonical_pdbx_smiles = descriptor
if not fallback_pdbx_smiles and descriptor_type == 'SMILES':
fallback_pdbx_smiles = descriptor
pdbx_smiles = canonical_pdbx_smiles or fallback_pdbx_smiles
return ComponentInfo(
name=front_or_empty(names),
type=type_,
pdbx_synonyms=front_or_empty(pdbx_synonyms),
formula=front_or_empty(formulas),
formula_weight=front_or_empty(formula_weights),
mon_nstd_parent_comp_id=mon_nstd_parent_comp_id,
mon_nstd_flag=mon_nstd_flag,
pdbx_smiles=pdbx_smiles,
)
@functools.lru_cache(maxsize=128)
def component_name_to_info(ccd: Ccd, res_name: str) -> ComponentInfo | None:
component = ccd.get(res_name)
if component is None:
return None
return mmcif_to_info(component)
def type_symbol(ccd: Ccd, res_name: str, atom_name: str) -> str:
"""Returns the element type for the given component name and atom name.
Args:
ccd: The chemical components dictionary.
res_name: The component name, e.g. ARG.
atom_name: The atom name, e.g. CB, OXT, or NH1.
Returns:
Element type, e.g. C for (ARG, CB), O for (ARG, OXT), N for (ARG, NH1).
"""
res = ccd.get(res_name)
if res is None:
return '?'
try:
return res['_chem_comp_atom.type_symbol'][
res['_chem_comp_atom.atom_id'].index(atom_name)
]
except (ValueError, IndexError, KeyError):
return '?'