Skip to content

openmm

openmm

OpenMM molecular mechanics backend.

Provides a full-featured MM engine using OpenMM for energy, minimization, Hessian, and frequency calculations. Supports both harmonic and MM3 functional forms with runtime parameter updates via :class:_OpenMMState.

OpenMMBackend

OpenMMBackend(platform_name: str | None = None, precision: str | None = None)

Molecular mechanics backend powered by OpenMM.

Supports both harmonic (AMBER-style) and MM3 functional forms. A prepared session holds a reusable :class:_OpenMMState so parameter updates during an optimization loop do not rebuild the OpenMM system.

Initialize the OpenMM backend.

Parameters:

Name Type Description Default
platform_name str | None

OpenMM platform to use (e.g. "CPU", "CUDA", "OpenCL"). When None, the fastest available platform is auto-detected via :func:detect_best_platform (CUDA > OpenCL > CPU > Reference).

None
precision str | None

Floating-point precision for GPU platforms ("single", "mixed", or "double"). Ignored for CPU/Reference platforms.

None

Raises:

Type Description
BackendUnavailableError

If OpenMM is not installed.

BackendConfigurationError

If precision is invalid.

Source code in q2mm/backends/mm/openmm.py
def __init__(
    self,
    platform_name: str | None = None,
    precision: str | None = None,
) -> None:
    """Initialize the OpenMM backend.

    Args:
        platform_name: OpenMM platform to use (e.g. ``"CPU"``,
            ``"CUDA"``, ``"OpenCL"``).  When ``None``, the fastest
            available platform is auto-detected via
            :func:`detect_best_platform` (CUDA > OpenCL > CPU >
            Reference).
        precision: Floating-point precision for GPU platforms
            (``"single"``, ``"mixed"``, or ``"double"``).  Ignored
            for CPU/Reference platforms.

    Raises:
        BackendUnavailableError: If OpenMM is not installed.
        BackendConfigurationError: If *precision* is invalid.

    """
    if not _HAS_OPENMM:
        raise BackendUnavailableError(
            'OpenMM is not installed. Install with `pip install openmm` or `pip install -e ".[openmm]"`.'
        )
    if platform_name is None:
        platform_name = detect_best_platform()
    self._platform_name = platform_name

    _VALID_PRECISIONS = {"single", "mixed", "double"}
    if precision is not None:
        precision = precision.strip().lower()
        if precision not in _VALID_PRECISIONS:
            raise BackendConfigurationError(
                f"Invalid precision {precision!r}. Allowed values: {', '.join(sorted(_VALID_PRECISIONS))}."
            )
    self._precision = precision
    logger.info("OpenMM platform: %s", self._platform_name)

info property

Immutable capability declaration for this backend.

The platform name is baked into the provenance/name, so this is built per instance. Both HARMONIC and MM3 forms use CustomForce objects with global parameters, so analytical parameter gradients are available; batched energy and Hessian-parameter Jacobians are not.

prepare

prepare(request: PreparationRequest) -> PreparedOpenMM

Build a prepared session for one training case.

Parameters:

Name Type Description Default
request PreparationRequest

Preparation request carrying the molecule and base force field.

required

Returns:

Name Type Description
PreparedOpenMM PreparedOpenMM

A per-case session owning a reusable :class:_OpenMMState.

Raises:

Type Description
PreparationError

If no force field is supplied, its functional form is unsupported, or the OpenMM system cannot be built.

Source code in q2mm/backends/mm/openmm.py
def prepare(self, request: PreparationRequest) -> PreparedOpenMM:
    """Build a prepared session for one training case.

    Args:
        request: Preparation request carrying the molecule and base
            force field.

    Returns:
        PreparedOpenMM: A per-case session owning a reusable
            :class:`_OpenMMState`.

    Raises:
        PreparationError: If no force field is supplied, its functional
            form is unsupported, or the OpenMM system cannot be built.

    """
    from q2mm.models.parameters import ParameterLayout

    if request.force_field is None:
        raise PreparationError("OpenMM requires a base ForceField in the PreparationRequest.")
    info = self.info
    form = request.force_field.functional_form.value
    if not info.supports_form(form):
        raise PreparationError(
            f"OpenMM does not support functional form {form!r}. Supported: {sorted(info.functional_forms)}"
        )
    layout = ParameterLayout.from_force_field(request.force_field)
    try:
        state = self._build_state(request.molecule, request.force_field)
    except (BackendConfigurationError, PreparationError):
        raise
    except Exception as exc:  # noqa: BLE001
        raise PreparationError(f"OpenMM failed to prepare case {request.case_id!r}: {exc}") from exc
    return PreparedOpenMM(
        backend=self,
        info=info,
        case_id=request.case_id,
        molecule=request.molecule,
        force_field=request.force_field,
        layout=layout,
        state=state,
    )

PreparedOpenMM

PreparedOpenMM(*, backend: OpenMMBackend, info: BackendInfo, case_id: str, molecule: Molecule, force_field: ForceField, layout: Any, state: _OpenMMState)

Bases: AbstractPreparedBackend

Prepared OpenMM session for a single training case.

Owns the molecule, base force field, parameter layout, and one reusable private :class:_OpenMMState. Energy, Hessian, and frequency evaluations update the state's global parameters in place (reusing native state), while minimization and analytical parameter gradients build a throwaway state so they never mutate the reusable energy/Hessian state.

Source code in q2mm/backends/mm/openmm.py
def __init__(
    self,
    *,
    backend: OpenMMBackend,
    info: BackendInfo,
    case_id: str,
    molecule: Molecule,
    force_field: ForceField,
    layout: Any,
    state: _OpenMMState,
) -> None:
    super().__init__(
        info=info,
        case_id=case_id,
        molecule=molecule,
        force_field=force_field,
        layout=layout,
    )
    self._backend = backend
    self._state = state

detect_best_platform

detect_best_platform() -> str

Return the name of the fastest available OpenMM platform.

If the OPENMM_DEFAULT_PLATFORM environment variable is set, its value is returned directly (no validation against installed platforms). This allows test harnesses to force CPU-only execution.

Otherwise, platform preference order: CUDA > OpenCL > CPU > Reference.

Logs a warning when CUDA is unavailable and the function falls back to OpenCL on a system with an NVIDIA GPU — OpenCL on modern NVIDIA GPUs gives very poor utilisation (~14%). The warning is suppressed on non-NVIDIA systems where OpenCL may be the intended backend.

Returns:

Name Type Description
str str

Name of the best available platform.

Raises:

Type Description
ImportError

If OpenMM is not installed.

Source code in q2mm/backends/mm/openmm.py
def detect_best_platform() -> str:
    """Return the name of the fastest available OpenMM platform.

    If the ``OPENMM_DEFAULT_PLATFORM`` environment variable is set, its
    value is returned directly (no validation against installed
    platforms).  This allows test harnesses to force CPU-only execution.

    Otherwise, platform preference order: CUDA > OpenCL > CPU > Reference.

    Logs a warning when CUDA is unavailable and the function falls back
    to OpenCL on a system with an NVIDIA GPU — OpenCL on modern NVIDIA
    GPUs gives very poor utilisation (~14%).  The warning is suppressed
    on non-NVIDIA systems where OpenCL may be the intended backend.

    Returns:
        str: Name of the best available platform.

    Raises:
        ImportError: If OpenMM is not installed.

    """
    _ensure_openmm()
    import os

    env_platform = os.environ.get("OPENMM_DEFAULT_PLATFORM", "").strip()
    if env_platform:
        return env_platform
    available = {mm.Platform.getPlatform(i).getName() for i in range(mm.Platform.getNumPlatforms())}
    for name in _PLATFORM_PRIORITY:
        if name in available:
            if name == "OpenCL" and "CUDA" not in available:
                # Only warn on NVIDIA GPUs where CUDA should be available
                _nvidia_present = False
                try:
                    import subprocess

                    result = subprocess.run(
                        ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
                        capture_output=True,
                        text=True,
                        timeout=5,
                    )
                    _nvidia_present = result.returncode == 0 and bool(result.stdout.strip())
                except Exception:
                    pass
                if _nvidia_present:
                    logger.warning(
                        "CUDA platform not available, falling back to OpenCL. "
                        "GPU utilization will be poor (~14%%). "
                        "Consider installing OpenMM-CUDA-12 or using WSL2."
                    )
            return name
    # Fallback — shouldn't happen since OpenMM always has Reference
    return mm.Platform.getPlatform(0).getName()  # pragma: no cover