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.
- multioptpy/Calculator/ase_calculation_tools.py +21 -8
- multioptpy/Calculator/ase_tools/gxtb_dev.py +41 -0
- multioptpy/Calculator/ase_tools/orca.py +228 -14
- multioptpy/MD/thermostat.py +236 -123
- multioptpy/ModelHessian/fischerd3.py +240 -295
- multioptpy/Optimizer/rsirfo.py +112 -4
- multioptpy/Optimizer/rsprfo.py +1005 -698
- multioptpy/OtherMethod/spring_pair_method.py +314 -0
- multioptpy/entrypoints.py +406 -16
- multioptpy/ieip.py +14 -2
- multioptpy/interface.py +3 -1
- multioptpy/moleculardynamics.py +21 -13
- {multioptpy-1.20.4.dist-info → multioptpy-1.20.6.dist-info}/METADATA +20 -20
- {multioptpy-1.20.4.dist-info → multioptpy-1.20.6.dist-info}/RECORD +18 -16
- {multioptpy-1.20.4.dist-info → multioptpy-1.20.6.dist-info}/WHEEL +1 -1
- {multioptpy-1.20.4.dist-info → multioptpy-1.20.6.dist-info}/entry_points.txt +0 -0
- {multioptpy-1.20.4.dist-info → multioptpy-1.20.6.dist-info}/licenses/LICENSE +0 -0
- {multioptpy-1.20.4.dist-info → multioptpy-1.20.6.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
10
|
-
self.basis_set = kwargs.get('basis_set',
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|