Skip to content

Commit

Permalink
Merge pull request #249 from Becksteinlab/drop-37
Browse files Browse the repository at this point in the history
drop python 3.7 and testing improvements
  • Loading branch information
orbeckst committed Jun 30, 2023
2 parents 5b8aa3d + ab102ed commit 172b354
Show file tree
Hide file tree
Showing 19 changed files with 203 additions and 113 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ jobs:
python-version: ["3.10"]
gromacs-version: ["4.6.5", "2018.6", "2020.6", "2021.1"]
include:
- os: ubuntu-latest
python-version: "3.7"
gromacs-version: "2021.1"
- os: ubuntu-latest
python-version: "3.8"
gromacs-version: "2021.1"
Expand Down
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Changes
EnsembleAnalysis.run() method, no longer needed (per comments, #199)
* added support for Python 3.10 (#202)
* dropped testing on Python 3.6 (PR #220, #202)
* dropped testing on Python 3.7 (minimally supported Python >= 3.8, #248)
* use pymbar >= 4 and alchemlyb >= 2 (#246)
* internal log_banner() now uses logger as argument (PR #247)

Expand All @@ -32,6 +33,8 @@ Enhancements
* new workflows module (#217)
* new automated dihedral analysis workflow (detect dihedrals with SMARTS,
analyze with EnsembleAnalysis, and generate seaborn violinplots) (#217)
* add new exit_on_error=False|True argument to run.runMD_or_exit() so
that failures just raise exceptions and not call sys.exit() (PR #249)

Fixes

Expand Down
4 changes: 2 additions & 2 deletions INSTALL.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Quick installation instructions for *MDPOW*
=============================================

MDPOW is compatible with Python 3.7+ and tested
MDPOW is compatible with Python >=3.8 and tested
on Ubuntu and Mac OS.

We recommend that you install MDPOW in a virtual environment.
Expand All @@ -28,7 +28,7 @@ GROMACS_.
Conda environment with pre-requisites
-------------------------------------

Make a conda environment with the latest packages for Python 3.7 and
Make a conda environment with the latest packages for Python 3.8 or
higher with the name *mdpow*; this installs the larger dependencies that are
pre-requisites for MDPOW::

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Installation
------------

See `INSTALL`_ for detailed instructions. MDPOW currently supports and
is tested with Python 3.7 to 3.9.
is tested with Python 3.8 to 3.10.

You will also need `Gromacs`_ (currently tested with versions 4.6.5,
2018, 2020, 2021 but 2016 and 2019 should also work).
Expand Down
32 changes: 25 additions & 7 deletions mdpow/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import errno

import gromacs.run
import gromacs.exceptions

from .config import (get_configuration, set_gromacsoutput,
NoSectionError)
Expand Down Expand Up @@ -93,7 +94,7 @@ def get_mdp_files(cfg, protocols):
logger.debug("%(protocol)s: Using MDP file %(mdp)r from config file", vars())
return mdpfiles

def runMD_or_exit(S, protocol, params, cfg, **kwargs):
def runMD_or_exit(S, protocol, params, cfg, exit_on_error=True, **kwargs):
"""run simulation
Can launch :program:`mdrun` itself (:class:`gromacs.run.MDrunner`) or exit so
Expand All @@ -107,10 +108,16 @@ def runMD_or_exit(S, protocol, params, cfg, **kwargs):
- The directory in which the simulation input files reside can be provided
as keyword argument *dirname* or taken from `S.dirs[protocol]`.
- The `exit_on_error` kwarg determines if :func:`sys.exit` is called if
mdrun fails to complete (``True``, default) or if instead we raise an
:exc:`gromacs.exceptions.GromacsError` (``False``).
- Other *kwargs* are interpreted as options for
:class:`~gromacs.tools.Mdrun`.
It never returns ``False`` but instead does a :func:`sys.exit`.
.. versionchanged:: 0.9.0
New kwarg `exit_on_error`.
"""
dirname = kwargs.pop("dirname", None)
if dirname is None:
Expand All @@ -135,18 +142,29 @@ def runMD_or_exit(S, protocol, params, cfg, **kwargs):
if not simulation_done:
# should probably stop
logger.critical("Failed %(protocol)s, investigate manually.", vars())
sys.exit(1)
if exit_on_error:
sys.exit(1)
else:
raise gromacs.exceptions.GromacsError(
f"Failed {protocol}, investigate manually.")
else:
# must check if the simulation was run externally
logfile = os.path.join(dirname, params['deffnm']+os.extsep+"log")
logger.debug("Checking logfile %r if simulation has been completed.", logfile)
simulation_done = gromacs.run.check_mdrun_success(logfile) ### broken??
if simulation_done is None:
logger.info("Now go and run %(protocol)s in directory %(dirname)r.", vars())
sys.exit(0)
if exit_on_error:
sys.exit(0)
else:
return simulation_done
elif simulation_done is False:
logger.warning("Simulation %(protocol)s in directory %(dirname)r is incomplete (log=%)logfile)s).", vars())
sys.exit(1)
logger.warning("Simulation %(protocol)s in directory %(dirname)r is incomplete (log=%(logfile)s).", vars())
if exit_on_error:
sys.exit(1)
else:
raise gromacs.exceptions.MissingDataError(
f"Simulation {protocol} in directory {dirname} is incomplete (log={logfile}).")
logger.info("Simulation %(protocol)s seems complete (log=%(logfile)s)", vars())
return simulation_done

Expand Down Expand Up @@ -272,7 +290,7 @@ def fep_simulation(cfg, solvent, **kwargs):
recommended to use ``runlocal = False`` in the run input file
and submit all window simulations to a cluster.
"""

exit_on_error = kwargs.pop('exit_on_error', True)
deffnm = kwargs.pop('deffnm', "md")
EquilSimulations = {
'water': equil.WaterSimulation,
Expand Down
2 changes: 2 additions & 0 deletions mdpow/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

RESOURCES = py.path.local(resource_filename(__name__, 'testing_resources'))

MANIFEST = RESOURCES / "manifest.yml"

MOLECULES = {
"benzene": RESOURCES.join("molecules", "benzene"),
}
Expand Down
4 changes: 2 additions & 2 deletions mdpow/tests/test_Gsolv.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

class Test_Gsolv_manual(object):

def setup(self):
def setup_method(self):
self.tmpdir = td.TempDir()
self.m = pybol.Manifest(str(RESOURCES / 'manifest.yml'))
self.m.assemble('md_npt',self.tmpdir.name)
Expand All @@ -24,7 +24,7 @@ def setup(self):
self.S.dirs.includes = os.path.join(self.tmpdir.name, 'top')
self.S.save()

def teardown(self):
def teardown_method(self):
self.tmpdir.dissolve()

def _setup(self, **kwargs):
Expand Down
13 changes: 10 additions & 3 deletions mdpow/tests/test_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,23 @@ def get_Gsolv(self, pth):

@staticmethod
def assert_DeltaA(G):
# Tests are sensitive to the versions of dependencies (probably primarily
# scipy/numpy). Only the error estimate varies (which is computed in a
# a complicated manner via numkit) but the mean is robust.
# - July 2021: with more recent versions of pandas/alchemlyb/numpy the
# original values are only reproduced to 5 decimals, see PR #166"
# - June 2023: in CI, >= 3.8 results differ from reference values (although
# locally no changes are obvious) after ~4 decimals for unknown reasons.
DeltaA = G.results.DeltaA
assert_array_almost_equal(DeltaA.Gibbs.astuple(),
(-3.7217472974883794, 2.3144288928034911),
decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166")
decimal=3)
assert_array_almost_equal(DeltaA.coulomb.astuple(),
(8.3346255170099575, 0.73620918517131495),
decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166")
decimal=3)
assert_array_almost_equal(DeltaA.vdw.astuple(),
(-4.6128782195215781, 2.1942144688960972),
decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166")
decimal=3)

def test_convert_edr(self, fep_benzene_directory):
G = self.get_Gsolv(fep_benzene_directory)
Expand Down
68 changes: 21 additions & 47 deletions mdpow/tests/test_automated_dihedral_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
import numpy as np
import pandas as pd

import rdkit
from rdkit import Chem

import seaborn

from numpy.testing import assert_almost_equal
Expand All @@ -22,7 +19,7 @@

import py.path

from ..workflows import dihedrals
from mdpow.workflows import dihedrals

from pkg_resources import resource_filename

Expand Down Expand Up @@ -76,7 +73,7 @@ def dihedral_data(self, SM25_tmp_dir, atom_indices):
(1, 2, 3, 4),(1, 12, 13, 14),(2, 3, 4, 5),(2, 3, 4, 9),
(2, 1, 12, 13),(3, 2, 1, 12),(5, 4, 3, 11),(5, 4, 3, 10),
(9, 4, 3, 11),(9, 4, 3, 10),(12, 13, 14, 15),(12, 13, 14, 19))

# tuple-tuples of dihedral atom group indices
# collected using alternate SMARTS input (explicitly defined)
# see: fixture - atom_indices().atom_group_indices_alt
Expand Down Expand Up @@ -124,48 +121,37 @@ def dihedral_data(self, SM25_tmp_dir, atom_indices):
# results included in 'df_aug'
ADG_C13141520_mean = 91.71943996962284
ADG_C13141520_var = 0.8773028474908289

def test_build_universe(self, SM25_tmp_dir):
u = dihedrals.build_universe(dirname=SM25_tmp_dir)
solute = u.select_atoms('resname UNK')
solute_names = solute.atoms.names
assert solute_names.all() == self.universe_solute_atom_names.all()

# the following 'reason' affects every downstream function that relies
# on the atom indices returned for dihedral atom group selections
# issue raised (#239) to identify and resolve exact package/version responsible
@pytest.mark.skipif(sys.version_info < (3, 8), reason='pytest=7.2.0, build=py37h89c1867_0, '
'returns incorrect atom_indices for dihedral atom group selections')
# Use set comparison because ordering of indices appears to change
# between RDKIT versions; issue raised (#239) to identify and
# resolve exact package/version responsible
def test_dihedral_indices(self, atom_indices):
atom_group_indices = atom_indices[0]
assert atom_group_indices == self.check_atom_group_indices
assert set(atom_group_indices) == set(self.check_atom_group_indices)

# the following 'reason' affects every downstream function that relies
# on the atom indices returned for dihedral atom group selections
# issue raised (#239) to identify and resolve exact package/version responsible
@pytest.mark.skipif(sys.version_info < (3, 8), reason='pytest=7.2.0, build=py37h89c1867_0, '
'returns incorrect atom_indices for dihedral atom group selections')
# Possible ordering issue (#239)
def test_SMARTS(self, atom_indices):
atom_group_indices_alt = atom_indices[1]
assert atom_group_indices_alt == self.check_atom_group_indices_alt

# the following 'reason' affects every downstream function that relies
# on the atom indices returned for dihedral atom group selections
# issue raised (#239) to identify and resolve exact package/version responsible
@pytest.mark.skipif(sys.version_info < (3, 8), reason='pytest=7.2.0, build=py37h89c1867_0, '
'returns incorrect atom_indices for dihedral atom group selections')
# Use set comparison because ordering of indices appears to change
# between RDKIT versions; issue raised (#239) to identify and
# resolve exact package/version responsible
def test_dihedral_groups(self, SM25_tmp_dir):
groups = dihedrals.dihedral_groups(dirname=SM25_tmp_dir, resname=self.resname)
i = 0
while i < len(groups):
assert groups[i].all() == self.check_groups[i].all()
i+=1

# the following 'reason' affects every downstream function that relies
# on the atom indices returned for dihedral atom group selections
# issue raised (#239) to identify and resolve exact package/version responsible
@pytest.mark.skipif(sys.version_info < (3, 8), reason='pytest=7.2.0, build=py37h89c1867_0, '
'returns incorrect atom_indices for dihedral atom group selections')

values = [g.all() for g in groups]
reference = [g.all() for g in self.check_groups]

assert set(values) == set(reference)

# Possible ordering issue (#239)
def test_dihedral_groups_ensemble(self, dihedral_data):

df = dihedral_data[0]
Expand Down Expand Up @@ -195,11 +181,7 @@ def test_save_df_info(self, dihedral_data, SM25_tmp_dir, caplog):
dihedrals.save_df(df=dihedral_data[0], df_save_dir=SM25_tmp_dir, molname='SM25')
assert f'Results DataFrame saved as {SM25_tmp_dir}/SM25/SM25_full_df.csv.bz2' in caplog.text, 'Save location not logged or returned'

# the following 'reason' affects every downstream function that relies
# on the atom indices returned for dihedral atom group selections
# issue raised (#239) to identify and resolve exact package/version responsible
@pytest.mark.skipif(sys.version_info < (3, 8), reason='pytest=7.2.0, build=py37h89c1867_0, '
'returns incorrect atom_indices for dihedral atom group selections')
# Possible ordering issue (#239)
def test_periodic_angle(self, dihedral_data):

df_aug = dihedral_data[1]
Expand All @@ -212,22 +194,14 @@ def test_periodic_angle(self, dihedral_data):
aug_dh2_mean == pytest.approx(self.ADG_C13141520_mean)
aug_dh2_var == pytest.approx(self.ADG_C13141520_var)

# the following 'reason' affects every downstream function that relies
# on the atom indices returned for dihedral atom group selections
# issue raised (#239) to identify and resolve exact package/version responsible
@pytest.mark.skipif(sys.version_info < (3, 8), reason='pytest=7.2.0, build=py37h89c1867_0, '
'returns incorrect atom_indices for dihedral atom group selections')
# Possible ordering issue (#239)
def test_save_fig(self, SM25_tmp_dir):
dihedrals.automated_dihedral_analysis(dirname=SM25_tmp_dir, figdir=SM25_tmp_dir,
resname=self.resname, molname='SM25',
solvents=('water',))
assert (SM25_tmp_dir / 'SM25' / 'SM25_C10-C5-S4-O11_violins.pdf').exists(), 'PDF file not generated'

# the following 'reason' affects every downstream function that relies
# on the atom indices returned for dihedral atom group selections
# issue raised (#239) to identify and resolve exact package/version responsible
@pytest.mark.skipif(sys.version_info < (3, 8), reason='pytest=7.2.0, build=py37h89c1867_0, '
'returns incorrect atom_indices for dihedral atom group selections')
# Possible ordering issue (#239)
def test_save_fig_info(self, SM25_tmp_dir, caplog):
caplog.clear()
caplog.set_level(logging.INFO, logger='mdpow.workflows.dihedrals')
Expand Down
4 changes: 2 additions & 2 deletions mdpow/tests/test_dihedral.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ class TestDihedral(object):
DG48910_var = 0.20311120667628546
DG491011_var = 0.006976126708773456

def setup(self):
def setup_method(self):
self.tmpdir = td.TempDir()
self.m = pybol.Manifest(str(RESOURCES / 'manifest.yml'))
self.m.assemble('example_FEP', self.tmpdir.name)
self.Ens = Ensemble(dirname=self.tmpdir.name, solvents=['water'])

def teardown(self):
def teardown_method(self):
self.tmpdir.dissolve()

def test_dataframe(self):
Expand Down
12 changes: 6 additions & 6 deletions mdpow/tests/test_ensemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@


class TestEnsemble(object):
def setup(self):
def setup_method(self):
self.tmpdir = td.TempDir()
self.m = pybol.Manifest(str(RESOURCES / 'manifest.yml'))
self.m.assemble('example_FEP', self.tmpdir.name)

def teardown(self):
def teardown_method(self):
self.tmpdir.dissolve()

def test_build_ensemble(self):
Expand Down Expand Up @@ -161,23 +161,23 @@ def _single_universe(self):

Sim = Ensemble(dirname=self.tmpdir.name, solvents=['water'])
TestRun = TestAnalysis(Sim)

with pytest.raises(NotImplementedError):
TestRun._single_frame()

def test_ensemble_analysis_run_universe(self):
class TestAnalysis(EnsembleAnalysis):
def __init__(self, test_ensemble):
super(TestAnalysis, self).__init__(test_ensemble)

self._ens = test_ensemble

def _single_frame(self):
pass

Sim = Ensemble(dirname=self.tmpdir.name, solvents=['water'])
TestRun = TestAnalysis(Sim)

with pytest.raises(NotImplementedError):
TestRun._single_universe()

Expand Down
4 changes: 2 additions & 2 deletions mdpow/tests/test_equilibration_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
from . import RESOURCES

class TestEquilibriumScript(object):
def setup(self):
def setup_method(self):
self.tmpdir = td.TempDir()
self.old_path = os.getcwd()
self.resources = RESOURCES
m = pybol.Manifest(str(self.resources / 'manifest.yml'))
m.assemble('base', self.tmpdir.name)

def teardown(self):
def teardown_method(self):
self.tmpdir.dissolve()

def _run_equil(self, solvent, dirname):
Expand Down

0 comments on commit 172b354

Please sign in to comment.