88from tempfile import TemporaryDirectory
99
1010from ..molecules import Molecule
11- from ..prog import ORCAConfig
11+ from ..prog import ORCAConfig , XTBConfig
1212from .base import QMMethod
13+ from .xtb import XTB , get_xtb_path
1314
1415
1516class 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 \t MaxIter { self .cfg .scf_cycles } \n "
353+ if not optimization :
354+ orca_input += "\t Convergence 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