######################################################################
# BioSimSpace: Making biomolecular simulation a breeze!
#
# Copyright: 2017-2025
#
# Authors: Lester Hedges <lester.hedges@gmail.com>
#
# BioSimSpace is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BioSimSpace is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BioSimSpace. If not, see <http://www.gnu.org/licenses/>.
#####################################################################
"""Functionality for running simulations using NAMD."""
__author__ = "Lester Hedges"
__email__ = "lester.hedges@gmail.com"
__all__ = ["Namd"]
from .._Utils import _try_import
import math as _math
import os as _os
_pygtail = _try_import("pygtail")
import timeit as _timeit
import warnings as _warnings
from sire.legacy import Base as _SireBase
from sire.legacy import IO as _SireIO
from sire.legacy import Mol as _SireMol
from sire.legacy.Maths import Vector as _Vector
from .. import _isVerbose
from .._Exceptions import IncompatibleError as _IncompatibleError
from .._Exceptions import MissingSoftwareError as _MissingSoftwareError
from ..Protocol._position_restraint_mixin import _PositionRestraintMixin
from .._SireWrappers import System as _System
from ..Types._type import Type as _Type
from .. import IO as _IO
from .. import Protocol as _Protocol
from .. import Trajectory as _Trajectory
from .. import Units as _Units
from .. import _Utils
from . import _process
[docs]
class Namd(_process.Process):
    """A class for running simulations using NAMD."""
[docs]
    def __init__(
        self,
        system,
        protocol,
        reference_system=None,
        exe=None,
        name="namd",
        work_dir=None,
        seed=None,
        property_map={},
        **kwargs,
    ):
        """
        Constructor.
        Parameters
        ----------
        system : :class:`System <BioSimSpace._SireWrappers.System>`
            The molecular system.
        protocol : :class:`Protocol <BioSimSpace.Protocol>`
            The protocol for the NAMD process.
        reference_system : :class:`System <BioSimSpace._SireWrappers.System>` or None
            An optional system to use as a source of reference coordinates for position
            restraints. It is assumed that this system has the same topology as "system".
            If this is None, then "system" is used as a reference.
        exe : str
            The full path to the NAMD executable.
        name : str
            The name of the process.
        work_dir :
            The working directory for the process.
        seed : int
            A random number seed.
        property_map : dict
            A dictionary that maps system "properties" to their user defined
            values. This allows the user to refer to properties with their
            own naming scheme, e.g. { "charge" : "my-charge" }
        kwargs : dict
            Additional keyword arguments.
        """
        # Call the base class constructor.
        super().__init__(
            system,
            protocol,
            reference_system=reference_system,
            name=name,
            work_dir=work_dir,
            seed=seed,
            property_map=property_map,
        )
        # Set the package name.
        self._package_name = "NAMD"
        # This process can generate trajectory data.
        self._has_trajectory = True
        # If the path to the executable wasn't specified, then search
        # for it in $PATH.
        if exe is None:
            try:
                self._exe = _SireBase.findExe("namd2").absoluteFilePath()
            except:
                raise _MissingSoftwareError(
                    "'BioSimSpace.Process.Namd' is not supported. "
                    "Please install NAMD (http://www.ks.uiuc.edu/Research/namd)."
                ) from None
        else:
            # Make sure executable exists.
            if _os.path.isfile(exe):
                self._exe = exe
            else:
                raise IOError("NAMD executable doesn't exist: '%s'" % exe)
        # Initialise the stdout dictionary and title header.
        self._stdout_dict = _process._MultiDict()
        self._stdout_title = None
        # The names of the input files.
        self._psf_file = _os.path.join(str(self._work_dir), f"{name}.psf")
        self._top_file = _os.path.join(str(self._work_dir), f"{name}.pdb")
        self._param_file = _os.path.join(str(self._work_dir), f"{name}.params")
        self._velocity_file = None
        self._restraint_file = None
        # The name of the trajectory file.
        self._traj_file = _os.path.join(str(self._work_dir), f"{name}_out.dcd")
        # Set the path for the NAMD configuration file.
        self._config_file = _os.path.join(str(self._work_dir), f"{name}.cfg")
        # Create the list of input files.
        self._input_files = [
            self._config_file,
            self._psf_file,
            self._top_file,
            self._param_file,
        ]
        # Now set up the working directory for the process.
        self._setup() 
    def _setup(self):
        """Setup the input files and working directory ready for simulation."""
        # Create the input files...
        # Create a copy of the system.
        system = self._system.copy()
        # Check for perturbable molecules and convert to the chosen end state.
        system = self._checkPerturbable(system)
        # PSF and parameter files.
        try:
            file = _os.path.splitext(self._psf_file)[0]
            _IO.saveMolecules(file, system, "psf", property_map=self._property_map)
        except Exception as e:
            msg = "Failed to write system to 'CHARMMPSF' format."
            if _isVerbose():
                raise IOError(msg) from e
            else:
                raise IOError(msg) from None
        # PDB file.
        try:
            pdb = _SireIO.PDB2(system._sire_object, self._property_map)
            pdb.writeToFile(self._top_file)
        except Exception as e:
            msg = "Failed to write system to 'PDB' format."
            if _isVerbose():
                raise IOError(msg) from e
            else:
                raise IOError(msg) from None
        # Try to write a PDB "velocity" restart file.
        # The file will only be generated if all atoms in the system have
        # a "velocity" property.
        # First generate a name for the velocity file.
        velocity_file = _os.path.splitext(self._top_file)[0] + ".vel"
        # Write the velocity file.
        has_velocities = pdb.writeVelocityFile(velocity_file)
        # If a file was written, store the name of the file and update the
        # list of input files.
        if has_velocities:
            self._velocity_file = velocity_file
            self._input_files.append(self._velocity_file)
        # NAMD requires donor, acceptor, and non-bonded exclusion record entries
        # in the PSF file. We check that these are present and append blank
        # records if not. The records are actually redundant (they have no
        # affect on the MD) so could be stripped (or zeroed) by the CharmmPSF
        # parser.
        has_donors = False
        has_acceptors = False
        has_non_bonded = False
        # When converting forcefields, improper terms may be represented as
        # dihedrals (cosine impropers). As such, there will be no improper
        # records in the PSF file.
        has_impropers = False
        # Open the PSF file for reading.
        with open(self._psf_file) as file:
            # Loop over all lines.
            for line in file:
                # There are improper records.
                if "!NIMPHI" in line:
                    has_impropers = True
                # There are donor records.
                elif "!NDON" in line:
                    has_donors = True
                # There are acceptor records.
                elif "NACC" in line:
                    has_acceptors = True
                # There are non-bonded exclusion records.
                elif "NNB" in line:
                    has_non_bonded = True
        # Append empty improper record.
        if not has_impropers:
            file = open(self._psf_file, "a")
            file.write("\n%8d !NIMPHI: impropers\n" % 0)
            file.close()
        # Append empty donor record.
        if not has_donors:
            file = open(self._psf_file, "a")
            file.write("\n%8d !NDON: donors\n" % 0)
            file.close()
        # Append empty acceptor record.
        if not has_acceptors:
            file = open(self._psf_file, "a")
            file.write("\n%8d !NACC: acceptors\n" % 0)
            file.close()
        # Append empty non-bonded exclusion record.
        if not has_non_bonded:
            file = open(self._psf_file, "a")
            file.write("\n%8d !NNB: excluded\n" % 0)
            file.close()
        # Generate the NAMD configuration file.
        if isinstance(self._protocol, _Protocol.Custom):
            self.setConfig(self._protocol.getConfig())
        else:
            self._generate_config()
        self.writeConfig(self._config_file)
        # Return the list of input files.
        return self._input_files
    def _generate_config(self):
        """Generate NAMD configuration file strings."""
        # Clear the existing configuration list.
        self._config = []
        # Flag that the system doesn't contain a box.
        has_box = False
        prop = self._property_map.get("space", "space")
        if prop in self._system._sire_object.propertyKeys():
            try:
                box = self._system._sire_object.property(prop)
                # Flag that we have found a periodic box.
                has_box = box.isPeriodic()
            except Exception:
                box = None
        # Check whether the system contains periodic box information.
        if has_box:
            # Periodic box.
            try:
                box_size = self._system._sire_object.property(prop).dimensions()
                v0 = _Vector(box_size.x().value(), 0, 0)
                v1 = _Vector(0, box_size.y().value(), 0)
                v2 = _Vector(0, 0, box_size.z().value())
            # TriclinicBox.
            except:
                box = self._system._sire_object.property(prop)
                v0 = box.vector0()
                v1 = box.vector1()
                v2 = box.vector2()
            # Work out the minimum box size.
            box_size = min(v0.magnitude(), v1.magnitude(), v2.magnitude())
            # Convert vectors to tuples.
            v0 = tuple(x.value() for x in v0)
            v1 = tuple(x.value() for x in v1)
            v2 = tuple(x.value() for x in v2)
            # Since the box is translationally invariant, we set the cell
            # origin to be the average of the atomic coordinates. This
            # ensures a consistent wrapping for coordinates in the NAMD
            # output files.
            origin = tuple(x.value() for x in self._system._getAABox().center())
        # No box information. Assume this is a gas phase simulation.
        else:
            _warnings.warn("No simulation box found. Assuming gas phase simulation.")
            has_box = False
        prop = self._property_map.get("param_format", "param_format")
        # Check whether the system contains parameter format information.
        if prop in self._system._sire_object.propertyKeys():
            # Get the parameter format.
            if self._system._sire_object.property(prop).toString() == "CHARMM":
                is_charmm = True
            else:
                is_charmm = False
        # No parameter format information. Assume these are CHARMM parameters.
        else:
            _warnings.warn(
                "No parameter format information found. Assuming CHARMM parameters."
            )
            is_charmm = True
        # Append generic configuration variables.
        # Topology.
        self.addToConfig("structure             %s" % _os.path.basename(self._psf_file))
        self.addToConfig("coordinates           %s" % _os.path.basename(self._top_file))
        # Velocities.
        if self._velocity_file is not None:
            self.addToConfig(
                "velocities            %s" % _os.path.basename(self._velocity_file)
            )
        # Parameters.
        if is_charmm:
            self.addToConfig("paraTypeCharmm        on")
        self.addToConfig(
            "parameters            %s" % _os.path.basename(self._param_file)
        )
        # Random number seed.
        if self._is_seeded:
            self.addToConfig("seed                  %d" % self._seed)
        # Exclusion policy.
        self.addToConfig("exclude               scaled1-4")
        # Non-bonded potential parameters.
        # Gas phase.
        if not has_box or not self._has_water:
            self.addToConfig("cutoff                999.")
            self.addToConfig("zeroMomentum          yes")
            self.addToConfig("switching             off")
        # Solvated.
        else:
            # Only use a cutoff if the box is large enough.
            if box_size > 26:
                self.addToConfig("cutoff                12.")
                self.addToConfig("pairlistdist          14.")
                self.addToConfig("switching             on")
                self.addToConfig("switchdist            10.")
            # Load the XSC file.
            self.addToConfig("extendedSystem        %s.xsc" % self._name)
            # We force the cell origin to be located at the system's centre
            # of geometry. This ensures a consistent periodic wrapping for
            # all NAMD output.
            self.addToConfig("cellOrigin            %.3f   %.3f   %.3f" % origin)
            # Add the cell vectors.
            self.addToConfig("cellBasisVector1      %.3f   %.3f   %.3f" % v0)
            self.addToConfig("cellBasisVector2      %.3f   %.3f   %.3f" % v1)
            self.addToConfig("cellBasisVector3      %.3f   %.3f   %.3f" % v2)
            # Wrap all molecular coordinates to the periodic box.
            self.addToConfig("wrapAll               on")
            # Periodic electrostatics.
            self.addToConfig("PME                   yes")
            self.addToConfig("PMEGridSpacing        1.")
        # Output file parameters.
        self.addToConfig("outputName            %s_out" % self._name)
        self.addToConfig("binaryOutput          no")
        self.addToConfig("binaryRestart         no")
        # Position restraints.
        if isinstance(self._protocol, _PositionRestraintMixin):
            restraint = self._protocol.getRestraint()
            if restraint is not None:
                # Create a restrained system.
                restrained = self._createRestrainedSystem(
                    self._reference_system, restraint
                )
                # Create a PDB object, mapping the "occupancy" property to "restrained".
                prop = self._property_map.get("occupancy", "occupancy")
                try:
                    p = _SireIO.PDB2(restrained._sire_object, {prop: "restrained"})
                    # File name for the restraint file.
                    self._restraint_file = _os.path.join(
                        str(self._work_dir), f"{self._name}.restrained"
                    )
                    # Write the PDB file.
                    p.writeToFile(self._restraint_file)
                except:
                    _warnings.warn(
                        "Failed to add restraints to PDB file. "
                        "Perhaps there are no atoms matching the restraint?"
                    )
                # Update the configuration file.
                self.addToConfig("constraints           yes")
                self.addToConfig("consref               %s.restrained" % self._name)
                self.addToConfig("conskfile             %s.restrained" % self._name)
                self.addToConfig("conskcol              O")
        # Add configuration variables for a minimisation simulation.
        if isinstance(self._protocol, _Protocol.Minimisation):
            # Output frequency.
            self.addToConfig("restartfreq           500")
            self.addToConfig("xstFreq               500")
            # Printing frequency.
            self.addToConfig("outputEnergies        100")
            self.addToConfig("outputTiming          1000")
            self.addToConfig("temperature           300")
            # Work out the number of steps. This must be a multiple of
            # stepspercycle, which is set the default of 20.
            steps = 20 * _math.ceil(self._protocol.getSteps() / 20)
            self.addToConfig("minimize              %d" % steps)
        # Add configuration variables for an equilibration simulation.
        elif isinstance(self._protocol, _Protocol.Equilibration):
            # Work out the number of integration steps.
            steps = _math.ceil(
                self._protocol.getRunTime() / self._protocol.getTimeStep()
            )
            # Get the report and restart intervals.
            report_interval = self._protocol.getReportInterval()
            restart_interval = self._protocol.getRestartInterval()
            # Cap the intervals at the total number of steps.
            if report_interval > steps:
                report_interval = steps
            if restart_interval > steps:
                restart_interval = steps
            # Output frequency.
            self.addToConfig("restartfreq           %d" % restart_interval)
            self.addToConfig("xstFreq               %d" % restart_interval)
            # Printing frequency.
            self.addToConfig("outputEnergies        %d" % report_interval)
            self.addToConfig("outputTiming          1000")
            # Set the Tcl temperature variable.
            if self._protocol.isConstantTemp():
                self.addToConfig(
                    "set temperature       %.2f"
                    % self._protocol.getStartTemperature().kelvin().value()
                )
            else:
                self.addToConfig(
                    "set temperature       %.2f"
                    % self._protocol.getEndTemperature().kelvin().value()
                )
            self.addToConfig("temperature           $temperature")
            # Integrator parameters.
            self.addToConfig(
                "timestep              %.2f"
                % self._protocol.getTimeStep().femtoseconds().value()
            )
            self.addToConfig("rigidBonds            all")
            self.addToConfig("nonbondedFreq         1")
            self.addToConfig("fullElectFrequency    2")
            # Constant temperature control.
            self.addToConfig("langevin              on")
            self.addToConfig("langevinDamping       1.")
            self.addToConfig("langevinTemp          $temperature")
            self.addToConfig("langevinHydrogen      no")
            # Constant pressure control.
            if self._protocol.getPressure() is not None:
                self.addToConfig("langevinPiston        on")
                self.addToConfig(
                    "langevinPistonTarget  %.5f"
                    % self._protocol.getPressure().bar().value()
                )
                self.addToConfig("langevinPistonPeriod  100.")
                self.addToConfig("langevinPistonDecay   50.")
                self.addToConfig("langevinPistonTemp    $temperature")
                self.addToConfig("useGroupPressure      yes")
                self.addToConfig("useFlexibleCell       no")
                self.addToConfig("useConstantArea       no")
            # Heating/cooling simulation.
            if not self._protocol.isConstantTemp():
                # Work out temperature step size (assuming a unit increment).
                denom = abs(
                    self._protocol.getEndTemperature().kelvin().value()
                    - self._protocol.getStartTemperature().kelvin().value()
                )
                freq = _math.floor(steps / denom)
                self.addToConfig("reassignFreq          %d" % freq)
                self.addToConfig(
                    "reassignTemp          %.2f"
                    % self._protocol.getStartTemperature().kelvin().value()
                )
                self.addToConfig("reassignIncr          1.")
                self.addToConfig(
                    "reassignHold          %.2f"
                    % self._protocol.getEndTemperature().kelvin().value()
                )
            # Trajectory output frequency.
            self.addToConfig("DCDfreq               %d" % restart_interval)
            # Run the simulation.
            self.addToConfig("run                   %d" % steps)
        # Add configuration variables for a production simulation.
        elif isinstance(self._protocol, _Protocol.Production):
            # Work out the number of integration steps.
            steps = _math.ceil(
                self._protocol.getRunTime() / self._protocol.getTimeStep()
            )
            # Get the report and restart intervals.
            report_interval = self._protocol.getReportInterval()
            restart_interval = self._protocol.getRestartInterval()
            # Cap the intervals at the total number of steps.
            if report_interval > steps:
                report_interval = steps
            if restart_interval > steps:
                restart_interval = steps
            # Output frequency.
            self.addToConfig("restartfreq           %d" % restart_interval)
            self.addToConfig("xstFreq               %d" % restart_interval)
            # Printing frequency.
            self.addToConfig("outputEnergies        %d" % report_interval)
            self.addToConfig("outputTiming          1000")
            # Set the Tcl temperature variable.
            self.addToConfig(
                "set temperature       %.2f"
                % self._protocol.getTemperature().kelvin().value()
            )
            self.addToConfig("temperature           $temperature")
            # Integrator parameters.
            self.addToConfig(
                "timestep              %.2f"
                % self._protocol.getTimeStep().femtoseconds().value()
            )
            self.addToConfig("rigidBonds            all")
            self.addToConfig("nonbondedFreq         1")
            self.addToConfig("fullElectFrequency    2")
            # Constant temperature control.
            self.addToConfig("langevin              on")
            self.addToConfig("langevinDamping       1.")
            self.addToConfig("langevinTemp          $temperature")
            self.addToConfig("langevinHydrogen      no")
            # Constant pressure control.
            if self._protocol.getPressure() is not None:
                self.addToConfig("langevinPiston        on")
                self.addToConfig(
                    "langevinPistonTarget  %.5f"
                    % self._protocol.getPressure().bar().value()
                )
                self.addToConfig("langevinPistonPeriod  100.")
                self.addToConfig("langevinPistonDecay   50.")
                self.addToConfig("langevinPistonTemp    $temperature")
                self.addToConfig("useGroupPressure      yes")
                self.addToConfig("useFlexibleCell       no")
                self.addToConfig("useConstantArea       no")
            # Work out number of steps needed to exceed desired running time.
            steps = _math.ceil(
                self._protocol.getRunTime() / self._protocol.getTimeStep()
            )
            # Trajectory output frequency.
            self.addToConfig("DCDfreq               %d" % restart_interval)
            # Run the simulation.
            self.addToConfig("run                   %d" % steps)
        else:
            raise _IncompatibleError(
                "Unsupported protocol: '%s'" % self._protocol.__class__.__name__
            )
        # Flag that this isn't a custom protocol.
        self._protocol._setCustomised(False)
[docs]
    def start(self):
        """
        Start the NAMD process.
        Returns
        -------
        process : :class:`Process.Namd <BioSimSpace.Process.Namd>`
            The process object.
        """
        # The process is currently queued.
        if self.isQueued():
            return
        # Process is already running.
        if self._process is not None:
            if self._process.isRunning():
                return
        # Clear any existing output.
        self._clear_output()
        # Run the process in the working directory.
        with _Utils.cd(self._work_dir):
            # Write the command-line process to a README.txt file.
            with open("README.txt", "w") as file:
                # Set the command-line string.
                self._command = "%s %s.cfg" % (self._exe, self._name)
                # Write the command to file.
                file.write("# NAMD was run with the following command:\n")
                file.write("%s\n" % self._command)
            # Start the timer.
            self._timer = _timeit.default_timer()
            # Start the simulation.
            self._process = _SireBase.Process.run(
                self._exe,
                "%s.cfg" % self._name,
                "%s.out" % self._name,
                "%s.err" % self._name,
            )
        return self 
[docs]
    def getSystem(self, block="AUTO"):
        """
        Get the latest molecular system.
        Parameters
        ----------
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        system : :class:`System <BioSimSpace._SireWrappers.System>`
            The latest molecular system.
        """
        # Wait for the process to finish.
        if block is True:
            self.wait()
        elif block == "AUTO" and self._is_blocked:
            self.wait()
        # Warn the user if the process has exited with an error.
        if self.isError():
            _warnings.warn("The process exited with an error!")
        # Read the PDB coordinate file and construct a parameterised molecular
        # system using the original PSF and param files.
        has_coor = False
        # First check for final configuration.
        if _os.path.isfile(
            _os.path.join(str(self._work_dir), f"{self._name}_out.coor")
        ):
            coor_file = _os.path.join(str(self._work_dir), f"{self._name}_out.coor")
            has_coor = True
        # Otherwise check for a restart file.
        elif _os.path.isfile(
            _os.path.join(str(self._work_dir), f"{self._name}_out.restart.coor")
        ):
            coor_file = _os.path.join(
                str(self._work_dir), f"{self._name}_out.restart.coor"
            )
            has_coor = True
        # Try to find an XSC file.
        has_xsc = False
        # First check for final XSC file.
        if _os.path.isfile(_os.path.join(str(self._work_dir), f"{self._name}_out.xsc")):
            xsc_file = _os.path.join(str(self._work_dir), f"{self._name}_out.xsc")
            has_xsc = True
        # Otherwise check for a restart XSC file.
        elif _os.path.isfile(
            _os.path.join(str(self._work_dir), f"{self._name}_out.restart.xsc")
        ):
            xsc_file = _os.path.join(
                str(self._work_dir), f"{self._name}_out.restart.xsc"
            )
            has_xsc = True
        # We found a coordinate file.
        if has_coor:
            # List of files.
            files = [coor_file, self._psf_file, self._param_file]
            # Add the box information.
            if has_xsc:
                files.append(xsc_file)
            # Create and return the molecular system.
            try:
                # Do we need to get coordinates for the lambda=1 state.
                if "is_lambda1" in self._property_map:
                    is_lambda1 = True
                else:
                    is_lambda1 = False
                # Load the restart file.
                new_system = _IO.readMolecules(files, property_map=self._property_map)
                # Create a copy of the existing system object.
                old_system = self._system.copy()
                # Update the coordinates and velocities and return a mapping between
                # the molecule indices in the two systems.
                sire_system, mapping = _SireIO.updateCoordinatesAndVelocities(
                    old_system._sire_object,
                    new_system._sire_object,
                    self._mapping,
                    is_lambda1,
                    self._property_map,
                    self._property_map,
                )
                # Update the underlying Sire object.
                old_system._sire_object = sire_system
                # Store the mapping between the MolIdx in both systems so we don't
                # need to recompute it next time.
                self._mapping = mapping
                # Update the box information in the original system.
                if "space" in new_system._sire_object.propertyKeys():
                    box = new_system._sire_object.property("space")
                    if box.isPeriodic():
                        old_system._sire_object.setProperty(
                            self._property_map.get("space", "space"), box
                        )
                return old_system
            except:
                return None
        else:
            return None 
[docs]
    def getCurrentSystem(self):
        """
        Get the latest molecular system.
        Returns
        -------
        system : :class:`System <BioSimSpace._SireWrappers.System>`
            The latest molecular system.
        """
        return self.getSystem(block=False) 
[docs]
    def getTrajectory(self, backend="AUTO", block="AUTO"):
        """
        Return a trajectory object.
        Parameters
        ----------
        backend : str
            The backend to use for trajectory parsing. To see supported backends,
            run BioSimSpace.Trajectory.backends(). Using "AUTO" will try each in
            sequence.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        trajectory : :class:`Trajectory <BioSimSpace.Trajectory>`
            The latest trajectory object.
        """
        if not isinstance(backend, str):
            raise TypeError("'backend' must be of type 'str'")
        if not isinstance(block, (bool, str)):
            raise TypeError("'block' must be of type 'bool' or 'str'")
        # Wait for the process to finish.
        if block is True:
            self.wait()
        elif block == "AUTO" and self._is_blocked:
            self.wait()
        # Warn the user if the process has exited with an error.
        if self.isError():
            _warnings.warn("The process exited with an error!")
        try:
            return _Trajectory.Trajectory(process=self, backend=backend)
        except:
            return None 
[docs]
    def getFrame(self, index):
        """
        Return a specific trajectory frame.
        Parameters
        ----------
        index : int
            The index of the frame.
        Returns
        -------
        frame : :class:`System <BioSimSpace._SireWrappers.System>`
            The System object of the corresponding frame.
        """
        if not type(index) is int:
            raise TypeError("'index' must be of type 'int'")
        max_index = (
            int(
                (self._protocol.getRunTime() / self._protocol.getTimeStep())
                / self._protocol.getRestartInterval()
            )
            - 1
        )
        if index < 0 or index > max_index:
            raise ValueError(f"'index' must be in range [0, {max_index}].")
        try:
            # Do we need to get coordinates for the lambda=1 state.
            if "is_lambda1" in self._property_map:
                is_lambda1 = True
            else:
                is_lambda1 = False
            # Load the latest trajectory frame.
            new_system = _Trajectory.getFrame(self._traj_file, self._top_file, index)
            # Create a copy of the existing system object.
            old_system = self._system.copy()
            # Update the coordinates and velocities and return a mapping between
            # the molecule indices in the two systems.
            sire_system, mapping = _SireIO.updateCoordinatesAndVelocities(
                old_system._sire_object,
                new_system._sire_object,
                self._mapping,
                is_lambda1,
                self._property_map,
                self._property_map,
            )
            # Update the underlying Sire object.
            old_system._sire_object = sire_system
            # Store the mapping between the MolIdx in both systems so we don't
            # need to recompute it next time.
            self._mapping = mapping
            # Update the box information in the original system.
            if "space" in new_system._sire_object.propertyKeys():
                box = new_system._sire_object.property("space")
                old_system._sire_object.setProperty(
                    self._property_map.get("space", "space"), box
                )
            return old_system
        except:
            return None 
[docs]
    def getRecord(self, record, time_series=False, unit=None, block="AUTO"):
        """
        Get a record from the stdout dictionary.
        Parameters
        ----------
        record : str
            The record key.
        time_series : bool
            Whether to return a list of time series records.
        unit : :class:`Unit <BioSimSpace.Units>`
            The unit to convert the record to.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        record : :class:`Type <BioSimSpace.Types>`
            The matching record.
        """
        # Wait for the process to finish.
        if block is True:
            self.wait()
        elif block == "AUTO" and self._is_blocked:
            self.wait()
        # Warn the user if the process has exited with an error.
        if self.isError():
            _warnings.warn("The process exited with an error!")
        self.stdout(0)
        return self._get_stdout_record(record, time_series, unit) 
[docs]
    def getCurrentRecord(self, record, time_series=False, unit=None):
        """
        Get a current record from the stdout dictionary.
        Parameters
        ----------
        record : str
            The record key.
        time_series : bool
            Whether to return a list of time series records.
        unit : :class:`Unit <BioSimSpace.Units>`
            The unit to convert the record to.
        Returns
        -------
        record : :class:`Type <BioSimSpace.Types>`
            The matching record.
        """
        # Warn the user if the process has exited with an error.
        if self.isError():
            _warnings.warn("The process exited with an error!")
        self.stdout(0)
        return self._get_stdout_record(record, time_series, unit) 
[docs]
    def getRecords(self, block="AUTO"):
        """
        Return the dictionary of stdout time-series records.
        Parameters
        ----------
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        records : dict
            The dictionary of time-series records.
        """
        # Wait for the process to finish.
        if block is True:
            self.wait()
        elif block == "AUTO" and self._is_blocked:
            self.wait()
        # Warn the user if the process has exited with an error.
        if self.isError():
            _warnings.warn("The process exited with an error!")
        self.stdout(0)
        return self._stdout_dict.copy() 
[docs]
    def getCurrentRecords(self):
        """
        Return the current dictionary of stdout time-series records.
        Parameters
        ----------
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        records : BioSimSpace.Process._process._MultiDict
           The dictionary of time-series records.
        """
        return self.getRecords(block=False) 
[docs]
    def getTime(self, time_series=False, block="AUTO"):
        """
        Get the simulation time.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        time : :class:`Time <BioSimSpace.Types.Time>`
            The current simulation time in nanoseconds.
        """
        if isinstance(self._protocol, _Protocol.Minimisation):
            return None
        else:
            # Get the list of time steps.
            time_steps = self.getRecord("TS", time_series, None, block)
            # Convert the time step to the default unit.
            timestep = self._protocol.getTimeStep()._to_default_unit()
            # Multiply by the integration time step.
            if time_steps is not None:
                if time_series:
                    return [x * timestep for x in time_steps]
                else:
                    return timestep * time_steps 
[docs]
    def getCurrentTime(self, time_series=False):
        """
        Get the current simulation time.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        time : :class:`Time <BioSimSpace.Types.Time>`
            The current simulation time in nanoseconds.
        """
        return self.getTime(time_series, block=False) 
[docs]
    def getStep(self, time_series=False, block="AUTO"):
        """
        Get the number of integration steps.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        step : int
            The current number of integration steps.
        """
        return self.getRecord("TS", time_series, None, block) 
[docs]
    def getCurrentStep(self, time_series=False):
        """
        Get the current number of integration steps.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        step : int
            The current number of integration steps.
        """
        return self.getStep(time_series, block=False) 
[docs]
    def getBondEnergy(self, time_series=False, block="AUTO"):
        """
        Get the bond energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The bond energy.
        """
        return self.getRecord("BOND", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentBondEnergy(self, time_series=False):
        """
        Get the current bond energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The bond energy.
        """
        return self.getBondEnergy(time_series, block=False) 
[docs]
    def getAngleEnergy(self, time_series=False, block="AUTO"):
        """
        Get the angle energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The angle energy.
        """
        return self.getRecord("ANGLE", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentAngleEnergy(self, time_series=False):
        """
        Get the current angle energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The angle energy.
        """
        return self.getAngleEnergy(time_series, block=False) 
[docs]
    def getDihedralEnergy(self, time_series=False, block="AUTO"):
        """
        Get the total dihedral energy (proper + improper).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The total dihedral energy.
        """
        # Get the proper and improper energies.
        proper = self.getRecord("DIHED", time_series, _Units.Energy.kcal_per_mol, block)
        improper = self.getRecord(
            "IMPRP", time_series, _Units.Energy.kcal_per_mol, block
        )
        # No records.
        if proper is None and improper is None:
            return None
        elif proper is None:
            return improper
        elif improper is None:
            return proper
        else:
            if time_series:
                return [x + y for x, y in zip(proper, improper)]
            else:
                return proper + improper 
[docs]
    def getCurrentDihedralEnergy(self, time_series=False):
        """
        Get the current total dihedral energy (proper + improper).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The dihedral energy.
        """
        return self.getDihedralEnergy(time_series, block=False) 
[docs]
    def getProperEnergy(self, time_series=False, block="AUTO"):
        """
        Get the proper dihedral energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The proper dihedral energy.
        """
        return self.getRecord("DIHED", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentProperEnergy(self, time_series=False):
        """
        Get the current proper dihedral energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The proper dihedral energy.
        """
        return self.getProperEnergy(time_series, block=False) 
[docs]
    def getImproperEnergy(self, time_series=False, block="AUTO"):
        """
        Get the improper energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The improper energy.
        """
        return self.getRecord("IMPRP", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentImproperEnergy(self, time_series=False):
        """
        Get the current improper energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The improper energy.
        """
        return self.getImproperEnergy(time_series, block=False) 
[docs]
    def getElectrostaticEnergy(self, time_series=False, block="AUTO"):
        """
        Get the electrostatic energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The electrostatic energy.
        """
        return self.getRecord("ELECT", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentElectrostaticEnergy(self, time_series=False):
        """
        Get the current electrostatic energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The electrostatic energy.
        """
        return self.getElectrostaticEnergy(time_series, block=False) 
[docs]
    def getVanDerWaalsEnergy(self, time_series=False, block="AUTO"):
        """
        Get the Van der Vaals energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The Van der Vaals energy.
        """
        return self.getRecord("VDW", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentVanDerWaalsEnergy(self, time_series=False):
        """
        Get the current Van der Waals energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The Van der Vaals energy.
        """
        return self.getVanDerWaalsEnergy(time_series, block=False) 
[docs]
    def getBoundaryEnergy(self, time_series=False, block="AUTO"):
        """
        Get the boundary energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The boundary energy.
        """
        return self.getRecord(
            "BOUNDARY", time_series, _Units.Energy.kcal_per_mol, block
        ) 
[docs]
    def getCurrentBoundaryEnergy(self, time_series=False):
        """
        Get the current boundary energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The boundary energy.
        """
        return self.getBoundaryEnergy(time_series, block=False) 
[docs]
    def getMiscEnergy(self, time_series=False, block="AUTO"):
        """
        Get the external energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The external energy.
        """
        return self.getRecord("MISC", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentMiscEnergy(self, time_series=False):
        """
        Get the current external energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The external energy.
        """
        return self.getMiscEnergy(time_series, block=False) 
[docs]
    def getKineticEnergy(self, time_series=False, block="AUTO"):
        """
        Get the kinetic energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The kinetic energy.
        """
        return self.getRecord("KINETIC", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentKineticEnergy(self, time_series=False):
        """
        Get the current kinetic energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The current kinetic energy.
        """
        return self.getKineticEnergy(time_series, block=False) 
[docs]
    def getPotentialEnergy(self, time_series=False, block="AUTO"):
        """
        Get the potential energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The potential energy.
        """
        return self.getRecord(
            "POTENTIAL", time_series, _Units.Energy.kcal_per_mol, block
        ) 
[docs]
    def getCurrentPotentialEnergy(self, time_series=False):
        """
        Get the current potential energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The potential energy.
        """
        return self.getPotentialEnergy(time_series, block=False) 
[docs]
    def getTotalEnergy(self, time_series=False, block="AUTO"):
        """
        Get the total energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The total energy.
        """
        return self.getRecord("TOTAL", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentTotalEnergy(self, time_series=False):
        """
        Get the current potential energy.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The total energy.
        """
        return self.getTotalEnergy(time_series, block=False) 
[docs]
    def getTotal2Energy(self, time_series=False, block="AUTO"):
        """
        Get the total energy. (Better KE conservation.).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The total energy.
        """
        return self.getRecord("TOTAL2", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentTotal2Energy(self, time_series=False):
        """
        Get the current total energy. (Better KE conservation.).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The total energy.
        """
        return self.getTotal2Energy(time_series, block=False) 
[docs]
    def getTotal3Energy(self, time_series=False, block="AUTO"):
        """
        Get the total energy. (Smaller short-time fluctuations.).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The total energy.
        """
        return self.getRecord("TOTAL3", time_series, _Units.Energy.kcal_per_mol, block) 
[docs]
    def getCurrentTotal3Energy(self, time_series=False):
        """
        Get the total energy. (Smaller short-time fluctuations.).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        energy : :class:`Energy <BioSimSpace.Types.Energy>`
            The total energy.
        """
        return self.getTotal3Energy(time_series, block=False) 
[docs]
    def getTemperature(self, time_series=False, block="AUTO"):
        """
        Get the temperature.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        temperature : :class:`Temperature <BioSimSpace.Types.Temperature>`
            The temperature.
        """
        return self.getRecord("TEMP", time_series, _Units.Temperature.kelvin, block) 
[docs]
    def getCurrentTemperature(self, time_series=False):
        """
        Get the temperature.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        temperature : :class:`Temperature <BioSimSpace.Types.Temperature>`
            The temperature.
        """
        return self.getTemperature(time_series, block=False) 
[docs]
    def getTemperatureAverage(self, time_series=False, block="AUTO"):
        """
        Get the average temperature.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        temperature : :class:`Temperature <BioSimSpace.Types.Temperature>`
            The average temperature.
        """
        return self.getRecord("TEMPAVG", time_series, _Units.Temperature.kelvin, block) 
[docs]
    def getCurrentTemperatureAverage(self, time_series=False):
        """
        Get the current average temperature.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        temperature : :class:`Temperature <BioSimSpace.Types.Temperature>`
            The average temperature.
        """
        return self.getTemperatureAverage(time_series, block=False) 
[docs]
    def getPressure(self, time_series=False, block="AUTO"):
        """
        Get the pressure.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        pressure : :class:`Pressure <BioSimSpace.Types.Pressure>`
            The pressure.
        """
        return self.getRecord("PRESSURE", time_series, _Units.Pressure.bar, block) 
[docs]
    def getCurrentPressure(self, time_series=False):
        """
        Get the current pressure.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        pressure : :class:`Pressure <BioSimSpace.Types.Pressure>`
            The pressure.
        """
        return self.getPressure(time_series, block=False) 
[docs]
    def getPressureAverage(self, time_series=False, block="AUTO"):
        """
        Get the average pressure.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        pressure : :class:`Pressure <BioSimSpace.Types.Pressure>`
            The average pressure.
        """
        return self.getRecord("PRESSAVG", time_series, _Units.Pressure.bar, block) 
[docs]
    def getCurrentPressureAverage(self, time_series=False):
        """
        Get the current average pressure.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        pressure : :class:`Pressure <BioSimSpace.Types.Pressure>`
            The average pressure.
        """
        return self.getPressureAverage(time_series, block=False) 
[docs]
    def getGPressure(self, time_series=False, block="AUTO"):
        """
        Get the pressure. (Hydrogens incorporated into bonded atoms.).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        pressure : :class:`Pressure <BioSimSpace.Types.Pressure>`
            The pressure.
        """
        return self.getRecord("GPRESSURE", time_series, _Units.Pressure.bar, block) 
[docs]
    def getCurrentGPressure(self, time_series=False):
        """
        Get the current pressure. (Hydrogens incorporated into bonded atoms.).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        pressure : :class:`Pressure <BioSimSpace.Types.Pressure>`
            The pressure.
        """
        return self.getGPressure(time_series, block=False) 
[docs]
    def getGPressureAverage(self, time_series=False, block="AUTO"):
        """
        Get the average pressure. (Hydrogens incorporated into bonded atoms.).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        pressure : :class:`Pressure <BioSimSpace.Types.Pressure>`
            The average pressure.
        """
        return self.getRecord("GPRESSAVG", time_series, _Units.Pressure.bar, block) 
[docs]
    def getCurrentGPressureAverage(self, time_series=False):
        """
        Get the current average pressure. (Hydrogens incorporated into bonded atoms.).
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        pressure : :class:`Pressure <BioSimSpace.Types.Pressure>`
            The average pressure.
        """
        return self.getGPressureAverage(time_series, block=False) 
[docs]
    def getVolume(self, time_series=False, block="AUTO"):
        """
        Get the volume.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        block : bool
            Whether to block until the process has finished running.
        Returns
        -------
        volume : :class:`Volume <BioSimSpace.Types.Volume>`
            The volume.
        """
        return self.getRecord("VOLUME", time_series, _Units.Volume.angstrom3, block) 
[docs]
    def getCurrentVolume(self, time_series=False):
        """
        Get the current volume.
        Parameters
        ----------
        time_series : bool
            Whether to return a list of time series records.
        Returns
        -------
        volume : :class:`Volume <BioSimSpace.Types.Volume>`
            The volume.
        """
        return self.getVolume(time_series, block=False) 
[docs]
    def eta(self):
        """
        Get the estimated time for the process to finish (in minutes).
        Returns
        -------
        eta : :class:`Time <BioSimSpace.Types.Time>`
            The estimated remaining time in minutes.
        """
        # Make sure the list of stdout records is up to date.
        # Print the last zero lines, i.e. no output.
        self.stdout(0)
        # Now search backwards through the list to find the last TIMING record.
        for _, record in reversed(list(enumerate(self._stdout))):
            # Split the record using whitespace.
            data = record.split()
            # We've found a TIMING record.
            if len(data) > 0:
                if data[0] == "TIMING:":
                    # Try to find the "hours" record.
                    # If found, return the entry precedeing it.
                    try:
                        return (
                            float(data[data.index("hours") - 1]) * 60
                        ) * _Units.Time.minutes
                    # No record found.
                    except ValueError:
                        return None 
[docs]
    def stdout(self, n=10):
        """
        Print the last n lines of the stdout buffer.
        Parameters
        ----------
        n : int
            The number of lines to print.
        """
        # Ensure that the number of lines is positive.
        if n < 0:
            raise ValueError("The number of lines must be positive!")
        # Append any new lines to the stdout list.
        for line in _pygtail.Pygtail(self._stdout_file):
            self._stdout.append(line.rstrip())
            # Split the record using whitespace.
            data = self._stdout[-1].split()
            # Make sure there is at least one record.
            if len(data) > 0:
                # Store the updated energy title.
                if data[0] == "ETITLE:":
                    self._stdout_title = data[1:]
                # This is an energy record.
                elif data[0] == "ENERGY:":
                    # Extract the data.
                    stdout_data = data[1:]
                    # Add the records to the dictionary.
                    if len(stdout_data) == len(self._stdout_title):
                        for title, data in zip(self._stdout_title, stdout_data):
                            self._stdout_dict[title] = data
        # Get the current number of lines.
        num_lines = len(self._stdout)
        # Set the line from which to start printing.
        if num_lines < n:
            start = 0
        else:
            start = num_lines - n
        # Print the lines.
        for x in range(start, num_lines):
            print(self._stdout[x]) 
    def _createRestrainedSystem(self, system, restraint):
        """
        Restrain protein backbone atoms.
        Parameters
        ----------
        system : :class:`System <BioSimSpace._SireWrappers.System>`
            The molecular system.
        restraint : str, [int]
            The type of restraint.
        Returns
        -------
        system : :class:`System <BioSimSpace._SireWrappers.System>`
            The molecular system with an added 'restrained' property.
        """
        # Get the force constant value in the default units. This is
        # the same as used by NAMD, i.e. kcal_per_mol/angstrom**2
        force_constant = self._protocol.getForceConstant().value()
        # Copy the original system.
        s = system.copy()
        # Convert to the chosen end state.
        s = self._checkPerturbable(s)
        # Keyword restraint.
        if isinstance(restraint, str):
            # Loop over all molecules by number.
            for x, mol in enumerate(s):
                # Get the indices of the restrained atoms for this molecule.
                atoms = s.getRestraintAtoms(
                    restraint, x, is_absolute=False, allow_zero_matches=True
                )
                # Extract the molecule and make it editable.
                edit_mol = mol._sire_object.edit()
                # First set all restraint force constants to 0, i.e. the restraint
                # will be ignored.
                for atom in edit_mol.atoms():
                    edit_mol = (
                        edit_mol.atom(atom.index())
                        .setProperty("restrained", 0.0)
                        .molecule()
                    )
                # Now apply restraints to the selected atoms.
                for idx in atoms:
                    edit_mol = (
                        edit_mol.atom(_SireMol.AtomIdx(idx))
                        .setProperty("restrained", 10.0)
                        .molecule()
                    )
                # Update the system.
                s._sire_object.update(edit_mol.commit())
        # A user-defined list of atoms.
        elif isinstance(restraint, (list, tuple)):
            # Create an empty multi dict for each MolNum.
            mol_atoms = {}
            for num in s._mol_nums:
                mol_atoms[num] = []
            # Now work out which MolNum corresponds to each atom in the restraint.
            for idx in restraint:
                try:
                    mol_idx, atom_idx = s._getRelativeIndices(idx)
                    mol_num = s._mol_nums[mol_idx]
                    atom_idx = _SireMol.AtomIdx(atom_idx)
                    mol_atoms[mol_num].append(atom_idx)
                except Exception as e:
                    msg = "Unable to find restrained atom in the system?"
                    if _isVerbose():
                        raise ValueError(msg) from e
                    else:
                        raise ValueError(msg) from None
            # Now loop over the multi-dict.
            for num, idxs in mol_atoms.items():
                # Extract the molecule and make it editable.
                edit_mol = s._sire_object[num].edit()
                # First set all restraints to zero.
                for atom in edit_mol.atoms():
                    edit_mol = (
                        edit_mol.atom(atom.index())
                        .setProperty("restrained", 0.0)
                        .molecule()
                    )
                # Now apply restraints to the selected atoms.
                # TODO: The fixed-width PDB format means that the force
                # constant can't exceed 999 kcal_per_mol/angstrom**2 when
                # written in the standard 2dp floating point format. We
                # could warn when the value is too large, or write as an
                # integer instead. (This latter would require tweaking the
                # PDB parser.
                for idx in idxs:
                    edit_mol = (
                        edit_mol.atom(idx)
                        .setProperty("restrained", force_constant)
                        .molecule()
                    )
                # Update the system.
                s._sire_object.update(edit_mol.commit())
        # Return the new system.
        return s
    def _get_stdout_record(self, key, time_series=False, unit=None):
        """
        Helper function to get a stdout record from the dictionary.
        Parameters
        ----------
        key : str
            The record key.
        time_series : bool
            Whether to return a time series of records.
        unit : BioSimSpace.Types._type.Type
            The unit to convert the record to.
        Returns
        -------
        record :
            The matching stdout record.
        """
        # No data!
        if len(self._stdout_dict) == 0:
            return None
        if not isinstance(time_series, bool):
            _warnings.warn("Non-boolean time-series flag. Defaulting to False!")
            time_series = False
        # Validate the unit.
        if unit is not None:
            if not isinstance(unit, _Type):
                raise TypeError("'unit' must be of type 'BioSimSpace.Types'")
        # Return the list of dictionary values.
        if time_series:
            try:
                if key == "TS":
                    return [int(x) for x in self._stdout_dict[key]]
                else:
                    if unit is None:
                        return [float(x) for x in self._stdout_dict[key]]
                    else:
                        return [
                            (float(x) * unit)._to_default_unit()
                            for x in self._stdout_dict[key]
                        ]
            except KeyError:
                return None
        # Return the most recent dictionary value.
        else:
            try:
                if key == "TS":
                    return int(self._stdout_dict[key][-1])
                else:
                    if unit is None:
                        return float(self._stdout_dict[key][-1])
                    else:
                        return (
                            float(self._stdout_dict[key][-1]) * unit
                        )._to_default_unit()
            except KeyError:
                return None