diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 393fc044..7fbc8547 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,20 +22,29 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macOS-latest] - python-version: [2.7] - #python-version: [2.7, 3.8] - #python-version: [2.7, 3.6, 3.7, 3.8, 3.9] + # only test all GROMACS version on the oldest and latest + # Python to keep the testing matrix manageable and only use 2 + # macos runners (latest GROMACS, oldes and latest Python) + + os: [ubuntu-latest] + python-version: [2.7, 3.9] gromacs-version: ["4.6.5", "2018.6", "2020.6", "2021.1"] - # only test one GROMACS version on macOS to keep the testing - # matrix manageable - exclude: - - os: macOS-latest - gromacs-version: "4.6.5" - - os: macOS-latest - gromacs-version: "2018.6" - - os: macOS-latest - gromacs-version: "2020.6" + include: + - os: ubuntu-latest + python-version: 3.6 + gromacs-version: 2021.1 + - os: ubuntu-latest + python-version: 3.7 + gromacs-version: 2021.1 + - os: ubuntu-latest + python-version: 3.8 + gromacs-version: 2021.1 + - os: macos-latest + python-version: 2.7 + gromacs-version: 2021.1 + - os: macos-latest + python-version: 3.9 + gromacs-version: 2021.1 env: MPLBACKEND: agg diff --git a/AUTHORS b/AUTHORS index 8fd4b3d6..8c220340 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,4 +32,4 @@ their first commit. GitHub handle is optional. 2021 ---- -- Alia Lescoulie (ALescoul) \ No newline at end of file +- Alia Lescoulie (ALescoulie) diff --git a/CHANGES b/CHANGES index f3c0c37f..0684102d 100644 --- a/CHANGES +++ b/CHANGES @@ -6,11 +6,13 @@ Add summary of changes for each release. Use ISO dates. Reference GitHub issues numbers and PR numbers. 2021-07-xx 0.7.0 -orbeckst, VOD555, ALescoul +orbeckst, VOD555, ALescoulie Changes * renamed package to MDPOW +* support Python 3.7 -- 3.9 on Linux and macOS (#84) +* tested with GROMACS 4.6.5, 2018.6, 2020.6, 2021.1 (PR #159, #164) * removed all generated docs from package * config parser MERGES user runinput.yml with the package defaults (#8) @@ -50,7 +52,7 @@ Enhancements * supported CHARMM and AMBER forcefield, including PRM parameter files with the "setup.prm" parameter in the configuration file (#104) * supported wet-octanol mixed solvent boxtype but this only works with - GROMACS >= 2018 (#98) + GROMACS >= 2018 (#98) * support OPLS-AA TIP4PD, AMBER TIP4PEW and cyclohexane, and CHARMM TIP5P and cyclohexane solvent types (#141) * supported forcefield options in scripts (#123) @@ -72,7 +74,7 @@ Fixes found. Either supply the name of the right checkpoint file or do not use -append": mdpow.run.runMD_or_exit() does not anymore add -append to GROMACS invocation (#128) -* fixed mdpow-pow and mdpow-pcw scripts (#138) +* fixed mdpow-pow and mdpow-pcw scripts (#138) * fixed template em_charmm.mdp to use standard CHARMM non-bonded parameters for energy minimization (PR #155) diff --git a/INSTALL.rst b/INSTALL.rst index aea04f13..0d769817 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -2,10 +2,9 @@ Quick installation instructions for *MDPOW* ============================================= -**Only Python 2.7 is supported** (Python 3 is *not* supported, see -`#84 `_). Python 2.7 -is rock-stable and frozen but not officially supported anymore by the -Python developers. +MDPOW is compatible with Python 2.7 and 3.7+ and tested +on Ubuntu and Mac OS. Python 2.7 is rock-stable and frozen but +not officially supported anymore by the Python developers. We recommend that you install MDPOW in a virtual environment. @@ -30,14 +29,24 @@ GROMACS_. Conda environment with pre-requisites ------------------------------------- -We make a conda environment with the latest packages for Python 2.7 +To make a conda environment with the latest packages for Python 2.7 and name it *mdpow*; this installs the larger dependencies that are pre-requisites for MDPOW:: - conda create -c conda-forge -n mdpow python=2.7 numpy scipy 'matplotlib<3.3' 'mdanalysis<2' 'mdanalysistests<2' pyyaml + conda create -c conda-forge -n mdpow python=2.7 numpy scipy 'matplotlib<3.3' 'mdanalysis<2' 'mdanalysistests<2' pyyaml six conda activate mdpow pip install gromacswrapper + +For Python 3.7 and up. + +*Note:* with Pandas version 1.3 there is an error with `Alchemlyb `_ +(see `issue #147 `_) which will be fixed in Alchemlyb 0.5.:: + + conda create -c conda-forge -n mdpow python=3.7 numpy scipy 'matplotlib' 'mdanalysis' 'mdanalysistests' pyyaml six + conda activate mdpow + pip install gromacswrapper + Installation from source ------------------------ diff --git a/README.rst b/README.rst index 56242305..21f90abf 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ *MDPOW* is a python package that automates the calculation of solvation free energies via molecular dynamics (MD) simulations. In particular, it facilitates the computation of partition -coeffcients. Currently implemented: +coefficients. Currently implemented: - *water-octanol* partition coefficient (|P_ow|) - *water-cyclohexane* partition coefficient (|P_cw|) @@ -43,8 +43,7 @@ Documentation Installation ------------ -See `INSTALL`_ for detailed instructions. Note that -**only Python 2.7** is supported. +See `INSTALL`_ for detailed instructions. MDPOW currently supports Python 2.7 and Python 3.7 to 3.9. You will also need `Gromacs`_ (currently tested with versions 4.6.5, 2018, 2020, 2021 but 2016 and 2019 should also work). diff --git a/doc/sphinx/source/index.txt b/doc/sphinx/source/index.txt index 4e647657..e9c79f9a 100644 --- a/doc/sphinx/source/index.txt +++ b/doc/sphinx/source/index.txt @@ -43,9 +43,9 @@ software package [#GromacsWrapperFramework]_. MDPOW is tested with * Gromacs 4.6.5 * Gromacs 2018.6 * Gromacs 2020.6 -* Gromacs 2021.1 +* Gromacs 2021.1 -but versions 5.x, 2016.x, and 2019.x should also work. +but versions 5.x, 2016.x, and 2019.x should also work. It should be possible to use any of these Gromacs versions without further adjustments, thanks to the underlying GromacsWrapper library [#GromacsWrapperFramework]_. @@ -99,9 +99,9 @@ also change (rarely) between MINOR releases. *MINOR* releases can introduce new functionality or deprecate old ones. The version information can be accessed from the attribute -:data:`gromacs.__version__`. +:data:`mdpow.__version__`. -.. autodata:: gromacs.__version__ +.. autodata:: mdpow.__version__ .. _semantic versioning: https://semver.org @@ -126,7 +126,7 @@ For current issues and open feature requests please look through the -.. Hide to use with alabaster +.. Hide to use with RTD theme .. Contents: .. toctree:: diff --git a/mdpow/config.py b/mdpow/config.py index f1a41d64..382c42eb 100644 --- a/mdpow/config.py +++ b/mdpow/config.py @@ -90,18 +90,20 @@ """ -from __future__ import absolute_import +from __future__ import absolute_import, division + +import six import os, errno from pkg_resources import resource_filename, resource_listdir import yaml import numpy as np +import gromacs.utilities import logging logger = logging.getLogger("mdpow.config") - # Reading of configuration files # ------------------------------ @@ -112,15 +114,15 @@ def merge_dicts(user, default): """Merge two dictionaries recursively. - - Based on https://stackoverflow.com/a/823240/334357 + Uses recursive method to accurately + merge nested dictionaries """ - if isinstance(user, dict) and isinstance(default, dict): - for k, v in default.iteritems(): - if k not in user: - user[k] = v - else: - user[k] = merge_dicts(user[k], v) + for key in default: + if key in user: + if isinstance(user[key], dict) and isinstance(default[key], dict): + merge_dicts(user[key], default[key]) + else: + user[key] = default[key] return user @@ -167,8 +169,12 @@ def get(self, section, option): Prior versions would convert case-insensitively (e.g. "NONE" and "none") """ - value = self.conf[section][option] - return value if value != "None" else None + + try: + value = self.conf[section][option] + return value if value != "None" else None + except TypeError: + return None # TODO: # The YAML parser does automatic conversion: the following @@ -233,7 +239,7 @@ def get_configuration(filename=None): def modify_gromacs_environment(name, value): from gromacs.environment import flags if flags[name] != value: - logger.warn("Changing GromacsWrapper environment: flags[%(name)r] = %(value)r", vars()) + logger.warning("Changing GromacsWrapper environment: flags[%(name)r] = %(value)r", vars()) flags[name] = value def set_gromacsoutput(cfg): @@ -285,7 +291,8 @@ def get_template(t): :Raises: :exc:`ValueError` if no file can be located. """ - templates = [_get_template(s) for s in asiterable(t)] + # Not sure if this is the best way to get asiterables + templates = [_get_template(s) for s in gromacs.utilities.asiterable(t)] if len(templates) == 1: return templates[0] return templates @@ -308,7 +315,7 @@ def get_templates(t): :Raises: :exc:`ValueError` if no file can be located. """ - return [_get_template(s) for s in utilities.asiterable(t)] + return [_get_template(s) for s in gromacs.utilities.asiterable(t)] def _get_template(t): """Return a single template *t*.""" @@ -339,8 +346,8 @@ def _get_template(t): def iterable(obj): """Returns ``True`` if *obj* can be iterated over and is *not* a string.""" - if isinstance(obj, basestring): - return False # avoid iterating over characters of a string + if isinstance(obj, six.string_types): + return False # avoid iterating over characters of a string if hasattr(obj, 'next'): return True # any iterator will do @@ -416,6 +423,6 @@ def asiterable(obj): logger.info("Using the bundled force fields from GMXLIB=%(includedir)r.", vars()) logger.info("If required, override this behaviour by setting the environment variable GMXLIB yourself.") else: - logger.warn("Using user-supplied environment variable GMXLIB=%r to find force fields", os.environ['GMXLIB']) + logger.warning("Using user-supplied environment variable GMXLIB=%r to find force fields", os.environ['GMXLIB']) logger.info("(You can use the MDPOW default by executing 'unset GMXLIB' in your shell before running MDPOW.)") diff --git a/mdpow/equil.py b/mdpow/equil.py index e14f6902..259aa357 100644 --- a/mdpow/equil.py +++ b/mdpow/equil.py @@ -29,11 +29,13 @@ .. autodata:: DIST """ -from __future__ import absolute_import, with_statement +from __future__ import absolute_import, division + +from six.moves import cPickle as pickle import os, errno import shutil -import cPickle + import MDAnalysis as mda try: @@ -227,7 +229,7 @@ def save(self, filename=None): else: self.filename = filename with open(filename, 'wb') as f: - cPickle.dump(self, f, protocol=cPickle.HIGHEST_PROTOCOL) + pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL) logger.debug("Instance pickled to %(filename)r" % vars()) def load(self, filename=None): @@ -238,7 +240,7 @@ def load(self, filename=None): logger.warning("No filename known, trying name %r", self.filename) filename = self.filename with open(filename, 'rb') as f: - instance = cPickle.load(f) + instance = pickle.load(f) self.__dict__.update(instance.__dict__) logger.debug("Instance loaded from %(filename)r" % vars()) @@ -273,8 +275,8 @@ def assinglet(m): self.mdp[key] = fn.replace(basedir, prefix) except AttributeError: pass - logger.warn("make_paths_relative(): check/manually adjust %s.dirs.includes = %r !", - self.__class__.__name__, self.dirs.includes) + logger.warning("make_paths_relative(): check/manually adjust %s.dirs.includes = %r !", + self.__class__.__name__, self.dirs.includes) def topology(self, itp='drug.itp', prm=None, **kwargs): """Generate a topology for compound *molecule*. @@ -305,7 +307,7 @@ def topology(self, itp='drug.itp', prm=None, **kwargs): itp = os.path.realpath(itp) _itp = os.path.basename(itp) - if prm==None: + if prm is None: prm_kw = '' else: prm = os.path.realpath(prm) @@ -466,8 +468,8 @@ def _MD(self, protocol, **kwargs): # so instead of fuffing with GMXLIB we just dump it into the directory try: shutil.copy(config.topfiles['residuetypes.dat'], self.dirs[protocol]) - except: - logger.warn("Failed to copy 'residuetypes.dat': mdrun will likely fail to write a final structure") + except IOError: + logger.warning("Failed to copy 'residuetypes.dat': mdrun will likely fail to write a final structure") self.journal.completed(protocol) return params diff --git a/mdpow/fep.py b/mdpow/fep.py index 753deb5a..efd149c3 100644 --- a/mdpow/fep.py +++ b/mdpow/fep.py @@ -120,35 +120,41 @@ See `Free Energy Tutorial`_. """ -from __future__ import absolute_import, with_statement +from __future__ import absolute_import, division + +import six +from six.moves import zip +from six.moves.configparser import NoOptionError import os import errno import copy from subprocess import call import warnings +from glob import glob import numpy import pandas as pd import scipy.integrate from scipy import constants + import numkit.integration import numkit.timeseries +from numkit.observables import QuantityWithError from alchemlyb.parsing.gmx import extract_dHdl, extract_u_nk from alchemlyb.estimators import TI, BAR, MBAR from alchemlyb.parsing.gmx import _extract_dataframe from pymbar.timeseries import (statisticalInefficiency, subsampleCorrelatedData, ) -import gromacs, gromacs.utilities +import gromacs +import gromacs.utilities try: import gromacs.setup except (ImportError, OSError): raise ImportError("Gromacs installation not found, source GMXRC?") from gromacs.utilities import asiterable, AttributeDict, in_dir, openany -from numkit.observables import QuantityWithError -from glob import glob import logging logger = logging.getLogger('mdpow.fep') @@ -230,7 +236,6 @@ def mdp_dict(self): @staticmethod def load(cfg, section): """Initialize a :class:`FEPschedule` from the *section* in the configuration *cfg*""" - keys = {} keys.update(FEPschedule.mdp_keywords) keys.update(FEPschedule.meta_keywords) @@ -244,15 +249,15 @@ def load(cfg, section): def getter(type, section, key): try: return cfg_get[type](section, key) - except ConfigParser.NoOptionError: + except NoOptionError: return None return FEPschedule((key, getter(keytype, section, key)) for key,keytype in keys.items() if getter(keytype, section, key) is not None) def __deepcopy__(self, memo): - x = type(self)() - for k,v in self.iteritems(): - x[k] = copy.deepcopy(v, memo) + x = FEPschedule() + for k, v in six.iteritems(self): + x[k] = copy.deepcopy(v) return x class Gsolv(Journalled): @@ -471,7 +476,8 @@ def __init__(self, molecule=None, top=None, struct=None, method="BAR", **kwargs) self.mdp = kwargs.pop('mdp', config.get_template(self.mdp_default)) # schedules (deepcopy because we might modify) - self.schedules = copy.deepcopy(self.schedules_default) + # For some reason 2.7 tests failed with deepcopy in 2.7 so used merge_dict instead + self.schedules = config.merge_dicts(self.schedules_default, {}) schedules = kwargs.pop('schedules', {}) self.schedules.update(schedules) self.lambdas = { @@ -504,7 +510,7 @@ def __init__(self, molecule=None, top=None, struct=None, method="BAR", **kwargs) wmsg = "Directory %(dirname)r already exists --- will overwrite " \ "existing files." % vars(self) warnings.warn(wmsg) - logger.warn(wmsg) + logger.warning(wmsg) # overrides pickle file so that we can run from elsewhere if not basedir is None: @@ -720,8 +726,8 @@ def dgdl_edr(self, *args): pattern = os.path.join(*args + (self.deffnm + '*.edr',)) edrs = glob(pattern) if not edrs: - logger.error("Missing dgdl.edr file %(pattern)r.", vars()) - raise IOError(errno.ENOENT, "Missing dgdl.edr file", pattern) + logger.error("Missing dgdl.edr file %(pattern)r.", vars()) + raise IOError(errno.ENOENT, "Missing dgdl.edr file", pattern) return [os.path.abspath(i) for i in edrs] def dgdl_tpr(self, *args): @@ -834,8 +840,8 @@ def compress_dgdl_xvg(self): # speed is similar to 'bzip2 -9 FILE' (using a 1 Mio buffer) # (Since GW 0.8, openany() does not take kwargs anymore so the write buffer cannot be # set anymore (buffering=1048576) so the performance might be lower in MDPOW >= 0.7.0) - with open(xvg, 'r', buffering=1048576) as source: - with openany(fnbz2, 'w') as target: + with open(xvg, 'rb', buffering=1048576) as source: + with openany(fnbz2, 'wb') as target: target.writelines(source) if os.path.exists(fnbz2) and os.path.exists(xvg): os.unlink(xvg) @@ -855,7 +861,6 @@ def contains_corrupted_xvgs(self): :attr:`Gsolv._corrupted` as dicts of dicts with the component as primary and the lambda as secondary key. """ - from itertools import izip def _lencorrupted(xvg): try: return len(xvg.corrupted_lineno) @@ -867,7 +872,7 @@ def _lencorrupted(xvg): self._corrupted = {} # debugging ... for component, (lambdas, xvgs) in self.results.xvg.items(): corrupted[component] = numpy.any([(_lencorrupted(xvg) > 0) for xvg in xvgs]) - self._corrupted[component] = dict(((l, _lencorrupted(xvg)) for l,xvg in izip(lambdas, xvgs))) + self._corrupted[component] = dict(((l, _lencorrupted(xvg)) for l,xvg in zip(lambdas, xvgs))) return numpy.any([x for x in corrupted.values()]) def analyze(self, force=False, stride=None, autosave=True, ncorrel=25000): @@ -960,7 +965,7 @@ def analyze(self, force=False, stride=None, autosave=True, ncorrel=25000): self.convert_edr() self.collect(stride=stride, autosave=False) else: - logger.exception() + logger.exception(err) raise else: logger.info("Analyzing stored data.") @@ -1064,7 +1069,7 @@ def analyze_alchemlyb(self, SI=True, start=0, stop=None, stride=None, force=Fals self.convert_edr() self.collect_alchemlyb(SI, start, stop, stride, autosave=False) else: - logger.exception() + logger.exception(err) raise else: logger.info("Analyzing stored data.") @@ -1384,14 +1389,15 @@ def p_transfer(G1, G2, **kwargs): logger.info("The solvent is %s .", G.solvent_type) logger.info("Estimator is %s.", estimator) logger.info("Free energy calculation method is %s.", G.method) - if estimator == 'mdpow': - G.analyze(**G_kwargs) - elif estimator == 'alchemlyb': - if G_kwargs['SI']: - logger.info("Statistical inefficiency analysis will be performed.") - else: - logger.info("Statistical inefficiency analysis won't be performed.") - G.analyze_alchemlyb(**G_kwargs) + + if estimator == 'mdpow': + G.analyze(**G_kwargs) + elif estimator == 'alchemlyb': + if G_kwargs['SI']: + logger.info("Statistical inefficiency analysis will be performed.") + else: + logger.info("Statistical inefficiency analysis won't be performed.") + G.analyze_alchemlyb(**G_kwargs) # x.Gibbs are QuantityWithError so they do error propagation transferFE = G2.results.DeltaA.Gibbs - G1.results.DeltaA.Gibbs diff --git a/mdpow/filelock.py b/mdpow/filelock.py index 4f8a93cf..9bf69f7a 100644 --- a/mdpow/filelock.py +++ b/mdpow/filelock.py @@ -62,7 +62,7 @@ def acquire(self): while True: try: self.fd = os.open(self.lockfile, os.O_CREAT|os.O_EXCL|os.O_RDWR) - break; + break except OSError as e: if e.errno != errno.EEXIST: raise diff --git a/mdpow/forcefields.py b/mdpow/forcefields.py index c0a570b2..e7683e8b 100644 --- a/mdpow/forcefields.py +++ b/mdpow/forcefields.py @@ -57,7 +57,6 @@ import logging logger = logging.getLogger("mdpow.forecefields") - #: Default force field. At the moment, only OPLS-AA is directly #: supported. DEFAULT_FORCEFIELD = "OPLS-AA" diff --git a/mdpow/restart.py b/mdpow/restart.py index e58248c8..1fb77c71 100644 --- a/mdpow/restart.py +++ b/mdpow/restart.py @@ -23,9 +23,10 @@ """ from __future__ import absolute_import +from six.moves import cPickle as pickle + import os import errno -import cPickle import logging logger = logging.getLogger('mdpow.checkpoint') @@ -240,7 +241,7 @@ def save(self, filename=None): else: self.filename = os.path.abspath(filename) with open(self.filename, 'wb') as f: - cPickle.dump(self, f, protocol=cPickle.HIGHEST_PROTOCOL) + pickle.dump(self, f) logger.debug("Instance pickled to %(filename)r" % vars(self)) def load(self, filename=None): @@ -260,6 +261,6 @@ def load(self, filename=None): logger.error(errmsg) raise ValueError(errmsg) with open(filename, 'rb') as f: - instance = cPickle.load(f) + instance = pickle.load(f) self.__dict__.update(instance.__dict__) logger.debug("Instance loaded from %(filename)r" % vars()) diff --git a/mdpow/run.py b/mdpow/run.py index b20c7367..7b2282b5 100644 --- a/mdpow/run.py +++ b/mdpow/run.py @@ -39,6 +39,8 @@ from __future__ import absolute_import +from six.moves import configparser + import sys import os import errno @@ -71,8 +73,6 @@ def setupMD(S, protocol, cfg): def get_mdp_files(cfg, protocols): """Get file names of MDP files from *cfg* for all *protocols*""" - import ConfigParser - mdpfiles = {} for protocol in protocols: try: @@ -81,7 +81,7 @@ def get_mdp_files(cfg, protocols): # skip anything for which we do not define sections, such as # the dummy run protocols mdp = None - except ConfigParser.NoOptionError: + except configparser.NoOptionError: # Should not happen... let's continue and wait for hard-coded defaults logger.error("No 'mdp' config file entry for protocol [%s]---check input files!", protocol) mdp = None @@ -147,7 +147,7 @@ def runMD_or_exit(S, protocol, params, cfg, **kwargs): logger.info("Now go and run %(protocol)s in directory %(dirname)r.", vars()) sys.exit(0) elif simulation_done is False: - logger.warn("Simulation %(protocol)s in directory %(dirname)r is incomplete (log=%)logfile)s).", vars()) + logger.warning("Simulation %(protocol)s in directory %(dirname)r is incomplete (log=%)logfile)s).", vars()) sys.exit(1) logger.info("Simulation %(protocol)s seems complete (log=%(logfile)s)", vars()) return simulation_done @@ -306,7 +306,7 @@ def fep_simulation(cfg, solvent, **kwargs): equil_savefilename = os.path.join(topdir, "%(solvent)s.simulation" % vars()) try: equil_S = EquilSimulation(filename=equil_savefilename) - except IOError, err: + except IOError as err: if err.errno == errno.ENOENT: logger.critical("Missing the equilibrium simulation %(equil_savefilename)r.", vars()) logger.critical("Run `mdpow-equilibrium -S %s %s' first!", solvent, "RUNINPUT.cfg") @@ -320,7 +320,7 @@ def fep_simulation(cfg, solvent, **kwargs): logger.info("Running FEP with saved settings from %r", savefilename) logger.info("Note: all configuration file options are ignored!") else: - # method to be used "TI"/"BAR" + # method to be used "TI"/"BAR" method = cfg.get("FEP", "method") logger.info("Running FEP with the %r implementation in Gromacs", method) # custom mdp file diff --git a/mdpow/tests/test_Gsolv.py b/mdpow/tests/test_Gsolv.py index 063067c8..6f60f722 100644 --- a/mdpow/tests/test_Gsolv.py +++ b/mdpow/tests/test_Gsolv.py @@ -1,4 +1,7 @@ -import tempdir as td +from __future__ import absolute_import + +from . import tempdir as td + import os import pybol import numpy as np diff --git a/mdpow/tests/test_analysis.py b/mdpow/tests/test_analysis.py index 436a305f..8b4179a7 100644 --- a/mdpow/tests/test_analysis.py +++ b/mdpow/tests/test_analysis.py @@ -1,4 +1,8 @@ +from __future__ import absolute_import + import os.path +import sys + import pytest import py.path @@ -63,7 +67,12 @@ def fep_benzene_directory(tmpdir_factory): class TestAnalyze(object): def get_Gsolv(self, pth): gsolv = pth.join("FEP", "water", "Gsolv.fep") - G = pickle.load(gsolv.open()) + if sys.version_info.major == 2: + G = pickle.load(gsolv.open()) + elif sys.version_info.major == 3: + # Needed to read old pickle files + with open(gsolv, 'rb') as f: + G = pickle.load(f, encoding='latin1') # patch paths G.basedir = pth.strpath G.filename = gsolv.strpath @@ -74,14 +83,13 @@ def assert_DeltaA(G): DeltaA = G.results.DeltaA assert_array_almost_equal(DeltaA.Gibbs.astuple(), (-3.7217472974883794, 2.3144288928034911), - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") assert_array_almost_equal(DeltaA.coulomb.astuple(), (8.3346255170099575, 0.73620918517131495), - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") assert_array_almost_equal(DeltaA.vdw.astuple(), (-4.6128782195215781, 2.1942144688960972), - decimal=6) - + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") def test_convert_edr(self, fep_benzene_directory): G = self.get_Gsolv(fep_benzene_directory) diff --git a/mdpow/tests/test_analysis_alchemlyb.py b/mdpow/tests/test_analysis_alchemlyb.py index 67388f27..fecd6738 100644 --- a/mdpow/tests/test_analysis_alchemlyb.py +++ b/mdpow/tests/test_analysis_alchemlyb.py @@ -1,4 +1,8 @@ +from __future__ import absolute_import + import os.path +import sys + import pytest import py.path @@ -6,6 +10,7 @@ import pybol from numpy.testing import assert_array_almost_equal +import pandas from six.moves import cPickle as pickle @@ -62,10 +67,16 @@ def fep_benzene_directory(tmpdir_factory): class TestAnalyze(object): def get_Gsolv(self, pth): gsolv = pth.join("FEP", "water", "Gsolv.fep") - G = pickle.load(gsolv.open()) - # patch paths + # Needed to load old pickle files in python 3 + if sys.version_info.major >= 3: + with open(gsolv, 'rb') as f: + G = pickle.load(f, encoding='latin1') + # patch paths + elif sys.version_info.major == 2: + G = pickle.load(gsolv.open()) G.basedir = pth.strpath G.filename = gsolv.strpath + return G @pytest.mark.parametrize('method, Gibbs, coulomb, vdw', [ @@ -82,6 +93,9 @@ def get_Gsolv(self, pth): (8.241836, 0.219235), (-1.448719, 0.421548)) ]) + + @pytest.mark.xfail(pandas.__version__.startswith("1.3.0"), + reason="bug in pandas 1.3.0 see alchemistry/alchemlyb#147") def test_estimator_alchemlyb(self, fep_benzene_directory, method, Gibbs, coulomb, vdw): G = self.get_Gsolv(fep_benzene_directory) @@ -100,12 +114,14 @@ def test_estimator_alchemlyb(self, fep_benzene_directory, method, err.strerror, err.filename)) DeltaA = G.results.DeltaA assert_array_almost_equal(DeltaA.Gibbs.astuple(), Gibbs, - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") assert_array_almost_equal(DeltaA.coulomb.astuple(), coulomb, - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") assert_array_almost_equal(DeltaA.vdw.astuple(), vdw, - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") + @pytest.mark.xfail(pandas.__version__.startswith("1.3.0"), + reason="bug in pandas 1.3.0 see alchemistry/alchemlyb#147") def test_SI(self, fep_benzene_directory): G = self.get_Gsolv(fep_benzene_directory) G.method = 'TI' @@ -119,12 +135,14 @@ def test_SI(self, fep_benzene_directory): err.strerror, err.filename)) DeltaA = G.results.DeltaA assert_array_almost_equal(DeltaA.Gibbs.astuple(), (-2.908885, 2.175976), - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") assert_array_almost_equal(DeltaA.coulomb.astuple(), (7.755779, 0.531481), - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") assert_array_almost_equal(DeltaA.vdw.astuple(), (-4.846894, 2.110071), - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") + @pytest.mark.xfail(pandas.__version__.startswith("1.3.0"), + reason="bug in pandas 1.3.0 see alchemistry/alchemlyb#147") def test_start_stop_stride(self, fep_benzene_directory): G = self.get_Gsolv(fep_benzene_directory) G.method = 'TI' @@ -139,8 +157,8 @@ def test_start_stop_stride(self, fep_benzene_directory): err.strerror, err.filename)) DeltaA = G.results.DeltaA assert_array_almost_equal(DeltaA.Gibbs.astuple(), (-3.318109, 0.905128), - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") assert_array_almost_equal(DeltaA.coulomb.astuple(), (8.146806, 0.348866), - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") assert_array_almost_equal(DeltaA.vdw.astuple(), (-4.828696, 0.835195), - decimal=6) + decimal=5) # with more recent versions of pandas/alchemlyb/numpy the original values are only reproduced to 5 decimals, see PR #166") diff --git a/mdpow/tests/test_emin.py b/mdpow/tests/test_emin.py index b745cc91..f0b550a8 100644 --- a/mdpow/tests/test_emin.py +++ b/mdpow/tests/test_emin.py @@ -1,5 +1,8 @@ +from __future__ import absolute_import + +from . import tempdir as td + import mdpow.equil -import tempdir as td import os class TestEnergyMinimization(object): diff --git a/mdpow/tests/test_equilibration_script.py b/mdpow/tests/test_equilibration_script.py index a031f206..34be0d06 100644 --- a/mdpow/tests/test_equilibration_script.py +++ b/mdpow/tests/test_equilibration_script.py @@ -1,5 +1,8 @@ +from __future__ import absolute_import + +from . import tempdir as td + import os.path -import tempdir as td import pybol from gromacs.utilities import in_dir diff --git a/mdpow/tests/test_fep.py b/mdpow/tests/test_fep.py index 81ed75fc..d63282c9 100644 --- a/mdpow/tests/test_fep.py +++ b/mdpow/tests/test_fep.py @@ -1,10 +1,18 @@ +from __future__ import absolute_import + +import sys +from six.moves import reload_module + +import pytest + import numpy as np from numpy.testing import assert_array_almost_equal, assert_almost_equal from scipy import constants +from copy import deepcopy -import mdpow +import mdpow.config import mdpow.fep - +import gromacs def test_molar_to_nm3(): assert_almost_equal(mdpow.fep.molar_to_nm3(1.5), 0.9033212684) @@ -58,6 +66,40 @@ def test_VDW(self): def test_Coulomb(self): return self._test_schedule('Coulomb') + @pytest.mark.parametrize('component', ['VDW', 'Coulomb']) + def test_copy(self, component): + section = 'FEP_schedule_{0}'.format(component) + schedule = deepcopy(mdpow.fep.FEPschedule.load(self.cfg, section)) + reference = self.reference[component] + + for k in schedule: + assert k in reference, "additional entry {0} in runinput.yml".format(k) + + for k in reference: + assert k in schedule, "missing entry {0} in runinput.yml".format(k) + + for k in schedule.keys(): + if k == "lambdas": + assert_array_almost_equal(schedule[k], reference[k], + err_msg="FEP schedule {0} mismatch".format(k)) + else: + assert schedule[k] == reference[k], \ + "mismatch between loaded FEP schedule entry {0} and reference".format(k) + + @pytest.mark.parametrize('component', ['VDW', 'Coulomb']) + def test_write(self, component): + self.cfg.write('cfg.yaml') + new_cfg = mdpow.config.get_configuration('cfg.yaml') + assert new_cfg.conf == self.cfg.conf + + def test_get(self): + tmp_cfg = mdpow.config.POWConfigParser() + item = tmp_cfg.get('VDW', 'label') + assert item is None + + def test_iterable(self): + assert not mdpow.config.iterable('test') + def _test_schedule(self, component): section = 'FEP_schedule_{0}'.format(component) schedule = mdpow.fep.FEPschedule.load(self.cfg, section) diff --git a/mdpow/tests/test_fep_script.py b/mdpow/tests/test_fep_script.py index 9742a880..4961cdbf 100644 --- a/mdpow/tests/test_fep_script.py +++ b/mdpow/tests/test_fep_script.py @@ -1,4 +1,7 @@ -import tempdir as td +from __future__ import absolute_import + +from . import tempdir as td + import os import pybol diff --git a/mdpow/tests/test_filelock.py b/mdpow/tests/test_filelock.py index 704d90eb..dec0b1cb 100644 --- a/mdpow/tests/test_filelock.py +++ b/mdpow/tests/test_filelock.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import os.path import pytest diff --git a/mdpow/tests/test_forcefields.py b/mdpow/tests/test_forcefields.py index db556f80..ad7d26f3 100644 --- a/mdpow/tests/test_forcefields.py +++ b/mdpow/tests/test_forcefields.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import os.path import pytest diff --git a/mdpow/tests/test_runinput.py b/mdpow/tests/test_runinput.py index a9dca87d..07662175 100644 --- a/mdpow/tests/test_runinput.py +++ b/mdpow/tests/test_runinput.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import os.path import numpy as np diff --git a/mdpow/tests/test_solvation.py b/mdpow/tests/test_solvation.py index e4d4da8f..5f3a7ec9 100644 --- a/mdpow/tests/test_solvation.py +++ b/mdpow/tests/test_solvation.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import os import shutil diff --git a/scripts/mdpow-cfg2yaml.py b/scripts/mdpow-cfg2yaml.py index a46f7c4c..ef5efac0 100755 --- a/scripts/mdpow-cfg2yaml.py +++ b/scripts/mdpow-cfg2yaml.py @@ -1,4 +1,5 @@ #! /bin/python +from __future__ import absolute_import, print_statement, division import sys import yaml diff --git a/scripts/mdpow-check b/scripts/mdpow-check index 25f609f1..ccd50eab 100755 --- a/scripts/mdpow-check +++ b/scripts/mdpow-check @@ -9,6 +9,7 @@ error. A quick way to find problems is to do a :: cat status.txt | gawk -F '|' '$2 !~ /OK/ {print $0}' """ +from __future__ import absolute_import, print_statement, division import logging logger = logging.getLogger('mdpow') @@ -28,7 +29,7 @@ if __name__ == "__main__": metavar="FILE") parser.set_defaults(outfile="status.txt") opts,args = parser.parse_args() - + logger.info("Writing status to %r", opts.outfile) # dir status type comment @@ -36,7 +37,7 @@ if __name__ == "__main__": with open(opts.outfile, 'w') as status: def swrite(*args): all_args = tuple([directory] + list(args)) - status.write(fmt % all_args) + status.write(fmt % all_args) sys.stderr.write(fmt % all_args) for directory in args: score = 0 @@ -47,13 +48,13 @@ if __name__ == "__main__": # directory itself if not os.path.exists(directory): swrite("NOTFOUND", "directory", "not found") - logger.warn("Directory %r not found, skipping...", directory) + logger.warning("Directory %r not found, skipping...", directory) continue # output from simulations solvents = ('water', 'octanol') nmin_fepsims = {'Coulomb':5, 'VDW':16} - for solvent in solvents: + for solvent in solvents: for feptype,nmin in nmin_fepsims.items(): xvg = os.path.join( directory, 'FEP', solvent, @@ -76,7 +77,7 @@ if __name__ == "__main__": swrite("OK", "pickle", fn) else: swrite("NOTFOUND", "pickle", picklefile) - + # combined graph from mdpow-pow graphs = glob(os.path.join(directory, 'dVdl_*.*')) if len(graphs) > 0: diff --git a/scripts/mdpow-equilibrium b/scripts/mdpow-equilibrium index f8523e97..1d93b4cf 100755 --- a/scripts/mdpow-equilibrium +++ b/scripts/mdpow-equilibrium @@ -21,6 +21,7 @@ You will require: # TODO: # # 2) Default config file in user's home dir. +from __future__ import absolute_import, print_statement, division from mdpow.run import equilibrium_simulation from mdpow.config import get_configuration diff --git a/scripts/mdpow-fep b/scripts/mdpow-fep index 3cc37a40..dc9bc9b4 100755 --- a/scripts/mdpow-fep +++ b/scripts/mdpow-fep @@ -20,6 +20,7 @@ You will require: # TODO: # # 2) Default config file in user's home dir. +from __future__ import absolute_import, print_statement, division from mdpow.run import fep_simulation from mdpow.config import get_configuration diff --git a/scripts/mdpow-get-runinput b/scripts/mdpow-get-runinput index 91edf186..eba22629 100755 --- a/scripts/mdpow-get-runinput +++ b/scripts/mdpow-get-runinput @@ -3,6 +3,7 @@ Generate a template RUNINPUTFILE. """ +from __future__ import absolute_import, print_statement, division from mdpow.config import get_configuration diff --git a/scripts/mdpow-ghyd b/scripts/mdpow-ghyd index 24d6faf2..bf2cbe3f 100755 --- a/scripts/mdpow-ghyd +++ b/scripts/mdpow-ghyd @@ -49,8 +49,7 @@ directory folder in which the simulations were stored """ - -from __future__ import with_statement +from __future__ import absolute_import, print_statement, division import os import warnings @@ -221,7 +220,7 @@ if __name__ == "__main__": for directory in args: if not os.path.exists(directory): - logger.warn("Directory %r not found, skipping...", directory) + logger.warning("Directory %r not found, skipping...", directory) continue logger.info("Analyzing directory %r... (can take a while)", directory) diff --git a/scripts/mdpow-pcw b/scripts/mdpow-pcw index 635fc60a..48d03755 100755 --- a/scripts/mdpow-pcw +++ b/scripts/mdpow-pcw @@ -32,8 +32,7 @@ wat_ok, cyc_ok directory folder in which the simulations were stored """ - -from __future__ import with_statement +from __future__ import absolute_import, print_statement, division import os import mdpow.fep @@ -249,7 +248,7 @@ if __name__ == "__main__": for directory in opts.directory: if not os.path.exists(directory): - logger.warn("Directory %r not found, skipping...", directory) + logger.warning("Directory %r not found, skipping...", directory) continue logger.info("Analyzing directory %r... (can take a while)", directory) @@ -262,9 +261,9 @@ if __name__ == "__main__": force=opts.force, stride=opts.stride, start=opts.start, stop=opts.stop, estimator=opts.estimator, method=opts.method, permissive=opts.permissive, SI=opts.SI and not opts.noSI) - except (OSError, IOError), err: + except (OSError, IOError) as err: logger.error("Running analysis in directory %r failed: %s", directory, str(err)) - except Exception, err: + except Exception as err: logger.fatal("Running analysis in directory %r failed", directory) logger.exception("Catastrophic problem occurred, see the stack trace for hints.") raise diff --git a/scripts/mdpow-pow b/scripts/mdpow-pow index dce2c56b..8edb8d17 100755 --- a/scripts/mdpow-pow +++ b/scripts/mdpow-pow @@ -32,8 +32,7 @@ wat_ok, octa_ok directory folder in which the simulations were stored """ - -from __future__ import with_statement +from __future__ import absolute_import, print_statement, division import os import mdpow.fep @@ -249,7 +248,7 @@ if __name__ == "__main__": for directory in opts.directory: if not os.path.exists(directory): - logger.warn("Directory %r not found, skipping...", directory) + logger.warning("Directory %r not found, skipping...", directory) continue logger.info("Analyzing directory %r... (can take a while)", directory) @@ -262,9 +261,9 @@ if __name__ == "__main__": force=opts.force, stride=opts.stride, start=opts.start, stop=opts.stop, estimator=opts.estimator, method=opts.method, permissive=opts.permissive, SI=opts.SI and not opts.noSI) - except (OSError, IOError), err: + except (OSError, IOError) as err: logger.error("Running analysis in directory %r failed: %s", directory, str(err)) - except Exception, err: + except Exception as err: logger.fatal("Running analysis in directory %r failed", directory) logger.exception("Catastrophic problem occurred, see the stack trace for hints.") raise diff --git a/scripts/mdpow-rebuild-fep b/scripts/mdpow-rebuild-fep index 8bf2e8a3..615d3dab 100755 --- a/scripts/mdpow-rebuild-fep +++ b/scripts/mdpow-rebuild-fep @@ -7,6 +7,7 @@ equilibrium simulation file under /FEP. (This should only be necessary when the fep file was destroyed due to a software error.) """ +from __future__ import absolute_import, print_statement, division import os import mdpow.equil @@ -30,8 +31,8 @@ def fixFEP(solvent, fepfile, simfile, basedir): G = Gsolv[solvent](simulation=S, basedir=basedir, permissive=True) except Exception as err: logger.error(str(err)) - logger.warn("Could not get the simulation pickle file %(simfile)r", vars()) - logger.warn("The FEP pickle file will be fixed as it is (no path adjustments)") + logger.warning("Could not get the simulation pickle file %(simfile)r", vars()) + logger.warning("The FEP pickle file will be fixed as it is (no path adjustments)") G = Gsolv[solvent](filename=fepfile) # remove pre-0.2 standard-state correction (wrong!) and pdV (wrong) @@ -96,7 +97,7 @@ if __name__ == "__main__": logger.debug("%r is not a directory, skipping!", directory) continue if not os.path.exists(directory): - logger.warn("Directory %r not found, skipping!", directory) + logger.warning("Directory %r not found, skipping!", directory) continue logger.info("Rebuilding under directory %r... (can take a while)", directory) with in_dir(directory, create=False): diff --git a/scripts/mdpow-rebuild-simulation b/scripts/mdpow-rebuild-simulation index 980b82b2..3d4f8e28 100755 --- a/scripts/mdpow-rebuild-simulation +++ b/scripts/mdpow-rebuild-simulation @@ -7,6 +7,7 @@ adjusted paths. (This should only be necessary when the directories are moved or copied to a different machine.) """ +from __future__ import absolute_import, print_statement, division import os import mdpow.equil @@ -58,7 +59,7 @@ if __name__ == "__main__": logger.debug("%r is not a directory, skipping!", directory) continue if not os.path.exists(directory): - logger.warn("Directory %r not found, skipping!", directory) + logger.warning("Directory %r not found, skipping!", directory) continue logger.info("Rebuilding under directory %r... (can take a while)", directory) with in_dir(directory, create=False): diff --git a/scripts/mdpow-solvationenergy b/scripts/mdpow-solvationenergy index fa32d33f..846b9e23 100755 --- a/scripts/mdpow-solvationenergy +++ b/scripts/mdpow-solvationenergy @@ -45,8 +45,7 @@ directory folder in which the simulations were stored """ - -from __future__ import with_statement +from __future__ import absolute_import, print_statement, division import os import mdpow.fep @@ -282,7 +281,7 @@ if __name__ == "__main__": for directory in opts.directory: if not os.path.exists(directory): - logger.warn("Directory %r not found, skipping...", directory) + logger.warning("Directory %r not found, skipping...", directory) continue logger.info("Analyzing directory %r... (can take a while)", directory) @@ -294,9 +293,9 @@ if __name__ == "__main__": force=opts.force, stride=opts.stride, start=opts.start, stop=opts.stop, estimator=opts.estimator, method=opts.method, permissive=opts.permissive, SI=opts.SI and not opts.noSI) - except (OSError, IOError), err: + except (OSError, IOError) as err: logger.error("Running analysis in directory %r failed: %s", directory, str(err)) - except Exception, err: + except Exception as err: logger.fatal("Running analysis in directory %r failed", directory) logger.exception("Catastrophic problem occurred, see the stack trace for hints.") raise diff --git a/setup.py b/setup.py index 42a8a193..d549c138 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,10 @@ 'Programming Language :: Python', 'Programming Language :: Python :: 2', "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Chemistry", "Topic :: Scientific/Engineering :: Physics", ],