Source code for qsarkit.chemistry.standardization._standardizer
"""Molecular standardization pipeline."""
from __future__ import annotations
from typing import Any, List, Optional
from qsarkit.base import InvalidMoleculeError, MoleculeToMoleculeTransformer
from qsarkit.base.exceptions import RDKIT_MOLECULE_ERRORS
[docs]
class MolecularStandardizer(MoleculeToMoleculeTransformer):
"""Standardize a batch of molecules into a canonical, model-ready form.
Applies, in order: sanitization, salt/solvent removal (keep the largest
organic fragment), charge neutralization, tautomer normalization,
stereochemistry handling, and explicit-hydrogen normalization. This
mirrors the "structure normalization" stage expected before any
downstream QSAR/curation step (OECD QSAR guidance recommends
normalized, unambiguous structures prior to model building).
Parameters
----------
remove_salts : bool, default True
Keep only the largest organic fragment (strips counter-ions,
solvates, hydrates).
neutralize : bool, default True
Neutralize charges where a neutral tautomer/protomer exists
(e.g. carboxylates, ammoniums), leaving permanent charges
(e.g. quaternary ammonium) untouched.
normalize_tautomers : bool, default True
Canonicalize to the RDKit-preferred tautomer using the
Sybyl/MolVS-derived tautomer scoring rules.
handle_stereochemistry : str, default "retain"
One of ``"retain"`` (keep stereo as parsed, but reassign
stereocenters from the 2D/3D structure), or ``"remove"`` (strip
all stereochemistry, useful when comparing 2D scaffolds).
normalize_hydrogens : bool, default True
Strip explicit hydrogens except where required for correct
valence/stereo perception (RDKit's implicit-H convention).
on_error : str, default "none"
``"none"`` -> failed molecules become ``None`` in the output list
(positional alignment preserved); ``"raise"`` -> raise
``InvalidMoleculeError`` on the first failure.
Examples
--------
Sodium acetylsalicylate loses its counter-ion and its charge:
>>> from rdkit import Chem
>>> from qsarkit.chemistry import MolecularStandardizer
>>> standardizer = MolecularStandardizer()
>>> mol = Chem.MolFromSmiles("CC(=O)Oc1ccccc1C(=O)[O-].[Na+]")
>>> Chem.MolToSmiles(standardizer.transform([mol])[0])
'CC(=O)Oc1ccccc1C(=O)O'
Failures do not shift the batch. An unparseable record becomes
``None`` in place, so a parallel array of activities stays aligned:
>>> out = standardizer.transform([mol, None])
>>> [m is None for m in out]
[False, True]
``handle_stereochemistry="remove"`` strips stereocentres, which is
what you want when comparing 2D scaffolds rather than modelling
enantiomer-specific activity:
>>> flat = MolecularStandardizer(handle_stereochemistry="remove")
>>> Chem.MolToSmiles(flat.transform([Chem.MolFromSmiles("C[C@H](N)C(=O)O")])[0])
'CC(N)C(=O)O'
References
----------
- Sitzmann et al. (2010). "Tautomerism in Large Databases." J. Comput.
Aided Mol. Des., 24, 521-551. https://doi.org/10.1007/s10822-010-9346-4
- RDKit MolStandardize documentation:
https://www.rdkit.org/docs/source/rdkit.Chem.MolStandardize.html
- OECD (2007). "Guidance Document on the Validation of (Quantitative)
Structure-Activity Relationship [(Q)SAR] Models," ENV/JM/MONO(2007)2.
https://doi.org/10.1787/9789264085442-en
"""
def __init__(
self,
remove_salts: bool = True,
neutralize: bool = True,
normalize_tautomers: bool = True,
handle_stereochemistry: str = "retain",
normalize_hydrogens: bool = True,
on_error: str = "none",
):
self.remove_salts = remove_salts
self.neutralize = neutralize
self.normalize_tautomers = normalize_tautomers
self.handle_stereochemistry = handle_stereochemistry
self.normalize_hydrogens = normalize_hydrogens
self.on_error = on_error
def _standardize_one(self, mol: Any) -> Optional[Any]:
from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize
try:
Chem.SanitizeMol(mol)
if self.remove_salts:
remover = rdMolStandardize.LargestFragmentChooser()
mol = remover.choose(mol)
if self.neutralize:
uncharger = rdMolStandardize.Uncharger()
mol = uncharger.uncharge(mol)
if self.normalize_tautomers:
enumerator = rdMolStandardize.TautomerEnumerator()
# By default RDKit strips sp3 stereo at atoms it considers
# tautomeric, which would silently destroy genuine
# stereocentres (e.g. the alpha carbon of an amino acid).
# Only allow that when the caller asked to drop stereo.
enumerator.SetRemoveSp3Stereo(self.handle_stereochemistry == "remove")
mol = enumerator.Canonicalize(mol)
if self.handle_stereochemistry == "remove":
Chem.RemoveStereochemistry(mol)
elif self.handle_stereochemistry == "retain":
Chem.AssignStereochemistry(mol, cleanIt=True, force=True)
else:
raise ValueError(
"handle_stereochemistry must be 'retain' or 'remove', "
f"got {self.handle_stereochemistry!r}"
)
if self.normalize_hydrogens:
mol = Chem.RemoveHs(mol)
Chem.SanitizeMol(mol)
return mol
except RDKIT_MOLECULE_ERRORS as exc:
if self.on_error == "raise":
raise InvalidMoleculeError(f"Standardization failed: {exc}") from exc
return None
def _transform(self, mols: List[Any]) -> List[Any]:
return [None if m is None else self._standardize_one(m) for m in mols]