Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions mindlessgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ basis = "def2-SVP"
gridsize = 1
# > Maximum number of SCF cycles: Options: <int>
scf_cycles = 100
# > Use xTB as an external ORCA driver to keep distance constraints during postprocessing. Options: <bool>
use_xtb_driver = false

[turbomole]
# > Path to the ridft executable. The name `ridft` is automatically searched for. Options: <str | Path>
Expand Down
2 changes: 1 addition & 1 deletion src/mindlessgen/generator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions src/mindlessgen/prog/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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):
"""
Expand Down
175 changes: 166 additions & 9 deletions src/mindlessgen/qm/orca.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@
from tempfile import TemporaryDirectory

from ..molecules import Molecule
from ..prog import ORCAConfig
from ..prog import ORCAConfig, XTBConfig
from .base import QMMethod
from .xtb import XTB, get_xtb_path


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.
"""
Expand All @@ -28,6 +31,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()
Expand All @@ -51,24 +55,38 @@ 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)

if self.cfg.use_xtb_driver:
optimized_molecule = self.optimize_xtb_driver(
temp_path=temp_path,
molecule=molecule,
xyz_filename=xyz_filename,
ncores=ncores,
max_cycles=max_cycles,
verbosity=verbosity,
Comment thread
jonathan-schoeps marked this conversation as resolved.
)
return optimized_molecule
inputname = "orca_opt.inp"
orca_input = self._gen_input(
molecule, "molecule.xyz", ncores, True, max_cycles
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
)
Expand All @@ -78,7 +96,6 @@ def optimize(
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()
Expand All @@ -103,10 +120,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)
Expand Down Expand Up @@ -174,6 +191,7 @@ def _gen_input(
self,
molecule: Molecule,
xyzfile: str,
_temp_path: Path,
ncores: int,
optimization: bool = False,
opt_cycles: int | None = None,
Expand All @@ -200,6 +218,145 @@ def _gen_input(
orca_input += f"* xyzfile {molecule.charge} {molecule.uhf + 1} {xyzfile}\n"
return orca_input

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("##################")
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
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,
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 = 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 = [
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])
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
) -> 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."
)
xtb_path = get_xtb_path()
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_xtb_driver(
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"
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"
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


# TODO: 1. Convert this to a @staticmethod of Class ORCA
# 2. Rename to `get_method` or similar to enable an abstract interface
Expand Down
Loading