@@ -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