Source code for qsarkit.chemistry.glycans._descriptors
"""Glycan-content descriptors."""
from __future__ import annotations
from collections import Counter
from typing import TYPE_CHECKING, Any, List, Optional, Set
if TYPE_CHECKING: # pragma: no cover
import pandas as pd
from qsarkit.base import MoleculeTransformer
from qsarkit.chemistry.glycans._detector import GlycanDetector, GlycanMatch
def _linkage_type(mol: Any, match: GlycanMatch, glycan_atoms: Set[int]) -> str:
"""Classify a sugar ring's linkage to the rest of the molecule as O/C/none."""
for idx in match.ring_atoms:
atom = mol.GetAtomWithIdx(idx)
for nbr in atom.GetNeighbors():
if nbr.GetIdx() in glycan_atoms:
continue
return "O-glycoside" if atom.GetSymbol() == "O" or nbr.GetSymbol() == "O" else "C-glycoside"
return "terminal"
[docs]
class GlycanDescriptors(MoleculeTransformer):
"""Compute glycan-content descriptors for a batch of molecules.
The three columns produced are:
``sugar_count``
Number of detected sugar rings (see :class:`GlycanDetector`).
``glycan_fraction``
Fraction of molecular weight contributed by glycan atoms.
``glycosylation_pattern``
Comma-separated summary such as ``"6-ring:O-glycoside x2"``
describing ring size and linkage type of each detected sugar.
Parameters
----------
detector : GlycanDetector, optional
Notes
-----
Unlike the transformers in :mod:`qsarkit.representation`, this one
returns a :class:`pandas.DataFrame` rather than a NumPy array,
because ``glycosylation_pattern`` is a string. Forcing it into an
array would give the whole block ``object`` dtype and lose the
numeric columns' types.
Examples
--------
>>> from rdkit import Chem
>>> from qsarkit.chemistry import GlycanDescriptors
>>> q3g = Chem.MolFromSmiles(
... "OC[C@H]1O[C@@H](Oc2c(-c3ccc(O)c(O)c3)oc3cc(O)cc(O)c3c2=O)"
... "[C@H](O)[C@@H](O)[C@@H]1O")
>>> df = GlycanDescriptors().transform([q3g, Chem.MolFromSmiles("CCO")])
>>> list(df.columns)
['sugar_count', 'glycan_fraction', 'glycosylation_pattern']
>>> df["sugar_count"].tolist()
[1, 0]
>>> df["glycosylation_pattern"][0]
'6-ring:terminal'
References
----------
- Fischer et al. (2020). "The Sugar Removal Utility (SRU)." Molecules,
25(8), 1988. https://doi.org/10.3390/molecules25081988
"""
def __init__(self, detector: Optional[GlycanDetector] = None):
# Stored exactly as given, per the scikit-learn convention: an
# __init__ that substitutes a default makes get_params() report
# something the caller did not pass, and clone() then produces a
# non-identical estimator. The default is resolved in _detector().
self.detector = detector
def _detector(self) -> GlycanDetector:
"""The configured detector, or a default one."""
return self.detector if self.detector is not None else GlycanDetector()
def _describe_one(self, mol: Any) -> dict:
detector = self._detector()
matches = detector.find_glycans(mol)
glycan_atoms = set()
for m in matches:
glycan_atoms |= m.all_atoms
det = detector.detect(mol)
patterns = Counter(
f"{m.ring_size}-ring:{_linkage_type(mol, m, glycan_atoms)}" for m in matches
)
pattern_str = ", ".join(
f"{p} x{n}" if n > 1 else p for p, n in sorted(patterns.items())
)
return {
"sugar_count": det["num_sugar_residues"],
"glycan_fraction": det["glycan_mw_fraction"],
"glycosylation_pattern": pattern_str,
}
def _transform(self, mols: List[Any]) -> "pd.DataFrame":
import pandas as pd
rows = [self._describe_one(m) if m is not None else None for m in mols]
return pd.DataFrame(
rows, columns=["sugar_count", "glycan_fraction", "glycosylation_pattern"]
)