Source code for scm.reactions_discovery.plams_job

import os
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union

from networkx import DiGraph

import scm.reactions_discovery.utils
from scm.base import KFError, KFFile
from scm.inputs import ReactionsDiscovery
from scm.plams import AMSJob, Molecule, Results, Settings, SingleJob
from scm.plams.core.errors import FileError
from scm.reactions_discovery.combined_ct_results import CombinedMol, CombinedReaction
from scm.reactions_discovery.reaction_network import ReactionNetwork
from scm.utils.conversions import chemsys_to_plams_molecule


[docs]class ReactionsDiscoveryResults(Results): """ Results class for ReactionsDiscoveryJob """
[docs] def get_errormsg(self) -> str: """Returns the error message of this calculation if any were raised. :return: String containing the error message. :rtype: str """ return self.job.get_errormsg()
[docs] def get_md_jobs(self) -> List[AMSJob]: """Returns the AMSJobs used during the calculation. :return: List of AMSJobs used during the calculation. :rtype: List[AMSJob] """ return self.job.get_md_jobs()
[docs] def rkfpath(self) -> str: """Returns path to reactions_discovery.rkf :return: Path to reactions_discovery.rkf :rtype: str """ return str((Path(self.job.path) / "reactions_discovery.rkf").resolve())
[docs] def get_network_rd(self) -> Tuple[DiGraph, Dict[str, List[CombinedMol]], Dict[int, CombinedReaction], List[str]]: """Returns the reaction network represented by Reactions Discovery CombinedMol and CombinedReaction classes. :raises KFError: If the KF file associated with this result does not contain the required information. :return: Graph representing the reaction network, a dictionairy of categories and lists of CombinedMol, a dictionairy of categories and CombinedReaction and a list of categories. :rtype: Tuple[DiGraph, Dict[str, List[CombinedMol]], Dict[int, CombinedReaction], List[str]] """ reaction_network = ReactionNetwork() reaction_network.read_extraction_results(self.job.path) reaction_network.molecules_to_graph() molecules = reaction_network.molecules graph = reaction_network.graph assert isinstance(graph, DiGraph) with KFFile(self.rkfpath()) as kf: try: categories = [category for category in kf.get_skeleton().get("Categories", []) if category[:3] != "Num"] assert isinstance(molecules, list) rd_molecules: Dict[str, List[CombinedMol]] = {} reactions: Dict[int, CombinedReaction] = {} for category in categories: mol_idxs = kf.read_ints_np("Categories", category) rd_molecules[category] = [molecules[idx - 1] for idx in mol_idxs] for molecule in molecules: for property in ["smiles", "name", "net_flux", "string_description", "molecular_formula", "id"]: molecule.plams_mol.properties[property] = getattr(molecule, property) for reaction in molecule.reactions_as_product + molecule.reactions_as_reactant: reactions[reaction.hash] = reaction except KFError: raise KFError(f"Could not read reaction network from {self.rkfpath()}") return graph, rd_molecules, reactions, categories
[docs] def get_network(self) -> Tuple[DiGraph, Dict[str, List[Molecule]], List[str]]: """Returns the reaction network represented by a DiGraph and a dictionairy of lists of PLAMS molecules. Each key in the dictionary is a category. :return: graph of the reaction network, dictionary of categories and lists of Molecules, and a list of categories. :rtype: Tuple[DiGraph, Dict[str, List[Molecule]], List[str]] """ graph, molecules, reactions, categories = self.get_network_rd() molecules = {category: [molecule.plams_mol for molecule in molecules[category]] for category in molecules} return graph, molecules, categories
[docs] def get_num_md_simulations(self) -> int: """Returns the number of MD simulations used during the Molecular Dynamics stage. :raises KFError: If the KF file associated with this result does not contain the right information. :return: The number of MD simulations used during the Molecular Dynamics stage. :rtype: int """ with KFFile(self.rkfpath()) as kf: try: njobs = kf.read_int("MolecularDynamicsResults", "NumSimulations") except KFError: raise KFError(f'"MolecularDynamicsResults%NumSimulations" not found on {self.rkfpath()}') from None return njobs
def recreate_molecule(self) -> Dict[str, Molecule]: """Obtain the input molecule(s) used to create this result from the rkf file associated with this result. :return: The input molecule(s). :rtype: Dict[str, Molecule] """ molecule = ReactionsDiscoveryJob.from_rkf(self.rkfpath()).molecule if isinstance(molecule, Molecule): molecule = {"": molecule} return molecule def recreate_settings(self) -> Settings: """Obtain the original input used to create this result from the rkf file associated with this result. :return: The original input Settings. :rtype: Settings """ return ReactionsDiscoveryJob.from_rkf(self.rkfpath()).settings
[docs]class ReactionsDiscoveryJob(SingleJob): """ PLAMS Job class for running Reactions Discovery. This class inherits from the PLAMS SingleJob class. For usage, see the SingleJob documentation. If you supply a Settings object to the constructor, it will be converted to a scm.inputs model. Attributes: * ``input``: an alias for self.settings.input * ``builder``: an alias for self.settings.input.MolecularDynamics.BuildSystem """ _result_type = ReactionsDiscoveryResults results: ReactionsDiscoveryResults _command = "reactions_discovery" _json_definitions = "reactions_discovery" _subblock_end = "End"
[docs] def __init__( self, name: str = "reactions_discovery_job", driver: Optional[ReactionsDiscovery] = None, settings: Optional[Settings] = None, molecule: Optional[Union[Molecule, Dict[str, Molecule]]] = None, **kwargs, ): """ Initialize the ReactionsDiscoveryJob. name : str The name of the job driver : scm.inputs.ReactionsDiscovery scm.inputs model describing the input to the ReactionsDiscovery program settings: scm.plams.Settings All settings for the job. Input settings in the PLAMS settings format under ``settings.input`` are automatically converted to a scm.inputs model. You can specify ``settings.runscript.nproc`` to set the total number of cores to run on. molecule: scm.plams.Molecule or Dict[str, scm.plams.Molecule] Two possibilities: * ``molecule`` is of type Molecule - it should then be the *complete* system as a PLAMS Molecule . Cannot be combined with the ``driver.input.MolecularDynamics.BuildSystem`` or ``settings.input.ams.MolecularDynamics.BuildSystem``. It will be written to the main System block in the input. * ``molecule`` is a dictionary with string keys and Molecule values - the keys should then be given in the ``driver.input.MolecularDynamics.BuildSystem.Molecule[i].SystemID`` input option. The molecules will then be used to build the system before the MD. """ super().__init__(name=name, settings=settings, molecule=molecule, **kwargs) if driver is not None: self.settings.input = driver elif self.settings.input: text_input = AMSJob(settings=self.settings).get_input() self.settings.input = ReactionsDiscovery.from_input(text_input) else: self.settings.input = ReactionsDiscovery()
[docs] @classmethod def from_rkf(cls, path: str) -> "ReactionsDiscoveryJob": """Initialize a job from a reactions_discovery.rkf file. :param path: Path to a reactions_discovery.rkf file :type path: str :return: A new ReactionsDiscoveryJob instance based on the information found in path. :rtype: ReactionsDiscoveryJob """ with KFFile(path) as kf: text_input = kf.read_string("General", "user input") return cls.from_input(text_input)
[docs] @classmethod def from_input(cls, text_input: str): """Initialize a job from text input. :param text_input: A multiline text input :type text_input: str :return: A ReactionsDiscoveryJob :rtype: ReactionsDiscoveryJob """ driver = ReactionsDiscovery.from_input(text_input) molecule = cls._extract_mol_from_input(driver) return ReactionsDiscoveryJob(driver=driver, molecule=molecule)
[docs] @staticmethod def _extract_mol_from_input( model: ReactionsDiscovery, ) -> Optional[Dict[str, Molecule]]: """Move any System block out of the ReactionsDiscovery input and return it as the job's system(s).""" systems = dict(model.Systems) if not systems: return None del model.Systems molecules = {name: chemsys_to_plams_molecule(system) for name, system in systems.items()} for name, molecule in molecules.items(): molecule.properties.name = name return molecules
[docs] def get_errormsg(self) -> str: """Returns the contents of the jobname.err file if it exists. If the file does not exist an empty string is returned. :return: The error message :rtype: str """ try: with open(self.results["$JN.err"], "r") as err: errlines = err.read() return errlines except FileNotFoundError: return ""
[docs] def get_runscript(self) -> str: """ Generates the runscript. Use ``self.settings.runscript.preamble_lines = ['line1', 'line2']`` or similarly for ``self.settings.runscript.postamble_lines`` to set custom settings. ``self.settings.runscript.nproc`` controls the total number of cores to run on. """ filename = self._filename("inp") ret = "" for line in self.settings.runscript.get("preamble_lines", ""): ret += f"{line}\n" # need to use `pwd` here and not "." since "." doesn't expand ret += f'AMS_JOBNAME="{self.name}" AMS_RESULTSDIR="`pwd`" "$AMSBIN/{self._command}" ' nproc = self.settings.runscript.get("nproc", None) if nproc: ret += f"-n {nproc} " ret += f'< "{filename}"\n' for line in self.settings.runscript.get("postamble_lines", ""): ret += f"{line}\n" return ret
[docs] def check(self) -> bool: """Returns True if "NORMAL TERMINATION" is given in the General section of reactions_discovery.rkf, AND all molecular dynamics jobs also have finished successfully. """ with KFFile(self.results.rkfpath()) as kf: termination = kf.read_string("General", "termination status") i_am_ok = "NORMAL TERMINATION" in termination if not i_am_ok: return False md_ok = all(job.check() for job in self.get_md_jobs()) return md_ok
[docs] def ok(self) -> bool: """Synonym for check()""" return self.check()
[docs] def get_md_jobs(self) -> List[AMSJob]: """Returns: List of AMSJob""" # this is very ugly because the user may create new rkf files inside the # directory and then those are counted.... results = scm.reactions_discovery.utils.load_results(self.path) return [x.job for x in results]
@property def input(self) -> ReactionsDiscovery: """scm.inputs model input""" return self.settings.input @input.setter def input(self, input: ReactionsDiscovery): self.settings.input = input @property def builder(self) -> ReactionsDiscovery.MolecularDynamicsBlock.BuildSystemBlock: return self.input.MolecularDynamics.BuildSystem @builder.setter def builder(self, builder: ReactionsDiscovery.MolecularDynamicsBlock.BuildSystemBlock): self.input.MolecularDynamics.BuildSystem = builder
[docs] @classmethod def load_external(cls, path: Union[str, Path], finalize: bool = False) -> "ReactionsDiscoveryJob": """Load a previous ReactionsDiscovery job from disk. :param path: A reactions discovery results folder. :type path: Union[str, Path] :param finalize: See SingleJob, defaults to False :type finalize: bool, optional :raises FileError: When the path does not exist. :return: An initialized ReactionsDiscoveryJob :rtype: ReactionsDiscoveryJob """ path = Path(path) if not os.path.isdir(path): if os.path.exists(path): path = os.path.dirname(os.path.abspath(path)) elif os.path.isdir(path / ".results"): path = path / ".results" elif os.path.isdir(path / "results"): path = path / "results" else: raise FileError("Path {} does not exist, cannot load from it.".format(path)) job = super(ReactionsDiscoveryJob, cls).load_external(path, finalize=finalize) if job.name.endswith(".results") and len(job.name) > 8: job.name = job.name[:-8] return job
[docs] def get_input(self) -> str: """Obtain the input string used to run the Reactions Discovery workflow script. :return: An input string. :rtype: str """ return AMSJob.get_input(self)
def _serialize_input(self, s): return AMSJob._serialize_input(self, s) def _serialize_molecule(self): return AMSJob._serialize_molecule(self) def _serialize_single_molecule(self, name, mol): return AMSJob._serialize_single_molecule(self, name, mol) @staticmethod def _atom_symbol(s): return AMSJob._atom_symbol(s) @staticmethod def _atom_suffix(s): return AMSJob._atom_suffix(s)