Skip to content

Commit 28a1c27

Browse files
committed
Switch to simplified patchIntrascale wrapper function.
1 parent d074644 commit 28a1c27

5 files changed

Lines changed: 76 additions & 57 deletions

File tree

src/BioSimSpace/Align/_align.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,6 +2035,7 @@ def merge(
20352035
roi=None,
20362036
property_map0={},
20372037
property_map1={},
2038+
**kwargs,
20382039
):
20392040
"""
20402041
Create a merged molecule from 'molecule0' and 'molecule1' based on the
@@ -2181,6 +2182,7 @@ def merge(
21812182
roi=roi,
21822183
property_map0=property_map0,
21832184
property_map1=property_map1,
2185+
**kwargs,
21842186
)
21852187

21862188

src/BioSimSpace/Align/_merge.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def merge(
3838
roi=None,
3939
property_map0={},
4040
property_map1={},
41+
**kwargs,
4142
):
4243
"""
4344
Merge this molecule with 'other'.
@@ -1313,34 +1314,34 @@ def merge(
13131314
else:
13141315
edit_mol.set_property("connectivity0", conn0)
13151316
edit_mol.set_property("connectivity1", conn1)
1316-
# Merge the intrascale properties of the two molecules.
1317+
# Build the intrascale matrices from the per-state connectivity.
13171318
ff = molecule0.property(ff0)
13181319
sf14 = _SireMM.CLJScaleFactor(
13191320
ff.electrostatic14_scale_factor(), ff.vdw14_scale_factor()
13201321
)
1321-
if roi is not None:
1322-
# For ROI protein merges, build the intrascale matrices directly from
1323-
# the per-state connectivity, bypassing mergeIntrascale. Protein
1324-
# mutations involve no ring-breaking and no GLYCAM-style per-pair
1325-
# overrides, so the overrideIntrascale step in mergeIntrascale is a
1326-
# no-op. Bypassing it avoids a Windows-specific performance issue
1327-
# where the O(n²) override loop is very slow for large proteins.
1328-
# TODO: investigate the root cause and remove this workaround.
1329-
merged_intrascale = [
1330-
_SireMM.CLJNBPairs(conn0, sf14),
1331-
_SireMM.CLJNBPairs(conn1, sf14),
1332-
]
1333-
else:
1334-
merged_intrascale = _SireIO.mergeIntrascale(
1322+
intra0 = _SireMM.CLJNBPairs(conn0, sf14)
1323+
intra1 = _SireMM.CLJNBPairs(conn1, sf14)
1324+
1325+
# For non-ROI merges, patch with any non-default per-pair scale factors
1326+
# from the individual molecule intrascales (e.g. GLYCAM funct=2 overrides).
1327+
# This step can be skipped via apply_scale_factors=False when the caller
1328+
# knows no non-default scale factors are present.
1329+
if "apply_scale_factors" in kwargs:
1330+
if not isinstance(kwargs["apply_scale_factors"], bool):
1331+
raise TypeError("'apply_scale_factors' must be of type 'bool'")
1332+
1333+
if kwargs.get("apply_scale_factors", roi is None):
1334+
intra0, intra1 = _SireIO.patchIntrascale(
13351335
molecule0.property("intrascale"),
13361336
molecule1.property("intrascale"),
1337-
conn0,
1338-
conn1,
1339-
sf14,
1337+
intra0,
1338+
intra1,
13401339
mol0_merged_mapping,
13411340
mol1_merged_mapping,
13421341
)
13431342

1343+
merged_intrascale = [intra0, intra1]
1344+
13441345
# Store the two molecular components.
13451346
edit_mol.set_property("molecule0", molecule0)
13461347
edit_mol.set_property("molecule1", molecule1)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,7 @@ def merge(
13701370
roi=None,
13711371
property_map0={},
13721372
property_map1={},
1373+
**kwargs,
13731374
):
13741375
"""
13751376
Create a merged molecule from 'molecule0' and 'molecule1' based on the
@@ -1504,6 +1505,7 @@ def merge(
15041505
roi=roi,
15051506
property_map0=property_map0,
15061507
property_map1=property_map1,
1508+
**kwargs,
15071509
)
15081510

15091511

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

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def merge(
3737
roi=None,
3838
property_map0={},
3939
property_map1={},
40+
**kwargs,
4041
):
4142
"""
4243
Merge this molecule with 'other'.
@@ -1306,30 +1307,30 @@ def merge(
13061307
ff.electrostatic14_scale_factor(), ff.vdw14_scale_factor()
13071308
)
13081309

1309-
# Merge the intrascale properties of the two molecules.
1310-
if roi is not None:
1311-
# For ROI protein merges, build the intrascale matrices directly from
1312-
# the per-state connectivity, bypassing mergeIntrascale. Protein
1313-
# mutations involve no ring-breaking and no GLYCAM-style per-pair
1314-
# overrides, so the overrideIntrascale step in mergeIntrascale is a
1315-
# no-op. Bypassing it avoids a Windows-specific performance issue
1316-
# where the O(n²) override loop is very slow for large proteins.
1317-
# TODO: investigate the root cause and remove this workaround.
1318-
merged_intrascale = [
1319-
_SireMM.CLJNBPairs(conn0, sf14),
1320-
_SireMM.CLJNBPairs(conn1, sf14),
1321-
]
1322-
else:
1323-
merged_intrascale = _SireIO.mergeIntrascale(
1310+
# Build the intrascale matrices from the per-state connectivity.
1311+
intra0 = _SireMM.CLJNBPairs(conn0, sf14)
1312+
intra1 = _SireMM.CLJNBPairs(conn1, sf14)
1313+
1314+
# For non-ROI merges, patch with any non-default per-pair scale factors
1315+
# from the individual molecule intrascales (e.g. GLYCAM funct=2 overrides).
1316+
# This step can be skipped via apply_scale_factors=False when the caller
1317+
# knows no non-default scale factors are present.
1318+
if "apply_scale_factors" in kwargs:
1319+
if not isinstance(kwargs["apply_scale_factors"], bool):
1320+
raise TypeError("'apply_scale_factors' must be of type 'bool'")
1321+
1322+
if kwargs.get("apply_scale_factors", roi is None):
1323+
intra0, intra1 = _SireIO.patchIntrascale(
13241324
molecule0.property("intrascale"),
13251325
molecule1.property("intrascale"),
1326-
conn0,
1327-
conn1,
1328-
sf14,
1326+
intra0,
1327+
intra1,
13291328
mol0_merged_mapping,
13301329
mol1_merged_mapping,
13311330
)
13321331

1332+
merged_intrascale = [intra0, intra1]
1333+
13331334
# Store the two molecular components.
13341335
edit_mol.set_property("molecule0", molecule0)
13351336
edit_mol.set_property("molecule1", molecule1)

tests/Align/test_align.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -857,13 +857,14 @@ def test_ring_opening_and_size_change(ligands, mapping):
857857
)
858858
def test_ring_breaking_intrascale():
859859
"""
860-
Regression test for mergeIntrascale with ring-breaking perturbations.
861-
862-
Before the fix, CLJNBPairs pairs that were excluded/1-4 in one end state
863-
but fully interacting (1,1) in the other were incorrectly left at the
864-
non-(1,1) value in the merged intrascale. For cyclopentane->cyclohexane
865-
this produced only 6 changed OpenMM exceptions instead of the correct 18,
866-
causing simulation instabilities due to missing non-bonded interactions.
860+
Test that ring-breaking merges produce correct intrascale matrices for a
861+
standard force field (GAFF2) with no non-default per-pair scale factors.
862+
863+
The intrascale matrices are built from per-state connectivity, which
864+
correctly captures bonded distances across the ring-closure bond in the
865+
merged atom space. Since GAFF2 has no non-default per-pair scale factors,
866+
patchIntrascale is a no-op — verified by checking that apply_scale_factors=False
867+
gives the same changed bond and exception counts as the default path.
867868
"""
868869
# Parameterise both molecules with GAFF2.
869870
cyclopentane = BSS.Parameters.gaff2("C1CCCC1").getMolecule()
@@ -928,6 +929,22 @@ def test_ring_breaking_intrascale():
928929
assert len(omm_rev.changed_bonds()) == len(ref_bonds)
929930
assert len(omm_rev.changed_exceptions()) == len(ref_exceptions)
930931

932+
# Verify patchIntrascale is a no-op for GAFF2: skipping it with
933+
# apply_scale_factors=False must give identical results.
934+
merged_nopatch = BSS.Align.merge(
935+
cyclopentane_aligned,
936+
cyclohexane,
937+
mapping,
938+
allow_ring_size_change=True,
939+
allow_ring_breaking=True,
940+
apply_scale_factors=False,
941+
)
942+
omm_nopatch = merged_nopatch._sire_object.perturbation().to_openmm(
943+
map={"coordinates": "coordinates0"}
944+
)
945+
assert len(omm_nopatch.changed_bonds()) == len(ref_bonds)
946+
assert len(omm_nopatch.changed_exceptions()) == len(ref_exceptions)
947+
931948

932949
@pytest.mark.skipif(
933950
not has_antechamber or not has_tleap,
@@ -937,24 +954,20 @@ def test_ring_breaking_intrascale():
937954
not has_openff,
938955
reason="Requires OpenFF to be installed.",
939956
)
940-
def test_ring_breaking_intrascale_connectivity():
957+
def test_ring_breaking_intrascale_m338():
941958
"""
942-
Regression test for mergeIntrascale using a real-world ring-breaking
943-
perturbation (int1 -> m338).
944-
945-
Validates that the merged intrascale matrices produced by mergeIntrascale
946-
(which builds CLJNBPairs from per-state connectivity and overrides with
947-
per-pair values) exactly match those produced directly from
948-
CLJNBPairs(conn0/conn1, sf14). For ring-breaking perturbations the two
949-
approaches must agree: discrepancies indicate that bonded distances in the
950-
merged topology are not being correctly captured, which causes missing or
951-
spurious OpenMM exceptions and simulation instabilities.
959+
Test that ring-breaking merges produce correct intrascale matrices for a
960+
real-world perturbation (int1 -> m338) with a standard force field (OpenFF).
961+
962+
Since OpenFF has no non-default per-pair scale factors, patchIntrascale is
963+
a no-op and the merged intrascale matrices must exactly match those built
964+
directly from CLJNBPairs(conn0/conn1, sf14).
952965
"""
953966
from sire.legacy import CAS as _SireCAS
954967
from sire.legacy import MM as _SireMM
955968
from sire.legacy import Mol as _SireMol
956969

957-
# Atom mapping: {int1_idx: m338_idx}, read from m338_int1_MCS.txt.
970+
# Atom mapping: {int1_idx: m338_idx}
958971
mapping = {
959972
21: 0,
960973
0: 1,
@@ -1013,11 +1026,11 @@ def test_ring_breaking_intrascale_connectivity():
10131026

10141027
sire_mol = merged._sire_object
10151028

1016-
# Extract the intrascale matrices produced by mergeIntrascale.
10171029
intra0 = sire_mol.property("intrascale0")
10181030
intra1 = sire_mol.property("intrascale1")
10191031

1020-
# Build the reference intrascale matrices from per-state connectivity,
1032+
# Build reference matrices directly from per-state connectivity. For OpenFF
1033+
# these must be identical to the merge output since patchIntrascale is a no-op.
10211034
ff = mol0._sire_object.property("forcefield")
10221035
sf14 = _SireMM.CLJScaleFactor(
10231036
ff.electrostatic14_scale_factor(), ff.vdw14_scale_factor()

0 commit comments

Comments
 (0)