From f6a32827a8c1885fa954bea313898724eff284b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Wed, 18 Dec 2024 13:00:31 +0100 Subject: [PATCH 1/4] Interface for the qm program turbomole MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- mindlessgen.toml | 10 +- src/mindlessgen/generator/main.py | 22 ++- src/mindlessgen/prog/__init__.py | 2 + src/mindlessgen/prog/config.py | 95 +++++++++++- src/mindlessgen/qm/__init__.py | 3 + src/mindlessgen/qm/tm.py | 239 ++++++++++++++++++++++++++++++ 6 files changed, 365 insertions(+), 6 deletions(-) create mode 100644 src/mindlessgen/qm/tm.py diff --git a/mindlessgen.toml b/mindlessgen.toml index 8151b53..f0b1761 100644 --- a/mindlessgen.toml +++ b/mindlessgen.toml @@ -60,7 +60,7 @@ hlgap = 0.5 debug = false [postprocess] -# > Engine for the post-processing part. Options: 'xtb', 'orca' +# > Engine for the post-processing part. Options: 'xtb', 'orca', 'turbomole' engine = "orca" # > Optimize geometry in the post-processing part. If `false`, only a single-point is conducted. Options: optimize = true @@ -88,3 +88,11 @@ basis = "def2-SVP" gridsize = 1 # > Maximum number of SCF cycles: Options: scf_cycles = 100 + +[turbomole] +# > Path to the turbomole executable. The names `turbomole` and `turbomole_dev` are automatically searched for. Options: +turbomole_path = "/path/to/turbomole" +# > Functional/Method: Options: +functional = "PBE" +# > Basis set: Options: +basis = "def2-SVP" diff --git a/src/mindlessgen/generator/main.py b/src/mindlessgen/generator/main.py index 8a36a54..c4a953c 100644 --- a/src/mindlessgen/generator/main.py +++ b/src/mindlessgen/generator/main.py @@ -10,7 +10,17 @@ import warnings from ..molecules import generate_random_molecule, Molecule -from ..qm import XTB, get_xtb_path, QMMethod, ORCA, get_orca_path, GP3, get_gp3_path +from ..qm import ( + XTB, + get_xtb_path, + QMMethod, + ORCA, + get_orca_path, + GP3, + get_gp3_path, + Turbomole, + get_turbomole_path, +) from ..molecules import iterative_optimization, postprocess_mol from ..prog import ConfigManager @@ -46,6 +56,7 @@ def generator(config: ConfigManager) -> tuple[list[Molecule] | None, int]: config, get_xtb_path, get_orca_path, # GP3 cannot be used anyway + get_turbomole_path, ) if config.general.postprocess: @@ -266,6 +277,7 @@ def setup_engines( cfg: ConfigManager, xtb_path_func: Callable, orca_path_func: Callable, + turbomole_path_func: Callable, gp3_path_func: Callable | None = None, ): """ @@ -287,6 +299,14 @@ def setup_engines( except ImportError as e: raise ImportError("orca not found.") from e return ORCA(path, cfg.orca) + elif engine_type == "turbomole": + try: + path = turbomole_path_func(cfg.turbomole.turbomole_path) + if not path: + raise ImportError("turbomole not found.") + except ImportError as e: + raise ImportError("turbomole not found.") from e + return Turbomole(path, cfg.turbomole) elif engine_type == "gp3": if gp3_path_func is None: raise ImportError("No callable function for determining the gp3 path.") diff --git a/src/mindlessgen/prog/__init__.py b/src/mindlessgen/prog/__init__.py index 8dee820..0f0810c 100644 --- a/src/mindlessgen/prog/__init__.py +++ b/src/mindlessgen/prog/__init__.py @@ -7,6 +7,7 @@ GeneralConfig, XTBConfig, ORCAConfig, + TURBOMOLEConfig, GenerateConfig, RefineConfig, PostProcessConfig, @@ -17,6 +18,7 @@ "GeneralConfig", "XTBConfig", "ORCAConfig", + "TURBOMOLEConfig", "GenerateConfig", "RefineConfig", "PostProcessConfig", diff --git a/src/mindlessgen/prog/config.py b/src/mindlessgen/prog/config.py index d598b4a..591fbb0 100644 --- a/src/mindlessgen/prog/config.py +++ b/src/mindlessgen/prog/config.py @@ -626,8 +626,8 @@ def engine(self, engine: str): """ if not isinstance(engine, str): raise TypeError("Refinement engine should be a string.") - if engine not in ["xtb", "orca"]: - raise ValueError("Refinement engine can only be xtb or orca.") + if engine not in ["xtb", "orca", "turbomole"]: + raise ValueError("Refinement engine can only be xtb, orca or turbomole.") self._engine = engine @property @@ -693,8 +693,8 @@ def engine(self, engine: str): """ if not isinstance(engine, str): raise TypeError("Postprocess engine should be a string.") - if engine not in ["xtb", "orca", "gp3"]: - raise ValueError("Postprocess engine can only be xtb or orca.") + if engine not in ["xtb", "orca", "gp3", "turbomole"]: + raise ValueError("Postprocess engine can only be xtb, orca turbomole.") self._engine = engine @property @@ -895,6 +895,86 @@ def scf_cycles(self, max_scf_cycles: int): self._scf_cycles = max_scf_cycles +class TURBOMOLEConfig(BaseConfig): + """ + Configuration class for TURBOMOLE. + """ + + def __init__(self: TURBOMOLEConfig) -> None: + self._turbomole_path: str | Path = "turbomole" + self._functional: str = "PBE" + self._basis: str = "def2-SVP" + self._scf_cycles: int = 100 + + def get_identifier(self) -> str: + return "turbomole" + + @property + def turbomole_path(self): + """ + Get the turbomole path. + """ + return self._turbomole_path + + @turbomole_path.setter + def turbomole_path(self, turbomole_path: str | Path): + """ + Set the turbomole path. + """ + if not isinstance(turbomole_path, str | Path): + raise TypeError("turbomole_path should be a string or Path.") + self._turbomole_path = turbomole_path + + @property + def functional(self): + """ + Get the TURBOMOLE functional/method. + """ + return self._functional + + @functional.setter + def functional(self, functional: str): + """ + Set the TURBOMOLE functional/method. + """ + if not isinstance(functional, str): + raise TypeError("Functional should be a string.") + self._functional = functional + + @property + def basis(self): + """ + Get the TURBOMOLE basis set. + """ + return self._basis + + @basis.setter + def basis(self, basis: str): + """ + Set the TURBOMOLE basis set. + """ + if not isinstance(basis, str): + raise TypeError("Basis should be a string.") + self._basis = basis + + @property + def scf_cycles(self): + """ + Get the maximum number of SCF cycles. + """ + return self._scf_cycles + + @scf_cycles.setter + def scf_cycles(self, max_scf_cycles: int): + """ + Set the maximum number of SCF cycles. + """ + if not isinstance(max_scf_cycles, int): + raise TypeError("Max SCF cycles should be an integer.") + if max_scf_cycles < 1: + raise ValueError("Max SCF cycles should be greater than 0.") + + class ConfigManager: """ Overall configuration manager for the program. @@ -907,6 +987,7 @@ def __init__(self, config_file: str | Path | None = None): self.general = GeneralConfig() self.xtb = XTBConfig() self.orca = ORCAConfig() + self.turbomole = TURBOMOLEConfig() self.refine = RefineConfig() self.postprocess = PostProcessConfig() self.generate = GenerateConfig() @@ -996,6 +1077,9 @@ def load_from_toml(self, config_file: str | Path) -> None: [orca] orca_option = "opt" + [turbomole] + turbomole_option = "opt" + Arguments: config_file (str): Path to the configuration file @@ -1018,6 +1102,9 @@ def load_from_dict(self, config_dict: dict) -> None: }, "orca": { "orca_option": "opt" + }, + "turbomole": { + "turbomole_option": "opt" } } diff --git a/src/mindlessgen/qm/__init__.py b/src/mindlessgen/qm/__init__.py index a5e8e3a..e21ef44 100644 --- a/src/mindlessgen/qm/__init__.py +++ b/src/mindlessgen/qm/__init__.py @@ -6,6 +6,7 @@ from .xtb import XTB, get_xtb_path from .orca import ORCA, get_orca_path from .gp3 import GP3, get_gp3_path +from .tm import Turbomole, get_turbomole_path __all__ = [ "XTB", @@ -15,4 +16,6 @@ "get_orca_path", "GP3", "get_gp3_path", + "Turbomole", + "get_turbomole_path", ] diff --git a/src/mindlessgen/qm/tm.py b/src/mindlessgen/qm/tm.py new file mode 100644 index 0000000..f6d43bf --- /dev/null +++ b/src/mindlessgen/qm/tm.py @@ -0,0 +1,239 @@ +""" +This module handles all Turbomole-related functionality. +""" + +from pathlib import Path +import shutil +import subprocess as sp +from tempfile import TemporaryDirectory + +from ..molecules import Molecule +from ..prog import TURBOMOLEConfig as turbomoleConfig +from .base import QMMethod + + +class Turbomole(QMMethod): + """ + This class handles all interaction with the turbomole external dependency. + """ + + def __init__(self, path: str | Path, turbomolecfg: turbomoleConfig) -> None: + """ + Initialize the turbomole class. + """ + if isinstance(path, str): + self.path: Path = Path(path).resolve() + elif isinstance(path, Path): + self.path = path + else: + raise TypeError("turbomole_path should be a string or a Path object.") + self.cfg = turbomolecfg + + def optimize( + self, molecule: Molecule, max_cycles: int | None = None, verbosity: int = 1 + ) -> Molecule: + """ + Optimize a molecule using turbomole. + """ + + # Create a unique temporary directory using TemporaryDirectory context manager + with TemporaryDirectory(prefix="turbomole_") as temp_dir: + temp_path = Path(temp_dir).resolve() + # write the molecule to a temporary file + molecule.write_xyz_to_file(temp_path / "molecule.xyz") + + inputname = "turbomole_opt.inp" + turbomole_input = self._gen_input( + molecule, "molecule.xyz", True, max_cycles + ) + if verbosity > 1: + print("turbomole input file:\n##################") + print(turbomole_input) + print("##################") + with open(temp_path / inputname, "w", encoding="utf8") as f: + f.write(turbomole_input) + + # run turbomole + arguments = [ + inputname, + ] + + turbomole_log_out, turbomole_log_err, return_code = self._run( + temp_path=temp_path, arguments=arguments + ) + if verbosity > 2: + print(turbomole_log_out) + if return_code != 0: + raise RuntimeError( + f"turbomole failed with return code {return_code}:\n{turbomole_log_err}" + ) + + # read the optimized molecule from the output file + xyzfile = Path(temp_path / inputname).resolve().with_suffix(".xyz") + optimized_molecule = molecule.copy() + optimized_molecule.read_xyz_from_file(xyzfile) + return optimized_molecule + + def singlepoint(self, molecule: Molecule, verbosity: int = 1) -> str: + """ + Perform a single point calculation using turbomole. + """ + # Create a unique temporary directory using TemporaryDirectory context manager + with TemporaryDirectory(prefix="turbomole_") as temp_dir: + temp_path = Path(temp_dir).resolve() + # write the molecule to a temporary file + molfile = "mol.xyz" + molecule.write_xyz_to_file(temp_path / molfile) + + # converte mol.xyz to tm format + arguments = [ + "x2t mol.xyz > coord", + ] + print("Running", arguments) + + # run cefine to write control file + arguments = [ + "cefine", + "-bas", + f"{self.cfg.basis}", + "-func", + f"{self.cfg.functional}", + ] + if molecule.charge != 0: + arguments += ["-chrg", str(molecule.charge)] + if molecule.uhf != 0: + arguments += ["-uhf", str(molecule.uhf)] + # if molecule.symmetry is not None: + # arguments += ["-sym", str(molecule.symmetry)] + print("Running cefine", arguments) + + # run turbomole + arguments = [ + "ridft > rifdt.out", + ] + turbomole_log_out, turbomole_log_err, return_code = self._run( + temp_path=temp_path, arguments=arguments + ) + if verbosity > 2: + print(turbomole_log_out) + if return_code != 0: + raise RuntimeError( + f"turbomole failed with return code {return_code}:\n{turbomole_log_err}" + ) + + return turbomole_log_out + + def check_gap( + self, molecule: Molecule, threshold: float, verbosity: int = 1 + ) -> bool: + """ + Check if the HL gap is larger than a given threshold. + """ + raise NotImplementedError("check_gap not implemented for turbomole.") + + def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]: + """ + Run turbomole with the given arguments. + + Arguments: + arguments (list[str]): The arguments to pass to turbomole. + + Returns: + tuple[str, str, int]: The output of the turbomole calculation (stdout and stderr) + and the return code + """ + try: + turbomole_out = sp.run( + [str(self.path)] + arguments, + cwd=temp_path, + capture_output=True, + check=True, + ) + # get the output of the turbomole calculation (of both stdout and stderr) + turbomole_log_out = turbomole_out.stdout.decode("utf8", errors="replace") + turbomole_log_err = turbomole_out.stderr.decode("utf8", errors="replace") + # check if the output contains "turbomole TERMINATED NORMALLY" + if "all done" not in turbomole_log_out: + raise sp.CalledProcessError( + 1, + str(self.path), + turbomole_log_out.encode("utf8"), + turbomole_log_err.encode("utf8"), + ) + return turbomole_log_out, turbomole_log_err, 0 + except sp.CalledProcessError as e: + turbomole_log_out = e.stdout.decode("utf8", errors="replace") + turbomole_log_err = e.stderr.decode("utf8", errors="replace") + return turbomole_log_out, turbomole_log_err, e.returncode + + def _cefine(self, molecule: Molecule) -> list[str]: + """ + Refine a molecule using turbomole. + """ + call = [ + "cefine", + "-bas", + f"{self.cfg.basis}", + "-func", + f"{self.cfg.functional}", + ] + if molecule.charge != 0: + call += ["-chrg", str(molecule.charge)] + if molecule.uhf != 0: + call += ["-uhf", str(molecule.uhf)] + # if molecule.symmetry is not None: + # arguments += ["-sym", str(molecule.symmetry)] + + return call + + def _gen_input( + self, + molecule: Molecule, + xyzfile: str, + optimization: bool = False, + opt_cycles: int | None = None, + ) -> str: + """ + Generate a default input file for turbomole. + """ + turbomole_input = f"! {self.cfg.functional} {self.cfg.basis}\n" + # turbomole_input += f"! DEFGRID{self.cfg.gridsize}\n" + turbomole_input += "! NoTRAH NoSOSCF SlowConv\n" + # "! AutoAux" keyword for super-heavy elements as def2/J ends at Rn + if any(atom >= 86 for atom in molecule.ati): + turbomole_input += "! AutoAux\n" + if optimization: + turbomole_input += "! OPT\n" + if opt_cycles is not None: + turbomole_input += f"%geom MaxIter {opt_cycles} end\n" + turbomole_input += ( + f"%scf\n\tMaxIter {self.cfg.scf_cycles}\n\tConvergence Medium\nend\n" + ) + turbomole_input += "%pal nprocs 1 end\n\n" + turbomole_input += f"* xyzfile {molecule.charge} {molecule.uhf + 1} {xyzfile}\n" + return turbomole_input + + +# TODO: 1. Convert this to a @staticmethod of Class turbomole +# 2. Rename to `get_method` or similar to enable an abstract interface +# 3. Add the renamed method to the ABC `QMMethod` +# 4. In `main.py`: Remove the passing of the path finder functions as arguments +# and remove the boiler plate code to make it more general. +def get_turbomole_path(binary_name: str | Path | None = None) -> Path: + """ + Get the path to the turbomole binary based on different possible names + that are searched for in the PATH. + """ + default_turbomole_names: list[str | Path] = ["turbomole", "turbomole_dev"] + # put binary name at the beginning of the lixt to prioritize it + if binary_name is not None: + binary_names = [binary_name] + default_turbomole_names + else: + binary_names = default_turbomole_names + # Get turbomole path from 'which turbomole' command + for binpath in binary_names: + which_turbomole = shutil.which(binpath) + if which_turbomole: + turbomole_path = Path(which_turbomole).resolve() + return turbomole_path + raise ImportError("'turbomole' binary could not be found.") From d6c60c28fd5919144b79f44adf9e1de7ef932552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Wed, 8 Jan 2025 12:54:49 +0100 Subject: [PATCH 2/4] support for turbomole MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- CHANGELOG.md | 1 + mindlessgen.toml | 4 +- src/mindlessgen/generator/main.py | 7 +- src/mindlessgen/prog/config.py | 2 +- src/mindlessgen/qm/tm.py | 230 +++++++++++++++++------------- 5 files changed, 140 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac4cf3..822da61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - support for `python-3.13` - option to set a fixed molecular charge, while ensuring `uhf = 0` - `element_composition` and `forbidden_elements` can now be directly set to a `dict` or `list`, respectively, via API access +- support for the qm program turbomole. ### Breaking Changes - Removal of the `dist_threshold` flag and in the `-toml` file. diff --git a/mindlessgen.toml b/mindlessgen.toml index f0b1761..4b6c9c4 100644 --- a/mindlessgen.toml +++ b/mindlessgen.toml @@ -90,9 +90,11 @@ gridsize = 1 scf_cycles = 100 [turbomole] -# > Path to the turbomole executable. The names `turbomole` and `turbomole_dev` are automatically searched for. Options: +# > Path to the turbomole executable. The names `ridft` and `jobex` are automatically searched for. Options: turbomole_path = "/path/to/turbomole" # > Functional/Method: Options: functional = "PBE" # > Basis set: Options: basis = "def2-SVP" +# > Maximum number of SCF cycles: Options: +scf_cycles = 100 diff --git a/src/mindlessgen/generator/main.py b/src/mindlessgen/generator/main.py index c4a953c..f80d2c4 100644 --- a/src/mindlessgen/generator/main.py +++ b/src/mindlessgen/generator/main.py @@ -17,7 +17,6 @@ ORCA, get_orca_path, GP3, - get_gp3_path, Turbomole, get_turbomole_path, ) @@ -61,7 +60,11 @@ def generator(config: ConfigManager) -> tuple[list[Molecule] | None, int]: if config.general.postprocess: postprocess_engine: QMMethod | None = setup_engines( - config.postprocess.engine, config, get_xtb_path, get_orca_path, get_gp3_path + config.postprocess.engine, + config, + get_xtb_path, + get_orca_path, + get_turbomole_path, ) else: postprocess_engine = None diff --git a/src/mindlessgen/prog/config.py b/src/mindlessgen/prog/config.py index 591fbb0..f657bda 100644 --- a/src/mindlessgen/prog/config.py +++ b/src/mindlessgen/prog/config.py @@ -902,7 +902,7 @@ class TURBOMOLEConfig(BaseConfig): def __init__(self: TURBOMOLEConfig) -> None: self._turbomole_path: str | Path = "turbomole" - self._functional: str = "PBE" + self._functional: str = "pbe" self._basis: str = "def2-SVP" self._scf_cycles: int = 100 diff --git a/src/mindlessgen/qm/tm.py b/src/mindlessgen/qm/tm.py index f6d43bf..fc8a766 100644 --- a/src/mindlessgen/qm/tm.py +++ b/src/mindlessgen/qm/tm.py @@ -4,6 +4,7 @@ from pathlib import Path import shutil +import os import subprocess as sp from tempfile import TemporaryDirectory @@ -30,53 +31,84 @@ def __init__(self, path: str | Path, turbomolecfg: turbomoleConfig) -> None: self.cfg = turbomolecfg def optimize( - self, molecule: Molecule, max_cycles: int | None = None, verbosity: int = 1 + self, + molecule: Molecule, + max_cycles: int | None = None, + verbosity: int = 1, ) -> Molecule: """ - Optimize a molecule using turbomole. + Optimize a molecule using ORCA. """ # Create a unique temporary directory using TemporaryDirectory context manager with TemporaryDirectory(prefix="turbomole_") as temp_dir: temp_path = Path(temp_dir).resolve() # write the molecule to a temporary file - molecule.write_xyz_to_file(temp_path / "molecule.xyz") + molfile = "molecule.xyz" + molecule.write_xyz_to_file(temp_path / molfile) - inputname = "turbomole_opt.inp" - turbomole_input = self._gen_input( - molecule, "molecule.xyz", True, max_cycles - ) + # convert molfile to coord file (tm format) + command = f"x2t {temp_path / molfile} > {temp_path / 'coord'}" + + try: + # run the command in a shell + sp.run(command, shell=True, check=True) + except sp.CalledProcessError as e: + print(f"The xyz file could not be converted to a coord file: {e}") + + if verbosity > 2: + with open(temp_path / "coord", encoding="utf8") as f: + tm_coordinates = f.read() + print(tm_coordinates) + + # write the input file + inputname = "control" + tm_input = self._gen_input(molecule) if verbosity > 1: - print("turbomole input file:\n##################") - print(turbomole_input) + print("Turbomole input file:\n##################") + print(tm_input) print("##################") with open(temp_path / inputname, "w", encoding="utf8") as f: - f.write(turbomole_input) + f.write(tm_input) - # run turbomole + # Setup the turbomole optimization command including the max number of optimization cycles arguments = [ - inputname, + "jobex", + "-ri", + "-c", + f"{max_cycles}", + ">", + "jobex.out", ] - turbomole_log_out, turbomole_log_err, return_code = self._run( + if verbosity > 2: + print(f"Running command: {' '.join(arguments)}") + + tm_log_out, tm_log_err, return_code = self._run( temp_path=temp_path, arguments=arguments ) if verbosity > 2: - print(turbomole_log_out) + print(tm_log_out) if return_code != 0: raise RuntimeError( - f"turbomole failed with return code {return_code}:\n{turbomole_log_err}" + f"Turbomole failed with return code {return_code}:\n{tm_log_err}" ) + # revert the coord file to xyz file + revert_command = f"t2x {temp_path / 'coord'} > {temp_path / 'molecule.xyz'}" + try: + sp.run(revert_command, shell=True, check=True) + except sp.CalledProcessError as e: + print(f"The coord file could not be converted to a xyz file: {e}") # read the optimized molecule from the output file - xyzfile = Path(temp_path / inputname).resolve().with_suffix(".xyz") + xyzfile = Path(temp_path / "molecule.xyz").resolve().with_suffix(".xyz") optimized_molecule = molecule.copy() optimized_molecule.read_xyz_from_file(xyzfile) return optimized_molecule def singlepoint(self, molecule: Molecule, verbosity: int = 1) -> str: """ - Perform a single point calculation using turbomole. + Perform a single point calculation using Turbomole. """ # Create a unique temporary directory using TemporaryDirectory context manager with TemporaryDirectory(prefix="turbomole_") as temp_dir: @@ -84,44 +116,44 @@ def singlepoint(self, molecule: Molecule, verbosity: int = 1) -> str: # write the molecule to a temporary file molfile = "mol.xyz" molecule.write_xyz_to_file(temp_path / molfile) + print(temp_path / molfile) - # converte mol.xyz to tm format - arguments = [ - "x2t mol.xyz > coord", - ] - print("Running", arguments) + # convert molfile to coord file + command = f"x2t {temp_path / molfile} > {temp_path / 'coord'}" - # run cefine to write control file - arguments = [ - "cefine", - "-bas", - f"{self.cfg.basis}", - "-func", - f"{self.cfg.functional}", - ] - if molecule.charge != 0: - arguments += ["-chrg", str(molecule.charge)] - if molecule.uhf != 0: - arguments += ["-uhf", str(molecule.uhf)] - # if molecule.symmetry is not None: - # arguments += ["-sym", str(molecule.symmetry)] - print("Running cefine", arguments) - - # run turbomole - arguments = [ - "ridft > rifdt.out", - ] - turbomole_log_out, turbomole_log_err, return_code = self._run( - temp_path=temp_path, arguments=arguments + try: + # run the command in a shell + sp.run(command, shell=True, check=True) + except sp.CalledProcessError as e: + print(f"The xyz file could not be converted to a coord file: {e}") + with open(temp_path / "coord", encoding="utf8") as f: + content = f.read() + print(content) + + # write the input file + inputname = "control" + tm_input = self._gen_input(molecule) + if verbosity > 1: + print("Turbomole input file:\n##################") + print(self._gen_input(molecule)) + print("##################") + with open(temp_path / inputname, "w", encoding="utf8") as f: + f.write(tm_input) + + # set up the turbomole single point calculation command + run_tm = ["ridft"] + + tm_log_out, tm_log_err, return_code = self._run( + temp_path=temp_path, arguments=run_tm ) if verbosity > 2: - print(turbomole_log_out) + print(tm_log_out) if return_code != 0: raise RuntimeError( - f"turbomole failed with return code {return_code}:\n{turbomole_log_err}" + f"Turbomole failed with return code {return_code}:\n{tm_log_err}" ) - return turbomole_log_out + return tm_log_out def check_gap( self, molecule: Molecule, threshold: float, verbosity: int = 1 @@ -142,76 +174,74 @@ def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]: tuple[str, str, int]: The output of the turbomole calculation (stdout and stderr) and the return code """ + try: - turbomole_out = sp.run( - [str(self.path)] + arguments, + # Change the enviroment variable to run the calculation on one cpu + original_threads = os.environ.get("PARNODES") + os.environ["PARNODES"] = "1" + + print(f"Temporäre Einstellung von PARNODES: {os.environ['PARNODES']}") + + sp.run( + arguments, cwd=temp_path, capture_output=True, check=True, + env={"PARNODES": "1", **os.environ}, ) - # get the output of the turbomole calculation (of both stdout and stderr) - turbomole_log_out = turbomole_out.stdout.decode("utf8", errors="replace") - turbomole_log_err = turbomole_out.stderr.decode("utf8", errors="replace") - # check if the output contains "turbomole TERMINATED NORMALLY" - if "all done" not in turbomole_log_out: + + # Read the job-last file to get the output of the calculation + output_file = temp_path / "job.last" + if output_file.exists(): + with open(output_file, encoding="utf-8") as file: + file_content = file.read() + turbomole_log_out = file_content + turbomole_log_err = "" + else: + raise FileNotFoundError(f"Output file {output_file} not found.") + + if "ridft : all done" not in turbomole_log_out: raise sp.CalledProcessError( 1, - str(self.path), + str(output_file), turbomole_log_out.encode("utf8"), turbomole_log_err.encode("utf8"), ) + # Revert the enviroment variable to the original setting + if original_threads is None: + os.environ.pop("PARNODES", None) + else: + os.environ["PARNODES"] = original_threads + + print( + f"Zurückgesetzte Einstellung von PARNODES: {os.environ.get('PARNODES', 'nicht gesetzt')}" + ) return turbomole_log_out, turbomole_log_err, 0 except sp.CalledProcessError as e: turbomole_log_out = e.stdout.decode("utf8", errors="replace") turbomole_log_err = e.stderr.decode("utf8", errors="replace") return turbomole_log_out, turbomole_log_err, e.returncode - def _cefine(self, molecule: Molecule) -> list[str]: - """ - Refine a molecule using turbomole. - """ - call = [ - "cefine", - "-bas", - f"{self.cfg.basis}", - "-func", - f"{self.cfg.functional}", - ] - if molecule.charge != 0: - call += ["-chrg", str(molecule.charge)] - if molecule.uhf != 0: - call += ["-uhf", str(molecule.uhf)] - # if molecule.symmetry is not None: - # arguments += ["-sym", str(molecule.symmetry)] - - return call - def _gen_input( self, molecule: Molecule, - xyzfile: str, - optimization: bool = False, - opt_cycles: int | None = None, ) -> str: """ - Generate a default input file for turbomole. + Generate a default input file for Turbomole. """ - turbomole_input = f"! {self.cfg.functional} {self.cfg.basis}\n" - # turbomole_input += f"! DEFGRID{self.cfg.gridsize}\n" - turbomole_input += "! NoTRAH NoSOSCF SlowConv\n" - # "! AutoAux" keyword for super-heavy elements as def2/J ends at Rn - if any(atom >= 86 for atom in molecule.ati): - turbomole_input += "! AutoAux\n" - if optimization: - turbomole_input += "! OPT\n" - if opt_cycles is not None: - turbomole_input += f"%geom MaxIter {opt_cycles} end\n" - turbomole_input += ( - f"%scf\n\tMaxIter {self.cfg.scf_cycles}\n\tConvergence Medium\nend\n" - ) - turbomole_input += "%pal nprocs 1 end\n\n" - turbomole_input += f"* xyzfile {molecule.charge} {molecule.uhf + 1} {xyzfile}\n" - return turbomole_input + tm_input = "$coord file=coord\n" + tm_input += f"$charge={molecule.charge} unpaired={molecule.uhf}\n" + tm_input += "$symmetry c1\n" + tm_input += "$atoms\n" + tm_input += f" basis={self.cfg.basis}\n" + tm_input += "$dft\n" + tm_input += f" functional {self.cfg.functional}\n" + tm_input += "$rij\n" + tm_input += f"$scfiterlimit {self.cfg.scf_cycles}\n" + tm_input += "$energy file=energy\n" + tm_input += "$grad file=gradient\n" + tm_input += "$end" + return tm_input # TODO: 1. Convert this to a @staticmethod of Class turbomole @@ -224,7 +254,7 @@ def get_turbomole_path(binary_name: str | Path | None = None) -> Path: Get the path to the turbomole binary based on different possible names that are searched for in the PATH. """ - default_turbomole_names: list[str | Path] = ["turbomole", "turbomole_dev"] + default_turbomole_names: list[str | Path] = ["ridft", "jobex"] # put binary name at the beginning of the lixt to prioritize it if binary_name is not None: binary_names = [binary_name] + default_turbomole_names @@ -232,8 +262,8 @@ def get_turbomole_path(binary_name: str | Path | None = None) -> Path: binary_names = default_turbomole_names # Get turbomole path from 'which turbomole' command for binpath in binary_names: - which_turbomole = shutil.which(binpath) - if which_turbomole: - turbomole_path = Path(which_turbomole).resolve() - return turbomole_path + which_ridft = shutil.which(binpath) + if which_ridft: + ridft_path = Path(which_ridft).resolve() + return ridft_path raise ImportError("'turbomole' binary could not be found.") From 0a20340cc1178b12cd0f983575c347afe3707552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Wed, 8 Jan 2025 14:27:18 +0100 Subject: [PATCH 3/4] corrected some typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- src/mindlessgen/prog/config.py | 2 +- src/mindlessgen/qm/tm.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mindlessgen/prog/config.py b/src/mindlessgen/prog/config.py index f657bda..0e549fd 100644 --- a/src/mindlessgen/prog/config.py +++ b/src/mindlessgen/prog/config.py @@ -694,7 +694,7 @@ def engine(self, engine: str): if not isinstance(engine, str): raise TypeError("Postprocess engine should be a string.") if engine not in ["xtb", "orca", "gp3", "turbomole"]: - raise ValueError("Postprocess engine can only be xtb, orca turbomole.") + raise ValueError("Postprocess engine can only be xtb, orca or turbomole.") self._engine = engine @property diff --git a/src/mindlessgen/qm/tm.py b/src/mindlessgen/qm/tm.py index fc8a766..0d19305 100644 --- a/src/mindlessgen/qm/tm.py +++ b/src/mindlessgen/qm/tm.py @@ -180,7 +180,7 @@ def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]: original_threads = os.environ.get("PARNODES") os.environ["PARNODES"] = "1" - print(f"Temporäre Einstellung von PARNODES: {os.environ['PARNODES']}") + print(f"Temporal setting for PARNODES: {os.environ['PARNODES']}") sp.run( arguments, @@ -214,7 +214,7 @@ def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]: os.environ["PARNODES"] = original_threads print( - f"Zurückgesetzte Einstellung von PARNODES: {os.environ.get('PARNODES', 'nicht gesetzt')}" + f"Revert the settings for PARNODES: {os.environ.get('PARNODES', 'not set')}" ) return turbomole_log_out, turbomole_log_err, 0 except sp.CalledProcessError as e: @@ -260,7 +260,7 @@ def get_turbomole_path(binary_name: str | Path | None = None) -> Path: binary_names = [binary_name] + default_turbomole_names else: binary_names = default_turbomole_names - # Get turbomole path from 'which turbomole' command + # Get turbomole path from 'which ridft' command for binpath in binary_names: which_ridft = shutil.which(binpath) if which_ridft: From 8bf6d513b24cef6f9beacca7c05ccac04ad85580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Fri, 10 Jan 2025 14:45:33 +0100 Subject: [PATCH 4/4] changed the PARNODES command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- src/mindlessgen/prog/config.py | 1 + src/mindlessgen/qm/tm.py | 51 +++++++++++----------------------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/mindlessgen/prog/config.py b/src/mindlessgen/prog/config.py index 0e549fd..c0cf2f4 100644 --- a/src/mindlessgen/prog/config.py +++ b/src/mindlessgen/prog/config.py @@ -973,6 +973,7 @@ def scf_cycles(self, max_scf_cycles: int): raise TypeError("Max SCF cycles should be an integer.") if max_scf_cycles < 1: raise ValueError("Max SCF cycles should be greater than 0.") + self._scf_cycles = max_scf_cycles class ConfigManager: diff --git a/src/mindlessgen/qm/tm.py b/src/mindlessgen/qm/tm.py index 0d19305..71148d8 100644 --- a/src/mindlessgen/qm/tm.py +++ b/src/mindlessgen/qm/tm.py @@ -4,7 +4,6 @@ from pathlib import Path import shutil -import os import subprocess as sp from tempfile import TemporaryDirectory @@ -72,14 +71,7 @@ def optimize( f.write(tm_input) # Setup the turbomole optimization command including the max number of optimization cycles - arguments = [ - "jobex", - "-ri", - "-c", - f"{max_cycles}", - ">", - "jobex.out", - ] + arguments = [f"PARNODES=1 jobex -ri -c {max_cycles} > jobex.out"] if verbosity > 2: print(f"Running command: {' '.join(arguments)}") @@ -141,7 +133,7 @@ def singlepoint(self, molecule: Molecule, verbosity: int = 1) -> str: f.write(tm_input) # set up the turbomole single point calculation command - run_tm = ["ridft"] + run_tm = ["PARNODES=1 ridft > ridft.out"] tm_log_out, tm_log_err, return_code = self._run( temp_path=temp_path, arguments=run_tm @@ -174,31 +166,29 @@ def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]: tuple[str, str, int]: The output of the turbomole calculation (stdout and stderr) and the return code """ - try: - # Change the enviroment variable to run the calculation on one cpu - original_threads = os.environ.get("PARNODES") - os.environ["PARNODES"] = "1" - - print(f"Temporal setting for PARNODES: {os.environ['PARNODES']}") - sp.run( arguments, cwd=temp_path, capture_output=True, check=True, - env={"PARNODES": "1", **os.environ}, + shell=True, ) - - # Read the job-last file to get the output of the calculation - output_file = temp_path / "job.last" - if output_file.exists(): - with open(output_file, encoding="utf-8") as file: - file_content = file.read() - turbomole_log_out = file_content + if "PARNODES=1 ridft > ridft.out" in arguments[0]: + with open(temp_path / "ridft.out", encoding="utf-8") as file: + ridft_file = file.read() + turbomole_log_out = ridft_file turbomole_log_err = "" else: - raise FileNotFoundError(f"Output file {output_file} not found.") + # Read the job-last file to get the output of the calculation + output_file = temp_path / "job.last" + if output_file.exists(): + with open(output_file, encoding="utf-8") as file: + file_content = file.read() + turbomole_log_out = file_content + turbomole_log_err = "" + else: + raise FileNotFoundError(f"Output file {output_file} not found.") if "ridft : all done" not in turbomole_log_out: raise sp.CalledProcessError( @@ -207,15 +197,6 @@ def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]: turbomole_log_out.encode("utf8"), turbomole_log_err.encode("utf8"), ) - # Revert the enviroment variable to the original setting - if original_threads is None: - os.environ.pop("PARNODES", None) - else: - os.environ["PARNODES"] = original_threads - - print( - f"Revert the settings for PARNODES: {os.environ.get('PARNODES', 'not set')}" - ) return turbomole_log_out, turbomole_log_err, 0 except sp.CalledProcessError as e: turbomole_log_out = e.stdout.decode("utf8", errors="replace")