diff --git a/.gitignore b/.gitignore index 94848984..fd280782 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ doc/epydoc/ doc/sphinx/build/ doc/html build/ +.idea/ +virtualenv/ dist gromacs.log doc/sphinx/*.log diff --git a/CHANGES b/CHANGES index bcce2b7a..fc08b0cb 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,8 @@ pslacerda, orbeckst automatically before setting up the Gromacs commands (#55) * doc strings for Gromacs tools are loaded lazily, which speeds up the initial import of the library to almost instantaneously (PR #76) +* guess which tools to load automatically if no tools option is provided (#68) +* new documentation page on how to set up a cfg file 2016-06-29 0.5.1 whitead, dotsdl, orbeckst diff --git a/doc/sphinx/source/configuration.txt b/doc/sphinx/source/configuration.txt new file mode 100644 index 00000000..d2544429 --- /dev/null +++ b/doc/sphinx/source/configuration.txt @@ -0,0 +1,123 @@ +============== + Configuration +============== + +.. highlight:: ini + +This section documents how to configure the **GromacsWrapper** package. There +are options to configure where log files and templates directories are located +and options to tell exactly which commands to load into this package. Any +configuration is optional and all options has sane defaults. Further +documentation can be found at :mod:`gromacs.config`. + +.. versionchanged:: 0.6.0 + The format of the ``tools`` variable in the ``[Gromacs]`` section of the + config file was changed for Gromacs 5 commands. + +Basic options +------------- + +Place an INI file named ``~/.gromacswrapper.cfg`` in your home directory, it +may look like the following document:: + + [Gromacs] + GMXRC = /usr/local/gromacs/bin/GMXRC + +The Gromacs software suite needs some environment variables that are set up +sourcing the ``GMXRC`` file. You may source it yourself or set an option like +the above one. If this option isn't provided, **GromacsWrapper** will guess +that Gromacs was globally installed like if it was installed by the ``apt-get`` +program. + +As there isn't yet any way to know which Gromacs version to use, +**GromacsWrapper** will first try to use Gromacs 5 if available, then to use +Gromacs 4. If you have both versions and want to use version 4 or just want +to document it, you may specify the which release version will be used:: + + [Gromacs] + GMXRC = /usr/local/gromacs/bin/GMXRC + release = 4.6.7 + +For now **GromacsWrapper** will guess which tools are available to put it into +:mod:`gromacs.tools`, but you can always configure it manually. Gromacs 5 has +up to 4 commands usually named:: + + [Gromacs] + tools = gmx gmx_d gmx_mpi gmx_mpi_d + + +This option will instruct which commands to load. For Gromacs 4 you'll need to +specify more tools:: + + [Gromacs] + GMXRC = /usr/local/gromacs/bin/GMXRC + release = 4 + tools = + g_cluster g_dyndom g_mdmat g_principal g_select g_wham mdrun + do_dssp g_clustsize g_enemat g_membed g_protonate g_sgangle g_wheel mdrun_d + editconf g_confrms g_energy g_mindist g_rama g_sham g_x2top mk_angndx + eneconv g_covar g_filter g_morph g_rdf g_sigeps genbox pdb2gmx + g_anadock g_current g_gyrate g_msd g_sorient genconf + g_anaeig g_density g_h2order g_nmeig g_rms g_spatial genion tpbconv + g_analyze g_densmap g_hbond g_nmens g_rmsdist g_spol genrestr trjcat + g_angle g_dielectric g_helix g_nmtraj g_rmsf g_tcaf gmxcheck trjconv + g_bar g_dih g_helixorient g_order g_rotacf g_traj gmxdump trjorder + g_bond g_dipoles g_kinetics g_pme_error g_rotmat g_tune_pme grompp + g_bundle g_disre g_lie g_polystat g_saltbr g_vanhove make_edi xpm2ps + g_chi g_dist g_luck g_potential g_sas g_velacc make_ndx + + +Commands will be available directly from the :mod:`gromacs`: + +.. code-block:: python + + import gromacs + gromacs.mdrun_d # either v5 `gmx_d mdrun` or v4 `mdrun_d` + gromacs.mdrun # either v5 `gmx mdrun` or v4 `mdrun` + + +More options +------------ + +Other options are to set where template for job submission systems and.mdp +files are located:: + + [DEFAULT] + # Directory to store user templates and rc files. + configdir = ~/.gromacswrapper + + # Directory to store user supplied queuing system scripts. + qscriptdir = %(configdir)s/qscripts + + # Directory to store user supplied template files such as mdp files. + templatesdir = %(configdir)s/templates + + # Directory to store manager configuration files + managerdir = %(configdir)s/managers + + +And there are yet options for how to handle logging:: + + [Logging] + # name of the logfile that is written to the current directory + logfilename = gromacs.log + + # loglevels (see Python's logging module for details) + # ERROR only fatal errors + # WARN only warnings + # INFO interesting messages + # DEBUG everything + + # console messages written to screen + loglevel_console = INFO + + # file messages written to logfilename + loglevel_file = DEBUG + +If needed you may set up basic configuration files and directories using +:func:`gromacs.config.setup`: + +.. code-block:: python + + import gromacs + gromacs.config.setup() diff --git a/doc/sphinx/source/index.txt b/doc/sphinx/source/index.txt index 2324cc81..900fe598 100644 --- a/doc/sphinx/source/index.txt +++ b/doc/sphinx/source/index.txt @@ -57,6 +57,7 @@ Contents :maxdepth: 2 installation + configuration gromacs analysis auxiliary diff --git a/gromacs/__init__.py b/gromacs/__init__.py index f1d82edb..8db385c9 100644 --- a/gromacs/__init__.py +++ b/gromacs/__init__.py @@ -3,8 +3,7 @@ # Released under the GNU Public License 3 (or higher, your choice) # See the file COPYING for details. -""" -:mod:`gromacs` -- GromacsWrapper Package Overview +""":mod:`gromacs` -- GromacsWrapper Package Overview ================================================= **GromacsWrapper** (package :mod:`gromacs`) is a thin shell around the `Gromacs`_ @@ -160,6 +159,11 @@ instead of displaying it on screen, as described under :ref:`input-output-label`. +Normally, one starts logging with the :func:`start_logging` function but in +order to obtain logging messages (typically at level *debug*) right from the +start one may set the environment variable :envvar:`GW_START_LOGGING` to any +value that evaluates to ``True`` (e.g., "True" or "1"). + .. _logging: http://docs.python.org/library/logging.html Version @@ -173,10 +177,13 @@ If the package was installed from a development version, the patch level will have the string "-dev" affixed to distinguish it from a release. + """ from __future__ import absolute_import __docformat__ = "restructuredtext en" +import os + from .version import VERSION, RELEASE, get_version, get_version_tuple # __all__ is extended with all gromacs command instances later @@ -240,6 +247,9 @@ def stop_logging(): logger.info("GromacsWrapper %s STOPPED logging", get_version()) log.clear_handlers(logger) # this _should_ do the job... +# for testing (maybe enable with envar GW_START_LOGGING) +if os.environ.get('GW_START_LOGGING', False): + start_logging() # Try to load environment variables set by GMXRC config.set_gmxrc_environment(config.cfg.getpath("Gromacs", "GMXRC")) diff --git a/gromacs/cbook.py b/gromacs/cbook.py index b1949712..13cf4e30 100644 --- a/gromacs/cbook.py +++ b/gromacs/cbook.py @@ -199,7 +199,7 @@ def _define_canned_commands(): try: _define_canned_commands() -except (OSError, ImportError, GromacsError): +except (OSError, ImportError, AttributeError, GromacsError): msg = ("Failed to define a number of commands in gromacs.cbook. Most " "likely the Gromacs installation cannot be found --- set GMXRC in " "~/.gromacswrapper.cfg or source GMXRC directly") @@ -1652,7 +1652,7 @@ class Transformer(utilities.FileUtils): """ - def __init__(self, s="topol.tpr", f="traj.xtc", n=None, force=None, + def __init__(self, s="topol.tpr", f="traj.xtc", n=None, force=None, dirname=os.path.curdir, outdir=None): """Set up Transformer with structure and trajectory. @@ -1732,7 +1732,7 @@ def center_fit(self, **kwargs): """Write compact xtc that is fitted to the tpr reference structure. See :func:`gromacs.cbook.trj_fitandcenter` for details and - description of *kwargs* (including *input*, *input1*, *n* and + description of *kwargs* (including *input*, *input1*, *n* and *n1* for how to supply custom index groups). The most important ones are listed here but in most cases the defaults should work. @@ -1846,7 +1846,7 @@ def fit(self, xy=False, **kwargs): logger.info("Fitted trajectory (fitmode=%s): %r.", fitmode, kwargs['o']) return {'tpr': self.rp(kwargs['s']), 'xtc': self.rp(kwargs['o'])} - def strip_water(self, os=None, o=None, on=None, compact=False, + def strip_water(self, os=None, o=None, on=None, compact=False, resn="SOL", groupname="notwater", **kwargs): """Write xtc and tpr with water (by resname) removed. @@ -1866,7 +1866,7 @@ def strip_water(self, os=None, o=None, on=None, compact=False, Index group used for centering ["Protein"] .. Note:: If *input* is provided (see below under *kwargs*) - then *centergroup* is ignored and the group for + then *centergroup* is ignored and the group for centering is taken as the first entry in *input*. *resn* @@ -1909,12 +1909,12 @@ def strip_water(self, os=None, o=None, on=None, compact=False, TRJCONV = trj_compact # input overrides centergroup if kwargs.get('centergroup') is not None and 'input' in kwargs: - logger.warn("centergroup = %r will be superceded by input[0] = %r", kwargs['centergroup'], kwargs['input'][0]) + logger.warn("centergroup = %r will be superceded by input[0] = %r", kwargs['centergroup'], kwargs['input'][0]) _input = kwargs.get('input', [kwargs.get('centergroup', 'Protein')]) kwargs['input'] = [_input[0], groupname] # [center group, write-out selection] del _input - logger.info("Creating a compact trajectory centered on group %r", kwargs['input'][0]) - logger.info("Writing %r to the output trajectory", kwargs['input'][1]) + logger.info("Creating a compact trajectory centered on group %r", kwargs['input'][0]) + logger.info("Writing %r to the output trajectory", kwargs['input'][1]) else: TRJCONV = gromacs.trjconv kwargs['input'] = [groupname] @@ -1937,7 +1937,7 @@ def strip_water(self, os=None, o=None, on=None, compact=False, logger.info("NDX of the new system %r", newndx) gromacs.make_ndx(f=newtpr, o=newndx, input=['q'], stderr=False, stdout=False) - # PROBLEM: If self.ndx contained a custom group required for fitting then we are loosing + # PROBLEM: If self.ndx contained a custom group required for fitting then we are loosing # this group here. We could try to merge only this group but it is possible that # atom indices changed. The only way to solve this is to regenerate the group with # a selection or only use Gromacs default groups. @@ -2083,7 +2083,7 @@ def strip_fit(self, **kwargs): - *fitgroup* is only passed to :meth:`fit` and just contains the group to fit to ("backbone" by default) - .. warning:: *fitgroup* can only be a Gromacs default group and not + .. warning:: *fitgroup* can only be a Gromacs default group and not a custom group (because the indices change after stripping) - By default *fit* = "rot+trans" (and *fit* is passed to :meth:`fit`, diff --git a/gromacs/config.py b/gromacs/config.py index 2090e3fd..2e6aea52 100644 --- a/gromacs/config.py +++ b/gromacs/config.py @@ -7,25 +7,11 @@ ========================================================== The config module provides configurable options for the whole package; -It mostly serves to define which gromacs tools and other scripts are -exposed in the :mod:`gromacs` package and where template files are -located. The user can configure *GromacsWrapper* by +It serves to define how to handle log files, set where template files are +located and which gromacs tools are exposed in the :mod:`gromacs` package. -1. editing the global configuration file ``~/.gromacswrapper.cfg`` - -2. placing template files into directories under ``~/.gromacswrapper`` - (:data:`gromacs.config.configdir`) which can be processed instead - of the files that come with *GromacsWrapper* - -In order to **set up a basic configuration file and the directories** -a user should execute :func:`gromacs.config.setup` at least once. It -will prepare the user configurable area in their home directory and it -will generate a default global configuration file -``~/.gromacswrapper.cfg`` (the name is defined in -:data:`CONFIGNAME`):: - - import gromacs - gromacs.config.setup() +In order to set up a basic configuration file and the directories +a user can execute :func:`gromacs.config.setup`. If the configuration file is edited then one can force a rereading of the new config file with :func:`gromacs.config.get_configuration`:: @@ -163,36 +149,34 @@ List of tools ~~~~~~~~~~~~~ -The list of Gromacs tools is specified in the config file in the ``[Gromacs]`` -section with the ``tools`` variable. +The list of Gromacs tools can be specified in the config file in the +``[Gromacs]`` section with the ``tools`` variable. -The tool groups are strings that contain white-space separated file -names of Gromacs tools. These lists determine which tools are made -available as classes in :mod:`gromacs.tools`. +The tool groups are a list of names that determines which tools are made +available as classes in :mod:`gromacs.tools`. If not provided +GromacsWrapper will first try to load Gromacs 5.x then Gromacs 4.x +tools. -A typical Gromacs tools section of the config file looks like this:: +If you choose to provide a list, the Gromacs tools section of the config +file can be like this:: [Gromacs] # Release of the Gromacs package to which information in this sections applies. release = 4.5.3 - # tools contains the file names of all Gromacs tools for which classes are generated. - # Editing this list has only an effect when the package is reloaded. + # tools contains the file names of all Gromacs tools for which classes are + # generated. Editing this list has only an effect when the package is + # reloaded. # (Note that this example has a much shorter list than the actual default.) tools = editconf make_ndx grompp genion genbox - grompp pdb2gmx mdrun - - # Additional gromacs tools that should be made available. - extra = - g_count g_flux - a_gridcalc a_ri3Dc g_ri3Dc + grompp pdb2gmx mdrun mdrun_d - # which tool groups to make available as gromacs.NAME + # which tool groups to make available groups = tools extra -For Gromacs 5.x use a section like the following, where the driver -command ``gmx`` is added as a prefix:: +For Gromacs 5.x use a section like the following, where driver commands +are supplied:: [Gromacs] # Release of the Gromacs package to which information in this sections applies. @@ -206,13 +190,8 @@ # tools contains the command names of all Gromacs tools for which classes are generated. # Editing this list has only an effect when the package is reloaded. # (Note that this example has a much shorter list than the actual default.) - tools = - gmx:editconf gmx:make_ndx gmx:grompp gmx:genion gmx:solvate - gmx:insert-molecule gmx:convert-tpr - gmx:grompp gmx:pdb2gmx gmx:mdrun + tools = gmx gmx_d - # which tool groups to make available as gromacs.NAME - groups = tools For example, on the commandline you would run :: @@ -222,14 +201,6 @@ gromacs.grompp(f="md.mdp", c="system.gro", p="topol.top", o="md.tpr") -(The driver command is stripped and only the "command name" is used to -identify the command. This makes it easier to migrate GromacsWrapper -scripts from Gromacs 4.x to 5.x.). - -The driver command can be changed per-tool, allowing for mpi -and non-mpi versions to be used. For example:: - -tools = gmx_mpi:mdrun gmx:pdb2gmx .. Note:: Because of `changes in the Gromacs tool in 5.x`_, GromacsWrapper scripts might break, even if the tool @@ -238,15 +209,6 @@ .. _`changes in the Gromacs tool in 5.x`: http://www.gromacs.org/Documentation/How-tos/Tool_Changes_for_5.0 -Developers should know that the lists of tools are stored in ``load_*`` -variables. These are lists that contain instructions to other parts of the code -as to which executables should be wrapped. - -.. autodata:: load_tools - -:data:`load_tools` is populated from lists of executable names in the -configuration file. - Location of template files @@ -266,21 +228,20 @@ """ from __future__ import absolute_import, with_statement -import os, errno, subprocess -from ConfigParser import SafeConfigParser +import os +import logging +import re +import subprocess +from ConfigParser import SafeConfigParser from pkg_resources import resource_filename, resource_listdir from . import utilities -# Defaults -# -------- -# hard-coded package defaults -#: name of the global configuration file. +#: Default name of the global configuration file. CONFIGNAME = os.path.expanduser(os.path.join("~",".gromacswrapper.cfg")) -#: Holds the default values for important file and directory locations. #: #: :data:`configdir` #: Directory to store user templates and configurations. @@ -296,20 +257,23 @@ #: Directory to store configuration files for different queuing system #: managers as used in :mod:`gromacs.manager`. #: The default value is ``~/.gromacswrapper/managers``. + +configdir = os.path.expanduser(os.path.join("~",".gromacswrapper")) defaults = { - 'configdir': os.path.expanduser(os.path.join("~",".gromacswrapper")), - 'logfilename': "gromacs.log", - 'loglevel_console': 'INFO', - 'loglevel_file': 'DEBUG', + 'configdir': configdir, + 'qscriptdir': os.path.join(configdir, 'qscripts'), + 'templatesdir': os.path.join(configdir, 'templates'), + 'managerdir': os.path.join(configdir, 'managers'), + + 'logfilename': "gromacs.log", + 'loglevel_console': 'INFO', + 'loglevel_file': 'DEBUG', } -defaults['qscriptdir'] = os.path.join(defaults['configdir'], 'qscripts') -defaults['templatesdir'] = os.path.join(defaults['configdir'], 'templates') -defaults['managerdir'] = os.path.join(defaults['configdir'], 'managers') # Logging # ------- -import logging + logger = logging.getLogger("gromacs.config") #: File name for the log file; all gromacs command and many utility functions (e.g. in @@ -327,9 +291,6 @@ # User-accessible configuration # ----------------------------- -# (written in a clumsy manner because of legacy code and because of -# the way I currently generate the documentation) - #: Directory to store user templates and rc files. #: The default value is ``~/.gromacswrapper``. configdir = defaults['configdir'] @@ -372,7 +333,7 @@ def _generate_template_dict(dirname): by external code. All template filenames are stored in :data:`config.templates`. """ - return dict((resource_basename(fn), resource_filename(__name__, dirname+'/'+fn)) + return dict((resource_basename(fn), resource_filename(__name__, dirname +'/'+fn)) for fn in resource_listdir(__name__, dirname) if not fn.endswith('~')) @@ -523,7 +484,6 @@ def __init__(self, *args, **kwargs): args = tuple([self] + list(args)) SafeConfigParser.__init__(*args, **kwargs) # old style class ... grmbl # defaults - self.set('DEFAULT', 'configdir', defaults['configdir']) self.set('DEFAULT', 'qscriptdir', os.path.join("%(configdir)s", os.path.basename(defaults['qscriptdir']))) self.set('DEFAULT', 'templatesdir', @@ -531,8 +491,9 @@ def __init__(self, *args, **kwargs): self.set('DEFAULT', 'managerdir', os.path.join("%(configdir)s", os.path.basename(defaults['managerdir']))) self.add_section('Gromacs') + self.set("Gromacs", "release", "") self.set("Gromacs", "GMXRC", "") - self.set("Gromacs", "tools", "pdb2gmx editconf grompp genbox genion mdrun trjcat trjconv") + self.set("Gromacs", "tools", "") self.set("Gromacs", "extra", "") self.set("Gromacs", "groups", "tools") self.add_section('Logging') @@ -644,42 +605,28 @@ def setup(filename=CONFIGNAME): def check_setup(): """Check if templates directories are setup and issue a warning and help. - Returns ``True`` if all files and directories are found and - ``False`` otherwise. + Set the environment variable :envvar:`GROMACSWRAPPER_SUPPRESS_SETUP_CHECK` + skip the check and make it always return ``True`` - Setting the environment variable - :envvar:`GROMACSWRAPPER_SUPPRESS_SETUP_CHECK` to 'true' ('yes' - and '1' also work) silence this function and make it always return ``True``. + :return ``True`` if directories were found and ``False`` otherwise .. versionchanged:: 0.3.1 - Uses :envvar:`GROMACSWRAPPER_SUPPRESS_SETUP_CHECK` to suppress output + Uses :envvar:`GROMACSWRAPPER_SUPPRESS_SETUP_CHECK` to suppress check (useful for scripts run on a server) """ - if os.environ.get("GROMACSWRAPPER_SUPPRESS_SETUP_CHECK", "false").lower() in ("1", "true", "yes"): - return True - is_complete = True - show_solution = False - - if not os.path.exists(CONFIGNAME): - is_complete = False - show_solution = True - print("NOTE: The global configuration file %r is missing." % CONFIGNAME) + if "GROMACSWRAPPER_SUPPRESS_SETUP_CHECK" in os.environ: + return True missing = [d for d in config_directories if not os.path.exists(d)] if len(missing) > 0: - is_complete = False - show_solution = True - print("NOTE: Some configuration directories are not set up yet: ") - print("\t%s" % '\n\t'.join(missing)) - - if show_solution: - print("NOTE: You can create the configuration file and directories with:") - print("\t>>> import gromacs") - print("\t>>> gromacs.config.setup()") - return is_complete - -check_setup() + print("NOTE: Some configuration directories are not set up yet: ") + print("\t%s" % '\n\t'.join(missing)) + print("NOTE: You can create the configuration file and directories with:") + print("\t>>> import gromacs") + print("\t>>> gromacs.config.setup()") + return False + return True def set_gmxrc_environment(gmxrc): @@ -692,11 +639,15 @@ def set_gmxrc_environment(gmxrc): then only a warning will be logged. Thus, it should be safe to just call this function. """ - envvars = ['GMXPREFIX', 'GMXBIN', 'GMXLDLIB', 'GMXMAN', 'GMXDATA', - 'GROMACS_DIR', 'LD_LIBRARY_PATH', 'MANPATH', 'PKG_CONFIG_PATH', - 'PATH'] + # only v5: 'GMXPREFIX', 'GROMACS_DIR' + envvars = ['GMXBIN', 'GMXLDLIB', 'GMXMAN', 'GMXDATA', + 'LD_LIBRARY_PATH', 'MANPATH', 'PKG_CONFIG_PATH', + 'PATH', + 'GMXPREFIX', 'GROMACS_DIR'] + # in order to keep empty values, add ___ sentinels around result + # (will be removed later) cmdargs = ['bash', '-c', ". {0} && echo {1}".format(gmxrc, - ' '.join(['${0}'.format(v) for v in envvars]))] + ' '.join(['___${{{0}}}___'.format(v) for v in envvars]))] if not gmxrc: logger.debug("set_gmxrc_environment(): no GMXRC, nothing done.") @@ -706,6 +657,7 @@ def set_gmxrc_environment(gmxrc): out = subprocess.check_output(cmdargs) out = out.strip().split() for key, value in zip(envvars, out): + value = value.replace('___', '') # remove sentinels os.environ[key] = value logger.debug("set_gmxrc_environment(): %s = %r", key, value) except (subprocess.CalledProcessError, OSError): @@ -722,3 +674,31 @@ def get_tool_names(): for group in cfg.get('Gromacs', 'groups').split(): names.extend(cfg.get('Gromacs', group).split()) return names + + +def get_extra_tool_names(): + """ Get tool names from all configured groups. + + :return: list of tool names + """ + return cfg.get('Gromacs', 'extra').split() + + +RELEASE = None +MAJOR_RELEASE = None + +if cfg.get('Gromacs', 'release'): + RELEASE = cfg.get('Gromacs', 'release') + MAJOR_RELEASE = RELEASE.split('.')[0] + +for name in get_tool_names(): + match = re.match(r'(gmx[^:]*):.*', name) + if match: + driver = match.group(1) + raise ValueError("'%s' isn't a valid tool name anymore." + " Replace it by '%s'.\n See " + "http://gromacswrapper.readthedocs.io/en/latest/" + "configuration.html" % (name, match.group(1))) + + +check_setup() diff --git a/gromacs/templates/gromacswrapper.cfg b/gromacs/templates/gromacswrapper.cfg index 916a9191..98d9bec6 100644 --- a/gromacs/templates/gromacswrapper.cfg +++ b/gromacs/templates/gromacswrapper.cfg @@ -16,27 +16,18 @@ managerdir = %(configdir)s/managers [Gromacs] # Release of the Gromacs package to which information in this sections applies. -release = 5.1.1 +# empty: try auto-loading tools (first Gromacs 5 then Gromacs 4) +## release = 5.1.2 # tools contains the file names of all Gromacs tools for which classes are generated. # Editing this list has only an effect when the package is reloaded. -# (Generated with 'ls [^Gac]*' from the Gromacs bin dir) -tools = - gmx:cluster gmx:dyndom gmx:mdmat gmx:principal gmx:select gmx:wham gmx:mdrun gmx:convert-tpr - gmx:do_dssp gmx:clustsize gmx:enemat gmx:protonate gmx:gangle gmx:wheel gmx:mdrun_d gmx:trjcat - gmx:editconf gmx:confrms gmx:energy gmx:mindist gmx:rama gmx:sham gmx:x2top gmx:trjconv - gmx:eneconv gmx:covar gmx:filter gmx:morph gmx:rdf gmx:sigeps gmx:solvate gmx:pdb2gmx - gmx:anadock gmx:current gmx:gyrate gmx:msd gmx:sorient gmx:genconf gmx:insert-molecules - gmx:anaeig gmx:density gmx:h2order gmx:nmeig gmx:rms gmx:spatial gmx:genion - gmx:analyze gmx:densmap gmx:hbond gmx:nmens gmx:rmsdist gmx:spol gmx:genrestr - gmx:angle gmx:dielectric gmx:helix gmx:nmtraj gmx:rmsf gmx:tcaf gmx:check - gmx:bar gmx:helixorient gmx:order gmx:rotacf gmx:traj gmx:dump gmx:trjorder - gmx:dipoles gmx:kinetics gmx:pme_error gmx:rotmat gmx:tune_pme gmx:grompp gmx:mk_angndx - gmx:bundle gmx:disre gmx:lie gmx:polystat gmx:saltbr gmx:vanhove gmx:make_edi - gmx:chi gmx:distance gmx:potential gmx:sas gmx:velacc gmx:make_ndx gmx:xpm2ps +# - for Gromacs 4: Generated with 'ls [^Gac]*' from the Gromacs bin dir +## tools = ... +# - for Gromacs 5: just the driver commands +## tools = gmx gmx_d # which tool groups to make available as gromacs.NAME -groups = tools +## groups = tools [Logging] # name of the logfile that is written to the current directory diff --git a/gromacs/tools.py b/gromacs/tools.py index ce4eee79..a08e2d50 100644 --- a/gromacs/tools.py +++ b/gromacs/tools.py @@ -1,39 +1,28 @@ # Copyright (c) 2009 Oliver Beckstein # Released under the GNU Public License 3 (or higher, your choice) # See the file COPYING for details. - """ :mod:`gromacs.tools` -- Gromacs commands classes ================================================ -A Gromacs command class acts as a factory function that produces an -instance of a gromacs command (:class:`gromacs.core.GromacsCommand`) -with initial default values. +A Gromacs command class produces an instance of a Gromacs tool command ( +:class:`gromacs.core.GromacsCommand`), any argument or keyword argument +supplied will be used as default values for when the command is run. -By convention, a class has the capitalized name of the corresponding Gromacs -tool; dots are replaced by underscores to make it a valid python identifier. -Gromacs 5 tools (e.g, `sasa`) are aliased to their Gromacs 4 tool names (e.g, `g_sas`) -for backwards compatibility. +Classes has the same name of the corresponding Gromacs tool with the first +letter capitalized and dot and dashes replaced by underscores to make it a +valid python identifier. Gromacs 5 tools are also aliased to their Gromacs 4 +tool names (e.g, `sasa` to `g_sas`) for backwards compatibility. -The list of Gromacs tools to be loaded is configured in -:data:`gromacs.config.gmx_tool_groups`. +The list of tools to be loaded is configured with the ``tools`` and ``groups`` +options of the ``~/.gromacswrapper.cfg`` file. Guesses are made if these +options are not provided. -It is also possible to extend the basic commands and patch in additional -functionality. For example, the :class:`GromacsCommandMultiIndex` class makes a -command accept multiple index files and concatenates them on the fly; the -behaviour mimics Gromacs' "multi-file" input that has not yet been enabled for -all tools. - -.. autoclass:: GromacsCommandMultiIndex - :members: run, _fake_multi_ndx, __del__ - -Example -------- +In the following example we create two instances of the +:class:`gromacs.tools.Trjconv` command (which runs the Gromacs ``trjconv`` +command):: -In this example we create two instances of the :class:`gromacs.tools.Trjconv` command (which -runs the Gromacs ``trjconv`` command):: - - import gromacs.tools as tools + from gromacs.tools import Trjconv trjconv = tools.Trjconv() trjconv_compact = tools.Trjconv(ur='compact', center=True, boxcenter='tric', pbc='mol', @@ -44,46 +33,72 @@ representation of the input data by taking into account the shape of the unit cell. Of course, the same effect can be obtained by providing the corresponding arguments to ``trjconv`` but by naming the more specific command differently -one can easily build up a library of small tools that will solve a specifi, +one can easily build up a library of small tools that will solve a specific, repeatedly encountered problem reliably. This is particularly helpful when doing interactive work. +Multi index +----------- + +It is possible to extend the tool commands and patch in additional +functionality. For example, the :class:`GromacsCommandMultiIndex` class makes a +command accept multiple index files and concatenates them on the fly; the +behaviour mimics Gromacs' "multi-file" input that has not yet been enabled for +all tools. + +.. autoclass:: GromacsCommandMultiIndex +.. autofunction:: merge_ndx + +Helpers +------- + +.. autofunction:: tool_factory +.. autofunction:: load_v4_tools +.. autofunction:: load_v5_tools +.. autofunction:: load_extra_tools +.. autofunction:: find_executables +.. autofunction:: make_valid_identifier +.. autoexception:: GromacsToolLoadingError + Gromacs tools ------------- -.. The docs for the tool classes are auto generated. -.. autoclass:: Mdrun - :members: """ from __future__ import absolute_import -__docformat__ = "restructuredtext en" import os.path import tempfile +import subprocess +import atexit +import logging from . import config -from .core import GromacsCommand, Command -from . import utilities - -def _generate_sphinx_class_string(clsname): - return ".. class:: %(clsname)s\n :noindex:\n" % vars() - -#flag for 5.0 style commands -b_gmx5 = False - -#: This dict holds all generated classes. -registry = {} - -# Auto-generate classes such as: -# class g_dist(GromacsCommand): -# command_name = 'g_dist' - -aliases5to4 = { +from .core import GromacsCommand + +logger = logging.getLogger("gromacs.tools") + +V4TOOLS = ("g_cluster", "g_dyndom", "g_mdmat", "g_principal", "g_select", + "g_wham", "mdrun", "do_dssp", "g_clustsize", "g_enemat", "g_membed", + "g_protonate", "g_sgangle", "g_wheel", "mdrun_d", "editconf", + "g_confrms", "g_energy", "g_mindist", "g_rama", "g_sham", "g_x2top", + "mk_angndx", "eneconv", "g_covar", "g_filter", "g_morph", "g_rdf", + "g_sigeps", "genbox", "pdb2gmx", "g_anadock", "g_current", + "g_gyrate", "g_msd", "g_sorient", "genconf", "g_anaeig", "g_density", + "g_h2order", "g_nmeig", "g_rms", "g_spatial", "genion", "tpbconv", + "g_analyze", "g_densmap", "g_hbond", "g_nmens", "g_rmsdist", + "g_spol", "genrestr", "trjcat", "g_angle", "g_dielectric", "g_helix", + "g_nmtraj", "g_rmsf", "g_tcaf", "gmxcheck", "trjconv", "g_bar", + "g_dih", "g_helixorient", "g_order", "g_rotacf", "g_traj", "gmxdump", + "trjorder", "g_bond", "g_dipoles", "g_kinetics", "g_pme_error", + "g_rotmat", "g_tune_pme", "grompp", "g_bundle", "g_disre", "g_lie", + "g_polystat", "g_saltbr", "g_vanhove", "make_edi", "xpm2ps", "g_chi", + "g_dist", "g_luck", "g_potential", "g_sas", "g_velacc", "make_ndx") + + +NAMES5TO4 = { + # same name in both versions 'grompp': 'grompp', 'eneconv': 'eneconv', - 'sasa': 'g_sas', - 'distance': 'g_dist', - 'convert_tpr': 'tpbconv', 'editconf': 'editconf', 'pdb2gmx': 'pdb2gmx', 'trjcat': 'trjcat', @@ -93,177 +108,252 @@ def _generate_sphinx_class_string(clsname): 'mdrun': 'mdrun', 'make_ndx': 'make_ndx', 'make_edi': 'make_edi', - 'dump': 'gmxdump', - 'check': 'gmxcheck', 'genrestr': 'genrestr', 'genion': 'genion', 'genconf': 'genconf', 'do_dssp': 'do_dssp', + + # changed names + 'convert_tpr': 'tpbconv', + 'dump': 'gmxdump', + 'check': 'gmxcheck', 'solvate': 'genbox', + 'distance': 'g_dist', + 'sasa': 'g_sas', + 'gangle': 'g_sgangle' } -for name in sorted(config.get_tool_names()): - # compatibility for 5.x 'gmx toolname': add as gmx:toolname - if name.find(':') != -1: - # Gromacs 5 - b_gmx5 = True - prefix = name.split(':')[0] - name = name.split(':')[1] - #make alias for backwards compatibility - - #the common case of just dropping the 'g_' - old_name = 'g_' + name - - #check against uncommon name changes - #have to check each one, since it's possible there are suffixes like for double precision - for c5, c4 in aliases5to4.iteritems(): - if name.startswith(c5): - #maintain suffix - old_name = c4 + name.split(c5)[1] - break - # make names valid python identifiers and use convention that class names are capitalized - clsname = name.replace('.','_').replace('-','_').capitalize() - old_clsname = old_name.replace('.','_').replace('-','_').capitalize() - cls = type(clsname, (GromacsCommand,), {'command_name': name, - 'driver' :prefix, - '__doc__': property(GromacsCommand._get_gmx_docs)}) - #add alias for old name - #No need to see if old_name == name since we'll just clobber the item in registry - registry[old_clsname] = cls - else: - # Gromacs 4: - # make names valid python identifiers and use convention that class names are capitalized - clsname = name.replace('.','_').replace('-','_').capitalize() - cls = type(clsname, (GromacsCommand,), {'command_name': name, - '__doc__': property(GromacsCommand._get_gmx_docs)}) - registry[clsname] = cls # registry keeps track of all classes - # dynamically build the module doc string - __doc__ += _generate_sphinx_class_string(clsname) - -# modify/fix classes as necessary -# Note: -# - check if class was defined in first place -# - replace class -# - update local context AND registry as done below +class GromacsToolLoadingError(Exception): + """Raised when no Gromacs tool could be found.""" + class GromacsCommandMultiIndex(GromacsCommand): - def __init__(self, **kwargs): - """Initialize instance. - - 1) Sets up the combined index file. - 2) Inititialize :class:`~gromacs.core.GromacsCommand` with the - new index file. - - See the documentation for :class:`gromacs.core.GromacsCommand` for details. - """ - kwargs = self._fake_multi_ndx(**kwargs) - super(GromacsCommandMultiIndex, self).__init__(**kwargs) - - def run(self,*args,**kwargs): - """Run the command; make a combined multi-index file if necessary.""" - kwargs = self._fake_multi_ndx(**kwargs) - return super(GromacsCommandMultiIndex, self).run(*args, **kwargs) - - def _fake_multi_ndx(self, **kwargs): - """Combine multiple index file into a single one and return appropriate kwargs. - - Calling the method combines multiple index files into a a single - temporary one so that Gromacs tools that do not (yet) support multi - file input for index files can be used transparently as if they did. - - If a temporary index file is required then it is deleted once the - object is destroyed. - - :Returns: - The method returns the input keyword arguments with the necessary - changes to use the temporary index files. - - :Keywords: - Only the listed keywords have meaning for the method: - - *n* : filename or list of filenames - possibly multiple index files; *n* is replaced by the name of - the temporary index file. - *s* : filename - structure file (tpr, pdb, ...) or ``None``; if a structure file is - supplied then the Gromacs default index groups are automatically added - to the temporary indexs file. - - :Example: - Used in derived classes that replace the standard - :meth:`run` (or :meth:`__init__`) methods with something like:: - - def run(self,*args,**kwargs): - kwargs = self._fake_multi_ndx(**kwargs) - return super(G_mindist, self).run(*args, **kwargs) - - """ - ndx = kwargs.get('n') - if not (ndx is None or type(ndx) is str): - if len(ndx) > 1: - # g_mindist cannot deal with multiple ndx files (at least 4.0.5) - # so we combine them in a temporary file; it is unlinked in __del__. - # self.multi_ndx stores file name for __del__ - fd, self.multi_ndx = tempfile.mkstemp(suffix='.ndx', prefix='multi_') - make_ndx = Make_ndx(f=kwargs.get('s'), n=ndx) - rc,out,err = make_ndx(o=self.multi_ndx, input=['q'], # concatenate all index files - stdout=False, stderr=False) - self.orig_ndx = ndx - kwargs['n'] = self.multi_ndx - return kwargs - - def __del__(self): - """Clean up temporary multi-index files if they were used.""" - # XXX: does not seem to work when closing the interpreter?! - try: - # self.multi_ndx <-- _fake_multi_index() - utilities.unlink_gmx(self.multi_ndx) - except (AttributeError, OSError): - pass - # XXX: type error --- can't use super in __del__? - #super(GromacsCommandMultiIndex, self).__del__() - -# patching up... - -if 'G_mindist' in registry: - - # let G_mindist handle multiple ndx files - class G_mindist(GromacsCommandMultiIndex): - """Gromacs tool 'g_mindist' (with patch to handle multiple ndx files).""" - command_name = registry['G_mindist'].command_name - driver = registry['G_mindist'].driver - __doc__ = registry['G_mindist'].__doc__ - - registry['G_mindist'] = G_mindist - if b_gmx5: - registry['Mindist'] = G_mindist - -if 'G_dist' in registry: - # let G_dist handle multiple ndx files - class G_dist(GromacsCommandMultiIndex): - """Gromacs tool 'g_dist' (with patch to handle multiple ndx files).""" - command_name = registry['G_dist'].command_name - driver = registry['G_dist'].driver - __doc__ = registry['G_dist'].__doc__ - - registry['G_dist'] = G_dist - if b_gmx5: - registry['Distance'] = G_dist - - -# TODO: generate multi index classes via type(), not copy&paste as above... - - -# 5.0.5 compatibility hack -if 'Convert_tpr' in registry: - registry['Tpbconv'] = registry['Convert_tpr'] - -# finally, add everything -globals().update(registry) # add classes to module's scope -__all__ = registry.keys() - -# and clean up the module scope -cls = clsname = name = rec = doc = None # make sure they exist, because the next line -del rec, name, cls, clsname, doc # would throw NameError if no tool was configured + """ Command class that accept multiple index files. + + It works combining multiple index files into a single temporary one so + that tools that do not (yet) support multi index files as input can be + used as if they did. + + It creates a new file only if multiple index files are supplied. + """ + def __init__(self, **kwargs): + kwargs = self._fake_multi_ndx(**kwargs) + super(GromacsCommandMultiIndex, self).__init__(**kwargs) + + def run(self,*args,**kwargs): + kwargs = self._fake_multi_ndx(**kwargs) + return super(GromacsCommandMultiIndex, self).run(*args, **kwargs) + + def _fake_multi_ndx(self, **kwargs): + ndx = kwargs.get('n') + if not (ndx is None or type(ndx) is basestring): + if len(ndx) > 1: + if 's' in kwargs: + ndx.append(kwargs.get('s')) + kwargs['n'] = merge_ndx(*ndx) + return kwargs + + +def tool_factory(clsname, name, driver, base=GromacsCommand): + """ Factory for GromacsCommand derived types. """ + clsdict = { + 'command_name': name, + 'driver': driver, + '__doc__': property(base._get_gmx_docs) + } + return type(clsname, (base,), clsdict) + + +def make_valid_identifier(name): + """ Turns tool names into valid identifiers. + + :param name: tool name + :return: valid identifier + """ + return name.replace('-', '_').capitalize() + + +def find_executables(path): + """ Find executables in a path. + + Searches executables in a directory excluding some know commands + unusable with GromacsWrapper. + + :param path: dirname to search for + :return: list of executables + """ + execs = [] + for exe in os.listdir(path): + fullexe = os.path.join(path, exe) + if (os.access(fullexe, os.X_OK) and not os.path.isdir(fullexe) and + exe not in ['GMXRC', 'GMXRC.bash', 'GMXRC.csh', 'GMXRC.zsh', + 'demux.pl', 'xplor2gmx.pl']): + execs.append(exe) + return execs + + +def load_v5_tools(): + """ Load Gromacs 5.x tools automatically using some heuristic. + + Tries to load tools (1) using the driver from configured groups (2) and + falls back to automatic detection from ``GMXBIN`` (3) then to rough guesses. + + In all cases the command ``gmx help`` is ran to get all tools available. + + :return: dict mapping tool names to GromacsCommand classes + """ + logger.debug("Loading v5 tools...") + + drivers = config.get_tool_names() + + if len(drivers) == 0 and 'GMXBIN' in os.environ: + drivers = find_executables(os.environ['GMXBIN']) + + if len(drivers) == 0 or len(drivers) > 4: + drivers = ['gmx', 'gmx_d', 'gmx_mpi', 'gmx_mpi_d'] + + tools = {} + for driver in drivers: + try: + out = subprocess.check_output([driver, '-quiet', 'help', + 'commands']) + for line in str(out).encode('ascii').splitlines()[5:-1]: + if line[4] != ' ': + + name = line[4:line.index(' ', 4)] + fancy = make_valid_identifier(name) + suffix = driver.partition('_')[2] + if suffix: + fancy = '%s_%s' % (fancy, suffix) + tools[fancy] = tool_factory(fancy, name, driver) + except (subprocess.CalledProcessError, OSError): + pass + + if not tools: + errmsg = "Failed to load v5 tools" + logger.debug(errmsg) + raise GromacsToolLoadingError(errmsg) + logger.debug("Loaded {0} v5 tools successfully!".format(len(tools))) + return tools + + +def load_v4_tools(): + """ Load Gromacs 4.x tools automatically using some heuristic. + + Tries to load tools (1) in configured tool groups (2) and fails back to + automatic detection from ``GMXBIN`` (3) then to a prefilled list. + + Also load any extra tool configured in ``~/.gromacswrapper.cfg`` + + :return: dict mapping tool names to GromacsCommand classes + """ + logger.debug("Loading v4 tools...") + + names = config.get_tool_names() + + if len(names) == 0 and 'GMXBIN' in os.environ: + names = find_executables(os.environ['GMXBIN']) + + if len(names) == 0 or len(names) > len(V4TOOLS) * 4: + names = V4TOOLS[:] + + names.extend(config.get_extra_tool_names()) + + tools = {} + for name in names: + fancy = make_valid_identifier(name) + tools[fancy] = tool_factory(fancy, name, None) + + if not tools: + errmsg = "Failed to load v4 tools" + logger.debug(errmsg) + raise GromacsToolLoadingError(errmsg) + logger.debug("Loaded {0} v4 tools successfully!".format(len(tools))) + return tools + + +def merge_ndx(*args): + """ Takes one or more index files and optionally one structure file and + returns a path for a new merged index file. + + :param args: index files and zero or one structure file + :return: path for the new merged index file + """ + ndxs = [] + struct = None + for fname in args: + if fname.endswith('.ndx'): + ndxs.append(fname) + else: + if struct is not None: + raise ValueError("only one structure file supported") + struct = fname + + fd, multi_ndx = tempfile.mkstemp(suffix='.ndx', prefix='multi_') + os.close(fd) + atexit.register(os.unlink, multi_ndx) + + if struct: + make_ndx = registry['Make_ndx'](f=struct, n=ndxs, o=multi_ndx) + else: + make_ndx = registry['Make_ndx'](n=ndxs, o=multi_ndx) + + _, _, _ = make_ndx(input=['q'], stdout=False, stderr=False) + return multi_ndx + + +# Load tools +if config.MAJOR_RELEASE == '5': + registry = load_v5_tools() +elif config.MAJOR_RELEASE == '4': + registry = load_v4_tools() +else: + try: + registry = load_v5_tools() + except GromacsToolLoadingError: + try: + registry = load_v4_tools() + except GromacsToolLoadingError: + raise GromacsToolLoadingError("Unable to load any tool") + + +# Aliases command names to run unmodified GromacsWrapper scripts on a machine +# with only 5.x +for fancy, cmd in registry.items(): + for c5, c4 in NAMES5TO4.iteritems(): + # have to check each one, since it's possible there are suffixes + # like for double precision + name = cmd.command_name + if name.startswith(c5): + if c4 == c5: + break + else: + # mantain suffix + name = c4 + fancy.lower().split(c5)[1] + registry[make_valid_identifier(name)] = registry[fancy] + break + else: + # the common case of just adding the 'g_' + registry['G_%s' % fancy.lower()] = registry[fancy] + + +# Patching up commands that may be useful to accept multiple index files +for name4, name5 in [('G_mindist', 'Mindist'), ('G_dist', 'Distance')]: + if name4 in registry: + cmd = registry[name4] + registry[name4] = tool_factory(name4, cmd.command_name, cmd.driver, + GromacsCommandMultiIndex) + if name5 in registry: + registry[name5] = registry[name4] + + +# Append class doc for each command +for name in registry.iterkeys(): + __doc__ += ".. class:: %s\n :noindex:\n" % name + +# Finally add command classes to module's scope +globals().update(registry) +__all__ = ['GromacsCommandMultiIndex', 'merge_ndx'] +__all__.extend(registry.keys()) diff --git a/setup.py b/setup.py index 6ada4254..36cf980e 100644 --- a/setup.py +++ b/setup.py @@ -5,16 +5,18 @@ # See the files INSTALL and README for details or visit # https://github.com/Becksteinlab/GromacsWrapper from __future__ import with_statement - from setuptools import setup, find_packages +import imp, os + with open("README.rst") as readme: long_description = readme.read() # Dynamically calculate the version based on gromacs.VERSION. # (but requires that we can actually import the package BEFORE it is # properly installed!) -version = __import__('gromacs.version').get_version() +version_file = os.path.join(os.path.dirname(__file__), 'gromacs', 'version.py') +version = imp.load_source('gromacs.version', version_file).get_version() setup(name="GromacsWrapper", version=version,