Skip to content
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

drop python 3.7 and testing improvements #249

Merged
merged 8 commits into from
Jun 30, 2023
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
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@orbeckst

Did the CI give you any trouble using this import method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but the tests should use an installed version of the package instead of the files that sit somewhere next to the tests.


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
Loading