Skip to content

Commit 94e8980

Browse files
committed
Update morse potential tests to support DMR, update changelog and update tutorial describing Morse potentials [ci skip]
1 parent 746d0f4 commit 94e8980

3 files changed

Lines changed: 75 additions & 11 deletions

File tree

doc/source/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ organisation on `GitHub <https://github.com/openbiosim/sire>`__.
3838

3939
* Add functionality for coupling one lambda lever to another.
4040

41+
* Added support for Direct Morse Replacement (DMR) feature in ``sire.restraints.morse_potential``
42+
which is enabled by default.
43+
4144
`2025.4.0 <https://github.com/openbiosim/sire/compare/2025.3.0...2025.4.0>`__ - February 2026
4245
---------------------------------------------------------------------------------------------
4346

doc/source/tutorial/part06/03_restraints.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ Morse Potential Restraints
360360
---------------------------
361361

362362
The :func:`sire.restraints.morse_potential` function is used to create Morse potential restraints,
363-
which can be used to carry harmonic bond annihilations or creations in alchemical relative binding free energy calculations.
363+
which can be used to carry out harmonic bond annihilations or creations in alchemical relative binding free energy calculations.
364364

365365
To create a Morse potential restraint, you need to specify the two atoms to be restrained. Like the distance restraints,
366366
the atoms can be specified using a search string, passing lists of atom indexes, or
@@ -370,7 +370,7 @@ If not supplied, automatic parametrisation feature can be used, which will detec
370370
annihilated or created and set the parameters accordingly (dissociation energy value still needs to be provided). For example,
371371

372372
>>> mols = sr.load_test_files("cyclopentane_cyclohexane.bss")
373-
>>> morse_restraints = sr.restraints.morse_potential(
373+
>>> morse_restraints, mols = sr.restraints.morse_potential(
374374
... mols,
375375
... atoms0=mols["molecule property is_perturbable and atomidx 0"],
376376
... atoms1=mols["molecule property is_perturbable and atomidx 4"],
@@ -385,14 +385,22 @@ MorsePotentialRestraint( 0 <=> 4, k=100 kcal mol-1 Å-2 : r0=1.5 Å : de=50 kcal
385385
creates a Morse potential restraint between atoms 0 and 4 using the specified parameters.
386386
Alternatively, if the molecule contains a bond that is being alchemically annihilated, e.g.
387387

388-
>>> morse_restraints = sr.restraints.morse_potential(mols, auto_parametrise=True, de="50 kcal mol-1")
388+
>>> morse_restraints, mols = sr.restraints.morse_potential(mols, auto_parametrise=True, de="50 kcal mol-1")
389389
>>> morse_restraint = morse_restraints[0]
390390
>>> print(morse_restraint)
391-
MorsePotentialRestraint( 0 <=> 4, k=228.89 kcal mol-1 Å-2 : r0=1.5354 Å : de=50 kcal mol-1 )
391+
MorsePotentialRestraint( 0 <=> 4, k=457.78 kcal mol-1 Å-2 : r0=1.5354 Å : de=50 kcal mol-1 )
392392

393393
creates a Morse potential restraint between atoms 0 and 4 by attempting to match the
394394
bond parameters of the bond being alchemically annihilated.
395395

396+
.. note::
397+
398+
Automatic bond parametrisation is enabled by default and will strip away the bond being
399+
alchemically annihilated or created from the system, and use the parameters of that bond
400+
to set the parameters of the Morse potential restraint. If you want to keep the original bond
401+
in the system, then you can disable automatic parametrisation by setting ``auto_parametrise=False``
402+
and explicitly providing the parameters for the Morse potential restraint.
403+
396404
Boresch Restraints
397405
---------------------------
398406

tests/restraints/test_morse_potential_restraints.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
def test_morse_potential_restraints_setup(cyclopentane_cyclohexane):
66
"""Tests that morse_potential restraints can be set up correctly with custom parameters."""
77
mols = cyclopentane_cyclohexane.clone()
8-
restraints = sr.restraints.morse_potential(
8+
restraints, mols = sr.restraints.morse_potential(
99
mols,
1010
atoms0=mols["molecule property is_perturbable and atomidx 0"],
1111
atoms1=mols["molecule property is_perturbable and atomidx 4"],
@@ -25,23 +25,23 @@ def test_morse_potential_restraint_annihiliation_auto_param(cyclopentane_cyclohe
2525
"""Tests that morse_potential restraints can be set up correctly with automatic parametrisation
2626
when a bond is to be annihilated."""
2727
mols = cyclopentane_cyclohexane.clone()
28-
restraints = sr.restraints.morse_potential(
28+
restraints, mols = sr.restraints.morse_potential(
2929
mols,
3030
de="25 kcal mol-1",
3131
auto_parametrise=True,
3232
)
3333
assert restraints.num_restraints() == 1
3434
assert restraints[0].atom0() == 0
3535
assert restraints[0].atom1() == 4
36-
assert restraints[0].k().value() == pytest.approx(228.89, rel=0.1)
36+
assert restraints[0].k().value() == pytest.approx(457.78, rel=0.1)
3737
assert restraints[0].de().value() == 25.0
3838

3939

4040
def test_morse_potential_restraint_creation_auto_param(propane_cyclopropane):
4141
"""Tests that morse_potential restraints can be set up correctly with automatic parametrisation
4242
when a bond is to be created."""
4343
mols = propane_cyclopropane.clone()
44-
restraints = sr.restraints.morse_potential(
44+
restraints, mols = sr.restraints.morse_potential(
4545
mols,
4646
de="25 kcal mol-1",
4747
auto_parametrise=True,
@@ -54,7 +54,7 @@ def test_morse_potential_restraint_creation_auto_param(propane_cyclopropane):
5454
def test_morse_potential_restraint_auto_param_override(cyclopentane_cyclohexane):
5555
"""Tests that morse_potential restraints can be set up correctly with automatic parametrisation and some parameters can be overwritten."""
5656
mols = cyclopentane_cyclohexane.clone()
57-
restraints = sr.restraints.morse_potential(
57+
restraints, mols = sr.restraints.morse_potential(
5858
mols,
5959
de="25 kcal mol-1",
6060
k="100 kcal mol-1 A-2",
@@ -70,18 +70,71 @@ def test_morse_potential_restraint_auto_param_override(cyclopentane_cyclohexane)
7070
def test_multiple_morse_potential_restraints(cyclopentane_cyclohexane):
7171
"""Tests that multiple morse_potential restraints can be set up correctly."""
7272
mols = cyclopentane_cyclohexane.clone()
73-
restraints = sr.restraints.morse_potential(
73+
restraints, mols = sr.restraints.morse_potential(
7474
mols,
7575
de="25 kcal mol-1",
7676
auto_parametrise=True,
77+
direct_morse_replacement=False,
7778
)
78-
restraint1 = sr.restraints.morse_potential(
79+
restraint1, mols = sr.restraints.morse_potential(
7980
mols,
8081
atoms0=mols["molecule property is_perturbable and atomidx 0"],
8182
atoms1=mols["molecule property is_perturbable and atomidx 1"],
8283
k="100 kcal mol-1 A-2",
8384
r0="1.5 A",
8485
de="50 kcal mol-1",
86+
direct_morse_replacement=False,
8587
)
8688
restraints.add(restraint1)
8789
assert restraints.num_restraints() == 2
90+
91+
def test_morse_potential_direct_morse_replacement(cyclopentane_cyclohexane):
92+
"""Tests that morse_potential restraints by default will remove the annihilated harmonic bond."""
93+
mols = cyclopentane_cyclohexane.clone()
94+
bonds0_org_mol = mols[0].property("bond0")
95+
bonds1_org_mol = mols[0].property("bond1")
96+
num_bonds0_org_mol = len(bonds0_org_mol.potentials())
97+
num_bonds1_org_mol = len(bonds1_org_mol.potentials())
98+
99+
restraints, mols = sr.restraints.morse_potential(
100+
mols,
101+
de="25 kcal mol-1",
102+
auto_parametrise=True,
103+
)
104+
105+
bonds0_mod_mol = mols[0].property("bond0")
106+
bonds1_mod_mol = mols[0].property("bond1")
107+
num_bonds0_mod_mol = len(bonds0_mod_mol.potentials())
108+
num_bonds1_mod_mol = len(bonds1_mod_mol.potentials())
109+
110+
# New bonds0 should be smaller by 1 than the original bonds0 if the function removed the bond
111+
assert num_bonds0_mod_mol == num_bonds0_org_mol - 1
112+
113+
# New bonds1 should be same as the original bonds1, as the bonds1 will already be smaller
114+
# by 1 than the original bonds0 which is introduced during the merge.
115+
assert num_bonds1_mod_mol == num_bonds1_org_mol
116+
assert num_bonds0_org_mol == num_bonds1_org_mol + 1
117+
118+
def test_morse_potential_direct_morse_replacement_retain_harmonic(cyclopentane_cyclohexane):
119+
"""Tests that morse_potential restraints can retain the annihilated harmonic bond, if specified."""
120+
mols = cyclopentane_cyclohexane.clone()
121+
bonds0_org_mol = mols[0].property("bond0")
122+
bonds1_org_mol = mols[0].property("bond1")
123+
num_bonds0_org_mol = len(bonds0_org_mol.potentials())
124+
num_bonds1_org_mol = len(bonds1_org_mol.potentials())
125+
126+
restraints, mols = sr.restraints.morse_potential(
127+
mols,
128+
de="25 kcal mol-1",
129+
auto_parametrise=True,
130+
retain_harmonic_bond=True,
131+
)
132+
bonds0_mod_mol = mols[0].property("bond0")
133+
bonds1_mod_mol = mols[0].property("bond1")
134+
num_bonds0_mod_mol = len(bonds0_mod_mol.potentials())
135+
num_bonds1_mod_mol = len(bonds1_mod_mol.potentials())
136+
137+
# If the harmonic bond is retained, the number of bonds in bonds0 and bonds1
138+
# should be the same as the original molecule
139+
assert num_bonds0_mod_mol == num_bonds0_org_mol
140+
assert num_bonds1_mod_mol == num_bonds1_org_mol

0 commit comments

Comments
 (0)