MultiOptPy 1.20.4__py3-none-any.whl → 1.20.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,8 @@ from multioptpy.Calculator.ase_tools.fairchem import ASE_FAIRCHEM
22
22
  from multioptpy.Calculator.ase_tools.mace import ASE_MACE
23
23
  from multioptpy.Calculator.ase_tools.mopac import ASE_MOPAC
24
24
  from multioptpy.Calculator.ase_tools.pygfn0 import ASE_GFN0
25
+ from multioptpy.Calculator.ase_tools.gxtb_dev import ASE_gxTB_Dev
26
+
25
27
 
26
28
  """
27
29
  referrence:
@@ -89,7 +91,9 @@ class Calculation:
89
91
  if self.software_type == "gaussian":
90
92
  print("Calculating exact Hessian using Gaussian...")
91
93
  exact_hess = calc_obj.calc_analytic_hessian() # in hartree/Bohr^2
92
-
94
+ elif self.software_type == "orca":
95
+ hess_path = calc_obj.run_frequency_analysis()
96
+ exact_hess = calc_obj.get_hessian_matrix(hess_path)
93
97
  else:
94
98
  vib = Vibrations(atoms=calc_obj.atom_obj, delta=0.001, name="z_hess_"+timestamp)
95
99
  vib.run()
@@ -124,7 +128,7 @@ class Calculation:
124
128
  file_list = glob.glob(file_directory+"/*_[0-9].xyz")
125
129
 
126
130
  for num, input_file in enumerate(file_list):
127
- try:
131
+ if True:#try:
128
132
  if geom_num_list is None:
129
133
  positions, _, electric_charge_and_multiplicity = xyz2list(input_file, electric_charge_and_multiplicity)
130
134
  else:
@@ -155,11 +159,11 @@ class Calculation:
155
159
  elif iter % self.FC_COUNT == 0 or self.hessian_flag:
156
160
  # exact numerical hessian
157
161
  _ = self.calc_exact_hess(calc_obj, positions, element_list)
158
- except Exception as error:
159
- print(error)
160
- print("This molecule could not be optimized.")
161
- finish_frag = True
162
- return np.array([0]), np.array([0]), np.array([0]), finish_frag
162
+ #except Exception as error:
163
+ # print(error)
164
+ # print("This molecule could not be optimized.")
165
+ # finish_frag = True
166
+ # return np.array([0]), np.array([0]), np.array([0]), finish_frag
163
167
 
164
168
  positions /= self.bohr2angstroms
165
169
  self.energy = e
@@ -286,7 +290,10 @@ class ASEEngine(CalculationEngine):
286
290
  timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S_%f")[:-2]
287
291
  if software_type == "gaussian":
288
292
  exact_hess = calc_obj.calc_analytic_hessian() # in hartree/Bohr^2
289
- exact_hess = exact_hess
293
+
294
+ elif software_type == "orca":
295
+ hess_path = calc_obj.run_frequency_analysis()
296
+ exact_hess = calc_obj.get_hessian_matrix(hess_path)
290
297
  else:
291
298
  vib = Vibrations(atoms=calc_obj.atom_obj, delta=0.001, name="z_hess_"+timestamp)
292
299
  vib.run()
@@ -431,6 +438,12 @@ def setup_calculator(atom_obj, software_type, electric_charge_and_multiplicity,
431
438
  input_file=input_file)
432
439
  return calc_obj
433
440
 
441
+ if software_type == "gxtb_dev":
442
+ calc_obj = ASE_gxTB_Dev(atom_obj=atom_obj,
443
+ electric_charge_and_multiplicity=electric_charge_and_multiplicity,
444
+ software_type=software_type)
445
+ return calc_obj
446
+
434
447
  # Unknown software type
435
448
  raise ValueError(f"Unsupported software type: {software_type}")
436
449
 
@@ -0,0 +1,41 @@
1
+ class ASE_gxTB_Dev:
2
+ """
3
+ Wrapper class to set up and run g-xTB (preliminary version) calculations via ASE.
4
+ $ pip install pygxtb==0.7.0
5
+
6
+ """
7
+ def __init__(self, **kwargs):
8
+
9
+ self.atom_obj = kwargs.get('atom_obj', None)
10
+ self.electric_charge_and_multiplicity = kwargs.get('electric_charge_and_multiplicity', None)
11
+ self.software_path = kwargs.get('software_path', None)
12
+ self.software_type = kwargs.get('software_type', None)
13
+ from pygxtb import PygxTB
14
+ self.pygxtb = PygxTB
15
+
16
+ def set_calculator(self):
17
+ """
18
+ Sets the ASE calculator object based on software_type.
19
+ """
20
+
21
+ charge = 0 # Default charge
22
+ if self.electric_charge_and_multiplicity is not None:
23
+ try:
24
+ # Get charge from [charge, multiplicity] list
25
+ charge = int(self.electric_charge_and_multiplicity[0])
26
+ except (IndexError, TypeError, ValueError):
27
+ print(f"Warning: Could not parse charge from {self.electric_charge_and_multiplicity}. Defaulting to 0.")
28
+ pass
29
+
30
+ # Instantiate GFN0 class and pass the charge
31
+ gxtb_calc = self.pygxtb(charge=charge)
32
+ return gxtb_calc
33
+
34
+
35
+ def run(self):
36
+ """
37
+ Attaches the calculator to the atoms object and returns it.
38
+ """
39
+ calc_obj = self.set_calculator()
40
+ self.atom_obj.calc = calc_obj
41
+ return self.atom_obj
@@ -1,22 +1,236 @@
1
1
  import os
2
+ import shutil
3
+ import subprocess
4
+ import numpy as np
5
+ from ase.calculators.orca import ORCA, OrcaProfile
6
+ from ase.data import atomic_numbers
7
+
8
+ """
9
+ Please specify the absolute path to the ORCA executable in software_path.conf using the format orca::<path>. For Linux, provide the path to the binary (e.g., /absolute/path/to/orca), and for Windows, provide the path to the executable file (e.g., C:\absolute\path\to\orca.exe).
10
+ """
11
+
2
12
 
3
13
  class ASE_ORCA:
14
+ TARGET_ORCA_VERSION = '6.1.0'
15
+
4
16
  def __init__(self, **kwargs):
5
17
  self.atom_obj = kwargs.get('atom_obj', None)
6
18
  self.electric_charge_and_multiplicity = kwargs.get('electric_charge_and_multiplicity', None)
7
- self.input_file = kwargs.get('input_file', None)
19
+
20
+ # NOTE: self.input_file is updated in _setup_calculator to enforce 'orca.inp' in CWD.
21
+ raw_input_file = kwargs.get('input_file', 'orca.inp')
22
+ self.input_file = os.path.abspath(raw_input_file).replace('\\', '/')
23
+
8
24
  self.orca_path = kwargs.get('orca_path', None)
9
- self.functional = kwargs.get('functional', None)
10
- self.basis_set = kwargs.get('basis_set', None)
11
-
25
+ self.functional = kwargs.get('functional', 'B3LYP')
26
+ self.basis_set = kwargs.get('basis_set', 'def2-SVP')
27
+
28
+ # Optional ORCA input blocks (raw string)
29
+ self.orca_blocks = kwargs.get('orca_blocks', '')
30
+
31
+ # Auto-fix for unsupported Pople basis sets on heavy elements
32
+ self.auto_fix_basis = kwargs.get('auto_fix_basis', True)
33
+ self.heavy_atom_basis = kwargs.get('heavy_atom_basis', 'def2-SVP')
34
+
35
+ def _resolve_orca_exe(self, provided_path):
36
+ """
37
+ Resolve the absolute path to the ORCA executable.
38
+ Handles directories, stripping whitespace from config files, and WSL/Windows paths.
39
+ """
40
+ if not provided_path:
41
+ return None
42
+
43
+ # CRITICAL FIX: Strip whitespace/newlines that might come from config parsing
44
+ clean_path = provided_path.strip()
45
+ # Expand ~ to home directory if present
46
+ clean_path = os.path.expanduser(clean_path)
47
+ clean_path = os.path.normpath(clean_path)
48
+
49
+ candidates = []
50
+ # If the path is a directory, look for the executable inside it
51
+ if os.path.isdir(clean_path):
52
+ candidates.append(os.path.join(clean_path, 'orca'))
53
+ candidates.append(os.path.join(clean_path, 'orca.exe'))
54
+ else:
55
+ # If it's a file path (or doesn't exist yet), use it as is
56
+ candidates.append(clean_path)
57
+
58
+ for candidate in candidates:
59
+ # Check if file exists
60
+ if os.path.exists(candidate) and os.path.isfile(candidate):
61
+ return os.path.abspath(candidate)
62
+
63
+ # Check system PATH
64
+ resolved = shutil.which(candidate)
65
+ if resolved and os.path.exists(resolved):
66
+ return os.path.abspath(resolved)
67
+
68
+ # Use repr() in error message to reveal hidden characters like \n
69
+ candidate_reprs = [repr(c) for c in candidates]
70
+ raise FileNotFoundError(f"Cannot locate ORCA executable. Checked: {', '.join(candidate_reprs)}")
71
+
72
+ def _is_pople_basis(self, basis_name):
73
+ if not basis_name: return False
74
+ b = basis_name.strip().lower()
75
+ return b.startswith("6-31") or b.startswith("6-311")
76
+
77
+ def _get_heavy_elements_for_pople(self):
78
+ if self.atom_obj is None: return []
79
+ symbols = self.atom_obj.get_chemical_symbols()
80
+ return sorted({s for s in symbols if atomic_numbers.get(s, 0) > 30})
81
+
82
+ def _build_orca_blocks(self):
83
+ blocks = (self.orca_blocks or "").strip()
84
+ heavy_elements = []
85
+ if self._is_pople_basis(self.basis_set):
86
+ heavy_elements = self._get_heavy_elements_for_pople()
87
+
88
+ if heavy_elements:
89
+ if not self.auto_fix_basis:
90
+ raise ValueError("Unsupported Pople basis for heavy elements.")
91
+
92
+ basis_lines = ["%basis"]
93
+ for elem in heavy_elements:
94
+ # Per user instruction: No ECP, just GTO
95
+ basis_lines.append(f' NewGTO {elem} "{self.heavy_atom_basis}"')
96
+ basis_lines.append("end")
97
+
98
+ if blocks:
99
+ blocks = blocks + "\n" + "\n".join(basis_lines)
100
+ else:
101
+ blocks = "\n".join(basis_lines)
102
+
103
+ return blocks if blocks else ""
104
+
105
+ def _print_orca_output_on_error(self):
106
+ """Helper to print ORCA output file content if it exists."""
107
+ if not hasattr(self, 'input_file'): return
108
+
109
+ out_file = os.path.splitext(self.input_file)[0] + ".out"
110
+ if os.path.exists(out_file):
111
+ print(f"\n--- ORCA OUTPUT ({out_file}) ---")
112
+ try:
113
+ with open(out_file, 'r', encoding='utf-8', errors='replace') as f:
114
+ content = f.read()
115
+ print(content[-3000:] if len(content) > 3000 else content)
116
+ except Exception as e:
117
+ print(f"Failed to read output file: {e}")
118
+ print("--- END ORCA OUTPUT ---\n")
119
+ else:
120
+ print(f"\n--- ORCA OUTPUT NOT FOUND ({out_file}) ---\n")
121
+
122
+ def _setup_calculator(self, task_keyword):
123
+ # Force usage of Current Working Directory + "orca.inp"
124
+ cwd = os.getcwd()
125
+ label_path = os.path.join(cwd, 'orca').replace('\\', '/')
126
+ self.input_file = label_path + '.inp'
127
+
128
+ print(f"DEBUG: ASE Label Path : {label_path}")
129
+
130
+ simple_input = f"{self.functional} {self.basis_set} {task_keyword}"
131
+
132
+ profile_obj = None
133
+ if self.orca_path:
134
+ real_exe_path = self._resolve_orca_exe(self.orca_path)
135
+ orca_dir = os.path.dirname(real_exe_path)
136
+ path_env = os.environ.get('PATH', '')
137
+ if orca_dir not in path_env:
138
+ os.environ['PATH'] = orca_dir + os.pathsep + path_env
139
+
140
+ ase_safe_path = real_exe_path.replace('\\', '/')
141
+ profile_obj = OrcaProfile(ase_safe_path)
142
+ print(f"DEBUG: ORCA Executable: {ase_safe_path}")
143
+
144
+ orca_blocks = self._build_orca_blocks()
145
+
146
+ calc = ORCA(
147
+ label=label_path,
148
+ profile=profile_obj,
149
+ charge=int(self.electric_charge_and_multiplicity[0]),
150
+ mult=int(self.electric_charge_and_multiplicity[1]),
151
+ orcasimpleinput=simple_input,
152
+ orcablocks=orca_blocks
153
+ )
154
+
155
+ self.atom_obj.calc = calc
156
+ return self.atom_obj
157
+
12
158
  def run(self):
13
-
14
- from ase.calculators.orca import ORCA
15
- input_dir = os.path.dirname(self.input_file)
16
- self.atom_obj.calc = ORCA(label=input_dir,
17
- profile=self.orca_path,
18
- charge=int(self.electric_charge_and_multiplicity[0]),
19
- mult=int(self.electric_charge_and_multiplicity[1]),
20
- orcasimpleinput=self.functional+' '+self.basis_set)
21
- #orcablocks='%pal nprocs 16 end')
22
- return self.atom_obj
159
+ self._setup_calculator("EnGrad")
160
+ print(f"--- Starting Gradient Calculation (ORCA {self.TARGET_ORCA_VERSION}) ---")
161
+ try:
162
+ forces = self.atom_obj.get_forces()
163
+ potential_energy = self.atom_obj.get_potential_energy()
164
+ print("Gradient calculation completed.")
165
+ return forces, potential_energy
166
+ except subprocess.CalledProcessError as e:
167
+ print(f"CRITICAL: ORCA execution failed with exit code {e.returncode}")
168
+ self._print_orca_output_on_error()
169
+ raise e
170
+ except Exception as e:
171
+ print(f"CRITICAL: An unexpected error occurred: {e}")
172
+ self._print_orca_output_on_error()
173
+ raise e
174
+
175
+ def run_frequency_analysis(self):
176
+ print(f"--- Starting Frequency Calculation (ORCA {self.TARGET_ORCA_VERSION}) ---")
177
+ self._setup_calculator("Freq")
178
+ try:
179
+ self.atom_obj.get_potential_energy()
180
+ print("Frequency calculation completed.")
181
+ # Use self.input_file to construct hess_path instead of relying on calc.label
182
+ hess_path = os.path.splitext(self.input_file)[0] + ".hess"
183
+ return hess_path
184
+ except subprocess.CalledProcessError as e:
185
+ print(f"CRITICAL: ORCA Frequency execution failed with exit code {e.returncode}")
186
+ self._print_orca_output_on_error()
187
+ raise e
188
+ except Exception as e:
189
+ print(f"CRITICAL: An unexpected error occurred during frequency analysis: {e}")
190
+ self._print_orca_output_on_error()
191
+ raise e
192
+
193
+ def get_hessian_matrix(self, hess_file_path=None):
194
+ if hess_file_path is None:
195
+ # Default to orca.hess in the same dir as input_file
196
+ input_dir = os.path.dirname(self.input_file)
197
+ hess_file_path = os.path.join(input_dir, 'orca.hess')
198
+
199
+ if not os.path.exists(hess_file_path):
200
+ raise FileNotFoundError(f"Hessian file not found: {hess_file_path}")
201
+
202
+ print(f"Reading Hessian from: {hess_file_path}")
203
+
204
+ with open(hess_file_path, 'r', encoding='utf-8') as f:
205
+ lines = f.readlines()
206
+
207
+ hessian_matrix = None
208
+ iterator = iter(lines)
209
+ for line in iterator:
210
+ if "$hessian" in line:
211
+ dim_line = next(iterator).strip().split()
212
+ # Parse dimensions: square matrix usually provides just one dimension
213
+ if len(dim_line) == 1:
214
+ n_rows = int(dim_line[0])
215
+ n_cols = n_rows
216
+ else:
217
+ n_rows, n_cols = map(int, dim_line[:2])
218
+
219
+ hessian_matrix = np.zeros((n_rows, n_cols))
220
+ col_pointer = 0
221
+ while col_pointer < n_cols:
222
+ header = next(iterator).strip()
223
+ if not header: break
224
+ col_indices = [int(c) for c in header.split()]
225
+ for r in range(n_rows):
226
+ row_data = next(iterator).strip().split()
227
+ row_idx = int(row_data[0])
228
+ values = [float(x) for x in row_data[1:]]
229
+ for i, val in enumerate(values):
230
+ hessian_matrix[row_idx, col_indices[i]] = val
231
+ col_pointer += len(col_indices)
232
+ break
233
+
234
+ if hessian_matrix is None:
235
+ raise ValueError("Could not find $hessian block in the file.")
236
+ return hessian_matrix