Skip to content

Commit 034ddd4

Browse files
authored
Merge pull request #254 from OpenBioSim/backport_251_253
Backport fixes from PR #251 and #253
2 parents 39c1ff5 + c5bcd2b commit 034ddd4

File tree

6 files changed

+118
-51
lines changed

6 files changed

+118
-51
lines changed

python/BioSimSpace/Sandpit/Exscientia/Align/_decouple.py

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def decouple(
7676
if not isinstance(value, bool):
7777
raise ValueError(f"{value} in {name} must be bool.")
7878

79-
# Change names of charge and LJ tuples to avoid clashes with properties
79+
# Change names of charge and LJ tuples to avoid clashes with properties.
8080
charge_tuple = charge
8181
LJ_tuple = LJ
8282

@@ -86,7 +86,7 @@ def decouple(
8686
# Invert the user property mappings.
8787
inv_property_map = {v: k for k, v in property_map.items()}
8888

89-
# Create a copy of this molecule and Sire object to check properties
89+
# Create a copy of this molecule and Sire object to check properties.
9090
mol = _Molecule(molecule)
9191
mol_sire = mol._sire_object
9292

@@ -97,7 +97,7 @@ def decouple(
9797
element = inv_property_map.get("element", "element")
9898
ambertype = inv_property_map.get("ambertype", "ambertype")
9999

100-
# Check for missing information
100+
# Check for missing information.
101101
if not mol_sire.hasProperty(ff):
102102
raise _IncompatibleError("Cannot determine 'forcefield' of 'molecule'!")
103103
if not mol_sire.hasProperty(LJ):
@@ -107,54 +107,32 @@ def decouple(
107107
if not mol_sire.hasProperty(element):
108108
raise _IncompatibleError("Cannot determine elements in molecule")
109109

110-
# Check for ambertype property (optional)
110+
# Check for ambertype property (optional).
111111
has_ambertype = True
112112
if not mol_sire.hasProperty(ambertype):
113113
has_ambertype = False
114114

115115
if not isinstance(intramol, bool):
116116
raise TypeError("'intramol' must be of type 'bool'")
117117

118-
# Edit the molecule
118+
# Edit the molecule.
119119
mol_edit = mol_sire.edit()
120120

121-
# Create dictionary to store charge and LJ tuples
121+
# Create dictionary to store charge and LJ tuples.
122122
mol_edit.setProperty(
123123
"decouple", {"charge": charge_tuple, "LJ": LJ_tuple, "intramol": intramol}
124124
)
125125

126126
# Set the "forcefield0" property.
127127
mol_edit.setProperty("forcefield0", molecule._sire_object.property(ff))
128128

129-
# Set starting properties based on fully-interacting molecule
129+
# Set starting properties based on fully-interacting molecule.
130130
mol_edit.setProperty("charge0", molecule._sire_object.property(charge))
131131
mol_edit.setProperty("LJ0", molecule._sire_object.property(LJ))
132132
mol_edit.setProperty("element0", molecule._sire_object.property(element))
133133
if has_ambertype:
134134
mol_edit.setProperty("ambertype0", molecule._sire_object.property(ambertype))
135135

136-
# Set final charges and LJ terms to 0, elements to "X" and (if required) ambertypes to du
137-
for atom in mol_sire.atoms():
138-
mol_edit = (
139-
mol_edit.atom(atom.index())
140-
.setProperty("charge1", 0 * _SireUnits.e_charge)
141-
.molecule()
142-
)
143-
mol_edit = (
144-
mol_edit.atom(atom.index())
145-
.setProperty("LJ1", _SireMM.LJParameter())
146-
.molecule()
147-
)
148-
mol_edit = (
149-
mol_edit.atom(atom.index())
150-
.setProperty("element1", _SireMol.Element(0))
151-
.molecule()
152-
)
153-
if has_ambertype:
154-
mol_edit = (
155-
mol_edit.atom(atom.index()).setProperty("ambertype1", "du").molecule()
156-
)
157-
158136
mol_edit.setProperty("annihilated", _SireBase.wrap(intramol))
159137

160138
# Flag that this molecule is decoupled.

python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
######################################################################
22
# BioSimSpace: Making biomolecular simulation a breeze!
33
#
4-
# Copyright: 2017-2023
4+
# Copyright: 2017-2024
55
#
66
# Authors: Lester Hedges <lester.hedges@gmail.com>
77
#
@@ -26,10 +26,10 @@
2626

2727
__all__ = ["Somd"]
2828

29-
from .._Utils import _try_import
30-
3129
import os as _os
3230

31+
from .._Utils import _try_import
32+
3333
_pygtail = _try_import("pygtail")
3434
import glob as _glob
3535
import random as _random
@@ -38,23 +38,21 @@
3838
import timeit as _timeit
3939
import warnings as _warnings
4040

41-
from sire.legacy import Base as _SireBase
4241
from sire.legacy import CAS as _SireCAS
4342
from sire.legacy import IO as _SireIO
4443
from sire.legacy import MM as _SireMM
44+
from sire.legacy import Base as _SireBase
4545
from sire.legacy import Mol as _SireMol
46+
from sire.legacy import Units as _SireUnits
4647

47-
from .. import _isVerbose
48+
from .. import IO as _IO
49+
from .. import Protocol as _Protocol
50+
from .. import Trajectory as _Trajectory
51+
from .. import _isVerbose, _Utils
4852
from .._Exceptions import IncompatibleError as _IncompatibleError
4953
from .._Exceptions import MissingSoftwareError as _MissingSoftwareError
5054
from .._SireWrappers import Molecule as _Molecule
5155
from .._SireWrappers import System as _System
52-
53-
from .. import IO as _IO
54-
from .. import Protocol as _Protocol
55-
from .. import Trajectory as _Trajectory
56-
from .. import _Utils
57-
5856
from . import _process
5957

6058

@@ -1001,6 +999,9 @@ def _to_pert_file(
1001999
if not isinstance(perturbation_type, str):
10021000
raise TypeError("'perturbation_type' must be of type 'str'")
10031001

1002+
if not isinstance(property_map, dict):
1003+
raise TypeError("'property_map' must be of type 'dict'")
1004+
10041005
# Convert to lower case and strip whitespace.
10051006
perturbation_type = perturbation_type.lower().replace(" ", "")
10061007

@@ -1027,6 +1028,60 @@ def _to_pert_file(
10271028
# Extract and copy the Sire molecule.
10281029
mol = molecule._sire_object.__deepcopy__()
10291030

1031+
# If the molecule is decoupled (for an ABFE calculation), then we need to
1032+
# set the end-state properties of the molecule.
1033+
if molecule.isDecoupled():
1034+
# Invert the user property mappings.
1035+
inv_property_map = {v: k for k, v in property_map.items()}
1036+
1037+
# Get required properties.
1038+
lj = inv_property_map.get("LJ", "LJ")
1039+
charge = inv_property_map.get("charge", "charge")
1040+
element = inv_property_map.get("element", "element")
1041+
ambertype = inv_property_map.get("ambertype", "ambertype")
1042+
1043+
# Check for missing information.
1044+
if not mol.hasProperty(lj):
1045+
raise _IncompatibleError("Cannot determine LJ terms for molecule")
1046+
if not mol.hasProperty(charge):
1047+
raise _IncompatibleError("Cannot determine charges for molecule")
1048+
if not mol.hasProperty(element):
1049+
raise _IncompatibleError("Cannot determine elements in molecule")
1050+
1051+
# Check for ambertype property.
1052+
has_ambertype = True
1053+
if not mol.hasProperty(ambertype):
1054+
has_ambertype = False
1055+
1056+
mol_edit = mol.edit()
1057+
1058+
# Set final charges and LJ terms to 0, elements to "X" and (if required) ambertypes to du
1059+
for atom in mol.atoms():
1060+
mol_edit = (
1061+
mol_edit.atom(atom.index())
1062+
.setProperty("charge1", 0 * _SireUnits.e_charge)
1063+
.molecule()
1064+
)
1065+
mol_edit = (
1066+
mol_edit.atom(atom.index())
1067+
.setProperty("LJ1", _SireMM.LJParameter())
1068+
.molecule()
1069+
)
1070+
mol_edit = (
1071+
mol_edit.atom(atom.index())
1072+
.setProperty("element1", _SireMol.Element(0))
1073+
.molecule()
1074+
)
1075+
if has_ambertype:
1076+
mol_edit = (
1077+
mol_edit.atom(atom.index())
1078+
.setProperty("ambertype1", "du")
1079+
.molecule()
1080+
)
1081+
1082+
# Update the Sire molecule object of the new molecule.
1083+
mol = mol_edit.commit()
1084+
10301085
# First work out the indices of atoms that are perturbed.
10311086
pert_idxs = []
10321087

python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1814,7 +1814,11 @@ def _extractMolecule(self, property_map={}, is_lambda1=False):
18141814
try:
18151815
search_result = mol.search(query, property_map)
18161816
except:
1817-
search_result = []
1817+
msg = "All atoms in the selection are dummies. Unable to extract."
1818+
if _isVerbose():
1819+
raise _IncompatibleError(msg) from e
1820+
else:
1821+
raise _IncompatibleError(msg) from None
18181822

18191823
# If there are no dummies, then simply return this molecule.
18201824
if len(search_result) == mol.nAtoms():

python/BioSimSpace/_SireWrappers/_molecule.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1746,7 +1746,11 @@ def _extractMolecule(self, property_map={}, is_lambda1=False):
17461746
try:
17471747
search_result = mol.search(query, property_map)
17481748
except:
1749-
search_result = []
1749+
msg = "All atoms in the selection are dummies. Unable to extract."
1750+
if _isVerbose():
1751+
raise _IncompatibleError(msg) from e
1752+
else:
1753+
raise _IncompatibleError(msg) from None
17501754

17511755
# If there are no dummies, then simply return this molecule.
17521756
if len(search_result) == mol.nAtoms():

tests/Sandpit/Exscientia/Align/test_decouple.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,11 @@ def test_topology(mol, tmp_path):
8181

8282

8383
def test_end_types(mol):
84-
"""Check that the correct properties have been set at either
85-
end of the perturbation."""
84+
"""
85+
Check that the correct properties have been set for the 0
86+
end of the perturbation. Note that for SOMD, the 1 end state
87+
properties are set in Process.Somd._to_pert_file.
88+
"""
8689

8790
decoupled_mol = decouple(mol)
8891
assert decoupled_mol._sire_object.property("charge0") == mol._sire_object.property(
@@ -95,8 +98,3 @@ def test_end_types(mol):
9598
assert decoupled_mol._sire_object.property(
9699
"ambertype0"
97100
) == mol._sire_object.property("ambertype")
98-
for atom in decoupled_mol._sire_object.atoms():
99-
assert atom.property("charge1") == 0 * _SireUnits.e_charge
100-
assert atom.property("LJ1") == _SireMM.LJParameter()
101-
assert atom.property("element1") == _SireMol.Element(0)
102-
assert atom.property("ambertype1") == "du"

tests/Sandpit/Exscientia/FreeEnergy/test_alchemical_free_energy.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import bz2
2+
from math import exp
23
import pandas as pd
34
import pathlib
45
import pytest
@@ -270,9 +271,11 @@ def freenrg():
270271
return freenrg
271272

272273
def test_files_exist(self, freenrg):
273-
"""Test if the files have been created. Note that e.g. gradients.dat
274+
"""
275+
Test if the files have been created. Note that e.g. gradients.dat
274276
are not created until later in the simulation, so their presence is
275-
not tested for."""
277+
not tested for.
278+
"""
276279
path = pathlib.Path(freenrg.workDir())
277280
for lam in ["0.0000", "0.5000", "1.0000"]:
278281
assert (path / f"lambda_{lam}" / "simfile.dat").is_file()
@@ -282,6 +285,31 @@ def test_files_exist(self, freenrg):
282285
assert (path / f"lambda_{lam}" / "somd.prm7").is_file()
283286
assert (path / f"lambda_{lam}" / "somd.err").is_file()
284287
assert (path / f"lambda_{lam}" / "somd.out").is_file()
288+
assert (path / f"lambda_{lam}" / "somd.pert").is_file()
289+
290+
def test_correct_pert_file(self, freenrg):
291+
"""Check that pert file is correct."""
292+
path = pathlib.Path(freenrg.workDir()) / "lambda_0.0000"
293+
with open(os.path.join(path, "somd.pert"), "rt") as f:
294+
lines = f.readlines()
295+
296+
for i, line in enumerate(lines):
297+
# Check that the end-state properties are correct.
298+
if "final_type" in line:
299+
assert "final_type du" in line
300+
if "final_LJ" in line:
301+
assert "final_LJ 0.00000 0.00000" in line
302+
if "final_charge" in line:
303+
assert "final_charge 0.00000" in line
304+
# Check that the initial state properties are correct for the first and last atoms.
305+
if line == " name C1\n":
306+
assert "initial_type c" in lines[i + 1]
307+
assert "initial_LJ 3.39967 0.08600" in lines[i + 3]
308+
assert "initial_charge 0.67120" in lines[i + 5]
309+
if "name O3" in line:
310+
assert "initial_type o" in lines[i + 1]
311+
assert "initial_LJ 2.95992 0.21000" in lines[i + 3]
312+
assert "initial_charge -0.52110" in lines[i + 5]
285313

286314
def test_correct_conf_file(self, freenrg):
287315
"""Check that lambda data is correct in somd.cfg"""

0 commit comments

Comments
 (0)