From 3b9d6b5f527d1f7787b9fca66aad5683b9b56e1a Mon Sep 17 00:00:00 2001 From: Paul Saxe Date: Sun, 3 Nov 2024 17:41:14 -0500 Subject: [PATCH] Bugfix: MOPAC files with references in comments * Fixed a bug that caused a crash when reading MOPAC files with references in the comments. * Updated the MOPAC reader to the new approach for running MOPAC in the cases that it is needed: Z-matrices and mixed inputs that OpenBabel can't handle. --- HISTORY.rst | 6 ++ read_structure_step/formats/mop/find_mopac.py | 3 +- read_structure_step/formats/mop/obabel.py | 16 ++-- read_structure_step/read.py | 5 ++ read_structure_step/read_structure.py | 80 ++++++++++++++++++- 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 191c555..e731007 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ ======= History ======= +2024.11.3 -- Bugfix: MOPAC files with references in comments + * Fixed a bug that caused a crash when reading MOPAC files with references in the + comments. + * Updated the MOPAC reader to the new approach for running MOPAC in the cases that + it is needed: Z-matrices and mixed inputs that OpenBabel can't handle. + 2024.8.23 -- Enhancements to directory handling * Changed the handling of paths to make them relative to the directory that the step is running in. In a loop, for instance, files are relative to the iteration diff --git a/read_structure_step/formats/mop/find_mopac.py b/read_structure_step/formats/mop/find_mopac.py index c220dbe..406d512 100644 --- a/read_structure_step/formats/mop/find_mopac.py +++ b/read_structure_step/formats/mop/find_mopac.py @@ -1,7 +1,8 @@ -import seamm_util from pathlib import Path import os +import seamm_util + mopac_error_identifiers = [] diff --git a/read_structure_step/formats/mop/obabel.py b/read_structure_step/formats/mop/obabel.py index ad977ba..2a3b6e0 100644 --- a/read_structure_step/formats/mop/obabel.py +++ b/read_structure_step/formats/mop/obabel.py @@ -16,9 +16,8 @@ from openbabel import openbabel from read_structure_step.formats.registries import register_reader -import seamm from seamm_util import Q_ -from .find_mopac import find_mopac +from .find_mopac import find_mopac # noqa: F401 if "OpenBabel_version" not in globals(): OpenBabel_version = None @@ -196,6 +195,7 @@ def load_mop( references=None, bibliography=None, save_data=True, + step=None, **kwargs, ): """Read a MOPAC input file. @@ -405,10 +405,6 @@ def load_mop( except Exception: logger.info("**** falling back to MOPAC") # Try using a MOPAC output file instead. Works for e.g. mixed coordinates - mopac_exe = find_mopac() - if mopac_exe is None: - raise FileNotFoundError("The MOPAC executable could not be found") - # Create an input file text = ["0SCF", "title", "description"] text.extend(raw_geometry_lines) @@ -418,10 +414,7 @@ def load_mop( logger.debug(f"MOPAC input file:\n\n{files['mopac.dat']}\n") - local = seamm.ExecLocal() - result = local.run( - cmd=[mopac_exe, "mopac.dat"], files=files, return_files=["mopac.out"] - ) + result = step.run_mopac(files=files, return_files=["mopac.out"]) if result["mopac.out"]["data"] is None: raise RuntimeError("MOPAC failed: " + result["mopac.out"]["exception"]) @@ -642,6 +635,7 @@ def load_mop( keyword = metadata[keyword] if value == "": print(f"Value for {keyword} missing in MOPAC .mop file") + print("\n\t".join(description_lines)) continue if "reference" in keyword: description = keyword.split(".")[0] @@ -672,7 +666,7 @@ def load_mop( ): stderr = float(stderr) * kcal2kJ system_properties.put(new_keyword, stderr) - if ( + if "reference" not in keyword and ( "heat capacity" in keyword or "enthalpy" in keyword or "entropy" in keyword diff --git a/read_structure_step/read.py b/read_structure_step/read.py index 299b76d..93daf4e 100644 --- a/read_structure_step/read.py +++ b/read_structure_step/read.py @@ -21,6 +21,7 @@ def read( printer=None, references=None, bibliography=None, + step=None, ): """ Calls the appropriate functions to parse the requested file. @@ -70,6 +71,9 @@ def read( bibliography : dict The bibliography as a dictionary. + step : seamm.Node = None + The node in the flowchart, used for running e.g. MOPAC. + Returns ------- [Configuration] @@ -124,6 +128,7 @@ def read( printer=printer, references=references, bibliography=bibliography, + step=step, ) return configurations diff --git a/read_structure_step/read_structure.py b/read_structure_step/read_structure.py index fead659..d094265 100644 --- a/read_structure_step/read_structure.py +++ b/read_structure_step/read_structure.py @@ -11,9 +11,13 @@ directory, and is used for all normal output from this step. """ +import configparser +import importlib import logging +import os from pathlib import PurePath, Path import pprint # noqa: F401 +import shutil import tarfile import tempfile import textwrap @@ -23,7 +27,7 @@ from .read import read import seamm from seamm_util import ureg, Q_ # noqa: F401 -from seamm_util import getParser +from seamm_util import Configuration, getParser import seamm_util.printing as printing from seamm_util.printing import FormattedText as __ from .utils import guess_extension @@ -259,6 +263,7 @@ def run(self): printer=printer.important, references=self.references, bibliography=self._bibliography, + step=self, ) # Finish the output @@ -371,6 +376,7 @@ def read_tarfile(self, tarfile_path, P): printer=printer.important, references=self.references, bibliography=self._bibliography, + step=self, ) tmp_path.unlink() @@ -384,3 +390,75 @@ def read_tarfile(self, tarfile_path, P): indent=4 * " ", ) ) + + def run_mopac(self, files=None, return_files=["mopac.out"]): + """Run MOPAC to parse the input file.""" + + import mopac_step + + # Access the options + seamm_options = self.global_options + + executor = self.flowchart.executor + + # Read configuration file for MOPAC if it exists + executor_type = executor.name + full_config = configparser.ConfigParser() + ini_dir = Path(seamm_options["root"]).expanduser() + path = ini_dir / "mopac.ini" + # If the config file doesn't exists, get the default + if not path.exists(): + resources = importlib.resources.files("mopac_step") / "data" + ini_text = (resources / "mopac.ini").read_text() + txt_config = Configuration(path) + txt_config.from_string(ini_text) + + # Work out the conda info needed + txt_config.set_value("local", "conda", os.environ["CONDA_EXE"]) + txt_config.set_value("local", "conda-environment", "seamm-mopac") + txt_config.save() + + full_config.read(ini_dir / "mopac.ini") + + # Getting desperate! Look for an executable in the path + if executor_type not in full_config: + path = shutil.which("mopac") + if path is None: + raise RuntimeError( + f"No section for '{executor_type}' in MOPAC ini file " + f"({ini_dir / 'mopac.ini'}), nor in the defaults, nor " + "in the path!" + ) + else: + txt_config = Configuration(path) + txt_config.add_section(executor_type) + txt_config.set_value(executor_type, "installation", "local") + txt_config.set_value(executor_type, "code", str(path)) + txt_config.save() + full_config.read(ini_dir / "mopac.ini") + + config = dict(full_config.items(executor_type)) + + # Use the matching version of the seamm-mopac image by default. + config["version"] = mopac_step.__version__ + + env = { + "OMP_NUM_THREADS": "1", + } + + result = executor.run( + cmd=["{code}", "mopac.dat", ">", "stdout.txt", "2>", "stderr.txt"], + config=config, + directory=self.directory, + files=files, + return_files=return_files, + in_situ=True, + shell=True, + env=env, + ) + + if not result: + self.logger.error("There was an error running MOPAC") + return None + + return result