Skip to content

Commit 73342c7

Browse files
authored
Merge pull request #542 from OpenBioSim/fix_ring_break_merge
2 parents bb4cc53 + ab5eb3b commit 73342c7

6 files changed

Lines changed: 258 additions & 50 deletions

File tree

src/BioSimSpace/Align/_align.py

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
__email__ = "lester.hedges@gmail.com"
2626

2727
__all__ = [
28+
"defaultMCSOptions",
2829
"generateNetwork",
2930
"matchAtoms",
3031
"viewMapping",
@@ -702,6 +703,29 @@ def generateNetwork(
702703
return edges, scores
703704

704705

706+
def defaultMCSOptions():
707+
"""
708+
Return the default options used for the RDKit maximum common substructure
709+
search. These can be overridden using the 'mcs_kwargs' argument of
710+
:class:`matchAtoms <BioSimSpace.Align.matchAtoms>`.
711+
712+
Returns
713+
-------
714+
715+
options : dict
716+
The default RDKit MCS options.
717+
"""
718+
return {
719+
"atomCompare": _rdFMCS.AtomCompare.CompareAny,
720+
"bondCompare": _rdFMCS.BondCompare.CompareAny,
721+
"completeRingsOnly": True,
722+
"ringMatchesRingOnly": True,
723+
"matchChiralTag": False,
724+
"matchValences": False,
725+
"maximizeBonds": False,
726+
}
727+
728+
705729
def matchAtoms(
706730
molecule0,
707731
molecule1,
@@ -719,6 +743,7 @@ def matchAtoms(
719743
prune_atom_types=False,
720744
property_map0={},
721745
property_map1={},
746+
mcs_kwargs={},
722747
):
723748
"""
724749
Find mappings between atom indices in molecule0 to those in molecule1.
@@ -775,6 +800,11 @@ def matchAtoms(
775800
option is only relevant to MCS performed using RDKit and will be
776801
ignored when falling back on Sire.
777802
803+
mcs_kwargs : dict
804+
A dictionary of keyword arguments used to override the defaults
805+
passed to the RDKit MCS search. This option is only relevant to MCS
806+
performed using RDKit and will be ignored when falling back on Sire.
807+
778808
roi : list
779809
The region of interest to match.
780810
Consists of a list of ROI residue indices.
@@ -881,6 +911,7 @@ def matchAtoms(
881911
prune_atom_types=prune_atom_types,
882912
property_map0=property_map0,
883913
property_map1=property_map1,
914+
mcs_kwargs=mcs_kwargs,
884915
)
885916
else:
886917
return _roiMatch(
@@ -912,6 +943,7 @@ def _matchAtoms(
912943
prune_atom_types=False,
913944
property_map0={},
914945
property_map1={},
946+
mcs_kwargs={},
915947
):
916948
import sys as _sys
917949

@@ -975,6 +1007,9 @@ def _matchAtoms(
9751007
if not isinstance(complete_rings_only, bool):
9761008
raise TypeError("'complete_rings_only' must be of type 'bool'")
9771009

1010+
if not isinstance(mcs_kwargs, dict):
1011+
raise TypeError("'mcs_kwargs' must be of type 'dict'")
1012+
9781013
if type(max_scoring_matches) is not int:
9791014
raise TypeError("'max_scoring_matches' must be of type 'int'")
9801015

@@ -1012,24 +1047,21 @@ def _matchAtoms(
10121047
_Convert.toRDKit(mol1, property_map=property_map1),
10131048
]
10141049

1050+
# Default MCS options, overridden by anything in 'mcs_kwargs'. The
1051+
# timeout is applied last so that it can't be overridden.
1052+
mcs_options = defaultMCSOptions()
1053+
mcs_options["completeRingsOnly"] = complete_rings_only
1054+
mcs_options.update(mcs_kwargs)
1055+
mcs_options["timeout"] = timeout
1056+
10151057
# Generate the MCS match.
1016-
mcs = _rdFMCS.FindMCS(
1017-
mols,
1018-
atomCompare=_rdFMCS.AtomCompare.CompareAny,
1019-
bondCompare=_rdFMCS.BondCompare.CompareAny,
1020-
completeRingsOnly=complete_rings_only,
1021-
ringMatchesRingOnly=True,
1022-
matchChiralTag=False,
1023-
matchValences=False,
1024-
maximizeBonds=False,
1025-
timeout=timeout,
1026-
)
1058+
mcs = _rdFMCS.FindMCS(mols, **mcs_options)
10271059

10281060
# Get the common substructure as a SMARTS string.
10291061
mcs_smarts = _Chem.MolFromSmarts(mcs.smartsString)
10301062

1031-
except:
1032-
raise RuntimeError("RDKit MCS mapping failed!")
1063+
except Exception as e:
1064+
raise RuntimeError(f"RDKit MCS mapping failed: {e}")
10331065

10341066
# Score the mappings and return them in sorted order (best to worst).
10351067
mappings, scores = _score_rdkit_mappings(
@@ -1066,6 +1098,9 @@ def _matchAtoms(
10661098
"Using Sire MCS. Ignoring unsupported 'complete_rings_only' option!"
10671099
)
10681100

1101+
if mcs_kwargs:
1102+
_warnings.warn("Using Sire MCS. Ignoring unsupported 'mcs_kwargs' options!")
1103+
10691104
# Convert timeout to a Sire Unit.
10701105
timeout = timeout * _SireUnits.second
10711106

@@ -2079,6 +2114,7 @@ def merge(
20792114
roi=None,
20802115
property_map0={},
20812116
property_map1={},
2117+
mcs_kwargs={},
20822118
**kwargs,
20832119
):
20842120
"""
@@ -2130,6 +2166,11 @@ def merge(
21302166
A dictionary that maps "properties" in molecule1 to their user
21312167
defined values.
21322168
2169+
mcs_kwargs : dict
2170+
A dictionary of keyword arguments used to override the defaults
2171+
passed to the RDKit MCS search. This is only used when 'mapping'
2172+
is None, i.e. when a mapping is autogenerated.
2173+
21332174
Returns
21342175
-------
21352176
@@ -2208,6 +2249,7 @@ def merge(
22082249
molecule1,
22092250
property_map0=property_map0,
22102251
property_map1=property_map1,
2252+
mcs_kwargs=mcs_kwargs,
22112253
)
22122254
molecule0 = rmsdAlign(molecule0, molecule1, mapping)
22132255

src/BioSimSpace/Align/_merge.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,10 +1589,10 @@ def _check_ring(conn0, conn1, idx0, idy0, idx1, idy1, max_path=50, max_ring_size
15891589
# Supplementary check for rings larger than max_path: find_paths may only
15901590
# find the direct-bond path and miss the long way around the ring, giving
15911591
# n=1 instead of n≥2. Sire's in_ring has no path-length limit and
1592-
# correctly identifies ring membership in macrocycles.
1593-
if (conn0.in_ring(idx0) and conn0.in_ring(idy0)) != (
1594-
conn1.in_ring(idx1) and conn1.in_ring(idy1)
1595-
):
1592+
# correctly identifies ring membership in macrocycles. The two-atom
1593+
# overload asks whether the atoms share a ring, so a ring built entirely
1594+
# from dummy atoms, which breaks no bond between mapped atoms, is ignored.
1595+
if conn0.in_ring(idx0, idy0) != conn1.in_ring(idx1, idy1):
15961596
return True, False
15971597

15981598
# A direct bond was replaced by a ring path (or vice versa), leaving the

src/BioSimSpace/Sandpit/Exscientia/Align/_align.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
__email__ = "lester.hedges@gmail.com"
2626

2727
__all__ = [
28+
"defaultMCSOptions",
2829
"generateNetwork",
2930
"matchAtoms",
3031
"viewMapping",
@@ -702,6 +703,29 @@ def generateNetwork(
702703
return edges, scores
703704

704705

706+
def defaultMCSOptions():
707+
"""
708+
Return the default options used for the RDKit maximum common substructure
709+
search. These can be overridden using the 'mcs_kwargs' argument of
710+
:class:`matchAtoms <BioSimSpace.Align.matchAtoms>`.
711+
712+
Returns
713+
-------
714+
715+
options : dict
716+
The default RDKit MCS options.
717+
"""
718+
return {
719+
"atomCompare": _rdFMCS.AtomCompare.CompareAny,
720+
"bondCompare": _rdFMCS.BondCompare.CompareAny,
721+
"completeRingsOnly": True,
722+
"ringMatchesRingOnly": True,
723+
"matchChiralTag": False,
724+
"matchValences": False,
725+
"maximizeBonds": False,
726+
}
727+
728+
705729
def matchAtoms(
706730
molecule0,
707731
molecule1,
@@ -717,6 +741,7 @@ def matchAtoms(
717741
max_scoring_matches=1000,
718742
property_map0={},
719743
property_map1={},
744+
mcs_kwargs={},
720745
):
721746
"""
722747
Find mappings between atom indices in molecule0 to those in molecule1.
@@ -770,6 +795,11 @@ def matchAtoms(
770795
option is only relevant to MCS performed using RDKit and will be
771796
ignored when falling back on Sire.
772797
798+
mcs_kwargs : dict
799+
A dictionary of keyword arguments used to override the defaults
800+
passed to the RDKit MCS search. This option is only relevant to MCS
801+
performed using RDKit and will be ignored when falling back on Sire.
802+
773803
prune_perturbed_constraints : bool
774804
Whether to remove hydrogen atoms that are perturbed to heavy atoms
775805
from the mapping. This is True for AMBER by default and False for
@@ -930,24 +960,21 @@ def matchAtoms(
930960
_Convert.toRDKit(molecule1, property_map=property_map1),
931961
]
932962

963+
# Default MCS options, overridden by anything in 'mcs_kwargs'. The
964+
# timeout is applied last so that it can't be overridden.
965+
mcs_options = defaultMCSOptions()
966+
mcs_options["completeRingsOnly"] = complete_rings_only
967+
mcs_options.update(mcs_kwargs)
968+
mcs_options["timeout"] = timeout
969+
933970
# Generate the MCS match.
934-
mcs = _rdFMCS.FindMCS(
935-
mols,
936-
atomCompare=_rdFMCS.AtomCompare.CompareAny,
937-
bondCompare=_rdFMCS.BondCompare.CompareAny,
938-
completeRingsOnly=complete_rings_only,
939-
ringMatchesRingOnly=True,
940-
matchChiralTag=False,
941-
matchValences=False,
942-
maximizeBonds=False,
943-
timeout=timeout,
944-
)
971+
mcs = _rdFMCS.FindMCS(mols, **mcs_options)
945972

946973
# Get the common substructure as a SMARTS string.
947974
mcs_smarts = _Chem.MolFromSmarts(mcs.smartsString)
948975

949-
except:
950-
raise RuntimeError("RDKit MCS mapping failed!")
976+
except Exception as e:
977+
raise RuntimeError(f"RDKit MCS mapping failed: {e}")
951978

952979
# Score the mappings and return them in sorted order (best to worst).
953980
mappings, scores = _score_rdkit_mappings(
@@ -984,6 +1011,9 @@ def matchAtoms(
9841011
"Using Sire MCS. Ignoring unsupported 'complete_rings_only' option!"
9851012
)
9861013

1014+
if mcs_kwargs:
1015+
_warnings.warn("Using Sire MCS. Ignoring unsupported 'mcs_kwargs' options!")
1016+
9871017
# Convert timeout to a Sire Unit.
9881018
timeout = timeout * _SireUnits.second
9891019

@@ -1370,6 +1400,7 @@ def merge(
13701400
roi=None,
13711401
property_map0={},
13721402
property_map1={},
1403+
mcs_kwargs={},
13731404
**kwargs,
13741405
):
13751406
"""
@@ -1417,6 +1448,11 @@ def merge(
14171448
A dictionary that maps "properties" in molecule1 to their user
14181449
defined values.
14191450
1451+
mcs_kwargs : dict
1452+
A dictionary of keyword arguments used to override the defaults
1453+
passed to the RDKit MCS search. This is only used when 'mapping'
1454+
is None, i.e. when a mapping is autogenerated.
1455+
14201456
Returns
14211457
-------
14221458
@@ -1488,6 +1524,7 @@ def merge(
14881524
molecule1,
14891525
property_map0=property_map0,
14901526
property_map1=property_map1,
1527+
mcs_kwargs=mcs_kwargs,
14911528
)
14921529
molecule0 = rmsdAlign(molecule0, molecule1, mapping)
14931530

src/BioSimSpace/Sandpit/Exscientia/Align/_merge.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,10 +1455,10 @@ def _check_ring(conn0, conn1, idx0, idy0, idx1, idy1, max_path=50, max_ring_size
14551455
# Supplementary check for rings larger than max_path: find_paths may only
14561456
# find the direct-bond path and miss the long way around the ring, giving
14571457
# n=1 instead of n≥2. Sire's in_ring has no path-length limit and
1458-
# correctly identifies ring membership in macrocycles.
1459-
if (conn0.in_ring(idx0) and conn0.in_ring(idy0)) != (
1460-
conn1.in_ring(idx1) and conn1.in_ring(idy1)
1461-
):
1458+
# correctly identifies ring membership in macrocycles. The two-atom
1459+
# overload asks whether the atoms share a ring, so a ring built entirely
1460+
# from dummy atoms, which breaks no bond between mapped atoms, is ignored.
1461+
if conn0.in_ring(idx0, idy0) != conn1.in_ring(idx1, idy1):
14621462
return True, False
14631463

14641464
# A direct bond was replaced by a ring path (or vice versa), leaving the

0 commit comments

Comments
 (0)