Source code for openff.interchange.drivers.lammps

"""Functions for running energy evluations with LAMMPS."""

import tempfile

import numpy
from openff.toolkit import Quantity, unit
from openff.utilities import MissingOptionalDependencyError, requires_package

from openff.interchange import Interchange
from openff.interchange.components.mdconfig import MDConfig
from openff.interchange.drivers.report import EnergyReport
from openff.interchange.exceptions import LAMMPSNotFoundError, LAMMPSRunError


[docs]def get_lammps_energies( interchange: Interchange, round_positions: int | None = None, detailed: bool = False, ) -> EnergyReport: """ Given an OpenFF Interchange object, return single-point energies as computed by LAMMPS. .. warning :: This API is experimental and subject to change. .. todo :: Split out _running_ LAMMPS into a separate internal function Parameters ---------- interchange : openff.interchange.Interchange An OpenFF Interchange object to compute the single-point energy of round_positions : int, optional The number of decimal places, in nanometers, to round positions. This can be useful when comparing to i.e. GROMACS energies, in which positions may be rounded. detailed : bool, optional If True, return a detailed energy report containing all energy components. Returns ------- report : EnergyReport An `EnergyReport` object containing the single-point energies. """ try: return _process( _get_lammps_energies(interchange, round_positions), detailed, ) except MissingOptionalDependencyError: raise LAMMPSNotFoundError
@requires_package("lammps") def _get_lammps_energies( interchange: Interchange, round_positions: int | None = None, ) -> dict[str, unit.Quantity]: import lammps if round_positions is not None: interchange.positions = numpy.round(interchange.positions, round_positions) with tempfile.TemporaryDirectory(): interchange.to_lammps("out.lmp") mdconfig = MDConfig.from_interchange(interchange) mdconfig.write_lammps_input( interchange=interchange, input_file="tmp.in", ) # By default, LAMMPS spits out logs to the screen, turn it off # https://matsci.org/t/how-to-remove-or-redirect-python-lammps-stdout/38075/5 # not that this is not sent to STDOUT, so `contextlib.redirect_stdout` won't work runner = lammps.lammps(cmdargs=["-screen", "none", "-nocite"]) try: runner.file("tmp.in") # LAMMPS does not raise a custom exception :( except Exception as error: raise LAMMPSRunError from error # thermo_style custom ebond eangle edihed eimp epair evdwl ecoul elong etail pe parsed_energies = [ Quantity(energy, "kilocalorie_per_mole") for energy in runner.last_thermo().values() ] # TODO: Sanely map LAMMPS's energy names to the ones we care about return { "Bond": parsed_energies[0], "Angle": parsed_energies[1], "ProperTorsion": parsed_energies[2], "ImproperTorsion": parsed_energies[3], "vdW": parsed_energies[5], "DispersionCorrection": parsed_energies[8], "ElectrostaticsShort": parsed_energies[6], "ElectrostaticsLong": parsed_energies[7], } def _process( energies: dict[str, unit.Quantity], detailed: bool = False, ) -> EnergyReport: if detailed: return EnergyReport(energies=energies) return EnergyReport( energies={ "Bond": energies["Bond"], "Angle": energies["Angle"], "Torsion": energies["ProperTorsion"] + energies["ImproperTorsion"], "vdW": energies["vdW"] + energies["DispersionCorrection"], "Electrostatics": ( energies["ElectrostaticsShort"] + energies["ElectrostaticsLong"] ), }, )