MultiOptPy 1.20.3__py3-none-any.whl → 1.20.5__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/OtherMethod/spring_pair_method.py +314 -0
- multioptpy/ieip.py +14 -2
- multioptpy/interface.py +3 -1
- multioptpy/optimization.py +32 -12
- {multioptpy-1.20.3.dist-info → multioptpy-1.20.5.dist-info}/METADATA +19 -19
- {multioptpy-1.20.3.dist-info → multioptpy-1.20.5.dist-info}/RECORD +13 -11
- {multioptpy-1.20.3.dist-info → multioptpy-1.20.5.dist-info}/WHEEL +0 -0
- {multioptpy-1.20.3.dist-info → multioptpy-1.20.5.dist-info}/entry_points.txt +0 -0
- {multioptpy-1.20.3.dist-info → multioptpy-1.20.5.dist-info}/licenses/LICENSE +0 -0
- {multioptpy-1.20.3.dist-info → multioptpy-1.20.5.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
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
from multioptpy.Utils.calc_tools import Calculationtools
|
|
6
|
+
from multioptpy.Visualization.visualization import Graph
|
|
7
|
+
|
|
8
|
+
class SpringPairMethod:
|
|
9
|
+
"""
|
|
10
|
+
Implementation of the Spring Pair Method (SPM) with Adaptive Step Size & Momentum.
|
|
11
|
+
Modified to accept a SINGLE initial structure and auto-generate the second one via perturbation.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, config):
|
|
14
|
+
self.config = config
|
|
15
|
+
|
|
16
|
+
# --- TUNING PARAMETERS ---
|
|
17
|
+
self.k_spring = 10.0
|
|
18
|
+
self.l_s = max(getattr(self.config, 'L_covergence', 0.1), 0.1)
|
|
19
|
+
self.initial_drift_step = 0.01
|
|
20
|
+
self.climb_step_size = 0.50
|
|
21
|
+
self.drift_limit = 100
|
|
22
|
+
self.momentum = 0.3
|
|
23
|
+
self.MAX_FORCE_THRESHOLD = 0.00100
|
|
24
|
+
self.RMS_FORCE_THRESHOLD = 0.00005
|
|
25
|
+
|
|
26
|
+
# Magnitude of random perturbation to generate Image 2 (Bohr)
|
|
27
|
+
self.perturbation_scale = 0.1
|
|
28
|
+
|
|
29
|
+
def RMS(self, mat):
|
|
30
|
+
return np.sqrt(np.mean(mat**2))
|
|
31
|
+
|
|
32
|
+
def print_info(self, dat, phase):
|
|
33
|
+
print(f"[[{phase} information]]")
|
|
34
|
+
print(f" image_1 image_2")
|
|
35
|
+
print(f"energy (Hartree) : {dat['energy_1']:.8f} {dat['energy_2']:.8f}")
|
|
36
|
+
print(f"gradient RMS : {self.RMS(dat['gradient_1']):.6f} {self.RMS(dat['gradient_2']):.6f}")
|
|
37
|
+
|
|
38
|
+
if "perp_force_1" in dat:
|
|
39
|
+
print(f"perp_force (Drift) : {self.RMS(dat['perp_force_1']):.6f} {self.RMS(dat['perp_force_2']):.6f}")
|
|
40
|
+
print(f"spring_force : {self.RMS(dat['spring_force_1']):.6f} {self.RMS(dat['spring_force_2']):.6f}")
|
|
41
|
+
|
|
42
|
+
print(f"spring_length (Bohr) : {dat['spring_length']:.6f}")
|
|
43
|
+
step_info = dat.get('step_size', 0)
|
|
44
|
+
print(f"DEBUG: k={self.k_spring:.1f}, step={step_info:.6f}")
|
|
45
|
+
print("-" * 80)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
def get_spring_vectors(self, geom_1, geom_2):
|
|
49
|
+
diff = geom_2 - geom_1
|
|
50
|
+
dist = np.linalg.norm(diff)
|
|
51
|
+
if dist < 1e-10:
|
|
52
|
+
rand_vec = np.random.randn(*diff.shape)
|
|
53
|
+
unit_vec = rand_vec / np.linalg.norm(rand_vec)
|
|
54
|
+
dist = 1e-10
|
|
55
|
+
else:
|
|
56
|
+
unit_vec = diff / dist
|
|
57
|
+
return dist, unit_vec
|
|
58
|
+
|
|
59
|
+
def decompose_gradient(self, gradient, unit_vec):
|
|
60
|
+
grad_flat = gradient.flatten()
|
|
61
|
+
vec_flat = unit_vec.flatten()
|
|
62
|
+
grad_par_mag = np.dot(grad_flat, vec_flat)
|
|
63
|
+
grad_par = grad_par_mag * unit_vec
|
|
64
|
+
grad_perp = gradient - grad_par
|
|
65
|
+
return grad_par, grad_perp
|
|
66
|
+
|
|
67
|
+
def _generate_perturbed_structure(self, geom, scale):
|
|
68
|
+
"""Generate a new structure by adding random perturbation to the given geometry."""
|
|
69
|
+
# Generate random noise
|
|
70
|
+
noise = np.random.randn(*geom.shape)
|
|
71
|
+
# Normalize to unit vectors per atom to distribute perturbation evenly
|
|
72
|
+
norms = np.linalg.norm(noise, axis=1, keepdims=True)
|
|
73
|
+
noise = noise / (norms + 1e-10)
|
|
74
|
+
# Scale the noise
|
|
75
|
+
perturbation = noise * scale
|
|
76
|
+
return geom + perturbation
|
|
77
|
+
|
|
78
|
+
def iteration(self, file_directory_1, SP1, element_list, electric_charge_and_multiplicity, FIO1):
|
|
79
|
+
"""
|
|
80
|
+
Main SPM Optimization Loop.
|
|
81
|
+
Accepts ONE initial structure. Image 2 is auto-generated.
|
|
82
|
+
"""
|
|
83
|
+
G = Graph(self.config.iEIP_FOLDER_DIRECTORY)
|
|
84
|
+
ENERGY_LIST_1, ENERGY_LIST_2 = [], []
|
|
85
|
+
GRAD_LIST_1, GRAD_LIST_2 = [], []
|
|
86
|
+
|
|
87
|
+
# --- Initialize Image 2 from Image 1 ---
|
|
88
|
+
print("### Initializing SPM ###")
|
|
89
|
+
print(f"Base Structure (Image 1): {file_directory_1}")
|
|
90
|
+
|
|
91
|
+
# 1. Get initial geometry of Image 1
|
|
92
|
+
# We perform a dummy single point calculation or just read the geom if possible.
|
|
93
|
+
# Here we run SP1 to get the geometry and energy reliably.
|
|
94
|
+
init_energy, init_grad, init_geom, err = SP1.single_point(
|
|
95
|
+
file_directory_1, element_list, "init_check",
|
|
96
|
+
electric_charge_and_multiplicity, self.config.force_data["xtb"]
|
|
97
|
+
)
|
|
98
|
+
if err:
|
|
99
|
+
print("[Error] Failed to read initial structure.")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# 2. Generate Image 2 geometry
|
|
103
|
+
print(f"Generating Image 2 with random perturbation (Scale: {self.perturbation_scale} Bohr)...")
|
|
104
|
+
init_geom_2 = self._generate_perturbed_structure(init_geom, self.perturbation_scale)
|
|
105
|
+
|
|
106
|
+
# Create a directory for Image 2 (internally managed)
|
|
107
|
+
# We can just use the same SP1 object but we need a file path for it.
|
|
108
|
+
# We will generate input files for Image 2 on the fly using FIO1 logic.
|
|
109
|
+
# Note: We reuse SP1 and FIO1 for both images since they share physics/settings.
|
|
110
|
+
|
|
111
|
+
# Initialize loop variables
|
|
112
|
+
geom_num_list_1 = init_geom
|
|
113
|
+
geom_num_list_2 = init_geom_2
|
|
114
|
+
|
|
115
|
+
velocity_1 = np.zeros((len(element_list), 3))
|
|
116
|
+
velocity_2 = np.zeros((len(element_list), 3))
|
|
117
|
+
|
|
118
|
+
current_drift_step = self.initial_drift_step
|
|
119
|
+
|
|
120
|
+
# Variables to store the latest geometry
|
|
121
|
+
new_geom_1 = None
|
|
122
|
+
new_geom_2 = None
|
|
123
|
+
|
|
124
|
+
for cycle in range(0, self.config.microiterlimit):
|
|
125
|
+
if os.path.isfile(self.config.iEIP_FOLDER_DIRECTORY+"end.txt"):
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
print(f"### Cycle {cycle} Start ###")
|
|
129
|
+
|
|
130
|
+
# =========================================================================
|
|
131
|
+
# 1. Drifting Phase
|
|
132
|
+
# =========================================================================
|
|
133
|
+
print(f"--- Drifting Phase (Cycle {cycle}) ---")
|
|
134
|
+
|
|
135
|
+
prev_force_drift_1 = None
|
|
136
|
+
prev_force_drift_2 = None
|
|
137
|
+
|
|
138
|
+
drift_temp_label = f"{cycle}_drift_temp"
|
|
139
|
+
|
|
140
|
+
for d_step in range(self.drift_limit):
|
|
141
|
+
iter_label = drift_temp_label
|
|
142
|
+
|
|
143
|
+
# Make input files for this step
|
|
144
|
+
# Note: file_directory_1/2 here are just strings for the input file path
|
|
145
|
+
# generated by _make_next_input.
|
|
146
|
+
# However, SP1.single_point expects the directory path or file path depending on implementation.
|
|
147
|
+
# Assuming _make_next_input returns the PATH to the input file.
|
|
148
|
+
|
|
149
|
+
input_path_1 = self._make_next_input(FIO1, geom_num_list_1, element_list, electric_charge_and_multiplicity, iter_label + "_img1")
|
|
150
|
+
input_path_2 = self._make_next_input(FIO1, geom_num_list_2, element_list, electric_charge_and_multiplicity, iter_label + "_img2")
|
|
151
|
+
|
|
152
|
+
# 1. QM Calculation
|
|
153
|
+
# We reuse SP1 for both. It's just a calculator.
|
|
154
|
+
energy_1, gradient_1, g1_read, error_flag_1 = SP1.single_point(input_path_1, element_list, iter_label + "_img1", electric_charge_and_multiplicity, self.config.force_data["xtb"])
|
|
155
|
+
energy_2, gradient_2, g2_read, error_flag_2 = SP1.single_point(input_path_2, element_list, iter_label + "_img2", electric_charge_and_multiplicity, self.config.force_data["xtb"])
|
|
156
|
+
|
|
157
|
+
# Update geom from read result to be safe, or stick to internal numpy array
|
|
158
|
+
# Using internal array (geom_num_list_1/2) is safer for consistency unless optimizer changes it.
|
|
159
|
+
# But let's align them just in case output orientation changed.
|
|
160
|
+
# g1_read, g2_read = Calculationtools().kabsch_algorithm(g1_read, g2_read)
|
|
161
|
+
# geom_num_list_1, geom_num_list_2 = g1_read, g2_read
|
|
162
|
+
|
|
163
|
+
# Align current internal geometries
|
|
164
|
+
geom_num_list_1, geom_num_list_2 = Calculationtools().kabsch_algorithm(geom_num_list_1, geom_num_list_2)
|
|
165
|
+
|
|
166
|
+
if error_flag_1 or error_flag_2:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# 2. Vector & Force Calculation
|
|
170
|
+
ds, vs = self.get_spring_vectors(geom_num_list_1, geom_num_list_2)
|
|
171
|
+
|
|
172
|
+
_, grad_perp_1 = self.decompose_gradient(gradient_1, vs)
|
|
173
|
+
_, grad_perp_2 = self.decompose_gradient(gradient_2, vs)
|
|
174
|
+
|
|
175
|
+
force_perp_1 = -grad_perp_1
|
|
176
|
+
force_perp_2 = -grad_perp_2
|
|
177
|
+
|
|
178
|
+
spring_force_mag = self.k_spring * (ds - self.l_s)
|
|
179
|
+
spring_force_1 = spring_force_mag * vs
|
|
180
|
+
spring_force_2 = spring_force_mag * (-vs)
|
|
181
|
+
|
|
182
|
+
total_force_1 = force_perp_1 + spring_force_1
|
|
183
|
+
total_force_2 = force_perp_2 + spring_force_2
|
|
184
|
+
|
|
185
|
+
# --- Adaptive Step Logic ---
|
|
186
|
+
if prev_force_drift_1 is not None:
|
|
187
|
+
dot_1 = np.sum(prev_force_drift_1 * total_force_1)
|
|
188
|
+
dot_2 = np.sum(prev_force_drift_2 * total_force_2)
|
|
189
|
+
|
|
190
|
+
if dot_1 < 0 or dot_2 < 0:
|
|
191
|
+
current_drift_step *= 0.5
|
|
192
|
+
velocity_1 *= 0.1
|
|
193
|
+
velocity_2 *= 0.1
|
|
194
|
+
print(f" [Auto-Brake] Oscillation detected at step {d_step}. Reduced drift step to {current_drift_step:.6f}")
|
|
195
|
+
else:
|
|
196
|
+
current_drift_step = min(current_drift_step * 1.05, self.initial_drift_step)
|
|
197
|
+
|
|
198
|
+
prev_force_drift_1 = total_force_1.copy()
|
|
199
|
+
prev_force_drift_2 = total_force_2.copy()
|
|
200
|
+
|
|
201
|
+
# Update Position
|
|
202
|
+
velocity_1 = self.momentum * velocity_1 + current_drift_step * total_force_1
|
|
203
|
+
velocity_2 = self.momentum * velocity_2 + current_drift_step * total_force_2
|
|
204
|
+
|
|
205
|
+
geom_num_list_1 += velocity_1
|
|
206
|
+
geom_num_list_2 += velocity_2
|
|
207
|
+
|
|
208
|
+
# Check Convergence
|
|
209
|
+
drift_metric = max(self.RMS(force_perp_1), self.RMS(force_perp_2))
|
|
210
|
+
|
|
211
|
+
if d_step % 5 == 0:
|
|
212
|
+
info_dat = {
|
|
213
|
+
"energy_1": energy_1, "energy_2": energy_2,
|
|
214
|
+
"gradient_1": gradient_1, "gradient_2": gradient_2,
|
|
215
|
+
"perp_force_1": force_perp_1, "perp_force_2": force_perp_2,
|
|
216
|
+
"spring_force_1": spring_force_1, "spring_force_2": spring_force_2,
|
|
217
|
+
"spring_length": ds,
|
|
218
|
+
"step_size": current_drift_step,
|
|
219
|
+
"convergence_metric": drift_metric
|
|
220
|
+
}
|
|
221
|
+
self.print_info(info_dat, f"Cycle {cycle} - Drifting {d_step}")
|
|
222
|
+
|
|
223
|
+
if drift_metric < self.RMS_FORCE_THRESHOLD:
|
|
224
|
+
print(f" >> Drifting converged at step {d_step}")
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
# =========================================================================
|
|
228
|
+
# 2. Climbing Phase
|
|
229
|
+
# =========================================================================
|
|
230
|
+
print(f"--- Climbing Phase (Cycle {cycle}) ---")
|
|
231
|
+
|
|
232
|
+
iter_label = f"{cycle}_climb"
|
|
233
|
+
|
|
234
|
+
input_path_1 = self._make_next_input(FIO1, geom_num_list_1, element_list, electric_charge_and_multiplicity, iter_label + "_img1")
|
|
235
|
+
input_path_2 = self._make_next_input(FIO1, geom_num_list_2, element_list, electric_charge_and_multiplicity, iter_label + "_img2")
|
|
236
|
+
|
|
237
|
+
energy_1, gradient_1, g1_read, error_flag_1 = SP1.single_point(input_path_1, element_list, iter_label + "_img1", electric_charge_and_multiplicity, self.config.force_data["xtb"])
|
|
238
|
+
energy_2, gradient_2, g2_read, error_flag_2 = SP1.single_point(input_path_2, element_list, iter_label + "_img2", electric_charge_and_multiplicity, self.config.force_data["xtb"])
|
|
239
|
+
|
|
240
|
+
geom_num_list_1, geom_num_list_2 = Calculationtools().kabsch_algorithm(geom_num_list_1, geom_num_list_2)
|
|
241
|
+
|
|
242
|
+
ds, vs = self.get_spring_vectors(geom_num_list_1, geom_num_list_2)
|
|
243
|
+
grad_par_1, _ = self.decompose_gradient(gradient_1, vs)
|
|
244
|
+
grad_par_2, _ = self.decompose_gradient(gradient_2, vs)
|
|
245
|
+
|
|
246
|
+
active_climb_step = self.climb_step_size
|
|
247
|
+
|
|
248
|
+
# Move UP
|
|
249
|
+
geom_num_list_1 += active_climb_step * grad_par_1
|
|
250
|
+
geom_num_list_2 += active_climb_step * grad_par_2
|
|
251
|
+
|
|
252
|
+
# Update 'new_geom' for final output reference
|
|
253
|
+
new_geom_1 = geom_num_list_1.copy()
|
|
254
|
+
new_geom_2 = geom_num_list_2.copy()
|
|
255
|
+
|
|
256
|
+
grad_norm_1 = np.linalg.norm(gradient_1)
|
|
257
|
+
grad_norm_2 = np.linalg.norm(gradient_2)
|
|
258
|
+
global_metric = min(grad_norm_1, grad_norm_2)
|
|
259
|
+
|
|
260
|
+
info_dat = {
|
|
261
|
+
"energy_1": energy_1, "energy_2": energy_2,
|
|
262
|
+
"gradient_1": gradient_1, "gradient_2": gradient_2,
|
|
263
|
+
"par_force_1": grad_par_1, "par_force_2": grad_par_2,
|
|
264
|
+
"spring_length": ds, "convergence_metric": global_metric,
|
|
265
|
+
"step_size": active_climb_step
|
|
266
|
+
}
|
|
267
|
+
self.print_info(info_dat, f"Cycle {cycle} - Climbing")
|
|
268
|
+
|
|
269
|
+
ENERGY_LIST_1.append(energy_1 * self.config.hartree2kcalmol)
|
|
270
|
+
ENERGY_LIST_2.append(energy_2 * self.config.hartree2kcalmol)
|
|
271
|
+
GRAD_LIST_1.append(grad_norm_1)
|
|
272
|
+
GRAD_LIST_2.append(grad_norm_2)
|
|
273
|
+
|
|
274
|
+
if cycle > 5 and global_metric < self.MAX_FORCE_THRESHOLD:
|
|
275
|
+
print("!!! Global Convergence Reached !!!")
|
|
276
|
+
print(f"Saddle point candidate found around cycle {cycle}")
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
# --- END OF OPTIMIZATION LOOP ---
|
|
280
|
+
|
|
281
|
+
# Save the optimized structure
|
|
282
|
+
if new_geom_1 is not None and new_geom_2 is not None:
|
|
283
|
+
avg_geom = (new_geom_1 + new_geom_2) / 2.0
|
|
284
|
+
avg_geom_ang = avg_geom * self.config.bohr2angstroms
|
|
285
|
+
|
|
286
|
+
dir_name = os.path.basename(os.path.normpath(self.config.iEIP_FOLDER_DIRECTORY))
|
|
287
|
+
output_xyz_name = f"{dir_name}_optimized.xyz"
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
with open(output_xyz_name, "w") as f:
|
|
291
|
+
num_atoms = len(element_list)
|
|
292
|
+
f.write(f"{num_atoms}\n")
|
|
293
|
+
f.write(f"SPM Optimized Saddle Point (Average)\n")
|
|
294
|
+
for i, elem in enumerate(element_list):
|
|
295
|
+
x, y, z = avg_geom_ang[i]
|
|
296
|
+
f.write(f"{elem} {x:.10f} {y:.10f} {z:.10f}\n")
|
|
297
|
+
print(f"\n[Success] Final optimized structure saved to: {output_xyz_name}")
|
|
298
|
+
except Exception as e:
|
|
299
|
+
print(f"\n[Error] Failed to save optimized structure: {e}")
|
|
300
|
+
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
def _make_next_input(self, FIO, geom, element_list, charge_mult, iter_label):
|
|
304
|
+
"""
|
|
305
|
+
Creates a PSI4 input file and returns its path.
|
|
306
|
+
"""
|
|
307
|
+
geom_tolist = (geom * self.config.bohr2angstroms).tolist()
|
|
308
|
+
for i, elem in enumerate(element_list):
|
|
309
|
+
geom_tolist[i].insert(0, elem)
|
|
310
|
+
geom_tolist.insert(0, charge_mult)
|
|
311
|
+
|
|
312
|
+
# FIO.make_psi4_input_file usually returns the directory or path.
|
|
313
|
+
# Ensure this matches your FIO implementation.
|
|
314
|
+
return FIO.make_psi4_input_file([geom_tolist], iter_label)
|
multioptpy/ieip.py
CHANGED
|
@@ -15,6 +15,7 @@ from multioptpy.OtherMethod.addf import ADDFlikeMethod
|
|
|
15
15
|
from multioptpy.OtherMethod.twopshs import twoPSHSlikeMethod
|
|
16
16
|
from multioptpy.OtherMethod.elastic_image_pair import ElasticImagePair
|
|
17
17
|
from multioptpy.OtherMethod.modelfunction import ModelFunctionOptimizer
|
|
18
|
+
from multioptpy.OtherMethod.spring_pair_method import SpringPairMethod
|
|
18
19
|
|
|
19
20
|
class IEIPConfig:
|
|
20
21
|
"""
|
|
@@ -174,6 +175,9 @@ class IEIPConfig:
|
|
|
174
175
|
self.dimer_trial_angle = getattr(args, 'dimer_trial_angle', np.pi / 32.0)
|
|
175
176
|
self.dimer_max_iterations = getattr(args, 'dimer_max_iterations', 1000)
|
|
176
177
|
|
|
178
|
+
# Add config flag check if needed
|
|
179
|
+
self.use_spm = getattr(args, 'use_spm', False)
|
|
180
|
+
|
|
177
181
|
# Create output directory
|
|
178
182
|
os.mkdir(self.iEIP_FOLDER_DIRECTORY)
|
|
179
183
|
|
|
@@ -205,7 +209,9 @@ class iEIP:
|
|
|
205
209
|
self.addf_like_method = ADDFlikeMethod(self.config)
|
|
206
210
|
self.twoPshs = twoPSHSlikeMethod(self.config)
|
|
207
211
|
self.dimer_method = DimerMethod(self.config)
|
|
208
|
-
|
|
212
|
+
self.spring_pair_method = SpringPairMethod(self.config)
|
|
213
|
+
|
|
214
|
+
|
|
209
215
|
|
|
210
216
|
def optimize(self):
|
|
211
217
|
"""Load calculation modules based on configuration and run optimization"""
|
|
@@ -322,7 +328,13 @@ class iEIP:
|
|
|
322
328
|
SP_list[0],
|
|
323
329
|
self.config.electric_charge_and_multiplicity_list[0],
|
|
324
330
|
FIO_img_list[0])
|
|
325
|
-
|
|
331
|
+
elif getattr(self.config, 'use_spm', False):
|
|
332
|
+
print("Using Spring Pair Method (SPM)")
|
|
333
|
+
self.spring_pair_method.iteration(
|
|
334
|
+
file_directory_list[0],
|
|
335
|
+
SP_list[0], element_list_list[0],
|
|
336
|
+
self.config.electric_charge_and_multiplicity_list[0],
|
|
337
|
+
FIO_img_list[0])
|
|
326
338
|
else:
|
|
327
339
|
self.elastic_image_pair.iteration(
|
|
328
340
|
file_directory_list[0], file_directory_list[1],
|
multioptpy/interface.py
CHANGED
|
@@ -5,7 +5,7 @@ import numpy as np
|
|
|
5
5
|
|
|
6
6
|
"""
|
|
7
7
|
MultiOptPy
|
|
8
|
-
Copyright (C) 2023-
|
|
8
|
+
Copyright (C) 2023-2026 ss0832
|
|
9
9
|
|
|
10
10
|
This program is free software: you can redistribute it and/or modify
|
|
11
11
|
it under the terms of the GNU General Public License as published by
|
|
@@ -133,6 +133,8 @@ def call_ieipparser(parser):
|
|
|
133
133
|
parser.add_argument('-dimer_sep','--dimer_separation', help="set dimer separation (default: 0.0001)", type=float, default=0.0001)
|
|
134
134
|
parser.add_argument('-dimer_trial_angle','--dimer_trial_angle', help="set dimer trial angle (default: pi/32)", type=float, default=np.pi / 32.0)
|
|
135
135
|
parser.add_argument('-dimer_maxiter','--dimer_max_iterations', help="set max iterations for dimer method (default: 1000)", type=int, default=1000)
|
|
136
|
+
parser.add_argument('-use_spm','--use_spm', help="Use Spring Pair method for searching direction of TS (default: False)", action='store_true'
|
|
137
|
+
)
|
|
136
138
|
return parser
|
|
137
139
|
|
|
138
140
|
def call_optimizeparser(parser):
|
multioptpy/optimization.py
CHANGED
|
@@ -339,8 +339,6 @@ class StandardHandler(BasePotentialHandler):
|
|
|
339
339
|
state.Model_hess = copy.deepcopy(self.calculator.Model_hess)
|
|
340
340
|
return state
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
344
342
|
class ModelFunctionHandler(BasePotentialHandler):
|
|
345
343
|
def __init__(self, calc1, calc2, mf_args, config, file_io, base_dir, force_data):
|
|
346
344
|
super().__init__(config, file_io, base_dir, force_data)
|
|
@@ -360,13 +358,11 @@ class ModelFunctionHandler(BasePotentialHandler):
|
|
|
360
358
|
self.bitss_geom2_history = []
|
|
361
359
|
self.bitss_ref_geom = None
|
|
362
360
|
|
|
363
|
-
|
|
364
361
|
self.bitss_initialized = False
|
|
365
362
|
|
|
366
363
|
if self.is_bitss:
|
|
367
364
|
self._setup_bitss_initialization()
|
|
368
365
|
|
|
369
|
-
|
|
370
366
|
def _load_mf_class(self):
|
|
371
367
|
if self.method_name == "opt_meci":
|
|
372
368
|
return OptMECI()
|
|
@@ -396,7 +392,6 @@ class ModelFunctionHandler(BasePotentialHandler):
|
|
|
396
392
|
if len(self.params) < 1:
|
|
397
393
|
raise ValueError("BITSS requires a reference geometry file path.")
|
|
398
394
|
|
|
399
|
-
|
|
400
395
|
temp_io = FileIO(self.base_dir, self.params[0])
|
|
401
396
|
g_list, _, _ = temp_io.make_geometry_list(self.config.electric_charge_and_multiplicity)
|
|
402
397
|
|
|
@@ -414,7 +409,6 @@ class ModelFunctionHandler(BasePotentialHandler):
|
|
|
414
409
|
n_atoms = len(self.single_element_list)
|
|
415
410
|
geom_1, geom_2 = state.geometry[:n_atoms], state.geometry[n_atoms:]
|
|
416
411
|
|
|
417
|
-
|
|
418
412
|
if not self.bitss_initialized:
|
|
419
413
|
self.mf_instance = BITSSModelFunction(geom_1, geom_2)
|
|
420
414
|
self._apply_config_params()
|
|
@@ -422,11 +416,18 @@ class ModelFunctionHandler(BasePotentialHandler):
|
|
|
422
416
|
else:
|
|
423
417
|
geom_1 = geom_2 = state.geometry
|
|
424
418
|
|
|
425
|
-
|
|
426
419
|
# State 1
|
|
427
420
|
e1, g1, ex1 = self._run_calc(self.calc1, geom_1, self.single_element_list, self.config.electric_charge_and_multiplicity, "State1", iter_idx)
|
|
421
|
+
|
|
428
422
|
# State 2
|
|
429
|
-
|
|
423
|
+
if self.is_bitss:
|
|
424
|
+
chg_mult_2 = self.config.electric_charge_and_multiplicity
|
|
425
|
+
else:
|
|
426
|
+
if len(self.params) >= 2:
|
|
427
|
+
chg_mult_2 = [int(self.params[0]), int(self.params[1])]
|
|
428
|
+
else:
|
|
429
|
+
chg_mult_2 = self.config.electric_charge_and_multiplicity
|
|
430
|
+
|
|
430
431
|
e2, g2, ex2 = self._run_calc(self.calc2, geom_2, self.single_element_list, chg_mult_2, "State2", iter_idx)
|
|
431
432
|
|
|
432
433
|
if ex1 or ex2:
|
|
@@ -530,7 +531,6 @@ class ModelFunctionHandler(BasePotentialHandler):
|
|
|
530
531
|
|
|
531
532
|
return state
|
|
532
533
|
|
|
533
|
-
|
|
534
534
|
def _make_block_diag_hess(self, h1, h2):
|
|
535
535
|
d1 = h1.shape[0]
|
|
536
536
|
d2 = h2.shape[0]
|
|
@@ -554,9 +554,30 @@ class ModelFunctionHandler(BasePotentialHandler):
|
|
|
554
554
|
os.makedirs(run_dir, exist_ok=True)
|
|
555
555
|
old_dir = calc_inst.BPA_FOLDER_DIRECTORY
|
|
556
556
|
calc_inst.BPA_FOLDER_DIRECTORY = run_dir
|
|
557
|
+
|
|
558
|
+
# Charge/Multiplicity update for PySCF compatibility
|
|
559
|
+
calc_inst.electronic_charge = chg_mult[0]
|
|
560
|
+
calc_inst.spin_multiplicity = chg_mult[1]
|
|
561
|
+
|
|
557
562
|
geom_str = self.file_io.print_geometry_list(geom * self.config.bohr2angstroms, elems, chg_mult, display_flag=True)
|
|
558
563
|
inp_path = self.file_io.make_psi4_input_file(geom_str, iter_idx, path=run_dir)
|
|
559
|
-
|
|
564
|
+
|
|
565
|
+
# Method string for xTB
|
|
566
|
+
method_str = getattr(calc_inst, "xtb_method", "")
|
|
567
|
+
if method_str is None:
|
|
568
|
+
method_str = ""
|
|
569
|
+
|
|
570
|
+
# [FIX] Convert list to numpy array (int) to avoid 'list has no attribute tolist' error in tblite tools
|
|
571
|
+
atom_nums = np.array([element_number(el) for el in elems], dtype=int)
|
|
572
|
+
|
|
573
|
+
e, g, _, ex = calc_inst.single_point(
|
|
574
|
+
inp_path,
|
|
575
|
+
atom_nums, # Passing numpy array instead of list
|
|
576
|
+
iter_idx,
|
|
577
|
+
chg_mult,
|
|
578
|
+
method=method_str
|
|
579
|
+
)
|
|
580
|
+
|
|
560
581
|
calc_inst.BPA_FOLDER_DIRECTORY = old_dir
|
|
561
582
|
return e, g, ex
|
|
562
583
|
|
|
@@ -569,8 +590,7 @@ class ModelFunctionHandler(BasePotentialHandler):
|
|
|
569
590
|
f.write(f"{len(g)}\nBITSS_Step {s}\n")
|
|
570
591
|
for i, atom in enumerate(g):
|
|
571
592
|
f.write(f"{self.single_element_list[i]:2s} {atom[0]:12.8f} {atom[1]:12.8f} {atom[2]:12.8f}\n")
|
|
572
|
-
|
|
573
|
-
|
|
593
|
+
|
|
574
594
|
class ONIOMHandler(BasePotentialHandler):
|
|
575
595
|
"""
|
|
576
596
|
Handles ONIOM calculations with microiterations.
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MultiOptPy
|
|
3
|
-
Version: 1.20.
|
|
3
|
+
Version: 1.20.5
|
|
4
4
|
Summary: Multifunctional geometry optimization tools for quantum chemical calculations.
|
|
5
5
|
Author-email: ss0832 <highlighty876@gmail.com>
|
|
6
|
-
License:
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
7
|
Requires-Python: >=3.12
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: numpy>=2.2.0
|
|
11
11
|
Requires-Dist: scipy>=1.13.0
|
|
12
|
-
Requires-Dist: matplotlib
|
|
12
|
+
Requires-Dist: matplotlib>=3.10.0
|
|
13
13
|
Requires-Dist: torch~=2.6.0
|
|
14
|
-
Requires-Dist: pyscf
|
|
15
|
-
Requires-Dist: tblite
|
|
14
|
+
Requires-Dist: pyscf>=2.9.0
|
|
15
|
+
Requires-Dist: tblite>=0.4.0
|
|
16
16
|
Requires-Dist: ase~=3.26.0
|
|
17
17
|
Requires-Dist: fairchem-core~=2.7.0
|
|
18
|
-
Requires-Dist: sympy
|
|
18
|
+
Requires-Dist: sympy>=1.13.0
|
|
19
19
|
Dynamic: license-file
|
|
20
20
|
|
|
21
21
|
# MultiOptPy
|
|
@@ -29,7 +29,7 @@ Dynamic: license-file
|
|
|
29
29
|
[](https://buymeacoffee.com/ss0832)
|
|
30
30
|
|
|
31
31
|
[](https://pepy.tech/projects/multioptpy)
|
|
32
|
-
[](https://doi.org/10.5281/zenodo.17973395)
|
|
33
33
|
|
|
34
34
|
If this tool helped your studies, education, or saved your time, I'd appreciate a coffee!
|
|
35
35
|
Your support serves as a great encouragement for this personal project and fuels my next journey.
|
|
@@ -80,9 +80,9 @@ conda create -n test_mop python=3.12.7
|
|
|
80
80
|
conda activate test_mop
|
|
81
81
|
|
|
82
82
|
## 3. Download and install MultiOptPy:
|
|
83
|
-
wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.20.
|
|
84
|
-
unzip v1.20.
|
|
85
|
-
cd MultiOptPy-1.20.
|
|
83
|
+
wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.20.4.zip
|
|
84
|
+
unzip v1.20.4.zip
|
|
85
|
+
cd MultiOptPy-1.20.4
|
|
86
86
|
pip install -r requirements.txt
|
|
87
87
|
|
|
88
88
|
## 4. Copy the test configuration file and run the AutoTS workflow:
|
|
@@ -107,10 +107,10 @@ python run_autots.py aldol_rxn.xyz -cfg config_autots_run_xtb_test.json
|
|
|
107
107
|
# Installation via pip (Linux)
|
|
108
108
|
conda create -n <env-name> python=3.12 pip
|
|
109
109
|
conda activate <env-name>
|
|
110
|
-
pip install git+https://github.com/ss0832/MultiOptPy.git@v1.20.
|
|
111
|
-
wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.20.
|
|
112
|
-
unzip v1.20.
|
|
113
|
-
cd MultiOptPy-1.20.
|
|
110
|
+
pip install git+https://github.com/ss0832/MultiOptPy.git@v1.20.4
|
|
111
|
+
wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.20.4.zip
|
|
112
|
+
unzip v1.20.4.zip
|
|
113
|
+
cd MultiOptPy-1.20.4
|
|
114
114
|
|
|
115
115
|
## 💻 Command Line Interface (CLI) Functionality (v1.20.2)
|
|
116
116
|
# The following eight core functionalities are available as direct executable commands in your terminal after installation:
|
|
@@ -412,13 +412,13 @@ If you use MultiOptPy in your research, please cite it as follows:
|
|
|
412
412
|
month = dec,
|
|
413
413
|
year = 2025,
|
|
414
414
|
publisher = {Zenodo},
|
|
415
|
-
version = {v1.20.
|
|
416
|
-
doi = {10.5281/zenodo.
|
|
417
|
-
url = {
|
|
415
|
+
version = {v1.20.4},
|
|
416
|
+
doi = {10.5281/zenodo.17973395},
|
|
417
|
+
url = {https://doi.org/10.5281/zenodo.17973395}
|
|
418
418
|
}
|
|
419
419
|
```
|
|
420
420
|
```
|
|
421
|
-
ss0832. (2025). MultiOptPy: Multifunctional geometry optimization tools for quantum chemical calculations (v1.20.
|
|
421
|
+
ss0832. (2025). MultiOptPy: Multifunctional geometry optimization tools for quantum chemical calculations (v1.20.4). Zenodo. https://doi.org/10.5281/zenodo.17973395
|
|
422
422
|
```
|
|
423
423
|
|
|
424
424
|
## Setting Up an Environment for Using NNP(UMA) on Windows 11
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
multioptpy/__init__.py,sha256=OJ7BAhHT2pWsIdC5wZfhCA5J8zW-odvaym9ySpPZ3Hw,125
|
|
2
2
|
multioptpy/entrypoints.py,sha256=rizxMJSbFq2aAKpcsPIy1CTIh_ZCGkDpCEEkvMOy6uc,39130
|
|
3
3
|
multioptpy/fileio.py,sha256=BJVhls9P_GU-ZB_nZgTgUPejru8-lJ1Qi-typHsG0WY,27175
|
|
4
|
-
multioptpy/ieip.py,sha256=
|
|
5
|
-
multioptpy/interface.py,sha256=
|
|
4
|
+
multioptpy/ieip.py,sha256=XjN3ii1xVclXdSVb6g1gO1Q_7VtOBFR9jK7MjdFZrFM,16786
|
|
5
|
+
multioptpy/interface.py,sha256=Xohjpk1ofcEmDygGg93dRh9Dzha6VjbtcefmcoXqoZE,82013
|
|
6
6
|
multioptpy/irc.py,sha256=lTb1atfC5_QgW9a7FqQC-e-CXlTEhi1lms-zie_dEhI,20777
|
|
7
7
|
multioptpy/moleculardynamics.py,sha256=FGA2fulBbNx26gCUSoauaOMoT0dIIddemXEiAoHC6p0,20359
|
|
8
8
|
multioptpy/neb.py,sha256=eQQsB8HrDOZ4rlvHOUIwfR52QNDvuhvH1am5h6K7Ho0,59339
|
|
9
|
-
multioptpy/optimization.py,sha256=
|
|
9
|
+
multioptpy/optimization.py,sha256=FBMueS5ChGCZvCHsY7S8hCLf4nrWvjNMcEfWfj8vIdU,101480
|
|
10
10
|
multioptpy/optimizer.py,sha256=Nm6uDEMiv9u5_CPdhQYqGzwmCqHNzDIdYeYIn9z5EwM,42047
|
|
11
11
|
multioptpy/Calculator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
multioptpy/Calculator/ase_calculation_tools.py,sha256=
|
|
12
|
+
multioptpy/Calculator/ase_calculation_tools.py,sha256=TxLpUsXM2Cg3U4jUfquZzq6IhGz7FbLj1GO8nWijzzw,20830
|
|
13
13
|
multioptpy/Calculator/dxtb_calculation_tools.py,sha256=s3LTuypTsfZEIrnkuOQUOCegfh9LCyUG0tI3zzYiliQ,16492
|
|
14
14
|
multioptpy/Calculator/emt_calculation_tools.py,sha256=aq4I3yM0_A7PFCX2sB5qHfEoij2LP0cqYu_BQkMztMQ,21762
|
|
15
15
|
multioptpy/Calculator/gpaw_calculation_tools.py,sha256=P2a8YSCzHvgSdvPS_HQo-liLkVBQ1ePr4tooHJm2aL4,8079
|
|
@@ -25,10 +25,11 @@ multioptpy/Calculator/ase_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
|
25
25
|
multioptpy/Calculator/ase_tools/fairchem.py,sha256=n7sZVEik1iw-gjTXyUx6uGQbJhn9FhhCJXatKQGK8oY,1627
|
|
26
26
|
multioptpy/Calculator/ase_tools/gamess.py,sha256=OvRD5TGze--x-ZKqLPUAC78UwVlZwLDf-B4VoHrjp9I,904
|
|
27
27
|
multioptpy/Calculator/ase_tools/gaussian.py,sha256=y4r64zX2o5zORUDt_h6SmnsBQ3wM8WQr7zP1XVmcw74,7829
|
|
28
|
+
multioptpy/Calculator/ase_tools/gxtb_dev.py,sha256=cBWenNu4PYtgQ-kelwmMqJ0FK4WxnCd-IGOA_BxX-y0,1529
|
|
28
29
|
multioptpy/Calculator/ase_tools/mace.py,sha256=AsyUkTFrOI-kSnDT4d7ehViQfCRT5fG8wdYUTn56RCw,928
|
|
29
30
|
multioptpy/Calculator/ase_tools/mopac.py,sha256=RUMNikbmVxIvoTi4H8JU3fXY2vBu6BzPxcv0NPd_L-k,723
|
|
30
31
|
multioptpy/Calculator/ase_tools/nwchem.py,sha256=0NPIaX1_mzkzLh_r9e3WJA6fEiNrksEbXFwbATHkNuw,1162
|
|
31
|
-
multioptpy/Calculator/ase_tools/orca.py,sha256
|
|
32
|
+
multioptpy/Calculator/ase_tools/orca.py,sha256=JjTCvW3YT1KwaImkIxyd8FcUgz8BHO5ZFGHqiX3lJKc,10141
|
|
32
33
|
multioptpy/Calculator/ase_tools/pygfn0.py,sha256=N-Rrtu8wvduPxRSGQMYK9LmQiWCtfCbkBOTXjhHK-gE,1468
|
|
33
34
|
multioptpy/Constraint/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
35
|
multioptpy/Constraint/constraint_condition.py,sha256=wS14fYCzP1V4_d1HLElW3a6fxKfEEX3EjxPvvbRPhZ8,44202
|
|
@@ -166,6 +167,7 @@ multioptpy/OtherMethod/dimer.py,sha256=54oqeExIqP_PPBpaZAwo59X11n6k1BgttpYIGKnJ7
|
|
|
166
167
|
multioptpy/OtherMethod/elastic_image_pair.py,sha256=YSXjfrmHRRnl5YZ5JVZMLsDSCyfY911Q2F5qp6dj1rQ,33586
|
|
167
168
|
multioptpy/OtherMethod/modelfunction.py,sha256=GX_YvA8rOzcKeaZcth6L-S0q9wdsTLA07E9sjOaFyRU,26168
|
|
168
169
|
multioptpy/OtherMethod/newton_traj.py,sha256=gozEqyOCpD4zbYMf3IwPKIMC7v4D7ddXCu1MUz8tzxE,20154
|
|
170
|
+
multioptpy/OtherMethod/spring_pair_method.py,sha256=6YdLCIO-OOt_Mzzf8JFFtWVYAIa1Tsdi1VCxEbbPuto,15577
|
|
169
171
|
multioptpy/OtherMethod/twopshs.py,sha256=GmBLKTNK-HqFpVmGgwmrtw9-IOf_hPYlUTslx95Sl20,49969
|
|
170
172
|
multioptpy/PESAnalyzer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
171
173
|
multioptpy/PESAnalyzer/calc_irc_curvature.py,sha256=YVZBydXcYLfO0Y8ZYGS7WXokkORfVQaO6zPbXwhQJt4,4986
|
|
@@ -241,9 +243,9 @@ multioptpy/Wrapper/ieip_wrapper.py,sha256=H0Fa7x5mzyFu5rnig7GdcZGX9LFclzRxDuREX5
|
|
|
241
243
|
multioptpy/Wrapper/md_wrapper.py,sha256=EGQKY8BwBbjWkXxKQGlelzht872OsUp9GmyxPDgWav4,3050
|
|
242
244
|
multioptpy/Wrapper/neb_wrapper.py,sha256=ZOyHhuSDKA5hABnQB7PYPfbQ9OyaAj9KKBeKu_HLiY4,3254
|
|
243
245
|
multioptpy/Wrapper/optimize_wrapper.py,sha256=b5DoiSb5Y4YjzYaSfxNa4A5QMn7_UzAvActPOpL78Ok,2667
|
|
244
|
-
multioptpy-1.20.
|
|
245
|
-
multioptpy-1.20.
|
|
246
|
-
multioptpy-1.20.
|
|
247
|
-
multioptpy-1.20.
|
|
248
|
-
multioptpy-1.20.
|
|
249
|
-
multioptpy-1.20.
|
|
246
|
+
multioptpy-1.20.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
247
|
+
multioptpy-1.20.5.dist-info/METADATA,sha256=FWxTaqF3RW7uxZd5o8HJGtc6kdUI6VRK8RX6cFnF--k,16712
|
|
248
|
+
multioptpy-1.20.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
249
|
+
multioptpy-1.20.5.dist-info/entry_points.txt,sha256=u2Y0tgYMu7UyZumhLCfoME92lCQXhurAAsixL_B3caw,404
|
|
250
|
+
multioptpy-1.20.5.dist-info/top_level.txt,sha256=_MXQNcS1xJbRbGnYUM-F_G-PlLK7wTIkuqx-EvfAhRc,11
|
|
251
|
+
multioptpy-1.20.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|