Source code for pyretis.engines

# Copyright (c) 2026, PyRETIS Development Team.
# Distributed under the LGPLv2.1+ License. See LICENSE for more info.
"""Definition of engines.

This package defines engines for PyRETIS. The engines are responsible
for carrying out dynamics for a system. This can in principle both
be molecular dynamics or Monte Carlo dynamics. Typically, with RETIS,
this will be molecular dynamics in some form in order to propagate
the equations of motion and obtain new trajectories.


Package structure
-----------------

Modules
~~~~~~~

cp2k.py (:py:mod:`pyretis.engines.cp2k`)
    Defines an engine for use with CP2K.

engine.py (:py:mod:`pyretis.engines.engine`)
    Defines the base engine class.

external.py (:py:mod:`pyretis.engines.external`)
    Defines the interface for external engines.

gromacs.py (:py:mod:`pyretis.engines.gromacs`)
    Defines the canonical (streaming) engine for use with GROMACS. It
    does not rely on continuously starting and stopping the GROMACS
    executable.

gromacs_steps.py (:py:mod:`pyretis.engines.gromacs_steps`)
    Defines the relaunch implementation of the GROMACS engine, which
    starts and stops the GROMACS executable once per order-parameter
    point. Reached as ``class = "gromacs_steps"``.

internal.py (:py:mod:`pyretis.engines.internal`)
    Defines internal PyRETIS engines.

lammps.py (:py:mod:`pyretis.engines.lammps`)
    Defines the canonical (streaming) engine for use with LAMMPS.

lammps_steps.py (:py:mod:`pyretis.engines.lammps_steps`)
    Defines the relaunch implementation of the LAMMPS engine. Reached
    as ``class = "lammps_steps"``.

openmm.py (:py:mod:`pyretis.engines.openmm`)
    Defines an engine for use with OpenMM.

Important methods defined here
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

engine_factory (:py:func:`.engine_factory`)
    A method to create engines from settings.

get_engine_class (:py:func:`.get_engine_class`)
    Return the engine class matching a name.

resolve_engine_class (:py:func:`.resolve_engine_class`)
    Resolve the engine class from full settings.

get_default_units (:py:func:`.get_default_units`)
    Return the default unit system for an engine.
"""
import logging
import os
import warnings
from pyretis.core.common import generic_factory, import_from
from .internal import MDEngine, Verlet, VelocityVerlet, Langevin
from .external import ExternalMDEngine
from .gromacs import GromacsEngine
from .gromacs_steps import GromacsEngineSteps
from .cp2k import CP2KEngine
from .cp2k_steps import CP2KEngineSteps
from .openmm import OpenMMEngine
from .lammps import LAMMPSEngine
from .lammps_steps import LAMMPSEngineSteps

logger = logging.getLogger(__name__)  # pylint: disable=invalid-name
logger.addHandler(logging.NullHandler())

__all__ = [
    'MDEngine',
    'Verlet',
    'VelocityVerlet',
    'Langevin',
    'ExternalMDEngine',
    'GromacsEngine',
    'GromacsEngineSteps',
    'CP2KEngine',
    'CP2KEngineSteps',
    'OpenMMEngine',
    'LAMMPSEngine',
    'LAMMPSEngineSteps',
    'engine_factory',
    'get_engine_class',
    'resolve_engine_class',
    'get_default_units',
]

# Engine-naming convention (decided 2026-06-03, MERGE_TODO E4.5; supersedes the
# earlier debrand_plan "*_external.py" map):
#   * the bare name (``gromacs``, ``lammps``) is the *canonical* engine and
#     points at the fast, streaming implementation (one external-process
#     launch per propagated segment, frames read on the fly);
#   * the ``_steps`` suffix selects the relaunch-per-step implementation (a
#     fresh external-process launch for every order-parameter point) --
#     correct but much slower, kept available for provenance / debugging.
# Class naming (4b-2b): the canonical streaming engine owns the plain
# class name and module -- :py:class:`pyretis.engines.gromacs.GromacsEngine`
# and :py:class:`pyretis.engines.lammps.LAMMPSEngine` -- while the relaunch
# implementation is :py:class:`.GromacsEngineSteps` /
# :py:class:`.LAMMPSEngineSteps` in the ``*_steps`` modules. The old
# ``GromacsEngine2`` / ``LAMMPSEngine2`` class names and ``gromacs2`` /
# ``lammps2`` modules are retired; ``gromacs2`` / ``lammps2`` survive only
# as deprecated *settings* aliases below.
# Migration is staged so committed references stay valid:
#   * Phase A (DONE): the ``_steps`` aliases were added.
#   * Phase B (DONE, GROMACS): bare ``gromacs`` resolves to the streaming
#     GromacsEngine; the relaunch engine is reached as ``gromacs_steps``.
#     Every committed input that selected the relaunch engine was moved to
#     ``gromacs_steps`` in the same change, so no existing run changes its
#     engine -- only the meaning of the bare name for *new* inputs changes.
#   * Phase C (DONE, LAMMPS): bare ``lammps`` resolves to the streaming
#     LAMMPSEngine (one persistent run read on-the-fly); the relaunch engine
#     is reached as ``lammps_steps``. As with GROMACS, every committed input
#     that selected the relaunch engine was moved to ``lammps_steps`` in
#     lockstep, so no existing run changes its engine.
#   * 4b-2b (DONE): the streaming classes were renamed off the confusing
#     ``*2`` spelling onto the canonical class names/modules. ``gromacs2`` /
#     ``lammps2`` stay as deprecated settings aliases for the streaming
#     engine. The inf-flavour ``gromacs`` / ``lammps`` (build_engine_map)
#     are unaffected.
ENGINE_MAP = {
    'velocityverlet': {'cls': VelocityVerlet},
    'verlet': {'cls': Verlet},
    'langevin': {'cls': Langevin},
    'gromacs': {'cls': GromacsEngine},
    # ``gromacs2`` is the deprecated spelling of the streaming engine:
    # it resolves to the same class as the canonical ``gromacs`` but
    # warns the user to switch (see :func:`_warn_deprecated_engine`).
    'gromacs2': {'cls': GromacsEngine, 'deprecated_for': 'gromacs'},
    'gromacs_steps': {'cls': GromacsEngineSteps},
    'cp2k': {'cls': CP2KEngine},
    'cp2k_steps': {'cls': CP2KEngineSteps},
    'openmm': {'cls': OpenMMEngine},
    'lammps': {'cls': LAMMPSEngine},
    # ``lammps2`` is the deprecated spelling of the streaming engine,
    # kept for symmetry with ``gromacs2``.
    'lammps2': {'cls': LAMMPSEngine, 'deprecated_for': 'lammps'},
    'lammps_steps': {'cls': LAMMPSEngineSteps},
}


def _warn_deprecated_engine(name):
    """Warn if ``name`` is a deprecated engine alias.

    Deprecated aliases (e.g. ``gromacs2``) still resolve to their
    canonical class but emit a discontinuation warning pointing at the
    name to use instead. A ``DeprecationWarning`` is raised (so callers
    can catch it / Python de-duplicates it) and a user-visible
    ``logger.warning`` is logged in the run log.
    """
    if not name:
        return
    entry = ENGINE_MAP.get(name.lower())
    if not entry or 'deprecated_for' not in entry:
        return
    target = entry['deprecated_for']
    msg = (f'Engine class "{name}" is deprecated and will be '
           f'discontinued; use "{target}" instead.')
    logger.warning(msg)
    warnings.warn(msg, DeprecationWarning, stacklevel=3)


[docs]def engine_factory(settings): """Create an engine according to the given settings. This function is included as a convenient way of setting up and selecting an engine. It will return the created engine. Parameters ---------- settings : dict This defines how we set up and select the engine. Returns ------- out : object like :py:class:`.EngineBase` The object representing the engine to use in a simulation. """ _warn_deprecated_engine(settings.get('class')) return generic_factory(settings, ENGINE_MAP, name='engine')
[docs]def get_engine_class(name): """Return the class matching a given engine name.""" try: cls = ENGINE_MAP[name.lower()]['cls'] except (AttributeError, KeyError): return None _warn_deprecated_engine(name) return cls
[docs]def resolve_engine_class(settings): """Resolve an engine class from the given settings.""" if 'engine' in settings: engine_settings = settings['engine'] exe_path = settings.get('simulation', {}).get('exe_path') else: engine_settings = settings exe_path = engine_settings.get('exe_path') engine_object = engine_settings.get('obj') if engine_object is not None: return engine_object.__class__ klass = engine_settings.get('class') if klass is None: return None module = engine_settings.get('module') if module is None: cls = get_engine_class(klass) if cls is None: msg = f'Could not resolve engine class "{klass}"' raise ValueError(msg) return cls module_path = module if not os.path.isfile(module_path) and exe_path is not None: module_path = os.path.join(exe_path, module) return import_from(module_path, klass)
[docs]def get_default_units(settings): """Return the default unit system for the selected engine.""" klass = resolve_engine_class(settings) if klass is None: return None return klass.get_default_units(settings)
# Optional turtlemd registration -- the pyretis-native factory now # resolves ``class = "turtlemd"`` the same way the inf-flavour factory # does (see :py:func:`pyretis.engines.factory.build_engine_map`). # Functional integration with the pyretis-native simulation flow is # tracked under E4.4 / T2.4; this entry lands here so the cross-flavour # deterministic test can address the same engine by name through both # factories. # # The block sits at the bottom of the file on purpose: importing # ``pyretis.engines.turtlemd`` transitively imports # :py:mod:`pyretis.setup.common`, which itself imports # :py:func:`engine_factory` from this module. Pulling it in earlier # tripped a partial-import circular reference. The import is also # optional, because turtlemd is a ``pyretis[turtlemd]`` extra. try: from .turtlemd import TurtleMDEngine # noqa: E402 ENGINE_MAP['turtlemd'] = {'cls': TurtleMDEngine} except ImportError: logger.debug( "turtlemd-backed engine not registered " "(install with `pip install pyretis[turtlemd]`)" )