Skip to content

Commit 8ba863f

Browse files
authored
Merge pull request #400 from OpenBioSim/sync_recursion2
Sync with Recursion remote
2 parents 974cdd1 + de9db8a commit 8ba863f

File tree

9 files changed

+147
-112
lines changed

9 files changed

+147
-112
lines changed

python/BioSimSpace/Parameters/_parameters.py

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -534,57 +534,6 @@ def _parameterise_openff(
534534
"must be in your PATH."
535535
) from None
536536

537-
# Check the Antechamber version. Open Force Field requires Antechamber >= 22.0.
538-
try:
539-
# Antechamber returns an exit code of 1 when requesting version information.
540-
# As such, we wrap the call within a try-except block in case it fails.
541-
542-
import shlex as _shlex
543-
import subprocess as _subprocess
544-
545-
# Generate the command-line string. (Antechamber must be in the PATH,
546-
# so no need to use AMBERHOME.
547-
command = "antechamber -v"
548-
549-
# Run the command as a subprocess.
550-
proc = _subprocess.run(
551-
_Utils.command_split(command),
552-
shell=False,
553-
text=True,
554-
stdout=_subprocess.PIPE,
555-
stderr=_subprocess.STDOUT,
556-
)
557-
558-
# Get stdout and split into lines.
559-
lines = proc.stdout.split("\n")
560-
561-
# If present, version information is on line 1.
562-
string = lines[1]
563-
564-
# Delete the welcome message.
565-
string = string.replace("Welcome to antechamber", "")
566-
567-
# Extract the version and convert to float.
568-
version = float(string.split(":")[0])
569-
570-
# The version is okay, enable Open Force Field support.
571-
if version >= 22:
572-
is_compatible = True
573-
# Disable Open Force Field support.
574-
else:
575-
is_compatible = False
576-
577-
del _shlex
578-
del _subprocess
579-
580-
# Something went wrong, disable Open Force Field support.
581-
except:
582-
is_compatible = False
583-
raise
584-
585-
if not is_compatible:
586-
raise _IncompatibleError(f"'{forcefield}' requires Antechamber >= 22.0")
587-
588537
# Validate arguments.
589538

590539
if not isinstance(molecule, (_Molecule, str)):

python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -534,57 +534,6 @@ def _parameterise_openff(
534534
"must be in your PATH."
535535
) from None
536536

537-
# Check the Antechamber version. Open Force Field requires Antechamber >= 22.0.
538-
try:
539-
# Antechamber returns an exit code of 1 when requesting version information.
540-
# As such, we wrap the call within a try-except block in case it fails.
541-
542-
import shlex as _shlex
543-
import subprocess as _subprocess
544-
545-
# Generate the command-line string. (Antechamber must be in the PATH,
546-
# so no need to use AMBERHOME.
547-
command = "antechamber -v"
548-
549-
# Run the command as a subprocess.
550-
proc = _subprocess.run(
551-
_Utils.command_split(command),
552-
shell=False,
553-
text=True,
554-
stdout=_subprocess.PIPE,
555-
stderr=_subprocess.STDOUT,
556-
)
557-
558-
# Get stdout and split into lines.
559-
lines = proc.stdout.split("\n")
560-
561-
# If present, version information is on line 1.
562-
string = lines[1]
563-
564-
# Delete the welcome message.
565-
string = string.replace("Welcome to antechamber", "")
566-
567-
# Extract the version and convert to float.
568-
version = float(string.split(":")[0])
569-
570-
# The version is okay, enable Open Force Field support.
571-
if version >= 22:
572-
is_compatible = True
573-
# Disable Open Force Field support.
574-
else:
575-
is_compatible = False
576-
577-
del _shlex
578-
del _subprocess
579-
580-
# Something went wrong, disable Open Force Field support.
581-
except:
582-
is_compatible = False
583-
raise
584-
585-
if not is_compatible:
586-
raise _IncompatibleError(f"'{forcefield}' requires Antechamber >= 22.0")
587-
588537
# Validate arguments.
589538

590539
if not isinstance(molecule, (_Molecule, str)):

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,29 @@ def getCurrentPressure(self, time_series=False):
19301930
"""
19311931
return self.getPressure(time_series, block=False)
19321932

1933+
def getDensity(self, time_series=False, block="AUTO"):
1934+
"""
1935+
Get the Density.
1936+
1937+
Parameters
1938+
----------
1939+
1940+
time_series : bool
1941+
Whether to return a list of time series records.
1942+
1943+
block : bool
1944+
Whether to block until the process has finished running.
1945+
1946+
Returns
1947+
-------
1948+
1949+
density : :class:`GeneralUnit <BioSimSpace.Units.Mass.kilogram / BioSimSpace.Units.Volume.meter3>`
1950+
The Density.
1951+
"""
1952+
return self.getRecord(
1953+
"DENSITY", time_series, _Units.Mass.kilogram / _Units.Volume.meter3, block
1954+
)
1955+
19331956
def getPressureDC(self, time_series=False, block="AUTO"):
19341957
"""
19351958
Get the DC pressure.
@@ -2489,7 +2512,7 @@ def _parse_energy_units(text):
24892512
elif unit == "nm/ps":
24902513
units.append(_Units.Length.nanometer / _Units.Time.picosecond)
24912514
elif unit == "kg/m^3":
2492-
units.append(_Types._GeneralUnit("kg/m3"))
2515+
units.append(_Units.Mass.kilogram / _Units.Volume.meter3)
24932516
else:
24942517
units.append(1.0)
24952518
_warnings.warn(
@@ -2935,6 +2958,11 @@ def _saveMetric(
29352958
_Units.Temperature.kelvin,
29362959
"getTemperature",
29372960
),
2961+
(
2962+
"Density (g/cm^3)",
2963+
_Units.Mass.gram / _Units.Volume.centimeter3,
2964+
"getDensity",
2965+
),
29382966
]
29392967
)
29402968
df = self._convert_datadict_keys(datadict_keys)

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

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import traceback
3333

3434
import pandas as pd
35+
from loguru import logger
3536

3637
from .._Utils import _try_import
3738

@@ -53,6 +54,7 @@
5354
from ..Protocol._protocol import Protocol as _Protocol
5455
from .._SireWrappers import System as _System
5556
from ..Types._type import Type as _Type
57+
from ..Types import Time as _Time
5658
from .. import Units as _Units
5759
from .. import _Utils
5860
from ..FreeEnergy._restraint import Restraint as _Restraint
@@ -898,7 +900,7 @@ def setSeed(self, seed):
898900
else:
899901
self._seed = seed
900902

901-
def wait(self, max_time=None):
903+
def wait(self, max_time=None, inactivity_timeout: None | _Time = None):
902904
"""
903905
Wait for the process to finish.
904906
@@ -939,11 +941,52 @@ def wait(self, max_time=None):
939941
self._process.wait(max_time)
940942

941943
else:
942-
# Wait for the process to finish.
943-
self._process.wait()
944+
if inactivity_timeout is None:
945+
# Wait for the process to finish.
946+
self._process.wait()
944947

945-
# Store the final run time.
946-
self.runTime()
948+
# Store the final run time.
949+
self.runTime()
950+
else:
951+
inactivity_timeout = int(inactivity_timeout.milliseconds().value())
952+
last_time = self._getLastTime()
953+
if last_time is None:
954+
# Wait for the process to finish.
955+
self._process.wait()
956+
957+
# Store the final run time.
958+
self.runTime()
959+
else:
960+
while self.isRunning():
961+
self._process.wait(inactivity_timeout)
962+
if self.isRunning():
963+
current_time = self._getLastTime()
964+
if current_time > last_time:
965+
logger.info(
966+
f"Current simulation time ({current_time})."
967+
)
968+
last_time = current_time
969+
else:
970+
logger.warning(
971+
f"Current simulation time ({current_time}) has not advanced compared "
972+
f"to the last time ({last_time}). The process "
973+
f"might have hung and will be killed."
974+
)
975+
with open(
976+
f"{self.workDir()}/{self._name}.out", "a+"
977+
) as f:
978+
f.write("Process Hung. Killed.")
979+
self.kill()
980+
981+
def _getLastTime(self) -> float | None:
982+
"""This is the base method in the Process base class.
983+
Each subclass, such as AMBER or GROMACS, is expected to override this method
984+
to provide their own implementation for returning the current time.
985+
986+
If this method is not overridden, it will return None,
987+
and the `inactivity_timeout` feature will be skipped.
988+
"""
989+
return None
947990

948991
def isQueued(self):
949992
"""

python/BioSimSpace/Sandpit/Exscientia/Units/Volume/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424
__author__ = "Lester Hedges"
2525
__email__ = "lester.hedges@gmail.com"
2626

27-
__all__ = ["meter3", "nanometer3", "angstrom3", "picometer3"]
27+
__all__ = ["meter3", "nanometer3", "angstrom3", "picometer3", "centimeter3"]
2828

2929
from ...Types import Volume as _Volume
3030

3131
meter3 = _Volume(1, "meter3")
3232
nanometer3 = _Volume(1, "nanometer3")
3333
angstrom3 = _Volume(1, "angstrom3")
3434
picometer3 = _Volume(1, "picometer3")
35+
centimeter3 = _Volume(1e-6, "meter3")
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from unittest.mock import MagicMock
2+
3+
import BioSimSpace.Sandpit.Exscientia as BSS
4+
from BioSimSpace.Sandpit.Exscientia.Process._process import Process
5+
6+
7+
def test_max_time():
8+
process = MagicMock()
9+
Process.wait(process, max_time=1)
10+
process._process.wait.assert_called_once_with(60000)
11+
12+
13+
def test_None_inactivity_timeout():
14+
process = MagicMock()
15+
Process.wait(process, max_time=None, inactivity_timeout=None)
16+
process._process.wait.assert_called_once()
17+
18+
19+
def test_inactivity_timeout_no_getLastTime():
20+
process = MagicMock()
21+
process._getLastTime.return_value = None
22+
Process.wait(process, max_time=None, inactivity_timeout=BSS.Units.Time.nanosecond)
23+
process._process.wait.assert_called_once()
24+
25+
26+
def test_hang(tmp_path):
27+
process = MagicMock()
28+
process.workDir.return_value = str(tmp_path)
29+
process._name = "test"
30+
# Using TEST_HANG_COUNTER to mimic simulation progress
31+
global TEST_HANG_COUNTER
32+
TEST_HANG_COUNTER = 0
33+
process.isRunning.return_value = True
34+
35+
def _getLastTime():
36+
global TEST_HANG_COUNTER
37+
TEST_HANG_COUNTER += 1
38+
# Mimic simulation hang after 10 calls
39+
return min(TEST_HANG_COUNTER, 10)
40+
41+
process._getLastTime = _getLastTime
42+
43+
def mock_kill():
44+
# Mock kill to stop the simulation
45+
process.isRunning.return_value = False
46+
47+
process.kill.side_effect = mock_kill
48+
49+
Process.wait(process, max_time=None, inactivity_timeout=BSS.Units.Time.nanosecond)
50+
51+
assert process._process.wait.call_count == 10
52+
process.kill.assert_called_once()
53+
54+
with open(f"{tmp_path}/test.out", "r") as f:
55+
assert f.read() == "Process Hung. Killed."

tests/Parameters/test_parameters.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,11 @@ def test_smiles_stereo():
169169
assert rdmol0_smiles == rdmol1_smiles
170170

171171

172+
# This test is currently skipped since it fails with AnteChamber verssion
173+
# 24.0 and above and there is no way to query the version number from
174+
# the command-line. (The version output has been removed.)
172175
@pytest.mark.skipif(
173-
has_antechamber is False or has_tleap is False,
176+
True or has_antechamber is False or has_tleap is False,
174177
reason="Requires AmberTools/antechamber and tLEaP to be installed.",
175178
)
176179
def test_acdoctor():

tests/Sandpit/Exscientia/Parameters/test_parameters.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,11 @@ def test_smiles_stereo():
174174
assert rdmol0_smiles == rdmol1_smiles
175175

176176

177+
# This test is currently skipped since it fails with AnteChamber verssion
178+
# 24.0 and above and there is no way to query the version number from
179+
# the command-line. (The version output has been removed.)
177180
@pytest.mark.skipif(
178-
has_antechamber is False or has_tleap is False,
181+
True or has_antechamber is False or has_tleap is False,
179182
reason="Requires AmberTools/antechamber and tLEaP to be installed.",
180183
)
181184
def test_acdoctor():

tests/Sandpit/Exscientia/Process/test_gromacs.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
from BioSimSpace.Sandpit.Exscientia.Units.Angle import radian
1313
from BioSimSpace.Sandpit.Exscientia.Units.Energy import kcal_per_mol, kj_per_mol
1414
from BioSimSpace.Sandpit.Exscientia.Units.Length import angstrom
15+
from BioSimSpace.Sandpit.Exscientia.Units.Mass import gram
1516
from BioSimSpace.Sandpit.Exscientia.Units.Pressure import bar
1617
from BioSimSpace.Sandpit.Exscientia.Units.Temperature import kelvin
1718
from BioSimSpace.Sandpit.Exscientia.Units.Time import picosecond
18-
from BioSimSpace.Sandpit.Exscientia.Units.Volume import nanometer3
19+
from BioSimSpace.Sandpit.Exscientia.Units.Volume import centimeter3, nanometer3
1920
from tests.Sandpit.Exscientia.conftest import (
2021
has_alchemtest,
2122
has_amber,
@@ -359,6 +360,8 @@ def setup(perturbable_system):
359360
("getPressureDC", False, -215.590363, bar),
360361
("getVolume", True, 44.679958, nanometer3),
361362
("getVolume", False, 44.523510, nanometer3),
363+
("getDensity", False, 1.027221558, gram / centimeter3),
364+
("getDensity", True, 1.023624695, gram / centimeter3),
362365
],
363366
)
364367
def test_get(self, setup, func, time_series, value, unit):
@@ -378,6 +381,7 @@ def test_metric_parquet(self, setup):
378381
assert np.isclose(df["Volume (nm^3)"][0.0], 44.679958)
379382
assert np.isclose(df["Pressure (bar)"][0.0], 119.490417)
380383
assert np.isclose(df["Temperature (kelvin)"][0.0], 306.766907)
384+
assert np.isclose(df["Density (g/cm^3)"][0.0], 1.023624695)
381385

382386
def test_dhdl_parquet_exist(self, setup):
383387
assert Path(f"{setup.workDir()}/dHdl.parquet").exists()

0 commit comments

Comments
 (0)