# GromacsWrapper
# Copyright (c) 2009-2010 Oliver Beckstein <orbeckst@gmail.com>
# Released under the GNU Public License 3 (or higher, your choice)
# See the file COPYING for details.

"""
GromacsWrapper Overview
=======================

**GromacsWrapper** (package :mod:`gromacs`) is a thin shell around the `Gromacs`_
tools for light-weight integration into python scripts or interactive use in
`ipython`_.

.. _`Gromacs`: http://www.gromacs.org
.. _`ipython`: http://ipython.scipy.org


Modules
-------

:mod:`gromacs`
     The top level module contains all gromacs tools; each tool can be
     run directly or queried for its documentation. It also defines
     the root logger class (name *gromacs* by default).

:mod:`gromacs.config`
     Configuration options. Not really used much at the moment.

:mod:`gromacs.cbook`
     The Gromacs cook book contains typical applications of the tools. In many
     cases this not more than just an often-used combination of parameters for
     a tool.

:mod:`gromacs.tools`
     Contains classes that wrap the gromacs tools. They are automatically
     generated from the list of tools in :data:`gromacs.tools.gmx_tools`.

:mod:`gromacs.fileformats`
     Classes to represent data files in various formats such as
     xmgrace graphs. The classes allow reading and writing and for
     graphs, also plotting of the data.

:mod:`gromacs.utilities`
     Convenience functions and mixin-classes that are used as helpers
     in other modules.

:mod:`gromacs.setup`
     Functions to set up a MD simulation, containing tasks such as solvation
     and adding ions, energy minimizqtion, MD with position-restraints, and
     equilibrium MD.

:mod:`gromacs.qsub`
     Functions to handle batch submission queuing systems.

:mod:`gromacs.run`
     Classes to run :program:`mdrun` in various way, including on
     multiprocessor systems.


Examples
--------

The following examples should simply convey the flavour of using the
package. See the individual modules for more examples.

Getting help
............

In python::

   gromacs.g_dist.help()
   gromacs.g_dist.help(long=True)

In ``ipython``::

   gromacs.g_dist ?


Simple usage
............

Gromacs flags are given as python keyword arguments::

   gromacs.g_dist(v=True, s='topol.tpr', f='md.xtc', o='dist.xvg', dist=1.2)

Input to stdin of the command can be supplied::

   gromacs.make_ndx(f='topol.tpr', o='md.ndx',
                    input=('keep "SOL"', '"SOL" | r NA | r CL', 'name 2 solvent', 'q'))

Output of the command can be caught in a variable and analyzed::

   rc, output, junk = gromacs.grompp(..., stdout=False)        # collects command output
   for line in output.split('\\n'):
       line = line.strip()
       if line.startswith('System has non-zero total charge:'):
             qtot = float(line[34:])
             break

(See :func:`gromacs.cbook.grompp_qtot` for a more robust implementation of this
application.)


Warnings and Exceptions
-----------------------

A number of package-specific exceptions (:exc:`GromacsError`) and
warnings (:exc:`GromacsFailureWarning`, :exc:`GromacsImportWarning`,
:exc:`GromacsValueWarning`, :exc:`AutoCorrectionWarning`,
:exc:`BadParameterWarning`) can be raised.

If you want to stop execution at, for instance, a :exc:`AutoCorrectionWarning` or
:exc:`BadParameterWarning` then use the python :mod:`warnings` filter::

  import warnings
  warnings.simplefilter('error', gromacs.AutoCorrectionWarning)
  warnings.simplefilter('error', gromacs.BadParameterWarning)

This will make python raise an exception instead of moving on. The default is
to always report, eg::

  warnings.simplefilter('always', gromacs.BadParameterWarning)

The following *exceptions* are defined:

.. autoexception:: GromacsError
.. autoexception:: MissingDataError
.. autoexception:: ParseError

The following *warnings* are defined:

.. autoexception:: GromacsFailureWarning
.. autoexception:: GromacsImportWarning
.. autoexception:: GromacsValueWarning
.. autoexception:: AutoCorrectionWarning
.. autoexception:: BadParameterWarning
.. autoexception:: MissingDataWarning
.. autoexception:: UsageWarning
.. autoexception:: LowAccuracyWarning


Logging
-------

The library uses python's logging_ module to keep a history of what it has been
doing. In particular, every wrapped Gromacs command logs its command line
(including piped input) to the log file (configured in
:data:`gromacs.config.logfilename`). This facilitates debugging or simple
re-use of command lines for very quick and dirty work. The logging facilty
appends to the log file and time-stamps every entry. See :mod:`gromacs.config`
for more details on configuration.

It is also possible to capture output from Gromacs commands in a file
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
-------

The package version is recorded in the :const:`gromacs.__version__` variable.

.. autodata:: __version__

"""
__docformat__ = "restructuredtext en"

import os
import warnings
import logging

# __all__ is extended with all gromacs command instances later
__all__ = ["config", "tools", "cbook", "fileformats"]

from . import _version

#: Version of the package, following `semantic versioning`_ in the form
#: MAJOR.MINOR.PATCH. When PATCH increases, bugs are fixed or documentation
#: or metadata are updated. Increases in MINOR can introduce new features and
#: deprecate old code. API-breaking and backwards incompatible changes can only
#: occur when MAJOR is increased, except during initial development while MAJOR
#: == 0, in which also increases in MINOR may (rarely) introduce breaking
#: changes.
#:
#: Additional information after PATCH indicates that you are working with an
#: unreleased version, with the number of git commits after the release and
#: the commit ID encoded in the trailing string.
#:
#: .. _`semantic versioning`: https://semver.org/
__version__ = _version.__version__
del _version

from . import fileformats
from .exceptions import (
    GromacsError,
    MissingDataError,
    ParseError,
    GromacsFailureWarning,
    GromacsImportWarning,
    GromacsValueWarning,
    AutoCorrectionWarning,
    BadParameterWarning,
    MissingDataWarning,
    UsageWarning,
    LowAccuracyWarning,
)


# Import configuration before anything else
from . import config


# NOTE: logging is still iffy; when I reload I add a new logger each
# time and output is repeated for each reload. Probably should heed
# the advice on logging and libraries in
# http://docs.python.org/library/logging.html?#configuring-logging-for-a-library
class NullHandler(logging.Handler):
    def emit(self, record):
        pass


# default silent logger --- just here for illustration; below we
# we get a proper logger from log.create()
h = NullHandler()
logging.getLogger("gromacs").addHandler(h)
del h

# The top level logger of the library is named 'gromacs'.
# Note that we are configuring this logger with console output. If the root logger also
# does this then we will get two output lines to the console. We'll live with this because
# this is a simple convenience library and most people will not bother
# with a logger (I think...)
#
# In modules that use loggers get a logger like so:
#     import logging
#     logger = logging.getLogger('gromacs.MODULENAME')


def start_logging(logfile="gromacs.log"):
    """Start logging of messages to file and console.

    The default logfile is named ``gromacs.log`` and messages are
    logged with the tag *gromacs*.
    """
    from . import log

    log.create("gromacs", logfile=logfile)
    logging.getLogger("gromacs").info(
        "GromacsWrapper %s STARTED logging to %r", __version__, logfile
    )


def stop_logging():
    """Stop logging to logfile and console."""
    from . import log

    logger = logging.getLogger("gromacs")
    logger.info("GromacsWrapper %s STOPPED logging", __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"))


# Add gromacs command **instances** to the top level.
# These serve as the equivalence of running commands in the shell.
# (Note that each gromacs command is actually run when the instance is
# created in order to gather the documentation string.)
from . import tools

# Ignore warnings from a few programs that do not produce
# documentation when run with '-h' (only applies when the default for
# failuremode of core.GromacsCommand is changed to 'warn')
warnings.simplefilter("ignore", GromacsFailureWarning)
_have_g_commands = []
_missing_g_commands = []
for clsname, cls in tools.registry.items():
    name = clsname[0].lower() + clsname[1:]  # instances should start with lower case
    try:
        globals()[name] = cls()  # add instance of command for immediate use
        _have_g_commands.append(name)
    except:
        _missing_g_commands.append(name)
warnings.simplefilter("always", GromacsFailureWarning)
warnings.simplefilter("always", GromacsImportWarning)

_have_g_commands.sort()
_missing_g_commands.sort()
if len(_missing_g_commands) > 0:
    warnings.warn(
        "Some Gromacs commands were NOT found; "
        "maybe source GMXRC first? The following are missing:\n%r\n"
        % _missing_g_commands,
        category=GromacsImportWarning,
    )

del name, cls, clsname

# get ALL active command instances with 'from gromacs import *'
__all__.extend(_have_g_commands)


# cbook should come after the whole of init as it relies on command
# instances in the top level name space -- do we really need cbook at top level? [orbeckst]
# try:
#     from . import cbook
# except OSError as err:
#     warnings.warn("Some Gromacs commands were NOT found when importing gromacs.cbook:\n"+str(err),
#                   category=GromacsImportWarning)

# convenience functions for warnings

less_important_warnings = ["AutoCorrectionWarning", "UsageWarning"]


def filter_gromacs_warnings(action, categories=None):
    """Set the :meth:`warnings.simplefilter` to *action*.

    *categories* must be a list of warning classes or strings.
    ``None`` selects the defaults,  :data:`gromacs.less_important_warnings`.
    """

    if categories is None:
        categories = less_important_warnings
    for c in categories:
        try:
            w = globals()[c]
        except KeyError:
            w = c
        if not issubclass(w, Warning):
            raise TypeError(
                "{0!r} is neither a Warning nor the name of a Gromacs warning.".format(
                    c
                )
            )
        warnings.simplefilter(action, category=w)


def disable_gromacs_warnings(categories=None):
    """Disable ("ignore") specified warnings from the gromacs package.

    *categories* must be a list of warning classes or strings.
    ``None`` selects the defaults.

    """
    filter_gromacs_warnings("ignore", categories=categories)


def enable_gromacs_warnings(categories=None):
    """Enable ("always") specified warnings from the gromacs package.

    *categories* must be a list of warning classes or strings.
    ``None`` selects the defaults, :data:`gromacs._less_important_warnings`.

    """
    filter_gromacs_warnings("always", categories=categories)
