|
|
|
|
| """Chirality detection and comparison.""" |
|
|
| from collections.abc import Mapping |
|
|
| from absl import logging |
| from flax_model.alphafold3 import structure |
| from flax_model.alphafold3.constants import chemical_components |
| from flax_model.alphafold3.data.tools import rdkit_utils |
| import rdkit.Chem as rd_chem |
|
|
| _CHIRAL_ELEMENTS = frozenset({'C', 'S'}) |
|
|
|
|
| def _find_chiral_centres(mol: rd_chem.Mol) -> dict[str, str]: |
| """Find chiral centres and detect their chirality. |
| |
| Only elements listed in _CHIRAL_ELEMENTS are considered as centres. |
| |
| Args: |
| mol: The molecule for which to detect chirality. |
| |
| Returns: |
| Map from chiral centre atom names to identified chirality. |
| """ |
| chiral_centres = rd_chem.FindMolChiralCenters( |
| mol, force=True, includeUnassigned=False, useLegacyImplementation=True |
| ) |
| atom_name_by_idx = { |
| atom.GetIdx(): atom.GetProp('atom_name') for atom in mol.GetAtoms() |
| } |
| atom_chirality_by_name = {atom_name_by_idx[k]: v for k, v in chiral_centres} |
| return { |
| k: v |
| for k, v in atom_chirality_by_name.items() |
| if any(k[: len(el)].upper() == el for el in _CHIRAL_ELEMENTS) |
| } |
|
|
|
|
| def _chiral_match(mol1: rd_chem.Mol, mol2: rd_chem.Mol) -> bool: |
| """Compares chirality of two Mols. Mol1 can match a subset of mol2.""" |
|
|
| mol1_atom_names = {a.GetProp('atom_name') for a in mol1.GetAtoms()} |
| mol2_atom_names = {a.GetProp('atom_name') for a in mol2.GetAtoms()} |
| if mol1_atom_names != mol2_atom_names: |
| if not mol1_atom_names.issubset(mol2_atom_names): |
| raise ValueError('Mol1 atoms are not a subset of mol2 atoms.') |
|
|
| mol1_chiral_centres = _find_chiral_centres(mol1) |
| mol2_chiral_centres = _find_chiral_centres(mol2) |
| if set(mol1_chiral_centres) != set(mol2_chiral_centres): |
| if not set(mol1_chiral_centres).issubset(mol2_chiral_centres): |
| return False |
| chirality_matches = { |
| centre_atom: chirality1 == mol2_chiral_centres[centre_atom] |
| for centre_atom, chirality1 in mol1_chiral_centres.items() |
| if '?' != mol2_chiral_centres[centre_atom] |
| } |
| return all(chirality_matches.values()) |
|
|
|
|
| def _mol_from_ligand_struc( |
| ligand_struc: structure.Structure, |
| ref_mol: rd_chem.Mol, |
| ) -> rd_chem.Mol | None: |
| """Creates a Mol object from a ligand structure and reference mol.""" |
|
|
| if ligand_struc.num_residues(count_unresolved=True) > 1: |
| raise ValueError('ligand_struc %s has more than one residue.') |
| coords_by_atom_name = dict(zip(ligand_struc.atom_name, ligand_struc.coords)) |
|
|
| ref_mol = rdkit_utils.sanitize_mol( |
| ref_mol, |
| sort_alphabetically=False, |
| remove_hydrogens=True, |
| ) |
|
|
| mol = rd_chem.Mol(ref_mol) |
| mol.RemoveAllConformers() |
|
|
| atom_indices_to_remove = [ |
| a.GetIdx() |
| for a in mol.GetAtoms() |
| if a.GetProp('atom_name') not in coords_by_atom_name |
| ] |
| editable_mol = rd_chem.EditableMol(mol) |
| |
| for atom_idx in atom_indices_to_remove[::-1]: |
| editable_mol.RemoveAtom(atom_idx) |
| mol = editable_mol.GetMol() |
|
|
| conformer = rd_chem.Conformer(mol.GetNumAtoms()) |
| for atom_idx, atom in enumerate(mol.GetAtoms()): |
| atom_name = atom.GetProp('atom_name') |
| coords = coords_by_atom_name[atom_name] |
| conformer.SetAtomPosition(atom_idx, coords.tolist()) |
| mol.AddConformer(conformer) |
| try: |
| rd_chem.AssignStereochemistryFrom3D(mol) |
| except RuntimeError as e: |
| |
| if 'Cannot normalize a zero length vector' in str(e): |
| return None |
| else: |
| raise |
| return mol |
|
|
|
|
| def _maybe_mol_from_ccd(res_name: str) -> rd_chem.Mol | None: |
| """Creates a Mol object from CCD information if res_name is in the CCD.""" |
| ccd = chemical_components.Ccd() |
| ccd_cif = ccd.get(res_name) |
| if not ccd_cif: |
| logging.warning('No ccd information for residue %s.', res_name) |
| return None |
| try: |
| mol = rdkit_utils.mol_from_ccd_cif(ccd_cif, force_parse=False) |
| except rdkit_utils.MolFromMmcifError as e: |
| logging.warning('Failed to create mol from ccd for %s: %s', res_name, e) |
| return None |
| if mol is None: |
| raise ValueError('Failed to create mol from ccd for %s.' % res_name) |
| mol = rdkit_utils.sanitize_mol( |
| mol, |
| sort_alphabetically=False, |
| remove_hydrogens=True, |
| ) |
| return mol |
|
|
|
|
| def compare_chirality( |
| test_struc: structure.Structure, |
| ref_mol_by_chain: Mapping[str, rd_chem.Mol] | None = None, |
| ) -> dict[str, bool]: |
| """Compares chirality of ligands in a structure with reference molecules. |
| |
| We do not enforce that ligand atoms exactly match, only that the ligand atoms |
| and chiral centres are a subset of those in ref mol. |
| |
| Args: |
| test_struc: The structure for whose ligands to match chirality. |
| ref_mol_by_chain: Optional dictionary mapping chain IDs to mol objects with |
| conformers to compare against. If this is not provided, the comparison is |
| to the corresponding ligands in the CCD if the ligand residue name is in |
| the CCD. |
| |
| Returns: |
| Dictionary mapping chain id to whether chirality mismatches the ref mol. |
| Only single residue ligands where reference molecules are available are |
| compared. |
| """ |
| ref_mol_by_chain = ref_mol_by_chain or {} |
| test_struc = test_struc.filter_to_entity_type(ligand=True) |
| name = test_struc.name |
| chiral_match_by_chain_id = {} |
| for chain_id in test_struc.chains: |
| chain_struc = test_struc.filter(chain_id=chain_id) |
| |
| if chain_struc.num_residues(count_unresolved=True) > 1: |
| logging.warning('%s: Chain %s has >1 residues. Skipping.', name, chain_id) |
| continue |
| if chain_id not in ref_mol_by_chain: |
| ref_mol = _maybe_mol_from_ccd(chain_struc.res_name[0]) |
| else: |
| ref_mol = ref_mol_by_chain[chain_id] |
| if ref_mol is None: |
| logging.warning( |
| '%s: Ref mol is None for chain %s. Skipping.', name, chain_id |
| ) |
| continue |
| mol = _mol_from_ligand_struc( |
| ligand_struc=chain_struc, |
| ref_mol=ref_mol, |
| ) |
| if mol is None: |
| logging.warning( |
| '%s: Failed to create mol for chain %s. Skipping.', name, chain_id |
| ) |
| continue |
| chiral_match_by_chain_id[chain_id] = _chiral_match(mol, ref_mol) |
| return chiral_match_by_chain_id |
|
|