Skip to content

Commit 3b50ecc

Browse files
authored
Merge pull request #514 from OpenBioSim/fix_ring_break_merge
Fix ring break merge
2 parents 38ed3b3 + 9e5b68f commit 3b50ecc

3 files changed

Lines changed: 139 additions & 2 deletions

File tree

src/BioSimSpace/Align/_merge.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1314,10 +1314,16 @@ def merge(
13141314
edit_mol.set_property("connectivity0", conn0)
13151315
edit_mol.set_property("connectivity1", conn1)
13161316
# Merge the intrascale properties of the two molecules.
1317+
ff = molecule0.property(ff0)
1318+
sf14 = _SireMM.CLJScaleFactor(
1319+
ff.electrostatic14_scale_factor(), ff.vdw14_scale_factor()
1320+
)
13171321
merged_intrascale = _SireIO.mergeIntrascale(
13181322
molecule0.property("intrascale"),
13191323
molecule1.property("intrascale"),
1320-
edit_mol.info(),
1324+
conn0,
1325+
conn1,
1326+
sf14,
13211327
mol0_merged_mapping,
13221328
mol1_merged_mapping,
13231329
)

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1302,10 +1302,16 @@ def merge(
13021302
edit_mol.set_property("connectivity", conn)
13031303

13041304
# Merge the intrascale properties of the two molecules.
1305+
ff = molecule0.property(ff0)
1306+
sf14 = _SireMM.CLJScaleFactor(
1307+
ff.electrostatic14_scale_factor(), ff.vdw14_scale_factor()
1308+
)
13051309
merged_intrascale = _SireIO.mergeIntrascale(
13061310
molecule0.property("intrascale"),
13071311
molecule1.property("intrascale"),
1308-
edit_mol.info(),
1312+
conn0,
1313+
conn1,
1314+
sf14,
13091315
mol0_merged_mapping,
13101316
mol1_merged_mapping,
13111317
)

tests/Align/test_align.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,3 +927,128 @@ def test_ring_breaking_intrascale():
927927
)
928928
assert len(omm_rev.changed_bonds()) == len(ref_bonds)
929929
assert len(omm_rev.changed_exceptions()) == len(ref_exceptions)
930+
931+
932+
@pytest.mark.skipif(
933+
not has_openff,
934+
reason="Requires OpenFF to be installed.",
935+
)
936+
def test_ring_breaking_intrascale_connectivity():
937+
"""
938+
Regression test for mergeIntrascale using a real-world ring-breaking
939+
perturbation (int1 -> m338).
940+
941+
Validates that the merged intrascale matrices produced by mergeIntrascale
942+
(which builds CLJNBPairs from per-state connectivity and overrides with
943+
per-pair values) exactly match those produced directly from
944+
CLJNBPairs(conn0/conn1, sf14). For ring-breaking perturbations the two
945+
approaches must agree: discrepancies indicate that bonded distances in the
946+
merged topology are not being correctly captured, which causes missing or
947+
spurious OpenMM exceptions and simulation instabilities.
948+
"""
949+
from sire.legacy import CAS as _SireCAS
950+
from sire.legacy import MM as _SireMM
951+
from sire.legacy import Mol as _SireMol
952+
953+
# Atom mapping: {int1_idx: m338_idx}, read from m338_int1_MCS.txt.
954+
mapping = {
955+
21: 0,
956+
0: 1,
957+
23: 2,
958+
18: 3,
959+
1: 4,
960+
2: 5,
961+
3: 6,
962+
4: 7,
963+
5: 8,
964+
6: 9,
965+
7: 10,
966+
8: 11,
967+
9: 12,
968+
10: 13,
969+
19: 14,
970+
11: 15,
971+
12: 16,
972+
13: 17,
973+
14: 18,
974+
15: 19,
975+
16: 20,
976+
20: 21,
977+
30: 22,
978+
24: 23,
979+
22: 24,
980+
26: 25,
981+
17: 26,
982+
25: 27,
983+
27: 28,
984+
32: 29,
985+
33: 30,
986+
34: 31,
987+
35: 32,
988+
36: 33,
989+
37: 34,
990+
28: 35,
991+
38: 36,
992+
}
993+
994+
mol0 = BSS.Parameters.openff_unconstrained_2_2_1(
995+
BSS.IO.readMolecules(f"{url}/int1.sdf")[0]
996+
).getMolecule()
997+
mol1 = BSS.Parameters.openff_unconstrained_2_2_1(
998+
BSS.IO.readMolecules(f"{url}/m338.sdf")[0]
999+
).getMolecule()
1000+
1001+
mol0_aligned = BSS.Align.rmsdAlign(mol0, mol1, mapping)
1002+
merged = BSS.Align.merge(
1003+
mol0_aligned,
1004+
mol1,
1005+
mapping,
1006+
allow_ring_breaking=True,
1007+
allow_ring_size_change=True,
1008+
)
1009+
1010+
sire_mol = merged._sire_object
1011+
1012+
# Extract the intrascale matrices produced by mergeIntrascale.
1013+
intra0 = sire_mol.property("intrascale0")
1014+
intra1 = sire_mol.property("intrascale1")
1015+
1016+
# Build the reference intrascale matrices from per-state connectivity,
1017+
# replicating the debug_merge approach.
1018+
ff = mol0._sire_object.property("forcefield")
1019+
sf14 = _SireMM.CLJScaleFactor(
1020+
ff.electrostatic14_scale_factor(), ff.vdw14_scale_factor()
1021+
)
1022+
1023+
conn0_edit = _SireMol.Connectivity(sire_mol.info()).edit()
1024+
conn1_edit = _SireMol.Connectivity(sire_mol.info()).edit()
1025+
for bond in sire_mol.property("bond0").potentials():
1026+
ab = _SireMM.AmberBond(bond.function(), _SireCAS.Symbol("r"))
1027+
if ab.k() != 0.0:
1028+
conn0_edit.connect(bond.atom0(), bond.atom1())
1029+
for bond in sire_mol.property("bond1").potentials():
1030+
ab = _SireMM.AmberBond(bond.function(), _SireCAS.Symbol("r"))
1031+
if ab.k() != 0.0:
1032+
conn1_edit.connect(bond.atom0(), bond.atom1())
1033+
1034+
ref_intra0 = _SireMM.CLJNBPairs(conn0_edit.commit(), sf14)
1035+
ref_intra1 = _SireMM.CLJNBPairs(conn1_edit.commit(), sf14)
1036+
1037+
# The two approaches must agree on every atom pair.
1038+
n = sire_mol.num_atoms()
1039+
for i in range(n):
1040+
for j in range(i, n):
1041+
idx_i = _SireMol.AtomIdx(i)
1042+
idx_j = _SireMol.AtomIdx(j)
1043+
assert intra0.get(idx_i, idx_j).coulomb() == pytest.approx(
1044+
ref_intra0.get(idx_i, idx_j).coulomb()
1045+
), f"intra0 coulomb mismatch at ({i},{j})"
1046+
assert intra0.get(idx_i, idx_j).lj() == pytest.approx(
1047+
ref_intra0.get(idx_i, idx_j).lj()
1048+
), f"intra0 lj mismatch at ({i},{j})"
1049+
assert intra1.get(idx_i, idx_j).coulomb() == pytest.approx(
1050+
ref_intra1.get(idx_i, idx_j).coulomb()
1051+
), f"intra1 coulomb mismatch at ({i},{j})"
1052+
assert intra1.get(idx_i, idx_j).lj() == pytest.approx(
1053+
ref_intra1.get(idx_i, idx_j).lj()
1054+
), f"intra1 lj mismatch at ({i},{j})"

0 commit comments

Comments
 (0)