Skip to content

Sync with Recursion remote #400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 0 additions & 51 deletions python/BioSimSpace/Parameters/_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,57 +534,6 @@ def _parameterise_openff(
"must be in your PATH."
) from None

# Check the Antechamber version. Open Force Field requires Antechamber >= 22.0.
try:
# Antechamber returns an exit code of 1 when requesting version information.
# As such, we wrap the call within a try-except block in case it fails.

import shlex as _shlex
import subprocess as _subprocess

# Generate the command-line string. (Antechamber must be in the PATH,
# so no need to use AMBERHOME.
command = "antechamber -v"

# Run the command as a subprocess.
proc = _subprocess.run(
_Utils.command_split(command),
shell=False,
text=True,
stdout=_subprocess.PIPE,
stderr=_subprocess.STDOUT,
)

# Get stdout and split into lines.
lines = proc.stdout.split("\n")

# If present, version information is on line 1.
string = lines[1]

# Delete the welcome message.
string = string.replace("Welcome to antechamber", "")

# Extract the version and convert to float.
version = float(string.split(":")[0])

# The version is okay, enable Open Force Field support.
if version >= 22:
is_compatible = True
# Disable Open Force Field support.
else:
is_compatible = False

del _shlex
del _subprocess

# Something went wrong, disable Open Force Field support.
except:
is_compatible = False
raise

if not is_compatible:
raise _IncompatibleError(f"'{forcefield}' requires Antechamber >= 22.0")

# Validate arguments.

if not isinstance(molecule, (_Molecule, str)):
Expand Down
51 changes: 0 additions & 51 deletions python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,57 +534,6 @@ def _parameterise_openff(
"must be in your PATH."
) from None

# Check the Antechamber version. Open Force Field requires Antechamber >= 22.0.
try:
# Antechamber returns an exit code of 1 when requesting version information.
# As such, we wrap the call within a try-except block in case it fails.

import shlex as _shlex
import subprocess as _subprocess

# Generate the command-line string. (Antechamber must be in the PATH,
# so no need to use AMBERHOME.
command = "antechamber -v"

# Run the command as a subprocess.
proc = _subprocess.run(
_Utils.command_split(command),
shell=False,
text=True,
stdout=_subprocess.PIPE,
stderr=_subprocess.STDOUT,
)

# Get stdout and split into lines.
lines = proc.stdout.split("\n")

# If present, version information is on line 1.
string = lines[1]

# Delete the welcome message.
string = string.replace("Welcome to antechamber", "")

# Extract the version and convert to float.
version = float(string.split(":")[0])

# The version is okay, enable Open Force Field support.
if version >= 22:
is_compatible = True
# Disable Open Force Field support.
else:
is_compatible = False

del _shlex
del _subprocess

# Something went wrong, disable Open Force Field support.
except:
is_compatible = False
raise

if not is_compatible:
raise _IncompatibleError(f"'{forcefield}' requires Antechamber >= 22.0")

# Validate arguments.

if not isinstance(molecule, (_Molecule, str)):
Expand Down
30 changes: 29 additions & 1 deletion python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1930,6 +1930,29 @@ def getCurrentPressure(self, time_series=False):
"""
return self.getPressure(time_series, block=False)

def getDensity(self, time_series=False, block="AUTO"):
"""
Get the Density.

Parameters
----------

time_series : bool
Whether to return a list of time series records.

block : bool
Whether to block until the process has finished running.

Returns
-------

density : :class:`GeneralUnit <BioSimSpace.Units.Mass.kilogram / BioSimSpace.Units.Volume.meter3>`
The Density.
"""
return self.getRecord(
"DENSITY", time_series, _Units.Mass.kilogram / _Units.Volume.meter3, block
)

def getPressureDC(self, time_series=False, block="AUTO"):
"""
Get the DC pressure.
Expand Down Expand Up @@ -2489,7 +2512,7 @@ def _parse_energy_units(text):
elif unit == "nm/ps":
units.append(_Units.Length.nanometer / _Units.Time.picosecond)
elif unit == "kg/m^3":
units.append(_Types._GeneralUnit("kg/m3"))
units.append(_Units.Mass.kilogram / _Units.Volume.meter3)
else:
units.append(1.0)
_warnings.warn(
Expand Down Expand Up @@ -2935,6 +2958,11 @@ def _saveMetric(
_Units.Temperature.kelvin,
"getTemperature",
),
(
"Density (g/cm^3)",
_Units.Mass.gram / _Units.Volume.centimeter3,
"getDensity",
),
]
)
df = self._convert_datadict_keys(datadict_keys)
Expand Down
53 changes: 48 additions & 5 deletions python/BioSimSpace/Sandpit/Exscientia/Process/_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import traceback

import pandas as pd
from loguru import logger

from .._Utils import _try_import

Expand All @@ -53,6 +54,7 @@
from ..Protocol._protocol import Protocol as _Protocol
from .._SireWrappers import System as _System
from ..Types._type import Type as _Type
from ..Types import Time as _Time
from .. import Units as _Units
from .. import _Utils
from ..FreeEnergy._restraint import Restraint as _Restraint
Expand Down Expand Up @@ -898,7 +900,7 @@ def setSeed(self, seed):
else:
self._seed = seed

def wait(self, max_time=None):
def wait(self, max_time=None, inactivity_timeout: None | _Time = None):
"""
Wait for the process to finish.

Expand Down Expand Up @@ -939,11 +941,52 @@ def wait(self, max_time=None):
self._process.wait(max_time)

else:
# Wait for the process to finish.
self._process.wait()
if inactivity_timeout is None:
# Wait for the process to finish.
self._process.wait()

# Store the final run time.
self.runTime()
# Store the final run time.
self.runTime()
else:
inactivity_timeout = int(inactivity_timeout.milliseconds().value())
last_time = self._getLastTime()
if last_time is None:
# Wait for the process to finish.
self._process.wait()

# Store the final run time.
self.runTime()
else:
while self.isRunning():
self._process.wait(inactivity_timeout)
if self.isRunning():
current_time = self._getLastTime()
if current_time > last_time:
logger.info(
f"Current simulation time ({current_time})."
)
last_time = current_time
else:
logger.warning(
f"Current simulation time ({current_time}) has not advanced compared "
f"to the last time ({last_time}). The process "
f"might have hung and will be killed."
)
with open(
f"{self.workDir()}/{self._name}.out", "a+"
) as f:
f.write("Process Hung. Killed.")
self.kill()

def _getLastTime(self) -> float | None:
"""This is the base method in the Process base class.
Each subclass, such as AMBER or GROMACS, is expected to override this method
to provide their own implementation for returning the current time.

If this method is not overridden, it will return None,
and the `inactivity_timeout` feature will be skipped.
"""
return None

def isQueued(self):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@
__author__ = "Lester Hedges"
__email__ = "lester.hedges@gmail.com"

__all__ = ["meter3", "nanometer3", "angstrom3", "picometer3"]
__all__ = ["meter3", "nanometer3", "angstrom3", "picometer3", "centimeter3"]

from ...Types import Volume as _Volume

meter3 = _Volume(1, "meter3")
nanometer3 = _Volume(1, "nanometer3")
angstrom3 = _Volume(1, "angstrom3")
picometer3 = _Volume(1, "picometer3")
centimeter3 = _Volume(1e-6, "meter3")
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from unittest.mock import MagicMock

import BioSimSpace.Sandpit.Exscientia as BSS
from BioSimSpace.Sandpit.Exscientia.Process._process import Process


def test_max_time():
process = MagicMock()
Process.wait(process, max_time=1)
process._process.wait.assert_called_once_with(60000)


def test_None_inactivity_timeout():
process = MagicMock()
Process.wait(process, max_time=None, inactivity_timeout=None)
process._process.wait.assert_called_once()


def test_inactivity_timeout_no_getLastTime():
process = MagicMock()
process._getLastTime.return_value = None
Process.wait(process, max_time=None, inactivity_timeout=BSS.Units.Time.nanosecond)
process._process.wait.assert_called_once()


def test_hang(tmp_path):
process = MagicMock()
process.workDir.return_value = str(tmp_path)
process._name = "test"
# Using TEST_HANG_COUNTER to mimic simulation progress
global TEST_HANG_COUNTER
TEST_HANG_COUNTER = 0
process.isRunning.return_value = True

def _getLastTime():
global TEST_HANG_COUNTER
TEST_HANG_COUNTER += 1
# Mimic simulation hang after 10 calls
return min(TEST_HANG_COUNTER, 10)

process._getLastTime = _getLastTime

def mock_kill():
# Mock kill to stop the simulation
process.isRunning.return_value = False

process.kill.side_effect = mock_kill

Process.wait(process, max_time=None, inactivity_timeout=BSS.Units.Time.nanosecond)

assert process._process.wait.call_count == 10
process.kill.assert_called_once()

with open(f"{tmp_path}/test.out", "r") as f:
assert f.read() == "Process Hung. Killed."
5 changes: 4 additions & 1 deletion tests/Parameters/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,11 @@ def test_smiles_stereo():
assert rdmol0_smiles == rdmol1_smiles


# This test is currently skipped since it fails with AnteChamber verssion
# 24.0 and above and there is no way to query the version number from
# the command-line. (The version output has been removed.)
@pytest.mark.skipif(
has_antechamber is False or has_tleap is False,
True or has_antechamber is False or has_tleap is False,
reason="Requires AmberTools/antechamber and tLEaP to be installed.",
)
def test_acdoctor():
Expand Down
5 changes: 4 additions & 1 deletion tests/Sandpit/Exscientia/Parameters/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,11 @@ def test_smiles_stereo():
assert rdmol0_smiles == rdmol1_smiles


# This test is currently skipped since it fails with AnteChamber verssion
# 24.0 and above and there is no way to query the version number from
# the command-line. (The version output has been removed.)
@pytest.mark.skipif(
has_antechamber is False or has_tleap is False,
True or has_antechamber is False or has_tleap is False,
reason="Requires AmberTools/antechamber and tLEaP to be installed.",
)
def test_acdoctor():
Expand Down
6 changes: 5 additions & 1 deletion tests/Sandpit/Exscientia/Process/test_gromacs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
from BioSimSpace.Sandpit.Exscientia.Units.Angle import radian
from BioSimSpace.Sandpit.Exscientia.Units.Energy import kcal_per_mol, kj_per_mol
from BioSimSpace.Sandpit.Exscientia.Units.Length import angstrom
from BioSimSpace.Sandpit.Exscientia.Units.Mass import gram
from BioSimSpace.Sandpit.Exscientia.Units.Pressure import bar
from BioSimSpace.Sandpit.Exscientia.Units.Temperature import kelvin
from BioSimSpace.Sandpit.Exscientia.Units.Time import picosecond
from BioSimSpace.Sandpit.Exscientia.Units.Volume import nanometer3
from BioSimSpace.Sandpit.Exscientia.Units.Volume import centimeter3, nanometer3
from tests.Sandpit.Exscientia.conftest import (
has_alchemtest,
has_amber,
Expand Down Expand Up @@ -359,6 +360,8 @@ def setup(perturbable_system):
("getPressureDC", False, -215.590363, bar),
("getVolume", True, 44.679958, nanometer3),
("getVolume", False, 44.523510, nanometer3),
("getDensity", False, 1.027221558, gram / centimeter3),
("getDensity", True, 1.023624695, gram / centimeter3),
],
)
def test_get(self, setup, func, time_series, value, unit):
Expand All @@ -378,6 +381,7 @@ def test_metric_parquet(self, setup):
assert np.isclose(df["Volume (nm^3)"][0.0], 44.679958)
assert np.isclose(df["Pressure (bar)"][0.0], 119.490417)
assert np.isclose(df["Temperature (kelvin)"][0.0], 306.766907)
assert np.isclose(df["Density (g/cm^3)"][0.0], 1.023624695)

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