Source code for bonafide.features.spiro_bridgehead
"""Features for spiro and bridgehead atoms."""
from typing import Set, Tuple
from bonafide.utils.base_featurizer import BaseFeaturizer
[docs]
class _Bonafide2DAtomIsSpiroBridgehead(BaseFeaturizer):
"""Parent feature factory for the 2D atom features "is_spiro" and "is_bridgehead"."""
def __init__(self) -> None:
self.extraction_mode = "multi"
super().__init__()
[docs]
def calculate(self) -> None:
"""Calculate the ``bonafide2D-atom-is_spiro`` and ``bonafide2D-atom-is_bridgehead``
feature.
"""
spiro_atoms, bridgehead_atoms = self._get_spiro_bridgehead()
for atom in self.mol.GetAtoms():
atom_idx = atom.GetIdx()
_is_spiro = False
_is_bridgehead = False
if atom_idx in spiro_atoms:
_is_spiro = True
if atom_idx in bridgehead_atoms:
_is_bridgehead = True
self.results[atom_idx] = {
"bonafide2D-atom-is_spiro": _is_spiro,
"bonafide2D-atom-is_bridgehead": _is_bridgehead,
}
[docs]
def _get_spiro_bridgehead(self) -> Tuple[Set[int], Set[int]]:
"""Get the atom indices of spiro and bridgehead atoms in a molecule.
This is done based on the following heuristics:
1. If two rings share exactly one atom, then that atom is a spiro atom.
2. If two rings share more than one atom (intersecting ring), then the shared atoms are
classified as bridgehead atoms if a given atom of the shared atoms has at least 3
explicit neighbors that are part of the intersecting rings under consideration.
Returns
-------
Tuple[Set[int], Set[int]]
A tuple containing two sets: the atom indices of spiro and bridgehead atoms.
"""
# Get the RDKit ring info analysis, either from the cache or by calculating it
# Only cache AtomRings() instead of entire GetRingInfo() object to avoid potential memory
# errors
_feature_name = "rdkit2d-global-atom_ring_info"
if _feature_name not in self.global_feature_cache[self.conformer_idx]:
ring_info = self.mol.GetRingInfo().AtomRings()
self.global_feature_cache[self.conformer_idx][_feature_name] = ring_info
else:
ring_info = self.global_feature_cache[self.conformer_idx][_feature_name]
ring_atoms = [set(ring) for ring in ring_info]
# Identify spiro and bridgehead atoms simultaneously
spiro_atoms = set()
bridgehead_atoms = set()
for ring_idx1, ring1 in enumerate(ring_atoms):
for ring_idx2 in range(ring_idx1 + 1, len(ring_atoms)):
ring2 = ring_atoms[ring_idx2]
intersection = ring1.intersection(ring2)
# No shared atoms between these rings -> continue
if len(intersection) == 0:
continue
# Exactly one shared atom -> spiro atom
elif len(intersection) == 1:
spiro_atoms.update(intersection)
# More than one shared atom -> bridgehead atoms
else:
all_ring_indices = ring1.union(ring2)
# Check that the shared atoms have at least 3 intersecting ring atom neighbors
for atom_idx in intersection:
neighbor_indices = [
a.GetIdx() for a in self.mol.GetAtomWithIdx(atom_idx).GetNeighbors()
]
in_all_ring_indices = [n for n in neighbor_indices if n in all_ring_indices]
if len(in_all_ring_indices) >= 3:
bridgehead_atoms.add(atom_idx)
return spiro_atoms, bridgehead_atoms
[docs]
class Bonafide2DAtomIsSpiro(_Bonafide2DAtomIsSpiroBridgehead):
"""Feature factory for the 2D atom feature "is_spiro", implemented within this package.
The index of this feature is 29 (see the ``list_atom_features()`` and
``list_bond_features()`` method). The corresponding configuration settings can be found
under "bonafide.misc" in the _feature_config.toml file.
"""
def __init__(self) -> None:
super().__init__()
# Feature calculation is done in the parent class
[docs]
class Bonafide2DAtomIsBridgehead(_Bonafide2DAtomIsSpiroBridgehead):
"""Feature factory for the 2D atom feature "is_bridgehead", implemented within this package.
The index of this feature is 28 (see the ``list_atom_features()`` and
``list_bond_features()`` method). The corresponding configuration settings can be found
under "bonafide.misc" in the _feature_config.toml file.
"""
def __init__(self) -> None:
super().__init__()
# Feature calculation is done in the parent class