Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ring hybridization filter #36

Merged
merged 13 commits into from
Mar 29, 2024
1 change: 1 addition & 0 deletions src/kartograf/filters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
filter_ringsize_changes,
filter_ringbreak_changes,
filter_whole_rings_only,
filter_hybridization_rings,
)
71 changes: 68 additions & 3 deletions src/kartograf/filters/ring_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


def filter_ringsize_changes(
molA: Chem.Mol, molB: Chem.Mol, mapping: dict[int, int]
molA: Chem.Mol, molB: Chem.Mol, mapping: dict[int, int]
) -> dict[int, int]:
"""Prevents mutating the size of rings in the mapping"""
riA = molA.GetRingInfo()
Expand Down Expand Up @@ -41,7 +41,7 @@ def filter_ringsize_changes(


def filter_ringbreak_changes(
molA: Chem.Mol, molB: Chem.Mol, mapping: dict[int, int]
molA: Chem.Mol, molB: Chem.Mol, mapping: dict[int, int]
) -> dict[int, int]:
"""Prevent any ring cleaving transformations in the mapping

Expand All @@ -61,7 +61,7 @@ def filter_ringbreak_changes(


def filter_whole_rings_only(
molA: Chem.Mol, molB: Chem.Mol, mapping: dict[int, int]
molA: Chem.Mol, molB: Chem.Mol, mapping: dict[int, int]
) -> dict[int, int]:
"""Ensure that any mapped rings are wholly mapped"""
proposed_mapping = {**mapping}
Expand Down Expand Up @@ -97,3 +97,68 @@ def filter_whole_rings_only(
proposed_mapping = {v: k for k, v in filtered_mapping.items()}

return proposed_mapping


def filter_hybridization_rings(
molA: Chem.Mol, molB: Chem.Mol, mapping: dict[int, int]
) -> dict[int, int]:
"""Ensure that any mapped rings are either both aromatic or aliphatic

e.g. this filter would unmap hexane to benzene type transformations
"""

def get_atom_ring_hybridization_map(rdmol: Chem.Mol) -> dict[int, bool]:
"""
For each atom, determines information about ring hybridization

Parameters
----------
rdmol: Chem.Mol

Returns
-------
dict[int, bool]:
returns a dict, that maps each atom's index to if it is always in aromatic rings
(If True this atom exists entirely in aromatic rings, if False it is
not entirely aromatic and therefore not necessarily sterically restraint
by a pi-orbital-system.)
"""
riInf = rdmol.GetRingInfo()

# get_ring_hybridization
# for each ring, the ring is aromatic if all atoms within are aromatic
# maps ring index to aromaticity as bool
is_ring_aromatic = {}
for i, ring in enumerate(riInf.BondRings()):
is_ring_aromatic[i] = all(
rdmol.GetBondWithIdx(atomI).GetIsAromatic()
for atomI in ring)

# first iterate over all rings and determine if they are aromatic
# map atoms to ring aromaticities
atom_ring_map = defaultdict(list)
for ri, r in enumerate(riInf.AtomRings()):
for a in r:
atom_ring_map[a].append(is_ring_aromatic[ri])

# then with all rings traversed, crush this information down to a single bool per atom
# maps atom index to all ring aromaticity
atom_aromatic = {}
for a, v in atom_ring_map.items():
atom_aromatic[a] = all(v)

return atom_aromatic

atomA_ring_hyb_map = get_atom_ring_hybridization_map(molA)
atomB_ring_hyb_map = get_atom_ring_hybridization_map(molB)

# Filtering Mapping
filtered_mapping = {}
for ai, aj in mapping.items():
ai_only_arom_sys = atomA_ring_hyb_map[ai]
aj_only_arom_sys = atomB_ring_hyb_map[aj]

if ai_only_arom_sys == aj_only_arom_sys:
filtered_mapping[ai] = aj

return filtered_mapping
34 changes: 34 additions & 0 deletions src/kartograf/tests/test_ring_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,37 @@ def test_whole_rings_safe():
newmapping = filters.filter_whole_rings_only(m1, m2, mapping)

assert newmapping == mapping


@pytest.mark.parametrize('molA,molB,initial_mapping,expected_mapping', [
(Chem.MolFromSmiles("C1CCCC2C1CCCC2"), # 2rings: aliphatic/aliphatic
Chem.MolFromSmiles("C1CCCC2C1CCCC2"), # 2rings: aliphatic/aliphatic
{i: i for i in range(10)}, # initial_mapping
{i: i for i in range(10)}), # expected: map all atoms

(Chem.MolFromSmiles("c1cccc2c1cccc2"), # 2rings: aromatic/aromatic
Chem.MolFromSmiles("c1cccc2c1cccc2"), # 2rings: aromatic/aromatic
{i: i for i in range(10)}, # initial_mapping
{i: i for i in range(10)}), # expected: map all atoms

(Chem.MolFromSmiles("C1CCCc2c1cccc2"), # 2rings: aliphatic/aromatic
Chem.MolFromSmiles("C1CCCC2C1CCCC2"), # 2rings: aliphatic/aliphatic
{i: i for i in range(10)}, # initial_mapping
{i: i for i in range(6)}), # expected: map aliphatic rings onto each other

(Chem.MolFromSmiles("C1CCCC2C1cccc2"), # 2rings: aliphatic/aromatic
Chem.MolFromSmiles("C1CCCC2C1CCCC2"), # 2rings: aliphatic/aliphatic
{i: i for i in range(10)}, # initial_mapping
{i: i for i in range(10)}), # expected: map all atoms

(Chem.MolFromSmiles("c1cccc2c1CCCC2"), # 2rings: aromatic/aliphatic
Chem.MolFromSmiles("C1CCCC2C1CCCC2"), # 2rings: aliphatic/aliphatic
{i: i for i in range(10)}, # initial_mapping
{i: i for i in range(4, 10)}), # expected: map the aliphatic rings ontoeach other
])
RiesBen marked this conversation as resolved.
Show resolved Hide resolved
def test_ring_hybridization(molA, molB, initial_mapping, expected_mapping):
newmapping = filters.filter_hybridization_rings(molA, molB, initial_mapping)

assert newmapping != {}
assert len(newmapping) == len(expected_mapping)
assert newmapping == expected_mapping
Loading