Skip to content

Commit dfa2ea2

Browse files
Update dev/gp3_ipea (#146)
* Remove need for constraints and specific compositions (#143) * initial implementation of constraint input Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> * improve code Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> * added CHANGELOG and formatted it Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> * remove need for fixed composition and exact number of atoms Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> --------- Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> * If constrains are set in the refinement step now the same constrains are set in the postprocess via the usage of xtb with orca as a driver. (#145) * If constrains are set during refinement the postprocess step now uses xtb with orca as a driver to set the same constrains during postprocess. Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> * Implemented tests for the orca as a driver run when distance constrains are enabled Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> * Fix for the windows python crash in testsuit Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> * Try to fix for the windows python crash in testsuit Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> * Removal of copied code Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> * Implementation of comments from the Pull request, less complicated optimize in orca.py Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> * Implemented requested changes and updated tests Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> * changed the test_orca.py testsuite to match the test_xtb.py tests Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> --------- Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> --------- Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> Signed-off-by: Jonathan Schöps <s6jtscho@uni-bonn.de> Co-authored-by: Jonathan Schöps <106986430+jonathan-schoeps@users.noreply.github.com>
1 parent d6f5511 commit dfa2ea2

6 files changed

Lines changed: 431 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Distance constraints for xTB (normalized parsing, config validation, runtime enforcement, and optional integration test).
13+
- Distance constrains for ORCA (uses xTB as as driver to set the same distance constrains for the postprocess).
1314

1415
## [0.6.0] - 2025-04-01
1516

mindlessgen.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ basis = "def2-SVP"
106106
gridsize = 1
107107
# > Maximum number of SCF cycles: Options: <int>
108108
scf_cycles = 100
109+
# > Use xTB as an external ORCA driver to keep distance constraints during postprocessing. Options: <bool>
110+
use_xtb_driver = false
109111

110112
[turbomole]
111113
# > Path to the ridft executable. The name `ridft` is automatically searched for. Options: <str | Path>

src/mindlessgen/generator/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ def setup_engines(
508508
raise ImportError("orca not found.")
509509
except ImportError as e:
510510
raise ImportError("orca not found.") from e
511-
return ORCA(path, cfg.orca)
511+
return ORCA(path, cfg.orca, cfg.xtb)
512512
elif engine_type == "turbomole":
513513
try:
514514
jobex_path = jobex_path_func(cfg.turbomole.jobex_path)

src/mindlessgen/prog/config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,7 @@ def __init__(self: ORCAConfig) -> None:
11791179
self._basis: str = "def2-SVP"
11801180
self._gridsize: int = 1
11811181
self._scf_cycles: int = 100
1182+
self._use_xtb_driver: bool = False
11821183

11831184
def get_identifier(self) -> str:
11841185
return "orca"
@@ -1269,6 +1270,22 @@ def scf_cycles(self, max_scf_cycles: int):
12691270
raise ValueError("Max SCF cycles should be greater than 0.")
12701271
self._scf_cycles = max_scf_cycles
12711272

1273+
@property
1274+
def use_xtb_driver(self) -> bool:
1275+
"""
1276+
Determine whether the xTB external driver can be used during post-processing.
1277+
"""
1278+
return self._use_xtb_driver
1279+
1280+
@use_xtb_driver.setter
1281+
def use_xtb_driver(self, enabled: bool):
1282+
"""
1283+
Enable or disable the usage of the xTB external driver during post-processing.
1284+
"""
1285+
if not isinstance(enabled, bool):
1286+
raise TypeError("use_xtb_driver should be a boolean.")
1287+
self._use_xtb_driver = enabled
1288+
12721289

12731290
class TURBOMOLEConfig(BaseConfig):
12741291
"""

src/mindlessgen/qm/orca.py

Lines changed: 166 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@
88
from tempfile import TemporaryDirectory
99

1010
from ..molecules import Molecule
11-
from ..prog import ORCAConfig
11+
from ..prog import ORCAConfig, XTBConfig
1212
from .base import QMMethod
13+
from .xtb import XTB, get_xtb_path
1314

1415

1516
class ORCA(QMMethod):
1617
"""
1718
This class handles all interaction with the ORCA external dependency.
1819
"""
1920

20-
def __init__(self, path: str | Path, orcacfg: ORCAConfig) -> None:
21+
def __init__(
22+
self, path: str | Path, orcacfg: ORCAConfig, xtb_config: XTBConfig | None = None
23+
) -> None:
2124
"""
2225
Initialize the ORCA class.
2326
"""
@@ -28,6 +31,7 @@ def __init__(self, path: str | Path, orcacfg: ORCAConfig) -> None:
2831
else:
2932
raise TypeError("orca_path should be a string or a Path object.")
3033
self.cfg = orcacfg
34+
self.xtb_cfg = xtb_config
3135
# must be explicitly initialized in current parallelization implementation
3236
# as accessing parent class variables might not be possible
3337
self.tmp_dir = self.__class__.get_temporary_directory()
@@ -51,24 +55,38 @@ def optimize(
5155
# NOTE: "prefix" and "dir" are valid keyword arguments for TemporaryDirectory
5256
temp_path = Path(temp_dir).resolve()
5357
# write the molecule to a temporary file
54-
molecule.write_xyz_to_file(temp_path / "molecule.xyz")
58+
xyz_filename = "molecule.xyz"
59+
molecule.write_xyz_to_file(temp_path / xyz_filename)
5560

61+
if self.cfg.use_xtb_driver:
62+
optimized_molecule = self.optimize_xtb_driver(
63+
temp_path=temp_path,
64+
molecule=molecule,
65+
xyz_filename=xyz_filename,
66+
ncores=ncores,
67+
max_cycles=max_cycles,
68+
verbosity=verbosity,
69+
)
70+
return optimized_molecule
5671
inputname = "orca_opt.inp"
5772
orca_input = self._gen_input(
58-
molecule, "molecule.xyz", ncores, True, max_cycles
73+
molecule,
74+
xyz_filename,
75+
temp_path,
76+
ncores,
77+
True,
78+
max_cycles,
5979
)
6080
if verbosity > 1:
6181
print("ORCA input file:\n##################")
6282
print(orca_input)
6383
print("##################")
6484
with open(temp_path / inputname, "w", encoding="utf8") as f:
6585
f.write(orca_input)
66-
6786
# run orca
6887
arguments = [
6988
inputname,
7089
]
71-
7290
orca_log_out, orca_log_err, return_code = self._run(
7391
temp_path=temp_path, arguments=arguments
7492
)
@@ -78,7 +96,6 @@ def optimize(
7896
raise RuntimeError(
7997
f"ORCA failed with return code {return_code}:\n{orca_log_err}"
8098
)
81-
8299
# read the optimized molecule from the output file
83100
xyzfile = Path(temp_path / inputname).resolve().with_suffix(".xyz")
84101
optimized_molecule = molecule.copy()
@@ -103,10 +120,10 @@ def singlepoint(self, molecule: Molecule, ncores: int, verbosity: int = 1) -> st
103120

104121
# write the input file
105122
inputname = "orca.inp"
106-
orca_input = self._gen_input(molecule, molfile, ncores)
123+
orca_input = self._gen_input(molecule, molfile, temp_path, ncores)
107124
if verbosity > 1:
108125
print("ORCA input file:\n##################")
109-
print(self._gen_input(molecule, molfile, ncores))
126+
print(self._gen_input(molecule, molfile, temp_path, ncores))
110127
print("##################")
111128
with open(temp_path / inputname, "w", encoding="utf8") as f:
112129
f.write(orca_input)
@@ -174,6 +191,7 @@ def _gen_input(
174191
self,
175192
molecule: Molecule,
176193
xyzfile: str,
194+
_temp_path: Path,
177195
ncores: int,
178196
optimization: bool = False,
179197
opt_cycles: int | None = None,
@@ -200,6 +218,145 @@ def _gen_input(
200218
orca_input += f"* xyzfile {molecule.charge} {molecule.uhf + 1} {xyzfile}\n"
201219
return orca_input
202220

221+
def optimize_xtb_driver(
222+
self,
223+
temp_path: Path,
224+
molecule: Molecule,
225+
xyz_filename: str,
226+
ncores: int,
227+
max_cycles: int | None = None,
228+
verbosity: int = 1,
229+
) -> Molecule:
230+
"""
231+
Optimize a molecule using ORCA through the xTB external driver.
232+
"""
233+
234+
xtb_input = temp_path / "xtb.inp"
235+
inputname = "orca_opt.inp"
236+
self._write_xtb_input(molecule, xtb_input, inputname)
237+
orca_input = self._gen_input_xtb_driver(
238+
molecule,
239+
xyz_filename,
240+
temp_path,
241+
ncores,
242+
True,
243+
max_cycles,
244+
)
245+
if verbosity > 1:
246+
print("ORCA input file:\n##################")
247+
print(orca_input)
248+
print("##################")
249+
print("XTB input file:\n##################")
250+
print(xtb_input)
251+
print("##################")
252+
with open(temp_path / inputname, "w", encoding="utf8") as f:
253+
f.write(orca_input)
254+
# run orca with xTB as a driver
255+
orca_log_out, orca_log_err, return_code = self._run_xtb_driver(
256+
temp_path=temp_path,
257+
geometry_filename=xyz_filename,
258+
xcontrol_name=xtb_input.name,
259+
ncores=ncores,
260+
)
261+
if verbosity > 2:
262+
print(orca_log_out)
263+
if return_code != 0:
264+
raise RuntimeError(
265+
f"ORCA failed with return code {return_code}:\n{orca_log_err}"
266+
)
267+
268+
# read the optimized molecule from the output file
269+
xyzfile = temp_path / "xtbopt.xyz"
270+
if not xyzfile.exists():
271+
raise RuntimeError(
272+
"xTB-driven ORCA optimization did not produce 'xtbopt.xyz'."
273+
)
274+
optimized_molecule = molecule.copy()
275+
optimized_molecule.read_xyz_from_file(xyzfile)
276+
return optimized_molecule
277+
278+
def _run_xtb_driver(
279+
self,
280+
temp_path: Path,
281+
geometry_filename: str,
282+
xcontrol_name: str,
283+
ncores: int,
284+
) -> tuple[str, str, int]:
285+
"""
286+
Run the optimization through the xTB external driver when constraints are requested.
287+
"""
288+
xtb_executable = get_xtb_path()
289+
if self.xtb_cfg is None:
290+
raise RuntimeError(
291+
"xTB driver requested but no xTB configuration provided."
292+
)
293+
xtb_runner = XTB(path=xtb_executable, xtb_config=self.xtb_cfg)
294+
arguments = [
295+
geometry_filename,
296+
"--opt",
297+
]
298+
opt_level = getattr(self.cfg, "optlevel", None)
299+
if opt_level not in (None, ""):
300+
arguments.append(str(opt_level))
301+
arguments.extend(["--orca", "-I", xcontrol_name])
302+
xtb_log_out, xtb_log_err, returncode = xtb_runner._run(
303+
temp_path=temp_path, arguments=arguments
304+
)
305+
return xtb_log_out, xtb_log_err, returncode
306+
307+
def _write_xtb_input(
308+
self, molecule: Molecule, xtb_input: Path, input_file: str
309+
) -> None:
310+
"""
311+
Write the xcontrol file containing constraints and ORCA driver info.
312+
"""
313+
if not self.xtb_cfg:
314+
raise RuntimeError(
315+
"xTB configuration missing but constraints were requested."
316+
)
317+
xtb_path = get_xtb_path()
318+
xtb_writer = XTB(xtb_path, self.xtb_cfg)
319+
generated = xtb_writer._prepare_distance_constraint_file(
320+
molecule, xtb_input.parent
321+
)
322+
if not generated:
323+
raise RuntimeError(
324+
"xTB driver requested but no distance constraints were generated."
325+
)
326+
with xtb_input.open("a", encoding="utf8") as handle:
327+
handle.write("$external\n")
328+
handle.write(f" orca input file= {input_file}\n")
329+
handle.write(f" orca bin= {self.path}\n")
330+
handle.write("$end\n")
331+
332+
def _gen_input_xtb_driver(
333+
self,
334+
molecule: Molecule,
335+
xyzfile: str,
336+
temp_path: Path,
337+
ncores: int,
338+
optimization: bool = False,
339+
opt_cycles: int | None = None,
340+
) -> str:
341+
"""
342+
Generate a default input file for ORCA.
343+
"""
344+
orca_input = f"! {self.cfg.functional} {self.cfg.basis}\n"
345+
orca_input += f"! DEFGRID{self.cfg.gridsize}\n"
346+
orca_input += "! MiniPrint\n"
347+
orca_input += "! NoTRAH\n"
348+
orca_input += "! Engrad\n"
349+
# "! AutoAux" keyword for super-heavy elements as def2/J ends at Rn
350+
if any(atom >= 86 for atom in molecule.ati):
351+
orca_input += "! AutoAux\n"
352+
orca_input += f"%scf\n\tMaxIter {self.cfg.scf_cycles}\n"
353+
if not optimization:
354+
orca_input += "\tConvergence Medium\n"
355+
orca_input += "end\n"
356+
orca_input += f"%pal nprocs {ncores} end\n\n"
357+
orca_input += f"* xyzfile {molecule.charge} {molecule.uhf + 1} {xyzfile}\n"
358+
return orca_input
359+
203360

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

0 commit comments

Comments
 (0)