From adc691886dee61a31ac2809e09771bd0ade35309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Tue, 16 Dec 2025 17:51:34 +0100 Subject: [PATCH 1/8] If constrains are set during refinement the postprocess step now uses xtb with orca as a driver to set the same constrains during postprocess. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- src/mindlessgen/generator/main.py | 2 +- src/mindlessgen/qm/orca.py | 219 ++++++++++++++++++++++++++++-- 2 files changed, 206 insertions(+), 15 deletions(-) diff --git a/src/mindlessgen/generator/main.py b/src/mindlessgen/generator/main.py index 7087ce0..dcfc571 100644 --- a/src/mindlessgen/generator/main.py +++ b/src/mindlessgen/generator/main.py @@ -460,7 +460,7 @@ def setup_engines( raise ImportError("orca not found.") except ImportError as e: raise ImportError("orca not found.") from e - return ORCA(path, cfg.orca) + return ORCA(path, cfg.orca, cfg.xtb) elif engine_type == "turbomole": try: jobex_path = jobex_path_func(cfg.turbomole.jobex_path) diff --git a/src/mindlessgen/qm/orca.py b/src/mindlessgen/qm/orca.py index 0516ced..7fa08a5 100644 --- a/src/mindlessgen/qm/orca.py +++ b/src/mindlessgen/qm/orca.py @@ -2,14 +2,16 @@ This module handles all ORCA-related functionality. """ +from collections import defaultdict from pathlib import Path import shutil import subprocess as sp from tempfile import TemporaryDirectory from ..molecules import Molecule -from ..prog import ORCAConfig +from ..prog import DistanceConstraint, ORCAConfig, XTBConfig from .base import QMMethod +from .xtb import get_xtb_path class ORCA(QMMethod): @@ -17,7 +19,9 @@ class ORCA(QMMethod): This class handles all interaction with the ORCA external dependency. """ - def __init__(self, path: str | Path, orcacfg: ORCAConfig) -> None: + def __init__( + self, path: str | Path, orcacfg: ORCAConfig, xtb_config: XTBConfig | None = None + ) -> None: """ Initialize the ORCA class. """ @@ -28,6 +32,7 @@ def __init__(self, path: str | Path, orcacfg: ORCAConfig) -> None: else: raise TypeError("orca_path should be a string or a Path object.") self.cfg = orcacfg + self.xtb_cfg = xtb_config # must be explicitly initialized in current parallelization implementation # as accessing parent class variables might not be possible self.tmp_dir = self.__class__.get_temporary_directory() @@ -51,11 +56,22 @@ def optimize( # NOTE: "prefix" and "dir" are valid keyword arguments for TemporaryDirectory temp_path = Path(temp_dir).resolve() # write the molecule to a temporary file - molecule.write_xyz_to_file(temp_path / "molecule.xyz") + xyz_filename = "molecule.xyz" + molecule.write_xyz_to_file(temp_path / xyz_filename) inputname = "orca_opt.inp" + use_xtb_driver = self._should_use_xtb_driver() + xtb_input = temp_path / "xtb.inp" + if use_xtb_driver: + self._write_xtb_input(molecule, xtb_input, inputname) orca_input = self._gen_input( - molecule, "molecule.xyz", ncores, True, max_cycles + molecule, + xyz_filename, + temp_path, + ncores, + True, + max_cycles, + use_xtb_driver=use_xtb_driver, ) if verbosity > 1: print("ORCA input file:\n##################") @@ -65,13 +81,20 @@ def optimize( f.write(orca_input) # run orca - arguments = [ - inputname, - ] - - orca_log_out, orca_log_err, return_code = self._run( - temp_path=temp_path, arguments=arguments - ) + if use_xtb_driver: + orca_log_out, orca_log_err, return_code = self._run_xtb_driver( + temp_path=temp_path, + geometry_filename=xyz_filename, + xcontrol_name=xtb_input.name, + ncores=ncores, + ) + else: + arguments = [ + inputname, + ] + orca_log_out, orca_log_err, return_code = self._run( + temp_path=temp_path, arguments=arguments + ) if verbosity > 2: print(orca_log_out) if return_code != 0: @@ -80,7 +103,14 @@ def optimize( ) # read the optimized molecule from the output file - xyzfile = Path(temp_path / inputname).resolve().with_suffix(".xyz") + if use_xtb_driver: + xyzfile = temp_path / "xtbopt.xyz" + if not xyzfile.exists(): + raise RuntimeError( + "xTB-driven ORCA optimization did not produce 'xtbopt.xyz'." + ) + else: + xyzfile = Path(temp_path / inputname).resolve().with_suffix(".xyz") optimized_molecule = molecule.copy() optimized_molecule.read_xyz_from_file(xyzfile) return optimized_molecule @@ -103,10 +133,10 @@ def singlepoint(self, molecule: Molecule, ncores: int, verbosity: int = 1) -> st # write the input file inputname = "orca.inp" - orca_input = self._gen_input(molecule, molfile, ncores) + orca_input = self._gen_input(molecule, molfile, temp_path, ncores) if verbosity > 1: print("ORCA input file:\n##################") - print(self._gen_input(molecule, molfile, ncores)) + print(self._gen_input(molecule, molfile, temp_path, ncores)) print("##################") with open(temp_path / inputname, "w", encoding="utf8") as f: f.write(orca_input) @@ -170,13 +200,172 @@ def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]: orca_log_err = e.stderr.decode("utf8", errors="replace") return orca_log_out, orca_log_err, e.returncode + def _run_xtb_driver( + self, + temp_path: Path, + geometry_filename: str, + xcontrol_name: str, + ncores: int, + ) -> tuple[str, str, int]: + """ + Run the optimization through the xTB external driver when constraints are requested. + """ + xtb_executable = self._get_xtb_executable() + arguments = [ + str(xtb_executable), + geometry_filename, + "--opt", + ] + opt_level = getattr(self.cfg, "optlevel", None) + if opt_level not in (None, ""): + arguments.append(str(opt_level)) + arguments.extend(["--orca", "-I", xcontrol_name]) + try: + xtb_out = sp.run( + arguments, + cwd=temp_path, + capture_output=True, + check=True, + ) + xtb_log_out = xtb_out.stdout.decode("utf8", errors="replace") + xtb_log_err = xtb_out.stderr.decode("utf8", errors="replace") + return xtb_log_out, xtb_log_err, 0 + except sp.CalledProcessError as e: + xtb_log_out = e.stdout.decode("utf8", errors="replace") + xtb_log_err = e.stderr.decode("utf8", errors="replace") + return xtb_log_out, xtb_log_err, e.returncode + + def _get_xtb_executable(self) -> Path: + """ + Determine the path to the xTB executable for external ORCA optimizations. + """ + for attr_name in ("xtb_driver_path", "xtb_path"): + candidate = getattr(self.cfg, attr_name, None) + if candidate: + try: + return get_xtb_path(candidate) + except ImportError as exc: + raise RuntimeError( + f"xTB executable defined via '{attr_name}' could not be found." + ) from exc + try: + return get_xtb_path(None) + except ImportError as exc: + raise RuntimeError( + "xTB executable not found. Required for constrained ORCA optimizations." + ) from exc + + def _should_use_xtb_driver(self) -> bool: + """ + Determine if the xTB external driver should be used (constraints configured). + """ + return bool(self.xtb_cfg and self.xtb_cfg.distance_constraints) + + def _write_xtb_input( + self, molecule: Molecule, xtb_input: Path, input_file: str + ) -> None: + """ + Write the xcontrol file containing constraints and ORCA driver info. + """ + if not self.xtb_cfg: + raise RuntimeError( + "xTB configuration missing but constraints were requested." + ) + constraint_lines = self._prepare_distance_constraint_section(molecule) + lines: list[str] = [] + if constraint_lines: + lines.append("$constrain") + if self.xtb_cfg.distance_constraint_force_constant is not None: + lines.append( + f" force constant= {self.xtb_cfg.distance_constraint_force_constant}" + ) + lines.extend(constraint_lines) + lines.append("$end") + lines.append("$external") + lines.append(f" orca input file= {input_file}") + lines.append(f" orca bin= {self.path}") + lines.append("$end") + xtb_input.write_text("\n".join(lines) + "\n", encoding="utf8") + + def _prepare_distance_constraint_section(self, molecule: Molecule) -> list[str]: + """ + Convert configured distance constraints to xcontrol instructions. + """ + if not self.xtb_cfg or not self.xtb_cfg.distance_constraints: + return [] + element_map: defaultdict[int, list[int]] = defaultdict(list) + for idx, atomic_number in enumerate(molecule.ati): + element_map[int(atomic_number)].append(idx) + constraint_lines: list[str] = [] + for constraint in self.xtb_cfg.distance_constraints: + self._ensure_constraint_atoms_present(element_map, constraint) + pairs = self._generate_constraint_pairs(element_map, constraint) + if not pairs: + raise RuntimeError( + f"No atom pairs found for distance constraint {constraint}." + ) + for first, second in pairs: + constraint_lines.append( + f" distance: {first + 1}, {second + 1}, {constraint.distance:.5f}" + ) + return constraint_lines + + @staticmethod + def _generate_constraint_pairs( + element_map: dict[int, list[int]], constraint: DistanceConstraint + ) -> list[tuple[int, int]]: + """ + Generate index pairs for the provided constraint. + """ + atom_a, atom_b = constraint.atomic_numbers + atom_a_idx = atom_a - 1 + atom_b_idx = atom_b - 1 + indices_a = element_map.get(atom_a_idx, []) + indices_b = element_map.get(atom_b_idx, []) + + if atom_a == atom_b: + if len(indices_a) < 2: + return [] + first, second = sorted(indices_a[:2]) + return [(first, second)] + + if not indices_a or not indices_b: + return [] + + first, second = indices_a[0], indices_b[0] + if first == second: + return [] + if first > second: + first, second = second, first + return [(first, second)] + + @staticmethod + def _ensure_constraint_atoms_present( + element_map: dict[int, list[int]], constraint: DistanceConstraint + ) -> None: + """ + Validate that the molecule contains enough atoms for the constraint. + """ + for atomic_number, required in constraint.required_counts().items(): + idx = atomic_number - 1 + available = len(element_map.get(idx, [])) + if available < required: + symbol = constraint.symbol_for(atomic_number) + raise RuntimeError( + f"Distance constraint {constraint} requires at least " + f"{required} atom(s) of {symbol}, but only {available} present." + ) + def _gen_input( self, molecule: Molecule, xyzfile: str, + temp_path: Path, ncores: int, optimization: bool = False, opt_cycles: int | None = None, + *, + use_xtb_driver: bool = False, ) -> str: """ Generate a default input file for ORCA. @@ -185,6 +374,8 @@ def _gen_input( orca_input += f"! DEFGRID{self.cfg.gridsize}\n" orca_input += "! MiniPrint\n" orca_input += "! NoTRAH\n" + if use_xtb_driver: + orca_input += "! Engrad\n" # "! AutoAux" keyword for super-heavy elements as def2/J ends at Rn if any(atom >= 86 for atom in molecule.ati): orca_input += "! AutoAux\n" From aa7c1cacffd5101a7ff74347d57c6b0e4e7abea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Tue, 16 Dec 2025 18:08:59 +0100 Subject: [PATCH 2/8] Implemented tests for the orca as a driver run when distance constrains are enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- test/test_qm/test_orca.py | 135 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 test/test_qm/test_orca.py diff --git a/test/test_qm/test_orca.py b/test/test_qm/test_orca.py new file mode 100644 index 0000000..f5c1db8 --- /dev/null +++ b/test/test_qm/test_orca.py @@ -0,0 +1,135 @@ +import subprocess as sp +from pathlib import Path +from types import SimpleNamespace +import pytest +from mindlessgen.qm.orca import ORCA + + +class DummyORCAConfig(SimpleNamespace): + def __init__(self, **kwargs): + defaults = dict( + functional="B3LYP", + basis="def2-SVP", + gridsize=2, + scf_cycles=50, + optlevel="", + xtb_driver_path=None, + xtb_path=None, + ) + defaults.update(kwargs) + super().__init__(**defaults) + + +class DummyXTBConfig(SimpleNamespace): + def __init__(self, **kwargs): + defaults = dict( + distance_constraints=None, distance_constraint_force_constant=None + ) + defaults.update(kwargs) + super().__init__(**defaults) + + +class DummyMolecule: + ati = [1, 1, 6, 8] + + +def make_orca(cfg=None, xtb_cfg=None): + cfg = cfg or DummyORCAConfig() + return ORCA(path="/usr/bin/orca", orcacfg=cfg, xtb_config=xtb_cfg) + + +def test_run_xtb_driver_success(monkeypatch, tmp_path): + orca = make_orca(cfg=DummyORCAConfig(optlevel="tight")) + monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) + captured = {} + + def fake_run(args, cwd, capture_output, check): + captured["args"] = args + assert cwd == tmp_path + assert capture_output and check + return SimpleNamespace(stdout=b"ok", stderr=b"") + + monkeypatch.setattr(sp, "run", fake_run) + out, err, code = orca._run_xtb_driver(tmp_path, "geom.xyz", "ctrl.inp", ncores=4) + assert captured["args"] == [ + "/fake/xtb", + "geom.xyz", + "--opt", + "tight", + "--orca", + "-I", + "ctrl.inp", + ] + assert out == "ok" + assert err == "" + assert code == 0 + + +def test_run_xtb_driver_failure_returns_error(monkeypatch, tmp_path): + orca = make_orca() + monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) + + def fake_run(*_, **__): + raise sp.CalledProcessError(1, "xtb", output=b"bad", stderr=b"worse") + + monkeypatch.setattr(sp, "run", fake_run) + out, err, code = orca._run_xtb_driver( # pylint: disable=protected-access + tmp_path, "geom.xyz", "ctrl.inp", ncores=1 + ) + assert (out, err, code) == ("bad", "worse", 1) + + +def test_get_xtb_executable_prefers_configured_path(monkeypatch): + cfg = DummyORCAConfig(xtb_driver_path="custom_xtb") + orca = make_orca(cfg=cfg) + called = {} + + def fake_get_xtb_path(candidate): + called["candidate"] = candidate + return Path("/resolved/xtb") + + monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", fake_get_xtb_path) + assert orca._get_xtb_executable() == Path("/resolved/xtb") + assert called["candidate"] == "custom_xtb" + + +def test_get_xtb_executable_raises_when_missing(monkeypatch): + orca = make_orca() + + def fake_get_xtb_path(candidate): + raise ImportError("not found") + + monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", fake_get_xtb_path) + with pytest.raises(RuntimeError, match="xTB executable not found"): + orca._get_xtb_executable() + + +def test_should_use_xtb_driver_checks_distance_constraints(): + orca = make_orca(xtb_cfg=DummyXTBConfig(distance_constraints=[object()])) + assert orca._should_use_xtb_driver() is True + orca_no_constraints = make_orca(xtb_cfg=DummyXTBConfig(distance_constraints=[])) + assert orca_no_constraints._should_use_xtb_driver() is False + + +def test_write_xtb_input_creates_expected_file(monkeypatch, tmp_path): + xtb_cfg = DummyXTBConfig( + distance_constraints=["dummy"], distance_constraint_force_constant=0.7 + ) + orca = make_orca(xtb_cfg=xtb_cfg) + monkeypatch.setattr( + ORCA, + "_prepare_distance_constraint_section", + lambda self, mol: [" distance: 1, 2, 1.00000"], + ) + target = tmp_path / "xtb.inp" + orca._write_xtb_input(DummyMolecule(), target, "orca.inp") + content = target.read_text().splitlines() + assert content[:4] == [ + "$constrain", + " force constant= 0.7", + " distance: 1, 2, 1.00000", + "$end", + ] + assert "$external" in content + assert " orca input file= orca.inp" in content + assert f" orca bin= {orca.path}" in content From bc2b7bde6ddc1f3859d6d094f0781477d73da259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Tue, 16 Dec 2025 18:36:42 +0100 Subject: [PATCH 3/8] Fix for the windows python crash in testsuit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- test/test_qm/test_orca.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_qm/test_orca.py b/test/test_qm/test_orca.py index f5c1db8..a0878f3 100644 --- a/test/test_qm/test_orca.py +++ b/test/test_qm/test_orca.py @@ -66,10 +66,12 @@ def fake_run(args, cwd, capture_output, check): def test_run_xtb_driver_failure_returns_error(monkeypatch, tmp_path): + """Ensure the ORCA wrapper surfaces errors from the xTB driver.""" orca = make_orca() monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) - def fake_run(*_, **__): + def fake_run(*_, **kwargs): + del kwargs raise sp.CalledProcessError(1, "xtb", output=b"bad", stderr=b"worse") monkeypatch.setattr(sp, "run", fake_run) From 20e96d52bc0f53a1d16d696da5fb0ee2d841d226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Tue, 16 Dec 2025 18:43:17 +0100 Subject: [PATCH 4/8] Try to fix for the windows python crash in testsuit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- test/test_qm/test_orca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_qm/test_orca.py b/test/test_qm/test_orca.py index a0878f3..d2b457f 100644 --- a/test/test_qm/test_orca.py +++ b/test/test_qm/test_orca.py @@ -52,7 +52,7 @@ def fake_run(args, cwd, capture_output, check): monkeypatch.setattr(sp, "run", fake_run) out, err, code = orca._run_xtb_driver(tmp_path, "geom.xyz", "ctrl.inp", ncores=4) assert captured["args"] == [ - "/fake/xtb", + str(Path("/fake/xtb")), "geom.xyz", "--opt", "tight", From 613265550431738bf50a138949251f976c10536b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Wed, 17 Dec 2025 18:12:18 +0100 Subject: [PATCH 5/8] Removal of copied code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- src/mindlessgen/qm/orca.py | 103 ++++++------------------------------- test/test_qm/test_orca.py | 26 ++++++++-- 2 files changed, 37 insertions(+), 92 deletions(-) diff --git a/src/mindlessgen/qm/orca.py b/src/mindlessgen/qm/orca.py index 7fa08a5..6cafd89 100644 --- a/src/mindlessgen/qm/orca.py +++ b/src/mindlessgen/qm/orca.py @@ -2,16 +2,15 @@ This module handles all ORCA-related functionality. """ -from collections import defaultdict from pathlib import Path import shutil import subprocess as sp from tempfile import TemporaryDirectory from ..molecules import Molecule -from ..prog import DistanceConstraint, ORCAConfig, XTBConfig +from ..prog import ORCAConfig, XTBConfig from .base import QMMethod -from .xtb import get_xtb_path +from .xtb import XTB, get_xtb_path class ORCA(QMMethod): @@ -271,90 +270,20 @@ def _write_xtb_input( raise RuntimeError( "xTB configuration missing but constraints were requested." ) - constraint_lines = self._prepare_distance_constraint_section(molecule) - lines: list[str] = [] - if constraint_lines: - lines.append("$constrain") - if self.xtb_cfg.distance_constraint_force_constant is not None: - lines.append( - f" force constant= {self.xtb_cfg.distance_constraint_force_constant}" - ) - lines.extend(constraint_lines) - lines.append("$end") - lines.append("$external") - lines.append(f" orca input file= {input_file}") - lines.append(f" orca bin= {self.path}") - lines.append("$end") - xtb_input.write_text("\n".join(lines) + "\n", encoding="utf8") - - def _prepare_distance_constraint_section(self, molecule: Molecule) -> list[str]: - """ - Convert configured distance constraints to xcontrol instructions. - """ - if not self.xtb_cfg or not self.xtb_cfg.distance_constraints: - return [] - element_map: defaultdict[int, list[int]] = defaultdict(list) - for idx, atomic_number in enumerate(molecule.ati): - element_map[int(atomic_number)].append(idx) - constraint_lines: list[str] = [] - for constraint in self.xtb_cfg.distance_constraints: - self._ensure_constraint_atoms_present(element_map, constraint) - pairs = self._generate_constraint_pairs(element_map, constraint) - if not pairs: - raise RuntimeError( - f"No atom pairs found for distance constraint {constraint}." - ) - for first, second in pairs: - constraint_lines.append( - f" distance: {first + 1}, {second + 1}, {constraint.distance:.5f}" - ) - return constraint_lines - - @staticmethod - def _generate_constraint_pairs( - element_map: dict[int, list[int]], constraint: DistanceConstraint - ) -> list[tuple[int, int]]: - """ - Generate index pairs for the provided constraint. - """ - atom_a, atom_b = constraint.atomic_numbers - atom_a_idx = atom_a - 1 - atom_b_idx = atom_b - 1 - indices_a = element_map.get(atom_a_idx, []) - indices_b = element_map.get(atom_b_idx, []) - - if atom_a == atom_b: - if len(indices_a) < 2: - return [] - first, second = sorted(indices_a[:2]) - return [(first, second)] - - if not indices_a or not indices_b: - return [] - - first, second = indices_a[0], indices_b[0] - if first == second: - return [] - if first > second: - first, second = second, first - return [(first, second)] - - @staticmethod - def _ensure_constraint_atoms_present( - element_map: dict[int, list[int]], constraint: DistanceConstraint - ) -> None: - """ - Validate that the molecule contains enough atoms for the constraint. - """ - for atomic_number, required in constraint.required_counts().items(): - idx = atomic_number - 1 - available = len(element_map.get(idx, [])) - if available < required: - symbol = constraint.symbol_for(atomic_number) - raise RuntimeError( - f"Distance constraint {constraint} requires at least " - f"{required} atom(s) of {symbol}, but only {available} present." - ) + xtb_path = self._get_xtb_executable() + xtb_writer = XTB(xtb_path, self.xtb_cfg) + generated = xtb_writer._prepare_distance_constraint_file( + molecule, xtb_input.parent + ) + if not generated: + raise RuntimeError( + "xTB driver requested but no distance constraints were generated." + ) + with xtb_input.open("a", encoding="utf8") as handle: + handle.write("$external\n") + handle.write(f" orca input file= {input_file}\n") + handle.write(f" orca bin= {self.path}\n") + handle.write("$end\n") def _gen_input( self, diff --git a/test/test_qm/test_orca.py b/test/test_qm/test_orca.py index d2b457f..ca76536 100644 --- a/test/test_qm/test_orca.py +++ b/test/test_qm/test_orca.py @@ -118,18 +118,34 @@ def test_write_xtb_input_creates_expected_file(monkeypatch, tmp_path): distance_constraints=["dummy"], distance_constraint_force_constant=0.7 ) orca = make_orca(xtb_cfg=xtb_cfg) + monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) + + def fake_prepare(self, molecule, temp_dir): + assert temp_dir == tmp_path + (temp_dir / "xtb.inp").write_text( + "\n".join( + [ + "$constrain", + " force constant= 0.7", + " distance: 1, 2, 1.00000", + "$end", + "", + ] + ), + encoding="utf8", + ) + return True + monkeypatch.setattr( - ORCA, - "_prepare_distance_constraint_section", - lambda self, mol: [" distance: 1, 2, 1.00000"], + "mindlessgen.qm.orca.XTB._prepare_distance_constraint_file", fake_prepare ) target = tmp_path / "xtb.inp" orca._write_xtb_input(DummyMolecule(), target, "orca.inp") content = target.read_text().splitlines() assert content[:4] == [ "$constrain", - " force constant= 0.7", - " distance: 1, 2, 1.00000", + " force constant= 0.7", + " distance: 1, 2, 1.00000", "$end", ] assert "$external" in content From f29fe5c0be1879ef1cadac5c3f89fca04f84fb50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Tue, 23 Dec 2025 16:00:58 +0100 Subject: [PATCH 6/8] Implementation of comments from the Pull request, less complicated optimize in orca.py 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 | 2 + src/mindlessgen/prog/config.py | 17 +++ src/mindlessgen/qm/orca.py | 197 +++++++++++++++++++++++---------- test/test_qm/test_orca.py | 64 +++++++---- 5 files changed, 198 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f7308..805df4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Distance constraints for xTB (normalized parsing, config validation, runtime enforcement, and optional integration test). +- Distance constrains for ORCA (uses xTB as as driver to set the same distance constrains for the postprocess). ## [0.6.0] - 2025-04-01 diff --git a/mindlessgen.toml b/mindlessgen.toml index 217cbe4..2d4ab68 100644 --- a/mindlessgen.toml +++ b/mindlessgen.toml @@ -106,6 +106,8 @@ basis = "def2-SVP" gridsize = 1 # > Maximum number of SCF cycles: Options: scf_cycles = 100 +# > Use xTB as an external ORCA driver to keep distance constraints during postprocessing. Options: +use_xtb_driver = false [turbomole] # > Path to the ridft executable. The name `ridft` is automatically searched for. Options: diff --git a/src/mindlessgen/prog/config.py b/src/mindlessgen/prog/config.py index efb49fb..e01590d 100644 --- a/src/mindlessgen/prog/config.py +++ b/src/mindlessgen/prog/config.py @@ -1135,6 +1135,7 @@ def __init__(self: ORCAConfig) -> None: self._basis: str = "def2-SVP" self._gridsize: int = 1 self._scf_cycles: int = 100 + self._use_xtb_driver: bool = False def get_identifier(self) -> str: return "orca" @@ -1225,6 +1226,22 @@ def scf_cycles(self, max_scf_cycles: int): raise ValueError("Max SCF cycles should be greater than 0.") self._scf_cycles = max_scf_cycles + @property + def use_xtb_driver(self) -> bool: + """ + Determine whether the xTB external driver can be used during post-processing. + """ + return self._use_xtb_driver + + @use_xtb_driver.setter + def use_xtb_driver(self, enabled: bool): + """ + Enable or disable the usage of the xTB external driver during post-processing. + """ + if not isinstance(enabled, bool): + raise TypeError("use_xtb_driver should be a boolean.") + self._use_xtb_driver = enabled + class TURBOMOLEConfig(BaseConfig): """ diff --git a/src/mindlessgen/qm/orca.py b/src/mindlessgen/qm/orca.py index 6cafd89..6f39c30 100644 --- a/src/mindlessgen/qm/orca.py +++ b/src/mindlessgen/qm/orca.py @@ -32,6 +32,9 @@ def __init__( raise TypeError("orca_path should be a string or a Path object.") self.cfg = orcacfg self.xtb_cfg = xtb_config + self.xtb_driver_enabled = bool(xtb_config) and bool( + getattr(self.cfg, "use_xtb_driver", False) + ) # must be explicitly initialized in current parallelization implementation # as accessing parent class variables might not be possible self.tmp_dir = self.__class__.get_temporary_directory() @@ -58,60 +61,48 @@ def optimize( xyz_filename = "molecule.xyz" molecule.write_xyz_to_file(temp_path / xyz_filename) - inputname = "orca_opt.inp" - use_xtb_driver = self._should_use_xtb_driver() - xtb_input = temp_path / "xtb.inp" - if use_xtb_driver: - self._write_xtb_input(molecule, xtb_input, inputname) - orca_input = self._gen_input( - molecule, - xyz_filename, - temp_path, - ncores, - True, - max_cycles, - use_xtb_driver=use_xtb_driver, - ) - if verbosity > 1: - print("ORCA input file:\n##################") - print(orca_input) - print("##################") - with open(temp_path / inputname, "w", encoding="utf8") as f: - f.write(orca_input) - - # run orca - if use_xtb_driver: - orca_log_out, orca_log_err, return_code = self._run_xtb_driver( + if self._should_use_xtb_driver(): + optimized_molecule = self.optimize_xtb_driver( temp_path=temp_path, - geometry_filename=xyz_filename, - xcontrol_name=xtb_input.name, + molecule=molecule, + xyz_filename=xyz_filename, ncores=ncores, + max_cycles=max_cycles, + verbosity=verbosity, ) else: + inputname = "orca_opt.inp" + orca_input = self._gen_input( + molecule, + xyz_filename, + temp_path, + ncores, + True, + max_cycles, + ) + if verbosity > 1: + print("ORCA input file:\n##################") + print(orca_input) + print("##################") + with open(temp_path / inputname, "w", encoding="utf8") as f: + f.write(orca_input) + # run orca arguments = [ inputname, ] orca_log_out, orca_log_err, return_code = self._run( temp_path=temp_path, arguments=arguments ) - if verbosity > 2: - print(orca_log_out) - if return_code != 0: - raise RuntimeError( - f"ORCA failed with return code {return_code}:\n{orca_log_err}" - ) - - # read the optimized molecule from the output file - if use_xtb_driver: - xyzfile = temp_path / "xtbopt.xyz" - if not xyzfile.exists(): + if verbosity > 2: + print(orca_log_out) + if return_code != 0: raise RuntimeError( - "xTB-driven ORCA optimization did not produce 'xtbopt.xyz'." + f"ORCA failed with return code {return_code}:\n{orca_log_err}" ) - else: + # 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) + optimized_molecule = molecule.copy() + optimized_molecule.read_xyz_from_file(xyzfile) return optimized_molecule def singlepoint(self, molecule: Molecule, ncores: int, verbosity: int = 1) -> str: @@ -199,6 +190,102 @@ def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]: orca_log_err = e.stderr.decode("utf8", errors="replace") return orca_log_out, orca_log_err, e.returncode + def _gen_input( + self, + molecule: Molecule, + xyzfile: str, + _temp_path: Path, + ncores: int, + optimization: bool = False, + opt_cycles: int | None = None, + ) -> str: + """ + Generate a default input file for ORCA. + """ + orca_input = f"! {self.cfg.functional} {self.cfg.basis}\n" + orca_input += f"! DEFGRID{self.cfg.gridsize}\n" + orca_input += "! MiniPrint\n" + orca_input += "! NoTRAH\n" + # "! AutoAux" keyword for super-heavy elements as def2/J ends at Rn + if any(atom >= 86 for atom in molecule.ati): + orca_input += "! AutoAux\n" + if optimization: + orca_input += "! OPT\n" + if opt_cycles is not None: + orca_input += f"%geom MaxIter {opt_cycles} end\n" + orca_input += f"%scf\n\tMaxIter {self.cfg.scf_cycles}\n" + if not optimization: + orca_input += "\tConvergence Medium\n" + orca_input += "end\n" + orca_input += f"%pal nprocs {ncores} end\n\n" + orca_input += f"* xyzfile {molecule.charge} {molecule.uhf + 1} {xyzfile}\n" + return orca_input + + def _should_use_xtb_driver(self) -> bool: + """ + Determine whether the xTB driver should be used for this optimization. + """ + if not self.xtb_driver_enabled or not self.xtb_cfg: + return False + if hasattr(self.xtb_cfg, "has_constraints"): + return self.xtb_cfg.has_constraints() + constraints = getattr(self.xtb_cfg, "distance_constraints", None) + return bool(constraints) + + def optimize_xtb_driver( + self, + temp_path: Path, + molecule: Molecule, + xyz_filename: str, + ncores: int, + max_cycles: int | None = None, + verbosity: int = 1, + ) -> Molecule: + """ + Optimize a molecule using ORCA through the xTB external driver. + """ + + xtb_input = temp_path / "xtb.inp" + inputname = "orca_opt.inp" + self._write_xtb_input(molecule, xtb_input, inputname) + orca_input = self._gen_input_xtb_driver( + molecule, + xyz_filename, + temp_path, + ncores, + True, + max_cycles, + ) + if verbosity > 1: + print("ORCA input file:\n##################") + print(orca_input) + print("##################") + with open(temp_path / inputname, "w", encoding="utf8") as f: + f.write(orca_input) + # run orca with xTB as a driver + orca_log_out, orca_log_err, return_code = self._run_xtb_driver( + temp_path=temp_path, + geometry_filename=xyz_filename, + xcontrol_name=xtb_input.name, + ncores=ncores, + ) + if verbosity > 2: + print(orca_log_out) + if return_code != 0: + raise RuntimeError( + f"ORCA failed with return code {return_code}:\n{orca_log_err}" + ) + + # read the optimized molecule from the output file + xyzfile = temp_path / "xtbopt.xyz" + if not xyzfile.exists(): + raise RuntimeError( + "xTB-driven ORCA optimization did not produce 'xtbopt.xyz'." + ) + optimized_molecule = molecule.copy() + optimized_molecule.read_xyz_from_file(xyzfile) + return optimized_molecule + def _run_xtb_driver( self, temp_path: Path, @@ -238,9 +325,14 @@ def _get_xtb_executable(self) -> Path: """ Determine the path to the xTB executable for external ORCA optimizations. """ - for attr_name in ("xtb_driver_path", "xtb_path"): - candidate = getattr(self.cfg, attr_name, None) - if candidate: + candidates: list[ORCAConfig | XTBConfig | None] = [self.xtb_cfg, self.cfg] + for source in candidates: + if source is None: + continue + for attr_name in ("xtb_path",): + candidate = getattr(source, attr_name, None) + if not candidate: + continue try: return get_xtb_path(candidate) except ImportError as exc: @@ -254,12 +346,6 @@ def _get_xtb_executable(self) -> Path: "xTB executable not found. Required for constrained ORCA optimizations." ) from exc - def _should_use_xtb_driver(self) -> bool: - """ - Determine if the xTB external driver should be used (constraints configured). - """ - return bool(self.xtb_cfg and self.xtb_cfg.distance_constraints) - def _write_xtb_input( self, molecule: Molecule, xtb_input: Path, input_file: str ) -> None: @@ -285,7 +371,7 @@ def _write_xtb_input( handle.write(f" orca bin= {self.path}\n") handle.write("$end\n") - def _gen_input( + def _gen_input_xtb_driver( self, molecule: Molecule, xyzfile: str, @@ -293,8 +379,6 @@ def _gen_input( ncores: int, optimization: bool = False, opt_cycles: int | None = None, - *, - use_xtb_driver: bool = False, ) -> str: """ Generate a default input file for ORCA. @@ -303,15 +387,10 @@ def _gen_input( orca_input += f"! DEFGRID{self.cfg.gridsize}\n" orca_input += "! MiniPrint\n" orca_input += "! NoTRAH\n" - if use_xtb_driver: - orca_input += "! Engrad\n" + orca_input += "! Engrad\n" # "! AutoAux" keyword for super-heavy elements as def2/J ends at Rn if any(atom >= 86 for atom in molecule.ati): orca_input += "! AutoAux\n" - if optimization: - orca_input += "! OPT\n" - if opt_cycles is not None: - orca_input += f"%geom MaxIter {opt_cycles} end\n" orca_input += f"%scf\n\tMaxIter {self.cfg.scf_cycles}\n" if not optimization: orca_input += "\tConvergence Medium\n" diff --git a/test/test_qm/test_orca.py b/test/test_qm/test_orca.py index ca76536..a559882 100644 --- a/test/test_qm/test_orca.py +++ b/test/test_qm/test_orca.py @@ -15,6 +15,7 @@ def __init__(self, **kwargs): optlevel="", xtb_driver_path=None, xtb_path=None, + use_xtb_driver=False, ) defaults.update(kwargs) super().__init__(**defaults) @@ -33,12 +34,16 @@ class DummyMolecule: ati = [1, 1, 6, 8] -def make_orca(cfg=None, xtb_cfg=None): - cfg = cfg or DummyORCAConfig() - return ORCA(path="/usr/bin/orca", orcacfg=cfg, xtb_config=xtb_cfg) +@pytest.fixture +def make_orca(): + def _factory(cfg=None, xtb_cfg_param=None): + cfg = cfg or DummyORCAConfig() + return ORCA(path="/usr/bin/orca", orcacfg=cfg, xtb_config=xtb_cfg_param) + return _factory -def test_run_xtb_driver_success(monkeypatch, tmp_path): + +def test_run_xtb_driver_success(monkeypatch, tmp_path, make_orca): orca = make_orca(cfg=DummyORCAConfig(optlevel="tight")) monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) captured = {} @@ -65,7 +70,7 @@ def fake_run(args, cwd, capture_output, check): assert code == 0 -def test_run_xtb_driver_failure_returns_error(monkeypatch, tmp_path): +def test_run_xtb_driver_failure_returns_error(monkeypatch, tmp_path, make_orca): """Ensure the ORCA wrapper surfaces errors from the xTB driver.""" orca = make_orca() monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) @@ -81,43 +86,54 @@ def fake_run(*_, **kwargs): assert (out, err, code) == ("bad", "worse", 1) -def test_get_xtb_executable_prefers_configured_path(monkeypatch): - cfg = DummyORCAConfig(xtb_driver_path="custom_xtb") - orca = make_orca(cfg=cfg) - called = {} +def test_get_xtb_executable_raises_when_missing(monkeypatch, make_orca): + orca = make_orca() def fake_get_xtb_path(candidate): - called["candidate"] = candidate - return Path("/resolved/xtb") + raise ImportError("not found") monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", fake_get_xtb_path) - assert orca._get_xtb_executable() == Path("/resolved/xtb") - assert called["candidate"] == "custom_xtb" + with pytest.raises(RuntimeError, match="xTB executable not found"): + orca._get_xtb_executable() -def test_get_xtb_executable_raises_when_missing(monkeypatch): - orca = make_orca() +def test_get_xtb_executable_prefers_xtb_cfg_path(monkeypatch, make_orca): + xtb_cfg_constraints = DummyXTBConfig() + xtb_cfg_constraints.xtb_path = "xtb_from_xtb_cfg" + orca = make_orca(xtb_cfg_param=xtb_cfg_constraints) + called = {} def fake_get_xtb_path(candidate): - raise ImportError("not found") + called.setdefault("candidates", []).append(candidate) + return Path("/resolved/xtb_cfg") monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", fake_get_xtb_path) - with pytest.raises(RuntimeError, match="xTB executable not found"): - orca._get_xtb_executable() + assert orca._get_xtb_executable() == Path("/resolved/xtb_cfg") + assert called["candidates"][0] == "xtb_from_xtb_cfg" -def test_should_use_xtb_driver_checks_distance_constraints(): - orca = make_orca(xtb_cfg=DummyXTBConfig(distance_constraints=[object()])) +def test_should_use_xtb_driver_checks_distance_constraints(make_orca): + cfg = DummyORCAConfig(use_xtb_driver=True) + xtb_constraints = DummyXTBConfig(distance_constraints=[object()]) + orca = make_orca(cfg=cfg, xtb_cfg_param=xtb_constraints) assert orca._should_use_xtb_driver() is True - orca_no_constraints = make_orca(xtb_cfg=DummyXTBConfig(distance_constraints=[])) + + no_constraints = DummyXTBConfig(distance_constraints=[]) + orca_no_constraints = make_orca(cfg=cfg, xtb_cfg_param=no_constraints) assert orca_no_constraints._should_use_xtb_driver() is False + cfg_disabled = DummyORCAConfig(use_xtb_driver=False) + orca_disabled = make_orca(cfg=cfg_disabled, xtb_cfg_param=xtb_constraints) + assert orca_disabled._should_use_xtb_driver() is False + orca_missing_xtb = make_orca(cfg=cfg, xtb_cfg_param=None) + assert orca_missing_xtb._should_use_xtb_driver() is False + -def test_write_xtb_input_creates_expected_file(monkeypatch, tmp_path): - xtb_cfg = DummyXTBConfig( +def test_write_xtb_input_creates_expected_file(monkeypatch, tmp_path, make_orca): + xtb_cfg_instance = DummyXTBConfig( distance_constraints=["dummy"], distance_constraint_force_constant=0.7 ) - orca = make_orca(xtb_cfg=xtb_cfg) + orca = make_orca(xtb_cfg_param=xtb_cfg_instance) monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) def fake_prepare(self, molecule, temp_dir): From 40ed4f22ea1682f36a961a1194e8a55e1cab2ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Fri, 2 Jan 2026 12:47:08 +0100 Subject: [PATCH 7/8] Implemented requested changes and updated tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- src/mindlessgen/qm/orca.py | 136 +++++++++++++------------------------ test/test_qm/test_orca.py | 57 ++++------------ 2 files changed, 59 insertions(+), 134 deletions(-) diff --git a/src/mindlessgen/qm/orca.py b/src/mindlessgen/qm/orca.py index 6f39c30..041bef0 100644 --- a/src/mindlessgen/qm/orca.py +++ b/src/mindlessgen/qm/orca.py @@ -32,9 +32,6 @@ def __init__( raise TypeError("orca_path should be a string or a Path object.") self.cfg = orcacfg self.xtb_cfg = xtb_config - self.xtb_driver_enabled = bool(xtb_config) and bool( - getattr(self.cfg, "use_xtb_driver", False) - ) # must be explicitly initialized in current parallelization implementation # as accessing parent class variables might not be possible self.tmp_dir = self.__class__.get_temporary_directory() @@ -61,7 +58,7 @@ def optimize( xyz_filename = "molecule.xyz" molecule.write_xyz_to_file(temp_path / xyz_filename) - if self._should_use_xtb_driver(): + if self.cfg.use_xtb_driver: optimized_molecule = self.optimize_xtb_driver( temp_path=temp_path, molecule=molecule, @@ -70,39 +67,39 @@ def optimize( max_cycles=max_cycles, verbosity=verbosity, ) - else: - inputname = "orca_opt.inp" - orca_input = self._gen_input( - molecule, - xyz_filename, - temp_path, - ncores, - True, - max_cycles, - ) - if verbosity > 1: - print("ORCA input file:\n##################") - print(orca_input) - print("##################") - with open(temp_path / inputname, "w", encoding="utf8") as f: - f.write(orca_input) - # run orca - arguments = [ - inputname, - ] - orca_log_out, orca_log_err, return_code = self._run( - temp_path=temp_path, arguments=arguments + return optimized_molecule + inputname = "orca_opt.inp" + orca_input = self._gen_input( + molecule, + xyz_filename, + temp_path, + ncores, + True, + max_cycles, + ) + if verbosity > 1: + print("ORCA input file:\n##################") + print(orca_input) + print("##################") + with open(temp_path / inputname, "w", encoding="utf8") as f: + f.write(orca_input) + # run orca + arguments = [ + inputname, + ] + orca_log_out, orca_log_err, return_code = self._run( + temp_path=temp_path, arguments=arguments + ) + if verbosity > 2: + print(orca_log_out) + if return_code != 0: + raise RuntimeError( + f"ORCA failed with return code {return_code}:\n{orca_log_err}" ) - if verbosity > 2: - print(orca_log_out) - if return_code != 0: - raise RuntimeError( - f"ORCA failed with return code {return_code}:\n{orca_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) + # 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, ncores: int, verbosity: int = 1) -> str: @@ -221,17 +218,6 @@ def _gen_input( orca_input += f"* xyzfile {molecule.charge} {molecule.uhf + 1} {xyzfile}\n" return orca_input - def _should_use_xtb_driver(self) -> bool: - """ - Determine whether the xTB driver should be used for this optimization. - """ - if not self.xtb_driver_enabled or not self.xtb_cfg: - return False - if hasattr(self.xtb_cfg, "has_constraints"): - return self.xtb_cfg.has_constraints() - constraints = getattr(self.xtb_cfg, "distance_constraints", None) - return bool(constraints) - def optimize_xtb_driver( self, temp_path: Path, @@ -260,6 +246,9 @@ def optimize_xtb_driver( print("ORCA input file:\n##################") print(orca_input) print("##################") + print("XTB input file:\n##################") + print(xtb_input) + print("##################") with open(temp_path / inputname, "w", encoding="utf8") as f: f.write(orca_input) # run orca with xTB as a driver @@ -296,9 +285,13 @@ def _run_xtb_driver( """ Run the optimization through the xTB external driver when constraints are requested. """ - xtb_executable = self._get_xtb_executable() + xtb_executable = get_xtb_path() + if self.xtb_cfg is None: + raise RuntimeError( + "xTB driver requested but no xTB configuration provided." + ) + xtb_runner = XTB(path=xtb_executable, xtb_config=self.xtb_cfg) arguments = [ - str(xtb_executable), geometry_filename, "--opt", ] @@ -306,45 +299,10 @@ def _run_xtb_driver( if opt_level not in (None, ""): arguments.append(str(opt_level)) arguments.extend(["--orca", "-I", xcontrol_name]) - try: - xtb_out = sp.run( - arguments, - cwd=temp_path, - capture_output=True, - check=True, - ) - xtb_log_out = xtb_out.stdout.decode("utf8", errors="replace") - xtb_log_err = xtb_out.stderr.decode("utf8", errors="replace") - return xtb_log_out, xtb_log_err, 0 - except sp.CalledProcessError as e: - xtb_log_out = e.stdout.decode("utf8", errors="replace") - xtb_log_err = e.stderr.decode("utf8", errors="replace") - return xtb_log_out, xtb_log_err, e.returncode - - def _get_xtb_executable(self) -> Path: - """ - Determine the path to the xTB executable for external ORCA optimizations. - """ - candidates: list[ORCAConfig | XTBConfig | None] = [self.xtb_cfg, self.cfg] - for source in candidates: - if source is None: - continue - for attr_name in ("xtb_path",): - candidate = getattr(source, attr_name, None) - if not candidate: - continue - try: - return get_xtb_path(candidate) - except ImportError as exc: - raise RuntimeError( - f"xTB executable defined via '{attr_name}' could not be found." - ) from exc - try: - return get_xtb_path(None) - except ImportError as exc: - raise RuntimeError( - "xTB executable not found. Required for constrained ORCA optimizations." - ) from exc + xtb_log_out, xtb_log_err, returncode = xtb_runner._run( + temp_path=temp_path, arguments=arguments + ) + return xtb_log_out, xtb_log_err, returncode def _write_xtb_input( self, molecule: Molecule, xtb_input: Path, input_file: str @@ -356,7 +314,7 @@ def _write_xtb_input( raise RuntimeError( "xTB configuration missing but constraints were requested." ) - xtb_path = self._get_xtb_executable() + xtb_path = get_xtb_path() xtb_writer = XTB(xtb_path, self.xtb_cfg) generated = xtb_writer._prepare_distance_constraint_file( molecule, xtb_input.parent diff --git a/test/test_qm/test_orca.py b/test/test_qm/test_orca.py index a559882..a2cbe99 100644 --- a/test/test_qm/test_orca.py +++ b/test/test_qm/test_orca.py @@ -44,8 +44,11 @@ def _factory(cfg=None, xtb_cfg_param=None): def test_run_xtb_driver_success(monkeypatch, tmp_path, make_orca): - orca = make_orca(cfg=DummyORCAConfig(optlevel="tight")) - monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) + orca = make_orca( + cfg=DummyORCAConfig(optlevel="tight"), + xtb_cfg_param=DummyXTBConfig(), + ) + monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: Path("/fake/xtb")) captured = {} def fake_run(args, cwd, capture_output, check): @@ -72,8 +75,8 @@ def fake_run(args, cwd, capture_output, check): def test_run_xtb_driver_failure_returns_error(monkeypatch, tmp_path, make_orca): """Ensure the ORCA wrapper surfaces errors from the xTB driver.""" - orca = make_orca() - monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) + orca = make_orca(xtb_cfg_param=DummyXTBConfig()) + monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: Path("/fake/xtb")) def fake_run(*_, **kwargs): del kwargs @@ -86,47 +89,11 @@ def fake_run(*_, **kwargs): assert (out, err, code) == ("bad", "worse", 1) -def test_get_xtb_executable_raises_when_missing(monkeypatch, make_orca): +def test_run_xtb_driver_requires_xtb_cfg(monkeypatch, tmp_path, make_orca): orca = make_orca() - - def fake_get_xtb_path(candidate): - raise ImportError("not found") - - monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", fake_get_xtb_path) - with pytest.raises(RuntimeError, match="xTB executable not found"): - orca._get_xtb_executable() - - -def test_get_xtb_executable_prefers_xtb_cfg_path(monkeypatch, make_orca): - xtb_cfg_constraints = DummyXTBConfig() - xtb_cfg_constraints.xtb_path = "xtb_from_xtb_cfg" - orca = make_orca(xtb_cfg_param=xtb_cfg_constraints) - called = {} - - def fake_get_xtb_path(candidate): - called.setdefault("candidates", []).append(candidate) - return Path("/resolved/xtb_cfg") - - monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", fake_get_xtb_path) - assert orca._get_xtb_executable() == Path("/resolved/xtb_cfg") - assert called["candidates"][0] == "xtb_from_xtb_cfg" - - -def test_should_use_xtb_driver_checks_distance_constraints(make_orca): - cfg = DummyORCAConfig(use_xtb_driver=True) - xtb_constraints = DummyXTBConfig(distance_constraints=[object()]) - orca = make_orca(cfg=cfg, xtb_cfg_param=xtb_constraints) - assert orca._should_use_xtb_driver() is True - - no_constraints = DummyXTBConfig(distance_constraints=[]) - orca_no_constraints = make_orca(cfg=cfg, xtb_cfg_param=no_constraints) - assert orca_no_constraints._should_use_xtb_driver() is False - - cfg_disabled = DummyORCAConfig(use_xtb_driver=False) - orca_disabled = make_orca(cfg=cfg_disabled, xtb_cfg_param=xtb_constraints) - assert orca_disabled._should_use_xtb_driver() is False - orca_missing_xtb = make_orca(cfg=cfg, xtb_cfg_param=None) - assert orca_missing_xtb._should_use_xtb_driver() is False + monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: Path("/fake/xtb")) + with pytest.raises(RuntimeError, match="xTB driver requested"): + orca._run_xtb_driver(tmp_path, "geom.xyz", "ctrl.inp", ncores=1) def test_write_xtb_input_creates_expected_file(monkeypatch, tmp_path, make_orca): @@ -134,7 +101,7 @@ def test_write_xtb_input_creates_expected_file(monkeypatch, tmp_path, make_orca) distance_constraints=["dummy"], distance_constraint_force_constant=0.7 ) orca = make_orca(xtb_cfg_param=xtb_cfg_instance) - monkeypatch.setattr(orca, "_get_xtb_executable", lambda: Path("/fake/xtb")) + monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: Path("/fake/xtb")) def fake_prepare(self, molecule, temp_dir): assert temp_dir == tmp_path From 5b5370ed24b96eebdd30851a6d842264261c7847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Sch=C3=B6ps?= Date: Mon, 5 Jan 2026 11:28:47 +0100 Subject: [PATCH 8/8] changed the test_orca.py testsuite to match the test_xtb.py tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonathan Schöps --- test/test_qm/test_orca.py | 168 +++++++++++++++++++++++++++++++------- 1 file changed, 138 insertions(+), 30 deletions(-) diff --git a/test/test_qm/test_orca.py b/test/test_qm/test_orca.py index a2cbe99..4ea1a29 100644 --- a/test/test_qm/test_orca.py +++ b/test/test_qm/test_orca.py @@ -1,7 +1,10 @@ import subprocess as sp from pathlib import Path from types import SimpleNamespace +import numpy as np import pytest +from mindlessgen.molecules import Molecule # type: ignore +from mindlessgen.prog import DistanceConstraint, XTBConfig # type: ignore from mindlessgen.qm.orca import ORCA @@ -21,19 +24,6 @@ def __init__(self, **kwargs): super().__init__(**defaults) -class DummyXTBConfig(SimpleNamespace): - def __init__(self, **kwargs): - defaults = dict( - distance_constraints=None, distance_constraint_force_constant=None - ) - defaults.update(kwargs) - super().__init__(**defaults) - - -class DummyMolecule: - ati = [1, 1, 6, 8] - - @pytest.fixture def make_orca(): def _factory(cfg=None, xtb_cfg_param=None): @@ -43,12 +33,84 @@ def _factory(cfg=None, xtb_cfg_param=None): return _factory -def test_run_xtb_driver_success(monkeypatch, tmp_path, make_orca): +@pytest.fixture +def xtb_cfg_oh(): + """ + xTB config with an O-H distance constraint. + """ + cfg = XTBConfig() + cfg.distance_constraints = [DistanceConstraint.from_cli_string("O,H,1.0")] + cfg.distance_constraint_force_constant = 0.5 + return cfg + + +@pytest.fixture +def xtb_cfg_ff(): + """ + xTB config with an F-F distance constraint. + """ + cfg = XTBConfig() + cfg.distance_constraints = [DistanceConstraint.from_cli_string("F,F,1.0")] + return cfg + + +@pytest.fixture +def xtb_cfg_fe3(): + """ + xTB config with a Fe-Fe distance constraint. + """ + cfg = XTBConfig() + cfg.distance_constraints = [ + DistanceConstraint.from_mapping({"pair": ["Fe", "Fe"], "distance": 2.5}) + ] + return cfg + + +@pytest.fixture +def mol_oh(): + """ + Simple O-H molecule for constraint tests. + """ + mol = Molecule("OH") + mol.ati = np.array([7, 0]) + return mol + + +@pytest.fixture +def mol_h2(): + """ + Simple H2 molecule for constraint tests. + """ + mol = Molecule("H2") + mol.ati = np.array([0, 0]) + return mol + + +@pytest.fixture +def mol_fe3(): + """ + Simple Fe3 molecule for constraint tests. + """ + mol = Molecule("Fe3") + mol.ati = np.array([25, 25, 25]) + return mol + + +@pytest.fixture +def fake_xtb_path(monkeypatch): + """ + Force xTB path discovery to a fake binary. + """ + xtb_path = Path("/fake/xtb") + monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: xtb_path) + return xtb_path + + +def test_run_xtb_driver_success(monkeypatch, tmp_path, make_orca, fake_xtb_path): orca = make_orca( cfg=DummyORCAConfig(optlevel="tight"), - xtb_cfg_param=DummyXTBConfig(), + xtb_cfg_param=XTBConfig(), ) - monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: Path("/fake/xtb")) captured = {} def fake_run(args, cwd, capture_output, check): @@ -57,10 +119,10 @@ def fake_run(args, cwd, capture_output, check): assert capture_output and check return SimpleNamespace(stdout=b"ok", stderr=b"") - monkeypatch.setattr(sp, "run", fake_run) + monkeypatch.setattr("mindlessgen.qm.xtb.sp.run", fake_run) out, err, code = orca._run_xtb_driver(tmp_path, "geom.xyz", "ctrl.inp", ncores=4) assert captured["args"] == [ - str(Path("/fake/xtb")), + str(fake_xtb_path), "geom.xyz", "--opt", "tight", @@ -73,35 +135,38 @@ def fake_run(args, cwd, capture_output, check): assert code == 0 -def test_run_xtb_driver_failure_returns_error(monkeypatch, tmp_path, make_orca): +def test_run_xtb_driver_failure_returns_error( + monkeypatch, tmp_path, make_orca, fake_xtb_path +): """Ensure the ORCA wrapper surfaces errors from the xTB driver.""" - orca = make_orca(xtb_cfg_param=DummyXTBConfig()) - monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: Path("/fake/xtb")) + orca = make_orca(xtb_cfg_param=XTBConfig()) def fake_run(*_, **kwargs): del kwargs raise sp.CalledProcessError(1, "xtb", output=b"bad", stderr=b"worse") - monkeypatch.setattr(sp, "run", fake_run) + monkeypatch.setattr("mindlessgen.qm.xtb.sp.run", fake_run) out, err, code = orca._run_xtb_driver( # pylint: disable=protected-access tmp_path, "geom.xyz", "ctrl.inp", ncores=1 ) assert (out, err, code) == ("bad", "worse", 1) -def test_run_xtb_driver_requires_xtb_cfg(monkeypatch, tmp_path, make_orca): +def test_run_xtb_driver_requires_xtb_cfg( + monkeypatch, tmp_path, make_orca, fake_xtb_path +): orca = make_orca() - monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: Path("/fake/xtb")) with pytest.raises(RuntimeError, match="xTB driver requested"): orca._run_xtb_driver(tmp_path, "geom.xyz", "ctrl.inp", ncores=1) -def test_write_xtb_input_creates_expected_file(monkeypatch, tmp_path, make_orca): - xtb_cfg_instance = DummyXTBConfig( - distance_constraints=["dummy"], distance_constraint_force_constant=0.7 - ) +def test_write_xtb_input_creates_expected_file( + monkeypatch, tmp_path, make_orca, fake_xtb_path, mol_oh +): + xtb_cfg_instance = XTBConfig() + xtb_cfg_instance.distance_constraints = [] + xtb_cfg_instance.distance_constraint_force_constant = 0.7 orca = make_orca(xtb_cfg_param=xtb_cfg_instance) - monkeypatch.setattr("mindlessgen.qm.orca.get_xtb_path", lambda: Path("/fake/xtb")) def fake_prepare(self, molecule, temp_dir): assert temp_dir == tmp_path @@ -123,7 +188,7 @@ def fake_prepare(self, molecule, temp_dir): "mindlessgen.qm.orca.XTB._prepare_distance_constraint_file", fake_prepare ) target = tmp_path / "xtb.inp" - orca._write_xtb_input(DummyMolecule(), target, "orca.inp") + orca._write_xtb_input(mol_oh, target, "orca.inp") content = target.read_text().splitlines() assert content[:4] == [ "$constrain", @@ -134,3 +199,46 @@ def fake_prepare(self, molecule, temp_dir): assert "$external" in content assert " orca input file= orca.inp" in content assert f" orca bin= {orca.path}" in content + + +def test_write_xtb_input_generates_constraints( + fake_xtb_path, tmp_path, make_orca, xtb_cfg_oh, mol_oh +): + orca = make_orca(xtb_cfg_param=xtb_cfg_oh) + + xtb_input = tmp_path / "xtb.inp" + orca._write_xtb_input(mol_oh, xtb_input, "orca.inp") + + contents = xtb_input.read_text(encoding="utf8").splitlines() + assert contents[0] == "$constrain" + assert "force constant= 0.5" in contents[1] + distance_lines = [line for line in contents if line.startswith(" distance:")] + assert distance_lines == [" distance: 1, 2, 1.0"] + assert "$external" in contents + assert " orca input file= orca.inp" in contents + assert f" orca bin= {orca.path}" in contents + + +def test_write_xtb_input_missing_atoms( + fake_xtb_path, tmp_path, make_orca, xtb_cfg_ff, mol_h2 +): + orca = make_orca(xtb_cfg_param=xtb_cfg_ff) + + with pytest.raises(RuntimeError): + orca._write_xtb_input(mol_h2, tmp_path / "xtb.inp", "orca.inp") + + +def test_distance_constraints_use_first_atoms( + fake_xtb_path, tmp_path, make_orca, xtb_cfg_fe3, mol_fe3 +): + orca = make_orca(xtb_cfg_param=xtb_cfg_fe3) + + xtb_input = tmp_path / "xtb.inp" + orca._write_xtb_input(mol_fe3, xtb_input, "orca.inp") + + contents = xtb_input.read_text(encoding="utf8").splitlines() + distance_lines = [line for line in contents if "distance:" in line] + + assert len(distance_lines) == 1 + assert "1, 2" in distance_lines[0] + assert ", 3," not in distance_lines[0]