MultiOptPy 1.20.2__py3-none-any.whl → 1.20.3__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 +13 -0
- multioptpy/Calculator/ase_tools/fairchem.py +12 -7
- multioptpy/Constraint/constraint_condition.py +208 -245
- multioptpy/ModelFunction/binary_image_ts_search_model_function.py +111 -18
- multioptpy/ModelFunction/opt_meci.py +94 -27
- multioptpy/ModelFunction/opt_mesx.py +47 -15
- multioptpy/ModelFunction/opt_mesx_2.py +35 -18
- multioptpy/Optimizer/crsirfo.py +182 -0
- multioptpy/Optimizer/mf_rsirfo.py +266 -0
- multioptpy/Optimizer/mode_following.py +273 -0
- multioptpy/Utils/calc_tools.py +1 -0
- multioptpy/fileio.py +13 -6
- multioptpy/interface.py +3 -2
- multioptpy/optimization.py +2139 -1259
- multioptpy/optimizer.py +158 -6
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/METADATA +497 -438
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/RECORD +21 -18
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/WHEEL +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/entry_points.txt +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/licenses/LICENSE +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/top_level.txt +0 -0
multioptpy/optimization.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import sys
|
|
2
1
|
import os
|
|
3
|
-
import
|
|
2
|
+
import sys
|
|
4
3
|
import glob
|
|
4
|
+
import copy
|
|
5
5
|
import itertools
|
|
6
|
+
import inspect
|
|
6
7
|
import datetime
|
|
7
|
-
|
|
8
|
-
|
|
9
8
|
import numpy as np
|
|
10
9
|
|
|
11
10
|
from multioptpy.optimizer import CalculateMoveVector
|
|
@@ -22,32 +21,46 @@ from multioptpy.Utils.calc_tools import CalculationStructInfo, Calculationtools
|
|
|
22
21
|
from multioptpy.Constraint.constraint_condition import ProjectOutConstrain
|
|
23
22
|
from multioptpy.irc import IRC
|
|
24
23
|
from multioptpy.Utils.bond_connectivity import judge_shape_condition
|
|
25
|
-
from multioptpy.Utils.oniom import
|
|
24
|
+
from multioptpy.Utils.oniom import (
|
|
25
|
+
separate_high_layer_and_low_layer,
|
|
26
|
+
specify_link_atom_pairs,
|
|
27
|
+
link_number_high_layer_and_low_layer,
|
|
28
|
+
)
|
|
26
29
|
from multioptpy.Utils.symmetry_analyzer import analyze_symmetry
|
|
27
30
|
from multioptpy.Thermo.normal_mode_analyzer import MolecularVibrations
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
from multioptpy.ModelFunction.opt_meci import OptMECI
|
|
32
|
+
from multioptpy.ModelFunction.opt_mesx import OptMESX
|
|
33
|
+
from multioptpy.ModelFunction.opt_mesx_2 import OptMESX2
|
|
34
|
+
from multioptpy.ModelFunction.seam_model_function import SeamModelFunction
|
|
35
|
+
from multioptpy.ModelFunction.conical_model_function import ConicalModelFunction
|
|
36
|
+
from multioptpy.ModelFunction.avoiding_model_function import AvoidingModelFunction
|
|
37
|
+
from multioptpy.ModelFunction.binary_image_ts_search_model_function import BITSSModelFunction
|
|
38
|
+
# =====================================================================================
|
|
39
|
+
# 1. Configuration (Immutable Settings)
|
|
40
|
+
# =====================================================================================
|
|
30
41
|
class OptimizationConfig:
|
|
31
42
|
"""
|
|
32
|
-
|
|
33
|
-
Initialized from 'args'.
|
|
43
|
+
Immutable settings derived from CLI args.
|
|
34
44
|
"""
|
|
45
|
+
|
|
35
46
|
def __init__(self, args):
|
|
36
|
-
# Constants like UVL
|
|
37
47
|
UVL = UnitValueLib()
|
|
38
48
|
np.set_printoptions(precision=12, floatmode="fixed", suppress=True)
|
|
49
|
+
|
|
50
|
+
# Physical constants
|
|
39
51
|
self.hartree2kcalmol = UVL.hartree2kcalmol
|
|
40
52
|
self.bohr2angstroms = UVL.bohr2angstroms
|
|
41
53
|
self.hartree2kjmol = UVL.hartree2kjmol
|
|
42
54
|
|
|
43
|
-
#
|
|
55
|
+
# Base args
|
|
56
|
+
self.args = args
|
|
44
57
|
self._set_convergence_criteria(args)
|
|
45
58
|
|
|
46
|
-
#
|
|
59
|
+
# Core parameters
|
|
47
60
|
self.microiter_num = 100
|
|
48
|
-
self.args = args # Keep a reference to args
|
|
49
61
|
self.FC_COUNT = args.calc_exact_hess
|
|
50
|
-
self.
|
|
62
|
+
self.mFC_COUNT = args.calc_model_hess
|
|
63
|
+
self.temperature = float(args.temperature) if args.temperature else 0.0
|
|
51
64
|
self.CMDS = args.cmds
|
|
52
65
|
self.PCA = args.pca
|
|
53
66
|
self.DELTA = "x" if args.DELTA == "x" else float(args.DELTA)
|
|
@@ -57,11 +70,11 @@ class OptimizationConfig:
|
|
|
57
70
|
self.BASIS_SET = args.basisset
|
|
58
71
|
self.FUNCTIONAL = args.functional
|
|
59
72
|
self.excited_state = args.excited_state
|
|
60
|
-
|
|
61
|
-
#
|
|
73
|
+
|
|
74
|
+
# Sub-basis and ECP
|
|
62
75
|
self._check_sub_basisset(args)
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
|
|
77
|
+
# Advanced settings
|
|
65
78
|
self.DC_check_dist = float(args.dissociate_check)
|
|
66
79
|
self.unrestrict = args.unrestrict
|
|
67
80
|
self.irc = args.intrinsic_reaction_coordinates
|
|
@@ -82,40 +95,62 @@ class OptimizationConfig:
|
|
|
82
95
|
self.software_path_file = args.software_path_file
|
|
83
96
|
self.koopman_analysis = args.koopman
|
|
84
97
|
self.detect_negative_eigenvalues = args.detect_negative_eigenvalues
|
|
98
|
+
self.excited_state = args.excited_state
|
|
99
|
+
self.spin_multiplicity = args.spin_multiplicity
|
|
100
|
+
self.electronic_charge = args.electronic_charge
|
|
101
|
+
self.model_function = args.model_function
|
|
102
|
+
|
|
103
|
+
|
|
85
104
|
|
|
86
105
|
def _set_convergence_criteria(self, args):
|
|
87
|
-
# Original _set_convergence_criteria method code
|
|
88
106
|
if args.tight_convergence_criteria and not args.loose_convergence_criteria:
|
|
89
107
|
self.MAX_FORCE_THRESHOLD = 0.000015
|
|
90
108
|
self.RMS_FORCE_THRESHOLD = 0.000010
|
|
91
109
|
self.MAX_DISPLACEMENT_THRESHOLD = 0.000060
|
|
92
110
|
self.RMS_DISPLACEMENT_THRESHOLD = 0.000040
|
|
111
|
+
|
|
112
|
+
if len(args.projection_constrain) > 0:
|
|
113
|
+
self.MAX_DISPLACEMENT_THRESHOLD *= 4
|
|
114
|
+
self.RMS_DISPLACEMENT_THRESHOLD *= 4
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
93
118
|
elif not args.tight_convergence_criteria and args.loose_convergence_criteria:
|
|
94
119
|
self.MAX_FORCE_THRESHOLD = 0.0030
|
|
95
120
|
self.RMS_FORCE_THRESHOLD = 0.0020
|
|
96
121
|
self.MAX_DISPLACEMENT_THRESHOLD = 0.0100
|
|
97
122
|
self.RMS_DISPLACEMENT_THRESHOLD = 0.0070
|
|
123
|
+
|
|
124
|
+
|
|
98
125
|
else:
|
|
99
126
|
self.MAX_FORCE_THRESHOLD = 0.0003
|
|
100
127
|
self.RMS_FORCE_THRESHOLD = 0.0002
|
|
101
128
|
self.MAX_DISPLACEMENT_THRESHOLD = 0.0015
|
|
102
129
|
self.RMS_DISPLACEMENT_THRESHOLD = 0.0010
|
|
103
|
-
|
|
130
|
+
if len(args.projection_constrain) > 0:
|
|
131
|
+
self.MAX_DISPLACEMENT_THRESHOLD *= 4
|
|
132
|
+
self.RMS_DISPLACEMENT_THRESHOLD *= 4
|
|
133
|
+
|
|
104
134
|
def _check_sub_basisset(self, args):
|
|
105
|
-
# Original _check_sub_basisset method code
|
|
106
135
|
if len(args.sub_basisset) % 2 != 0:
|
|
107
136
|
print("invalid input (-sub_bs)")
|
|
108
137
|
sys.exit(0)
|
|
109
|
-
|
|
138
|
+
|
|
139
|
+
self.electric_charge_and_multiplicity = [
|
|
140
|
+
int(args.electronic_charge),
|
|
141
|
+
int(args.spin_multiplicity),
|
|
142
|
+
]
|
|
110
143
|
self.electronic_charge = args.electronic_charge
|
|
111
144
|
self.spin_multiplicity = args.spin_multiplicity
|
|
112
|
-
|
|
145
|
+
|
|
113
146
|
if args.pyscf:
|
|
114
147
|
self.SUB_BASIS_SET = {}
|
|
115
148
|
if len(args.sub_basisset) > 0:
|
|
116
149
|
self.SUB_BASIS_SET["default"] = str(self.BASIS_SET)
|
|
117
150
|
for j in range(int(len(args.sub_basisset) / 2)):
|
|
118
|
-
self.SUB_BASIS_SET[args.sub_basisset[2 * j]] = args.sub_basisset[
|
|
151
|
+
self.SUB_BASIS_SET[args.sub_basisset[2 * j]] = args.sub_basisset[
|
|
152
|
+
2 * j + 1
|
|
153
|
+
]
|
|
119
154
|
print("Basis Sets defined by User are detected.")
|
|
120
155
|
print(self.SUB_BASIS_SET)
|
|
121
156
|
else:
|
|
@@ -125,10 +160,16 @@ class OptimizationConfig:
|
|
|
125
160
|
if len(args.sub_basisset) > 0:
|
|
126
161
|
self.SUB_BASIS_SET += "\nassign " + str(self.BASIS_SET) + "\n"
|
|
127
162
|
for j in range(int(len(args.sub_basisset) / 2)):
|
|
128
|
-
self.SUB_BASIS_SET +=
|
|
163
|
+
self.SUB_BASIS_SET += (
|
|
164
|
+
"assign "
|
|
165
|
+
+ args.sub_basisset[2 * j]
|
|
166
|
+
+ " "
|
|
167
|
+
+ args.sub_basisset[2 * j + 1]
|
|
168
|
+
+ "\n"
|
|
169
|
+
)
|
|
129
170
|
print("Basis Sets defined by User are detected.")
|
|
130
171
|
print(self.SUB_BASIS_SET)
|
|
131
|
-
|
|
172
|
+
|
|
132
173
|
if len(args.effective_core_potential) % 2 != 0:
|
|
133
174
|
print("invaild input (-ecp)")
|
|
134
175
|
sys.exit(0)
|
|
@@ -136,640 +177,1332 @@ class OptimizationConfig:
|
|
|
136
177
|
if args.pyscf:
|
|
137
178
|
self.ECP = {}
|
|
138
179
|
if len(args.effective_core_potential) > 0:
|
|
139
|
-
for j in range(int(len(args.effective_core_potential)/2)):
|
|
140
|
-
self.ECP[
|
|
180
|
+
for j in range(int(len(args.effective_core_potential) / 2)):
|
|
181
|
+
self.ECP[
|
|
182
|
+
args.effective_core_potential[2 * j]
|
|
183
|
+
] = args.effective_core_potential[2 * j + 1]
|
|
141
184
|
else:
|
|
142
185
|
self.ECP = ""
|
|
143
186
|
|
|
144
|
-
#
|
|
187
|
+
# =====================================================================================
|
|
188
|
+
# 2. State (Mutable Data)
|
|
189
|
+
# =====================================================================================
|
|
145
190
|
class OptimizationState:
|
|
146
191
|
"""
|
|
147
|
-
|
|
192
|
+
Dynamic state of the optimization.
|
|
148
193
|
"""
|
|
194
|
+
|
|
149
195
|
def __init__(self, element_list):
|
|
150
196
|
natom = len(element_list)
|
|
151
|
-
|
|
152
|
-
# Current step state
|
|
153
197
|
self.iter = 0
|
|
154
|
-
self.e = None # Hartree
|
|
155
|
-
self.B_e = None # Hartree
|
|
156
|
-
self.g = None # Hartree/Bohr
|
|
157
|
-
self.B_g = None # Hartree/Bohr
|
|
158
|
-
self.geom_num_list = None # Bohr
|
|
159
|
-
self.Model_hess = np.eye(natom * 3) # Model_hess is treated as state
|
|
160
|
-
self.element_list = element_list
|
|
161
198
|
|
|
162
|
-
#
|
|
163
|
-
self.
|
|
164
|
-
self.
|
|
165
|
-
self.
|
|
166
|
-
self.
|
|
167
|
-
self.
|
|
199
|
+
# Energies and gradients
|
|
200
|
+
self.energies = {}
|
|
201
|
+
self.gradients = {}
|
|
202
|
+
self.raw_energy = 0.0
|
|
203
|
+
self.raw_gradient = np.zeros((natom, 3), dtype="float64")
|
|
204
|
+
self.bias_energy = 0.0
|
|
205
|
+
self.bias_gradient = np.zeros((natom, 3), dtype="float64")
|
|
206
|
+
self.effective_energy = 0.0
|
|
207
|
+
self.effective_gradient = np.zeros((natom, 3), dtype="float64")
|
|
208
|
+
|
|
209
|
+
# Geometry
|
|
210
|
+
self.geometry = None # Bohr
|
|
211
|
+
self.initial_geometry = None # Bohr
|
|
212
|
+
|
|
213
|
+
# Previous step
|
|
214
|
+
self.pre_geometry = np.zeros((natom, 3), dtype="float64")
|
|
215
|
+
self.pre_effective_gradient = np.zeros((natom, 3), dtype="float64")
|
|
216
|
+
self.pre_bias_gradient = np.zeros((natom, 3), dtype="float64")
|
|
217
|
+
self.pre_effective_energy = 0.0
|
|
218
|
+
self.pre_bias_energy = 0.0
|
|
219
|
+
self.pre_raw_gradient = np.zeros((natom, 3), dtype="float64")
|
|
168
220
|
self.pre_move_vector = np.zeros((natom, 3), dtype="float64")
|
|
221
|
+
self.pre_raw_energy = 0.0
|
|
222
|
+
|
|
223
|
+
# Hessian
|
|
224
|
+
self.Model_hess = np.eye(natom * 3)
|
|
169
225
|
|
|
170
|
-
#
|
|
226
|
+
# Logs
|
|
227
|
+
self.history = {
|
|
228
|
+
"iter": [],
|
|
229
|
+
"energies": {},
|
|
230
|
+
"grad_rms": [],
|
|
231
|
+
"bias_grad_rms": [],
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# For plotting and outputs
|
|
171
235
|
self.ENERGY_LIST_FOR_PLOTTING = []
|
|
172
236
|
self.BIAS_ENERGY_LIST_FOR_PLOTTING = []
|
|
173
237
|
self.NUM_LIST = []
|
|
174
238
|
self.grad_list = []
|
|
175
239
|
self.bias_grad_list = []
|
|
176
|
-
self.cos_list = []
|
|
240
|
+
self.cos_list = []
|
|
177
241
|
|
|
178
|
-
# Final
|
|
242
|
+
# Final results
|
|
179
243
|
self.final_file_directory = None
|
|
180
244
|
self.final_geometry = None
|
|
181
245
|
self.final_energy = None
|
|
182
246
|
self.final_bias_energy = None
|
|
183
247
|
self.bias_pot_params_grad_list = None
|
|
184
248
|
self.bias_pot_params_grad_name_list = None
|
|
249
|
+
self.symmetry = None
|
|
185
250
|
|
|
186
251
|
# Flags
|
|
187
|
-
self.DC_check_flag = False
|
|
188
|
-
self.optimized_flag = False
|
|
189
252
|
self.exit_flag = False
|
|
253
|
+
self.converged_flag = False
|
|
254
|
+
self.dissociation_flag = False
|
|
255
|
+
self.optimized_flag = False
|
|
256
|
+
self.DC_check_flag = False
|
|
190
257
|
|
|
191
|
-
#
|
|
192
|
-
|
|
258
|
+
# =====================================================================================
|
|
259
|
+
# 3. Potential Handlers (Strategy)
|
|
260
|
+
# =====================================================================================
|
|
261
|
+
class BasePotentialHandler:
|
|
262
|
+
def __init__(self, config, file_io, base_dir, force_data):
|
|
263
|
+
self.config = config
|
|
264
|
+
self.file_io = file_io
|
|
265
|
+
self.base_dir = base_dir
|
|
266
|
+
self.force_data = force_data
|
|
267
|
+
self.bias_pot_calc = BiasPotentialCalculation(base_dir)
|
|
268
|
+
|
|
269
|
+
def compute(self, state: OptimizationState):
|
|
270
|
+
raise NotImplementedError("Subclasses must implement compute()")
|
|
271
|
+
|
|
272
|
+
def _add_bias_and_update_state(self, state, raw_energy, raw_gradient):
|
|
273
|
+
# Store raw
|
|
274
|
+
state.raw_energy = raw_energy
|
|
275
|
+
state.raw_gradient = raw_gradient
|
|
276
|
+
state.energies["raw"] = raw_energy
|
|
277
|
+
state.gradients["raw"] = raw_gradient
|
|
278
|
+
|
|
279
|
+
# Bias
|
|
280
|
+
_, bias_e, bias_g, bpa_hess = self.bias_pot_calc.main(
|
|
281
|
+
raw_energy,
|
|
282
|
+
raw_gradient,
|
|
283
|
+
state.geometry,
|
|
284
|
+
state.element_list,
|
|
285
|
+
self.force_data,
|
|
286
|
+
state.pre_bias_gradient,
|
|
287
|
+
state.iter,
|
|
288
|
+
initial_geom_num_list=state.initial_geometry,
|
|
289
|
+
)
|
|
290
|
+
state.bias_energy = bias_e
|
|
291
|
+
state.bias_gradient = bias_g
|
|
292
|
+
state.energies["bias"] = bias_e
|
|
293
|
+
state.gradients["bias"] = bias_g
|
|
294
|
+
|
|
295
|
+
# Effective
|
|
296
|
+
state.effective_energy = bias_e
|
|
297
|
+
state.effective_gradient = bias_g
|
|
298
|
+
state.energies["effective"] = state.effective_energy
|
|
299
|
+
state.gradients["effective"] = state.effective_gradient
|
|
300
|
+
|
|
301
|
+
# Attach bias Hessian for later use
|
|
302
|
+
state.bias_hessian = bpa_hess
|
|
303
|
+
return state
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class StandardHandler(BasePotentialHandler):
|
|
193
307
|
"""
|
|
194
|
-
|
|
195
|
-
It holds the Config, creates and updates the State,
|
|
196
|
-
and runs the main optimization logic.
|
|
308
|
+
Handles standard single-PES calculations.
|
|
197
309
|
"""
|
|
198
|
-
def __init__(self, args):
|
|
199
|
-
# 1. Set the Configuration (immutable)
|
|
200
|
-
self.config = OptimizationConfig(args)
|
|
201
|
-
|
|
202
|
-
# 2. State will be created freshly inside the run() method for each job.
|
|
203
|
-
self.state = None
|
|
204
310
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
self.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
self.optimized_struct_file = None
|
|
219
|
-
self.traj_file = None
|
|
220
|
-
self.symmetry = None
|
|
311
|
+
def __init__(self, calculator, *args, **kwargs):
|
|
312
|
+
super().__init__(*args, **kwargs)
|
|
313
|
+
self.calculator = calculator
|
|
314
|
+
|
|
315
|
+
def compute(self, state: OptimizationState):
|
|
316
|
+
file_path = self.file_io.make_psi4_input_file(
|
|
317
|
+
self.file_io.print_geometry_list(
|
|
318
|
+
state.geometry * self.config.bohr2angstroms,
|
|
319
|
+
state.element_list,
|
|
320
|
+
self.config.electric_charge_and_multiplicity,
|
|
321
|
+
),
|
|
322
|
+
state.iter,
|
|
323
|
+
)
|
|
221
324
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
325
|
+
self.calculator.Model_hess = copy.deepcopy(state.Model_hess)
|
|
326
|
+
e, g, geom_num_list, exit_flag = self.calculator.single_point(
|
|
327
|
+
file_path,
|
|
328
|
+
state.element_number_list,
|
|
329
|
+
state.iter,
|
|
330
|
+
self.config.electric_charge_and_multiplicity,
|
|
331
|
+
self.calculator.xtb_method,
|
|
332
|
+
)
|
|
333
|
+
state.geometry = np.array(geom_num_list, dtype="float64")
|
|
334
|
+
if exit_flag:
|
|
335
|
+
state.exit_flag = True
|
|
336
|
+
return state
|
|
337
|
+
|
|
338
|
+
state = self._add_bias_and_update_state(state, e, g)
|
|
339
|
+
state.Model_hess = copy.deepcopy(self.calculator.Model_hess)
|
|
340
|
+
return state
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class ModelFunctionHandler(BasePotentialHandler):
|
|
345
|
+
def __init__(self, calc1, calc2, mf_args, config, file_io, base_dir, force_data):
|
|
346
|
+
super().__init__(config, file_io, base_dir, force_data)
|
|
347
|
+
self.calc1 = calc1
|
|
348
|
+
self.calc2 = calc2
|
|
349
|
+
|
|
350
|
+
self.method_name = mf_args[0].lower()
|
|
351
|
+
self.params = mf_args[1:]
|
|
352
|
+
|
|
353
|
+
self.is_bitss = "bitss" in self.method_name
|
|
354
|
+
self.single_element_list = None
|
|
355
|
+
|
|
356
|
+
self.mf_instance = self._load_mf_class()
|
|
357
|
+
self._apply_config_params()
|
|
358
|
+
|
|
359
|
+
self.bitss_geom1_history = []
|
|
360
|
+
self.bitss_geom2_history = []
|
|
361
|
+
self.bitss_ref_geom = None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
self.bitss_initialized = False
|
|
365
|
+
|
|
366
|
+
if self.is_bitss:
|
|
367
|
+
self._setup_bitss_initialization()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _load_mf_class(self):
|
|
371
|
+
if self.method_name == "opt_meci":
|
|
372
|
+
return OptMECI()
|
|
373
|
+
if self.method_name == "opt_mesx":
|
|
374
|
+
return OptMESX()
|
|
375
|
+
if self.method_name == "opt_mesx_2":
|
|
376
|
+
return OptMESX2()
|
|
377
|
+
if self.method_name == "seam":
|
|
378
|
+
return SeamModelFunction()
|
|
379
|
+
if self.method_name == "conical":
|
|
380
|
+
return ConicalModelFunction()
|
|
381
|
+
if self.method_name == "avoiding":
|
|
382
|
+
return AvoidingModelFunction()
|
|
383
|
+
if self.method_name == "bitss":
|
|
384
|
+
return BITSSModelFunction(np.zeros(1), np.zeros(1))
|
|
385
|
+
raise ValueError(f"Unknown Model Function: {self.method_name}")
|
|
386
|
+
|
|
387
|
+
def _apply_config_params(self):
|
|
388
|
+
if hasattr(self.config.args, 'alpha') and self.config.args.alpha is not None:
|
|
389
|
+
if hasattr(self.mf_instance, 'alpha'):
|
|
390
|
+
self.mf_instance.alpha = float(self.config.args.alpha)
|
|
391
|
+
if hasattr(self.config.args, 'sigma') and self.config.args.sigma is not None:
|
|
392
|
+
if hasattr(self.mf_instance, 'sigma'):
|
|
393
|
+
self.mf_instance.sigma = float(self.config.args.sigma)
|
|
394
|
+
|
|
395
|
+
def _setup_bitss_initialization(self):
|
|
396
|
+
if len(self.params) < 1:
|
|
397
|
+
raise ValueError("BITSS requires a reference geometry file path.")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
temp_io = FileIO(self.base_dir, self.params[0])
|
|
401
|
+
g_list, _, _ = temp_io.make_geometry_list(self.config.electric_charge_and_multiplicity)
|
|
402
|
+
|
|
403
|
+
coords_ang = np.array([atom[1:4] for atom in g_list[0][2:]], dtype=float)
|
|
404
|
+
self.bitss_ref_geom = coords_ang / self.config.bohr2angstroms
|
|
226
405
|
|
|
227
|
-
def
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
self.START_FILE = file
|
|
233
|
-
timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S_%f")[:-2]
|
|
234
|
-
date = datetime.datetime.now().strftime("%Y_%m_%d")
|
|
235
|
-
base_dir = f"{date}/{self.START_FILE[:-4]}_OPT_"
|
|
406
|
+
def compute(self, state: OptimizationState):
|
|
407
|
+
iter_idx = state.iter
|
|
408
|
+
|
|
409
|
+
if self.single_element_list is None:
|
|
410
|
+
self.single_element_list = state.element_list[:len(state.element_list)//2] if self.is_bitss else state.element_list
|
|
236
411
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
412
|
+
# --- 1. Prepare Geometries ---
|
|
413
|
+
if self.is_bitss:
|
|
414
|
+
n_atoms = len(self.single_element_list)
|
|
415
|
+
geom_1, geom_2 = state.geometry[:n_atoms], state.geometry[n_atoms:]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if not self.bitss_initialized:
|
|
419
|
+
self.mf_instance = BITSSModelFunction(geom_1, geom_2)
|
|
420
|
+
self._apply_config_params()
|
|
421
|
+
self.bitss_initialized = True
|
|
245
422
|
else:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
423
|
+
geom_1 = geom_2 = state.geometry
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# State 1
|
|
427
|
+
e1, g1, ex1 = self._run_calc(self.calc1, geom_1, self.single_element_list, self.config.electric_charge_and_multiplicity, "State1", iter_idx)
|
|
428
|
+
# State 2
|
|
429
|
+
chg_mult_2 = self.config.electric_charge_and_multiplicity if self.is_bitss else [int(self.params[0]), int(self.params[1])]
|
|
430
|
+
e2, g2, ex2 = self._run_calc(self.calc2, geom_2, self.single_element_list, chg_mult_2, "State2", iter_idx)
|
|
431
|
+
|
|
432
|
+
if ex1 or ex2:
|
|
433
|
+
state.exit_flag = True
|
|
434
|
+
return state
|
|
435
|
+
|
|
436
|
+
h1 = self.calc1.Model_hess
|
|
437
|
+
h2 = self.calc2.Model_hess
|
|
438
|
+
|
|
439
|
+
# --- 3. Compute Model Function Energy, Gradient, Hessian ---
|
|
440
|
+
if self.is_bitss:
|
|
441
|
+
mf_E = self.mf_instance.calc_energy(e1, e2, geom_1, geom_2, g1, g2, iter_idx)
|
|
442
|
+
mf_G1, mf_G2 = self.mf_instance.calc_grad(e1, e2, geom_1, geom_2, g1, g2)
|
|
443
|
+
mf_G = np.vstack((np.array(mf_G1), np.array(mf_G2))).astype(np.float64)
|
|
444
|
+
|
|
445
|
+
if hasattr(self.mf_instance, "calc_hess"):
|
|
446
|
+
try:
|
|
447
|
+
raw_H = self.mf_instance.calc_hess(e1, e2, g1, g2, h1, h2)
|
|
448
|
+
if raw_H is not None:
|
|
449
|
+
mf_H = raw_H
|
|
450
|
+
else:
|
|
451
|
+
mf_H = self._make_block_diag_hess(h1, h2)
|
|
452
|
+
except:
|
|
453
|
+
mf_H = self._make_block_diag_hess(h1, h2)
|
|
454
|
+
else:
|
|
455
|
+
mf_H = self._make_block_diag_hess(h1, h2)
|
|
250
456
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
f.write(str(vars(self.config.args))) # Read from config
|
|
254
|
-
return
|
|
457
|
+
self.bitss_geom1_history.append(geom_1 * self.config.bohr2angstroms)
|
|
458
|
+
self.bitss_geom2_history.append(geom_2 * self.config.bohr2angstroms)
|
|
255
459
|
|
|
256
|
-
def _constrain_flag_check(self, force_data):
|
|
257
|
-
# (This method is pure, no changes needed)
|
|
258
|
-
if len(force_data["projection_constraint_condition_list"]) > 0:
|
|
259
|
-
projection_constrain = True
|
|
260
460
|
else:
|
|
261
|
-
|
|
461
|
+
# Standard Mode (3N)
|
|
462
|
+
mf_E = self.mf_instance.calc_energy(e1, e2)
|
|
463
|
+
|
|
464
|
+
raw_output = self.mf_instance.calc_grad(e1, e2, g1, g2)
|
|
465
|
+
if isinstance(raw_output, (tuple, list)):
|
|
466
|
+
raw_G = np.array(raw_output[0]).astype(np.float64)
|
|
467
|
+
else:
|
|
468
|
+
raw_G = np.array(raw_output).astype(np.float64)
|
|
469
|
+
|
|
470
|
+
if raw_G.ndim != 2:
|
|
471
|
+
if raw_G.size == len(self.single_element_list) * 3:
|
|
472
|
+
mf_G = raw_G.reshape(len(self.single_element_list), 3)
|
|
473
|
+
else:
|
|
474
|
+
mf_G = raw_G
|
|
475
|
+
else:
|
|
476
|
+
mf_G = raw_G
|
|
477
|
+
|
|
478
|
+
mf_H = None
|
|
479
|
+
if hasattr(self.mf_instance, "calc_hess"):
|
|
480
|
+
try:
|
|
481
|
+
raw_H = self._call_calc_hess_safely(self.mf_instance, e1, e2, g1, g2, h1, h2)
|
|
482
|
+
if raw_H is not None:
|
|
483
|
+
if isinstance(raw_H, (tuple, list)):
|
|
484
|
+
mf_H = raw_H[0]
|
|
485
|
+
else:
|
|
486
|
+
mf_H = raw_H
|
|
487
|
+
except Exception as e:
|
|
488
|
+
print(f"Note: calc_hess failed or not applicable ({e}), falling back to average.")
|
|
489
|
+
|
|
490
|
+
if mf_H is None:
|
|
491
|
+
mf_H = 0.5 * (h1 + h2)
|
|
492
|
+
|
|
493
|
+
# --- 4. Apply Bias Potential ---
|
|
494
|
+
if self.is_bitss:
|
|
495
|
+
_, be1, bg1, bh1 = self.bias_pot_calc.main(
|
|
496
|
+
0.0, g1 * 0.0, geom_1, self.single_element_list, self.force_data,
|
|
497
|
+
state.pre_bias_gradient[:len(geom_1)] if state.pre_bias_gradient is not None else None,
|
|
498
|
+
iter_idx
|
|
499
|
+
)
|
|
500
|
+
_, be2, bg2, bh2 = self.bias_pot_calc.main(
|
|
501
|
+
0.0, g2 * 0.0, geom_2, self.single_element_list, self.force_data,
|
|
502
|
+
state.pre_bias_gradient[len(geom_1):] if state.pre_bias_gradient is not None else None,
|
|
503
|
+
iter_idx
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
final_E = mf_E + be1 + be2
|
|
507
|
+
final_G = mf_G + np.vstack((bg1, bg2))
|
|
508
|
+
bias_H = self._make_block_diag_hess(bh1, bh2)
|
|
262
509
|
|
|
263
|
-
if len(force_data["fix_atoms"]) == 0:
|
|
264
|
-
allactive_flag = True
|
|
265
510
|
else:
|
|
266
|
-
|
|
511
|
+
_, final_E, final_G, bias_H = self.bias_pot_calc.main(
|
|
512
|
+
mf_E, mf_G, state.geometry, self.single_element_list, self.force_data,
|
|
513
|
+
state.pre_bias_gradient, iter_idx
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# --- 5. Update State ---
|
|
517
|
+
state.raw_energy = mf_E
|
|
518
|
+
state.raw_gradient = mf_G
|
|
519
|
+
state.bias_energy = final_E
|
|
520
|
+
state.bias_gradient = final_G
|
|
521
|
+
state.effective_gradient = final_G
|
|
522
|
+
|
|
523
|
+
state.energies["raw"] = mf_E
|
|
524
|
+
state.energies["effective"] = final_E
|
|
525
|
+
state.gradients["raw"] = mf_G
|
|
526
|
+
state.gradients["effective"] = final_G
|
|
527
|
+
|
|
528
|
+
state.Model_hess = mf_H
|
|
529
|
+
state.bias_hessian = bias_H
|
|
530
|
+
|
|
531
|
+
return state
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _make_block_diag_hess(self, h1, h2):
|
|
535
|
+
d1 = h1.shape[0]
|
|
536
|
+
d2 = h2.shape[0]
|
|
537
|
+
full_H = np.zeros((d1 + d2, d1 + d2))
|
|
538
|
+
full_H[:d1, :d1] = h1
|
|
539
|
+
full_H[d1:, d1:] = h2
|
|
540
|
+
return full_H
|
|
541
|
+
|
|
542
|
+
def _call_calc_hess_safely(self, instance, e1, e2, g1, g2, h1, h2):
|
|
543
|
+
sig = inspect.signature(instance.calc_hess)
|
|
544
|
+
params = sig.parameters
|
|
545
|
+
if len(params) == 2:
|
|
546
|
+
return instance.calc_hess(h1, h2)
|
|
547
|
+
elif len(params) == 4:
|
|
548
|
+
return instance.calc_hess(g1, g2, h1, h2)
|
|
549
|
+
else:
|
|
550
|
+
return instance.calc_hess(e1, e2, g1, g2, h1, h2)
|
|
551
|
+
|
|
552
|
+
def _run_calc(self, calc_inst, geom, elems, chg_mult, label, iter_idx):
|
|
553
|
+
run_dir = os.path.join(self.base_dir, label, f"iter{iter_idx}")
|
|
554
|
+
os.makedirs(run_dir, exist_ok=True)
|
|
555
|
+
old_dir = calc_inst.BPA_FOLDER_DIRECTORY
|
|
556
|
+
calc_inst.BPA_FOLDER_DIRECTORY = run_dir
|
|
557
|
+
geom_str = self.file_io.print_geometry_list(geom * self.config.bohr2angstroms, elems, chg_mult, display_flag=True)
|
|
558
|
+
inp_path = self.file_io.make_psi4_input_file(geom_str, iter_idx, path=run_dir)
|
|
559
|
+
e, g, _, ex = calc_inst.single_point(inp_path, [element_number(el) for el in elems], iter_idx, chg_mult, method="")
|
|
560
|
+
calc_inst.BPA_FOLDER_DIRECTORY = old_dir
|
|
561
|
+
return e, g, ex
|
|
562
|
+
|
|
563
|
+
def finalize_bitss_trajectory(self):
|
|
564
|
+
if not self.is_bitss or not self.bitss_geom1_history: return
|
|
565
|
+
filename = os.path.join(self.base_dir, f"{self.file_io.NOEXT_START_FILE}_traj.xyz")
|
|
566
|
+
full_seq = self.bitss_geom1_history + self.bitss_geom2_history[::-1]
|
|
567
|
+
with open(filename, 'w') as f:
|
|
568
|
+
for s, g in enumerate(full_seq):
|
|
569
|
+
f.write(f"{len(g)}\nBITSS_Step {s}\n")
|
|
570
|
+
for i, atom in enumerate(g):
|
|
571
|
+
f.write(f"{self.single_element_list[i]:2s} {atom[0]:12.8f} {atom[1]:12.8f} {atom[2]:12.8f}\n")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class ONIOMHandler(BasePotentialHandler):
|
|
575
|
+
"""
|
|
576
|
+
Handles ONIOM calculations with microiterations.
|
|
577
|
+
"""
|
|
267
578
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
579
|
+
def __init__(
|
|
580
|
+
self,
|
|
581
|
+
high_calc,
|
|
582
|
+
low_calc,
|
|
583
|
+
high_atoms,
|
|
584
|
+
link_atoms,
|
|
585
|
+
*args,
|
|
586
|
+
**kwargs,
|
|
587
|
+
):
|
|
588
|
+
super().__init__(*args, **kwargs)
|
|
589
|
+
self.high_calc = high_calc
|
|
590
|
+
self.low_calc = low_calc
|
|
591
|
+
self.high_atoms = high_atoms
|
|
592
|
+
self.link_atoms = link_atoms
|
|
593
|
+
|
|
594
|
+
def compute(self, state: OptimizationState):
|
|
595
|
+
raise "Not Implemented!"
|
|
596
|
+
# This handler runs one ONIOM iteration including microiterations.
|
|
597
|
+
# The logic mirrors optimize_oniom from the legacy code.
|
|
598
|
+
force_data = self.force_data
|
|
599
|
+
config = self.config
|
|
600
|
+
bohr2angstroms = config.bohr2angstroms
|
|
601
|
+
|
|
602
|
+
# Build mappings
|
|
603
|
+
linker_atom_pair_num = specify_link_atom_pairs(
|
|
604
|
+
state.geometry, state.element_list, self.high_atoms, self.link_atoms
|
|
605
|
+
)
|
|
606
|
+
real_2_highlayer_label_connect_dict, highlayer_2_real_label_connect_dict = (
|
|
607
|
+
link_number_high_layer_and_low_layer(self.high_atoms)
|
|
608
|
+
)
|
|
272
609
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
610
|
+
# Separate layers
|
|
611
|
+
high_layer_geom_num_list, high_layer_element_list = (
|
|
612
|
+
separate_high_layer_and_low_layer(
|
|
613
|
+
state.geometry, linker_atom_pair_num, self.high_atoms, state.element_list
|
|
614
|
+
)
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Prepare Hessians
|
|
618
|
+
LL_Model_hess = copy.deepcopy(state.Model_hess)
|
|
619
|
+
HL_Model_hess = np.eye(len(high_layer_element_list) * 3)
|
|
620
|
+
|
|
621
|
+
# Bias calculators
|
|
622
|
+
LL_Calc_BiasPot = BiasPotentialCalculation(self.base_dir)
|
|
623
|
+
CalcBiaspot_real = self.bias_pot_calc # Use parent
|
|
624
|
+
|
|
625
|
+
# Previous vars
|
|
626
|
+
pre_model_HL_B_e = 0.0
|
|
627
|
+
pre_model_HL_B_g = np.zeros((len(high_layer_element_list), 3))
|
|
628
|
+
pre_model_HL_g = np.zeros((len(high_layer_element_list), 3))
|
|
629
|
+
pre_real_LL_B_e = state.pre_bias_energy
|
|
630
|
+
pre_real_LL_e = state.pre_raw_energy
|
|
631
|
+
pre_real_LL_B_g = copy.deepcopy(state.pre_bias_gradient)
|
|
632
|
+
pre_real_LL_g = copy.deepcopy(state.pre_raw_gradient)
|
|
633
|
+
pre_real_LL_move_vector = copy.deepcopy(state.pre_move_vector)
|
|
634
|
+
pre_model_HL_move_vector = np.zeros((len(high_layer_element_list), 3))
|
|
635
|
+
|
|
636
|
+
# High-layer optimizer
|
|
637
|
+
HL_CMV = CalculateMoveVector(
|
|
638
|
+
config.DELTA,
|
|
639
|
+
high_layer_element_list[: len(self.high_atoms)],
|
|
640
|
+
config.args.saddle_order,
|
|
641
|
+
config.FC_COUNT,
|
|
642
|
+
config.temperature,
|
|
643
|
+
max_trust_radius=config.max_trust_radius,
|
|
644
|
+
min_trust_radius=config.min_trust_radius,
|
|
645
|
+
)
|
|
646
|
+
HL_optimizer_instances = HL_CMV.initialization(force_data["opt_method"])
|
|
647
|
+
for inst in HL_optimizer_instances:
|
|
648
|
+
inst.set_hessian(
|
|
649
|
+
HL_Model_hess[: len(self.high_atoms) * 3, : len(self.high_atoms) * 3]
|
|
650
|
+
)
|
|
651
|
+
if config.DELTA != "x":
|
|
652
|
+
inst.DELTA = config.DELTA
|
|
653
|
+
|
|
654
|
+
# Model low-layer calc
|
|
655
|
+
self.low_calc.Model_hess = LL_Model_hess
|
|
656
|
+
model_LL_e, model_LL_g, high_layer_geom_num_list, finish_frag = (
|
|
657
|
+
self.low_calc.single_point(
|
|
658
|
+
self.file_io.make_psi4_input_file(
|
|
659
|
+
self.file_io.print_geometry_list(
|
|
660
|
+
state.geometry * bohr2angstroms,
|
|
661
|
+
state.element_list,
|
|
662
|
+
config.electric_charge_and_multiplicity,
|
|
663
|
+
),
|
|
664
|
+
state.iter,
|
|
665
|
+
),
|
|
666
|
+
high_layer_element_list,
|
|
667
|
+
state.iter,
|
|
668
|
+
config.electric_charge_and_multiplicity,
|
|
669
|
+
force_data["oniom_flag"][2],
|
|
670
|
+
geom_num_list=high_layer_geom_num_list * bohr2angstroms,
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
if finish_frag:
|
|
674
|
+
state.exit_flag = True
|
|
675
|
+
return state
|
|
676
|
+
|
|
677
|
+
# Microiterations on low layer
|
|
678
|
+
LL_CMV = CalculateMoveVector(
|
|
679
|
+
config.DELTA,
|
|
680
|
+
state.element_list,
|
|
681
|
+
config.args.saddle_order,
|
|
682
|
+
config.FC_COUNT,
|
|
683
|
+
config.temperature,
|
|
684
|
+
)
|
|
685
|
+
LL_optimizer_instances = LL_CMV.initialization(["fire"])
|
|
686
|
+
LL_optimizer_instances[0].display_flag = False
|
|
687
|
+
|
|
688
|
+
current_geom_num_list = copy.deepcopy(state.geometry)
|
|
689
|
+
real_initial_geom_num_list = copy.deepcopy(state.geometry)
|
|
690
|
+
real_pre_geom = copy.deepcopy(state.geometry)
|
|
691
|
+
|
|
692
|
+
low_layer_converged = False
|
|
693
|
+
for microiter in range(config.microiter_num):
|
|
694
|
+
self.low_calc.Model_hess = LL_Model_hess
|
|
695
|
+
real_LL_e, real_LL_g, current_geom_num_list, finish_frag = (
|
|
696
|
+
self.low_calc.single_point(
|
|
697
|
+
self.file_io.make_psi4_input_file(
|
|
698
|
+
self.file_io.print_geometry_list(
|
|
699
|
+
current_geom_num_list * bohr2angstroms,
|
|
700
|
+
state.element_list,
|
|
701
|
+
config.electric_charge_and_multiplicity,
|
|
702
|
+
display_flag=False,
|
|
703
|
+
),
|
|
704
|
+
microiter,
|
|
705
|
+
),
|
|
706
|
+
state.element_list,
|
|
707
|
+
microiter,
|
|
708
|
+
config.electric_charge_and_multiplicity,
|
|
709
|
+
force_data["oniom_flag"][2],
|
|
710
|
+
geom_num_list=current_geom_num_list * bohr2angstroms,
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
LL_Model_hess = copy.deepcopy(self.low_calc.Model_hess)
|
|
714
|
+
LL_Calc_BiasPot.Model_hess = LL_Model_hess
|
|
715
|
+
_, real_LL_B_e, real_LL_B_g, LL_BPA_hessian = LL_Calc_BiasPot.main(
|
|
716
|
+
real_LL_e,
|
|
717
|
+
real_LL_g,
|
|
718
|
+
current_geom_num_list,
|
|
719
|
+
state.element_list,
|
|
720
|
+
force_data,
|
|
721
|
+
pre_real_LL_B_g,
|
|
722
|
+
microiter,
|
|
723
|
+
real_initial_geom_num_list,
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
for inst in LL_optimizer_instances:
|
|
727
|
+
inst.set_bias_hessian(LL_BPA_hessian)
|
|
728
|
+
if microiter % config.FC_COUNT == 0:
|
|
729
|
+
inst.set_hessian(LL_Model_hess)
|
|
730
|
+
|
|
731
|
+
if len(force_data["opt_fragment"]) > 0:
|
|
732
|
+
real_LL_B_g = self._calc_fragment_grads(
|
|
733
|
+
real_LL_B_g, force_data["opt_fragment"]
|
|
734
|
+
)
|
|
735
|
+
real_LL_g = self._calc_fragment_grads(
|
|
736
|
+
real_LL_g, force_data["opt_fragment"]
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
prev_geom = current_geom_num_list.copy()
|
|
740
|
+
current_geom_num_list_ang, LL_move_vector, LL_optimizer_instances = (
|
|
741
|
+
LL_CMV.calc_move_vector(
|
|
742
|
+
microiter,
|
|
743
|
+
current_geom_num_list,
|
|
744
|
+
real_LL_B_g,
|
|
745
|
+
pre_real_LL_B_g,
|
|
746
|
+
real_pre_geom,
|
|
747
|
+
real_LL_B_e,
|
|
748
|
+
pre_real_LL_B_e,
|
|
749
|
+
pre_real_LL_move_vector,
|
|
750
|
+
real_initial_geom_num_list,
|
|
751
|
+
real_LL_g,
|
|
752
|
+
pre_real_LL_g,
|
|
753
|
+
LL_optimizer_instances,
|
|
754
|
+
print_flag=False,
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
current_geom_num_list = current_geom_num_list_ang / bohr2angstroms
|
|
758
|
+
|
|
759
|
+
# Fix high-layer atoms
|
|
760
|
+
for key, value in highlayer_2_real_label_connect_dict.items():
|
|
761
|
+
current_geom_num_list[value - 1] = copy.deepcopy(
|
|
762
|
+
high_layer_geom_num_list[key - 1]
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
# Fix user-specified atoms
|
|
766
|
+
if len(force_data["fix_atoms"]) > 0:
|
|
767
|
+
for j in force_data["fix_atoms"]:
|
|
768
|
+
current_geom_num_list[j - 1] = copy.deepcopy(
|
|
769
|
+
real_initial_geom_num_list[j - 1]
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
displacement_vector = current_geom_num_list - prev_geom
|
|
773
|
+
low_layer_grads = []
|
|
774
|
+
low_layer_displacements = []
|
|
775
|
+
for i in range(len(state.element_list)):
|
|
776
|
+
if (i + 1) not in self.high_atoms:
|
|
777
|
+
low_layer_grads.append(real_LL_B_g[i])
|
|
778
|
+
low_layer_displacements.append(displacement_vector[i])
|
|
779
|
+
low_layer_grads = np.array(low_layer_grads)
|
|
780
|
+
low_layer_displacements = np.array(low_layer_displacements)
|
|
781
|
+
|
|
782
|
+
low_layer_rms_grad = self._calculate_rms_safely(low_layer_grads)
|
|
783
|
+
max_displacement = np.abs(displacement_vector).max()
|
|
784
|
+
rms_displacement = self._calculate_rms_safely(displacement_vector)
|
|
785
|
+
|
|
786
|
+
if (
|
|
787
|
+
(low_layer_rms_grad < 0.0003)
|
|
788
|
+
and (low_layer_grads.max() < 0.0006 if len(low_layer_grads) > 0 else True)
|
|
789
|
+
and (max_displacement < 0.003)
|
|
790
|
+
and (rms_displacement < 0.002)
|
|
791
|
+
):
|
|
792
|
+
low_layer_converged = True
|
|
793
|
+
break
|
|
794
|
+
|
|
795
|
+
# Update previous for microiter
|
|
796
|
+
pre_real_LL_B_e = real_LL_B_e
|
|
797
|
+
pre_real_LL_g = real_LL_g
|
|
798
|
+
pre_real_LL_B_g = real_LL_B_g
|
|
799
|
+
pre_real_LL_move_vector = LL_move_vector
|
|
800
|
+
real_pre_geom = current_geom_num_list
|
|
801
|
+
|
|
802
|
+
# Update state geometry after microiterations
|
|
803
|
+
state.geometry = current_geom_num_list
|
|
804
|
+
geom_num_list = current_geom_num_list
|
|
805
|
+
|
|
806
|
+
# Model high-layer calc
|
|
807
|
+
self.high_calc.Model_hess = HL_Model_hess
|
|
808
|
+
model_HL_e, model_HL_g, high_layer_geom_num_list, finish_frag = (
|
|
809
|
+
self.high_calc.single_point(
|
|
810
|
+
self.file_io.make_psi4_input_file(
|
|
811
|
+
self.file_io.print_geometry_list(
|
|
812
|
+
geom_num_list * bohr2angstroms,
|
|
813
|
+
state.element_list,
|
|
814
|
+
config.electric_charge_and_multiplicity,
|
|
815
|
+
),
|
|
816
|
+
state.iter,
|
|
817
|
+
),
|
|
818
|
+
high_layer_element_list,
|
|
819
|
+
state.iter,
|
|
820
|
+
config.electric_charge_and_multiplicity,
|
|
821
|
+
method="",
|
|
822
|
+
geom_num_list=high_layer_geom_num_list * bohr2angstroms,
|
|
823
|
+
)
|
|
824
|
+
)
|
|
825
|
+
HL_Model_hess = copy.deepcopy(self.high_calc.Model_hess)
|
|
826
|
+
if finish_frag:
|
|
827
|
+
state.exit_flag = True
|
|
828
|
+
return state
|
|
829
|
+
|
|
830
|
+
# Combine gradients
|
|
831
|
+
_, tmp_model_HL_B_e, tmp_model_HL_B_g, LL_BPA_hessian = LL_Calc_BiasPot.main(
|
|
832
|
+
0.0,
|
|
833
|
+
real_LL_g * 0.0,
|
|
834
|
+
geom_num_list,
|
|
835
|
+
state.element_list,
|
|
836
|
+
force_data,
|
|
837
|
+
pre_real_LL_B_g * 0.0,
|
|
838
|
+
state.iter,
|
|
839
|
+
real_initial_geom_num_list,
|
|
840
|
+
)
|
|
841
|
+
tmp_model_HL_g = tmp_model_HL_B_g * 0.0
|
|
842
|
+
for key, value in real_2_highlayer_label_connect_dict.items():
|
|
843
|
+
tmp_model_HL_B_g[key - 1] += model_HL_g[value - 1] - model_LL_g[value - 1]
|
|
844
|
+
tmp_model_HL_g[key - 1] += model_HL_g[value - 1] - model_LL_g[value - 1]
|
|
845
|
+
|
|
846
|
+
bool_list = []
|
|
847
|
+
for i in range(len(state.element_list)):
|
|
848
|
+
if i in self.high_atoms:
|
|
849
|
+
bool_list.extend([True, True, True])
|
|
278
850
|
else:
|
|
279
|
-
|
|
280
|
-
return PC
|
|
281
|
-
else:
|
|
282
|
-
return PC
|
|
851
|
+
bool_list.extend([False, False, False])
|
|
283
852
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
853
|
+
HL_BPA_hessian = LL_BPA_hessian[np.ix_(bool_list, bool_list)]
|
|
854
|
+
for inst in HL_optimizer_instances:
|
|
855
|
+
inst.set_bias_hessian(HL_BPA_hessian)
|
|
856
|
+
if state.iter % config.FC_COUNT == 0:
|
|
857
|
+
inst.set_hessian(
|
|
858
|
+
HL_Model_hess[: len(self.high_atoms) * 3, : len(self.high_atoms) * 3]
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
if len(force_data["opt_fragment"]) > 0:
|
|
862
|
+
tmp_model_HL_B_g = self._calc_fragment_grads(
|
|
863
|
+
tmp_model_HL_B_g, force_data["opt_fragment"]
|
|
864
|
+
)
|
|
865
|
+
tmp_model_HL_g = self._calc_fragment_grads(
|
|
866
|
+
tmp_model_HL_g, force_data["opt_fragment"]
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
model_HL_B_g = copy.deepcopy(model_HL_g)
|
|
870
|
+
model_HL_B_e = model_HL_e + tmp_model_HL_B_e
|
|
871
|
+
for key, value in real_2_highlayer_label_connect_dict.items():
|
|
872
|
+
model_HL_B_g[value - 1] += tmp_model_HL_B_g[key - 1]
|
|
873
|
+
|
|
874
|
+
pre_high_layer_geom_num_list = high_layer_geom_num_list.copy()
|
|
875
|
+
high_layer_geom_num_list_ang, move_vector, HL_optimizer_instances = (
|
|
876
|
+
HL_CMV.calc_move_vector(
|
|
877
|
+
state.iter,
|
|
878
|
+
high_layer_geom_num_list[: len(self.high_atoms)],
|
|
879
|
+
model_HL_B_g[: len(self.high_atoms)],
|
|
880
|
+
pre_model_HL_B_g[: len(self.high_atoms)],
|
|
881
|
+
pre_high_layer_geom_num_list[: len(self.high_atoms)],
|
|
882
|
+
model_HL_B_e,
|
|
883
|
+
pre_model_HL_B_e,
|
|
884
|
+
pre_model_HL_move_vector[: len(self.high_atoms)],
|
|
885
|
+
high_layer_geom_num_list[: len(self.high_atoms)],
|
|
886
|
+
model_HL_g[: len(self.high_atoms)],
|
|
887
|
+
pre_model_HL_g[: len(self.high_atoms)],
|
|
888
|
+
HL_optimizer_instances,
|
|
889
|
+
)
|
|
890
|
+
)
|
|
891
|
+
high_layer_geom_num_list = high_layer_geom_num_list_ang / bohr2angstroms
|
|
892
|
+
|
|
893
|
+
# Update full system geometry with high layer changes
|
|
894
|
+
for l in range(len(high_layer_geom_num_list) - len(linker_atom_pair_num)):
|
|
895
|
+
geom_num_list[
|
|
896
|
+
highlayer_2_real_label_connect_dict[l + 1] - 1
|
|
897
|
+
] = copy.deepcopy(high_layer_geom_num_list[l])
|
|
898
|
+
|
|
899
|
+
geom_num_list -= Calculationtools().calc_center_of_mass(
|
|
900
|
+
geom_num_list, state.element_list
|
|
901
|
+
)
|
|
902
|
+
geom_num_list, _ = Calculationtools().kabsch_algorithm(
|
|
903
|
+
geom_num_list, real_pre_geom
|
|
904
|
+
)
|
|
905
|
+
state.geometry = geom_num_list
|
|
906
|
+
high_layer_geom_num_list, high_layer_element_list = (
|
|
907
|
+
separate_high_layer_and_low_layer(
|
|
908
|
+
geom_num_list, linker_atom_pair_num, self.high_atoms, state.element_list
|
|
909
|
+
)
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
# Combine energies and gradients for real system
|
|
913
|
+
real_e = real_LL_e + model_HL_e - model_LL_e
|
|
914
|
+
real_B_e = real_LL_B_e + model_HL_B_e - model_LL_e
|
|
915
|
+
real_g = real_LL_g + tmp_model_HL_g
|
|
916
|
+
real_B_g = real_LL_B_g + tmp_model_HL_g
|
|
917
|
+
|
|
918
|
+
state.raw_energy = real_e
|
|
919
|
+
state.bias_energy = real_B_e
|
|
920
|
+
state.raw_gradient = real_g
|
|
921
|
+
state.bias_gradient = real_B_g
|
|
922
|
+
state.effective_energy = real_B_e
|
|
923
|
+
state.effective_gradient = real_B_g
|
|
924
|
+
state.Model_hess = LL_Model_hess # keep low layer hess for next step
|
|
925
|
+
state.bias_hessian = HL_BPA_hessian
|
|
926
|
+
state.energies["raw"] = real_e
|
|
927
|
+
state.energies["bias"] = real_B_e
|
|
928
|
+
state.energies["effective"] = real_B_e
|
|
929
|
+
state.gradients["raw"] = real_g
|
|
930
|
+
state.gradients["bias"] = real_B_g
|
|
931
|
+
state.gradients["effective"] = real_B_g
|
|
932
|
+
|
|
933
|
+
# Update previous caches for next iteration
|
|
934
|
+
state.pre_raw_energy = real_e
|
|
935
|
+
state.pre_bias_energy = real_B_e
|
|
936
|
+
state.pre_raw_gradient = real_g
|
|
937
|
+
state.pre_bias_gradient = real_B_g
|
|
938
|
+
state.pre_move_vector = move_vector
|
|
939
|
+
return state
|
|
940
|
+
|
|
941
|
+
@staticmethod
|
|
942
|
+
def _calc_fragment_grads(gradient, fragment_list):
|
|
943
|
+
calced_gradient = gradient
|
|
944
|
+
for fragment in fragment_list:
|
|
945
|
+
tmp_grad = np.array([0.0, 0.0, 0.0], dtype="float64")
|
|
946
|
+
for atom_num in fragment:
|
|
947
|
+
tmp_grad += gradient[atom_num - 1]
|
|
948
|
+
tmp_grad /= len(fragment)
|
|
949
|
+
for atom_num in fragment:
|
|
950
|
+
calced_gradient[atom_num - 1] = copy.deepcopy(tmp_grad)
|
|
951
|
+
return calced_gradient
|
|
952
|
+
|
|
953
|
+
@staticmethod
|
|
954
|
+
def _calculate_rms_safely(vector, threshold=1e-10):
|
|
955
|
+
filtered_vector = vector[np.abs(vector) > threshold]
|
|
956
|
+
if filtered_vector.size > 0:
|
|
957
|
+
return np.sqrt((filtered_vector**2).mean())
|
|
289
958
|
else:
|
|
290
|
-
|
|
291
|
-
pre_geom = initial_geom_num_list
|
|
292
|
-
|
|
293
|
-
return initial_geom_num_list, pre_geom
|
|
959
|
+
return 0.0
|
|
294
960
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
961
|
+
class EDEELHandler(BasePotentialHandler):
|
|
962
|
+
"""
|
|
963
|
+
Handles EDEEL calculations for Electron Transfer.
|
|
964
|
+
Computes V11 (Reactant) and V22 (Product) diabatic potentials using
|
|
965
|
+
energy decomposition of Complex, Donor, and Acceptor.
|
|
966
|
+
# ref.: https://doi.org/10.1039/D3RA05784D
|
|
967
|
+
# This is under constraction.
|
|
968
|
+
Target function can be switched between:
|
|
969
|
+
- 'reactant': Minimize V11
|
|
970
|
+
- 'product': Minimize V22
|
|
971
|
+
- 'sx': Minimize Seam of Crossing penalty function (Default)
|
|
972
|
+
"""
|
|
973
|
+
def __init__(self, complex_calc, donor_atoms, acceptor_atoms, ede_params, *args, **kwargs):
|
|
974
|
+
super().__init__(*args, **kwargs)
|
|
975
|
+
self.complex_calc = complex_calc
|
|
976
|
+
self.donor_atoms = np.array(donor_atoms)
|
|
977
|
+
self.acceptor_atoms = np.array(acceptor_atoms)
|
|
978
|
+
|
|
979
|
+
# Charges and Multiplicities for all 5 states
|
|
980
|
+
# Expected ede_params keys:
|
|
981
|
+
# 'complex': [chg, mult], 'd_ox': [chg, mult], 'd_red': [chg, mult], ...
|
|
982
|
+
self.params = ede_params
|
|
983
|
+
|
|
984
|
+
# SX penalty weight (sigma) and target mode
|
|
985
|
+
self.sigma = kwargs.get('sigma', 2.0)
|
|
986
|
+
self.target_mode = kwargs.get('target_mode', 'sx')
|
|
987
|
+
|
|
988
|
+
# Setup directories for components to avoid file collision
|
|
989
|
+
self.dirs = {
|
|
990
|
+
'complex': self.base_dir, # Complex runs in root
|
|
991
|
+
'd_ox': os.path.join(self.base_dir, "Components", "Donor_Ox"),
|
|
992
|
+
'd_red': os.path.join(self.base_dir, "Components", "Donor_Red"),
|
|
993
|
+
'a_ox': os.path.join(self.base_dir, "Components", "Acceptor_Ox"),
|
|
994
|
+
'a_red': os.path.join(self.base_dir, "Components", "Acceptor_Red"),
|
|
995
|
+
}
|
|
996
|
+
for d in self.dirs.values():
|
|
997
|
+
os.makedirs(d, exist_ok=True)
|
|
998
|
+
|
|
999
|
+
def compute(self, state: OptimizationState):
|
|
1000
|
+
config = self.config
|
|
1001
|
+
full_geom = state.geometry
|
|
1002
|
+
|
|
1003
|
+
# 1. Extract Fragment Geometries
|
|
1004
|
+
# Note: Atoms indices are 1-based, numpy is 0-based
|
|
1005
|
+
d_indices = self.donor_atoms - 1
|
|
1006
|
+
a_indices = self.acceptor_atoms - 1
|
|
1007
|
+
|
|
1008
|
+
geom_d = full_geom[d_indices]
|
|
1009
|
+
geom_a = full_geom[a_indices]
|
|
1010
|
+
|
|
1011
|
+
# Helper to run calculation for one component
|
|
1012
|
+
def run_component(label, calc_geom, atom_indices, chg_mult, target_dir):
|
|
1013
|
+
# Prepare file IO for this component
|
|
1014
|
+
# Note: We create a specific input file in the target directory
|
|
1015
|
+
input_content = self.file_io.print_geometry_list(
|
|
1016
|
+
calc_geom * config.bohr2angstroms,
|
|
1017
|
+
[state.element_list[i] for i in atom_indices],
|
|
1018
|
+
chg_mult,
|
|
1019
|
+
display_flag=False
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# Temporarily switch directory in calculator
|
|
1023
|
+
original_dir = self.complex_calc.BPA_FOLDER_DIRECTORY
|
|
1024
|
+
self.complex_calc.BPA_FOLDER_DIRECTORY = target_dir
|
|
1025
|
+
|
|
1026
|
+
# Run Single Point
|
|
1027
|
+
# Assuming make_psi4_input_file can accept a 'path' argument or we construct the path manually
|
|
1028
|
+
# Here we assume the file_io or calculator handles the path based on BPA_FOLDER_DIRECTORY
|
|
1029
|
+
e, g, _, exit_flag = self.complex_calc.single_point(
|
|
1030
|
+
self.file_io.make_psi4_input_file(input_content, state.iter, path=target_dir),
|
|
1031
|
+
[state.element_list[i] for i in atom_indices],
|
|
1032
|
+
state.iter,
|
|
1033
|
+
chg_mult,
|
|
1034
|
+
method=""
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
# Restore directory
|
|
1038
|
+
self.complex_calc.BPA_FOLDER_DIRECTORY = original_dir
|
|
1039
|
+
|
|
1040
|
+
return e, g, exit_flag
|
|
1041
|
+
|
|
1042
|
+
# 2. Run 5 Calculations
|
|
1043
|
+
# A. Complex (n+m)
|
|
1044
|
+
e_c, g_c, exit_c = run_component(
|
|
1045
|
+
'complex', full_geom, range(len(full_geom)),
|
|
1046
|
+
self.params['complex'], self.dirs['complex']
|
|
1047
|
+
)
|
|
308
1048
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
else:
|
|
315
|
-
proj_bpa_hess = BPA_hessian
|
|
316
|
-
optimizer_instances[i].set_bias_hessian(proj_bpa_hess)
|
|
317
|
-
else:
|
|
318
|
-
optimizer_instances[i].set_bias_hessian(BPA_hessian)
|
|
319
|
-
|
|
320
|
-
if self.state.iter % self.config.FC_COUNT == 0 or (self.config.use_model_hessian is not None and self.state.iter % self.config.mFC_COUNT == 0):
|
|
1049
|
+
# B. Donor Ox (n)
|
|
1050
|
+
e_do, g_do, exit_do = run_component(
|
|
1051
|
+
'd_ox', geom_d, d_indices,
|
|
1052
|
+
self.params['d_ox'], self.dirs['d_ox']
|
|
1053
|
+
)
|
|
321
1054
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
proj_model_hess = PC.calc_project_out_hess(geom_num_list, g, self.state.Model_hess)
|
|
328
|
-
optimizer_instances[i].set_hessian(proj_model_hess)
|
|
329
|
-
else:
|
|
330
|
-
optimizer_instances[i].set_hessian(self.state.Model_hess)
|
|
1055
|
+
# C. Donor Red (n+1)
|
|
1056
|
+
e_dr, g_dr, exit_dr = run_component(
|
|
1057
|
+
'd_red', geom_d, d_indices,
|
|
1058
|
+
self.params['d_red'], self.dirs['d_red']
|
|
1059
|
+
)
|
|
331
1060
|
|
|
332
|
-
|
|
1061
|
+
# D. Acceptor Ox (m)
|
|
1062
|
+
e_ao, g_ao, exit_ao = run_component(
|
|
1063
|
+
'a_ox', geom_a, a_indices,
|
|
1064
|
+
self.params['a_ox'], self.dirs['a_ox']
|
|
1065
|
+
)
|
|
333
1066
|
|
|
334
|
-
|
|
335
|
-
|
|
1067
|
+
# E. Acceptor Red (m+1)
|
|
1068
|
+
e_ar, g_ar, exit_ar = run_component(
|
|
1069
|
+
'a_red', geom_a, a_indices,
|
|
1070
|
+
self.params['a_red'], self.dirs['a_red']
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
if any([exit_c, exit_do, exit_dr, exit_ao, exit_ar]):
|
|
1074
|
+
state.exit_flag = True
|
|
1075
|
+
return state
|
|
1076
|
+
|
|
1077
|
+
# 3. Construct V11 and V22
|
|
1078
|
+
# V11 (Initial State) = E_Complex - E_Donor_Ox + E_Donor_Red
|
|
1079
|
+
# V22 (Final State) = E_Complex - E_Acceptor_Ox + E_Acceptor_Red
|
|
1080
|
+
|
|
1081
|
+
V11 = e_c - e_do + e_dr
|
|
1082
|
+
V22 = e_c - e_ao + e_ar
|
|
1083
|
+
|
|
1084
|
+
state.energies['V11'] = V11
|
|
1085
|
+
state.energies['V22'] = V22
|
|
1086
|
+
state.energies['gap'] = V11 - V22
|
|
1087
|
+
|
|
1088
|
+
# 4. Construct Gradients
|
|
1089
|
+
# Helper to map fragment gradients back to full system dimensions
|
|
1090
|
+
def map_grad(g_frag, indices):
|
|
1091
|
+
g_full = np.zeros_like(g_c)
|
|
1092
|
+
for i, idx in enumerate(indices):
|
|
1093
|
+
g_full[idx] = g_frag[i]
|
|
1094
|
+
return g_full
|
|
1095
|
+
|
|
1096
|
+
grad_V11 = g_c - map_grad(g_do, d_indices) + map_grad(g_dr, d_indices)
|
|
1097
|
+
grad_V22 = g_c - map_grad(g_ao, a_indices) + map_grad(g_ar, a_indices)
|
|
1098
|
+
|
|
1099
|
+
state.gradients['V11'] = grad_V11
|
|
1100
|
+
state.gradients['V22'] = grad_V22
|
|
1101
|
+
|
|
1102
|
+
# 5. Determine Effective Energy/Gradient for Optimizer
|
|
1103
|
+
if self.target_mode == 'reactant':
|
|
1104
|
+
eff_E = V11
|
|
1105
|
+
eff_G = grad_V11
|
|
1106
|
+
elif self.target_mode == 'product':
|
|
1107
|
+
eff_E = V22
|
|
1108
|
+
eff_G = grad_V22
|
|
1109
|
+
else: # 'sx' (Seam of Crossing Search)
|
|
1110
|
+
# Penalty function approach: L = mean(V) + sigma * (V1 - V2)^2
|
|
1111
|
+
mean_V = 0.5 * (V11 + V22)
|
|
1112
|
+
diff_V = V11 - V22
|
|
1113
|
+
penalty = self.sigma * (diff_V ** 2)
|
|
1114
|
+
|
|
1115
|
+
eff_E = mean_V + penalty
|
|
1116
|
+
|
|
1117
|
+
# Gradient of L: dL/dX = 0.5(g1 + g2) + 2*sigma*diff_V * (g1 - g2)
|
|
1118
|
+
eff_G = 0.5 * (grad_V11 + grad_V22) + 2 * self.sigma * diff_V * (grad_V11 - grad_V22)
|
|
1119
|
+
|
|
1120
|
+
state.energies['penalty'] = penalty
|
|
1121
|
+
|
|
1122
|
+
# Store results
|
|
1123
|
+
state.raw_energy = eff_E
|
|
1124
|
+
state.raw_gradient = eff_G
|
|
1125
|
+
|
|
1126
|
+
# Bias processing (if any)
|
|
1127
|
+
self._add_bias_and_update_state(state, eff_E, eff_G)
|
|
1128
|
+
|
|
1129
|
+
return state
|
|
1130
|
+
|
|
1131
|
+
# =====================================================================================
|
|
1132
|
+
# 4. Managers for constraints, convergence, Hessian, logging, result paths
|
|
1133
|
+
# =====================================================================================
|
|
1134
|
+
class ConstraintManager:
|
|
1135
|
+
def __init__(self, config):
|
|
1136
|
+
self.config = config
|
|
1137
|
+
|
|
1138
|
+
def constrain_flag_check(self, force_data):
|
|
1139
|
+
|
|
1140
|
+
projection_constrain = len(force_data["projection_constraint_condition_list"]) > 0 and any(s.lower() == "crsirfo" for s in self.config.args.opt_method)
|
|
1141
|
+
|
|
1142
|
+
allactive_flag = len(force_data["fix_atoms"]) == 0
|
|
1143
|
+
if (
|
|
1144
|
+
"x" in force_data["projection_constraint_condition_list"]
|
|
1145
|
+
or "y" in force_data["projection_constraint_condition_list"]
|
|
1146
|
+
or "z" in force_data["projection_constraint_condition_list"]
|
|
1147
|
+
):
|
|
1148
|
+
allactive_flag = False
|
|
1149
|
+
return projection_constrain, allactive_flag
|
|
1150
|
+
|
|
1151
|
+
def init_projection_constraint(self, PC, geom_num_list, iter_idx, projection_constrain, hessian=None):
|
|
1152
|
+
if iter_idx == 0:
|
|
1153
|
+
if projection_constrain:
|
|
1154
|
+
PC.initialize(geom_num_list, hessian=hessian)
|
|
1155
|
+
return PC
|
|
1156
|
+
else:
|
|
1157
|
+
return PC
|
|
1158
|
+
|
|
1159
|
+
def apply_projection_constraints(self, projection_constrain, PC, geom_num_list, g, B_g):
|
|
336
1160
|
if projection_constrain:
|
|
337
1161
|
g = copy.deepcopy(PC.calc_project_out_grad(geom_num_list, g))
|
|
338
1162
|
proj_d_B_g = copy.deepcopy(PC.calc_project_out_grad(geom_num_list, B_g - g))
|
|
339
1163
|
B_g = copy.deepcopy(g + proj_d_B_g)
|
|
340
|
-
|
|
1164
|
+
print("Projection was applied to gradient.")
|
|
341
1165
|
return g, B_g, PC
|
|
342
1166
|
|
|
343
|
-
def
|
|
344
|
-
|
|
1167
|
+
def apply_projection_constraints_to_geometry(self, projection_constrain, PC, new_geometry, hessian=None):
|
|
1168
|
+
if projection_constrain:
|
|
1169
|
+
tmp_new_geometry = new_geometry / self.config.bohr2angstroms
|
|
1170
|
+
adjusted_geometry = (
|
|
1171
|
+
PC.adjust_init_coord(tmp_new_geometry, hessian=hessian)
|
|
1172
|
+
* self.config.bohr2angstroms
|
|
1173
|
+
)
|
|
1174
|
+
return adjusted_geometry, PC
|
|
1175
|
+
return new_geometry, PC
|
|
1176
|
+
|
|
1177
|
+
def zero_fixed_atom_gradients(self, allactive_flag, force_data, g, B_g):
|
|
345
1178
|
if not allactive_flag:
|
|
346
1179
|
for j in force_data["fix_atoms"]:
|
|
347
|
-
g[j-1] = copy.deepcopy(g[j-1]*0.0)
|
|
348
|
-
B_g[j-1] = copy.deepcopy(B_g[j-1]*0.0)
|
|
349
|
-
|
|
1180
|
+
g[j - 1] = copy.deepcopy(g[j - 1] * 0.0)
|
|
1181
|
+
B_g[j - 1] = copy.deepcopy(B_g[j - 1] * 0.0)
|
|
350
1182
|
return g, B_g
|
|
351
1183
|
|
|
352
|
-
def
|
|
353
|
-
# (Reads self.config.bohr2angstroms)
|
|
1184
|
+
def project_out_translation_rotation(self, new_geometry, geom_num_list, allactive_flag):
|
|
354
1185
|
if allactive_flag:
|
|
355
|
-
# Convert to Bohr, apply Kabsch alignment algorithm, then convert back
|
|
356
1186
|
aligned_geometry, _ = Calculationtools().kabsch_algorithm(
|
|
357
|
-
new_geometry/self.config.bohr2angstroms, geom_num_list
|
|
1187
|
+
new_geometry / self.config.bohr2angstroms, geom_num_list
|
|
1188
|
+
)
|
|
358
1189
|
aligned_geometry *= self.config.bohr2angstroms
|
|
359
1190
|
return aligned_geometry
|
|
360
1191
|
else:
|
|
361
|
-
# If not all atoms are active, return the original geometry
|
|
362
1192
|
return new_geometry
|
|
363
1193
|
|
|
364
|
-
def
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
tmp_new_geometry = new_geometry / self.config.bohr2angstroms
|
|
368
|
-
adjusted_geometry = PC.adjust_init_coord(tmp_new_geometry, hessian=hessian) * self.config.bohr2angstroms
|
|
369
|
-
return adjusted_geometry, PC
|
|
370
|
-
|
|
371
|
-
return new_geometry, PC
|
|
372
|
-
|
|
373
|
-
def _reset_fixed_atom_positions(self, new_geometry, initial_geom_num_list, allactive_flag, force_data):
|
|
374
|
-
# (Reads self.config.bohr2angstroms)
|
|
1194
|
+
def reset_fixed_atom_positions(
|
|
1195
|
+
self, new_geometry, initial_geom_num_list, allactive_flag, force_data
|
|
1196
|
+
):
|
|
375
1197
|
if not allactive_flag:
|
|
376
1198
|
for j in force_data["fix_atoms"]:
|
|
377
|
-
new_geometry[j-1] = copy.deepcopy(
|
|
378
|
-
|
|
1199
|
+
new_geometry[j - 1] = copy.deepcopy(
|
|
1200
|
+
initial_geom_num_list[j - 1] * self.config.bohr2angstroms
|
|
1201
|
+
)
|
|
379
1202
|
return new_geometry
|
|
380
1203
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
# Get atom info
|
|
393
|
-
file_directory, electric_charge_and_multiplicity, element_list = self.write_input_files(FIO)
|
|
394
|
-
self.element_list = element_list # Store on self for helper methods
|
|
395
|
-
self.state.element_list = element_list # Store in state
|
|
396
|
-
|
|
397
|
-
element_number_list = np.array([element_number(elem) for elem in element_list], dtype="int")
|
|
398
|
-
natom = len(element_list)
|
|
399
|
-
|
|
400
|
-
# Constraint setup
|
|
401
|
-
PC = ProjectOutConstrain(force_data["projection_constraint_condition_list"],
|
|
402
|
-
force_data["projection_constraint_atoms"],
|
|
403
|
-
force_data["projection_constraint_constant"])
|
|
404
|
-
projection_constrain, allactive_flag = self._constrain_flag_check(force_data)
|
|
405
|
-
n_fix = len(force_data["fix_atoms"])
|
|
1204
|
+
@staticmethod
|
|
1205
|
+
def calc_fragment_grads(gradient, fragment_list):
|
|
1206
|
+
calced_gradient = gradient
|
|
1207
|
+
for fragment in fragment_list:
|
|
1208
|
+
tmp_grad = np.array([0.0, 0.0, 0.0], dtype="float64")
|
|
1209
|
+
for atom_num in fragment:
|
|
1210
|
+
tmp_grad += gradient[atom_num - 1]
|
|
1211
|
+
tmp_grad /= len(fragment)
|
|
1212
|
+
for atom_num in fragment:
|
|
1213
|
+
calced_gradient[atom_num - 1] = copy.deepcopy(tmp_grad)
|
|
1214
|
+
return calced_gradient
|
|
406
1215
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
# Check optimizer compatibility
|
|
418
|
-
for i in range(len(optimizer_instances)):
|
|
419
|
-
if CMV.newton_tag[i] is False and self.config.FC_COUNT > 0 and not "eigvec" in force_data["projection_constraint_condition_list"]:
|
|
420
|
-
print("Error: This optimizer method does not support exact Hessian calculations.")
|
|
421
|
-
print("Please either choose a different optimizer or set FC_COUNT=0 to disable exact Hessian calculations.")
|
|
422
|
-
sys.exit(0)
|
|
423
|
-
|
|
424
|
-
# Initialize optimizer instances
|
|
425
|
-
for i in range(len(optimizer_instances)):
|
|
426
|
-
optimizer_instances[i].set_hessian(self.state.Model_hess) # From state
|
|
427
|
-
if self.config.DELTA != "x":
|
|
428
|
-
optimizer_instances[i].DELTA = self.config.DELTA
|
|
429
|
-
|
|
430
|
-
if self.config.koopman_analysis:
|
|
431
|
-
KA = KoopmanAnalyzer(natom, file_directory=self.BPA_FOLDER_DIRECTORY)
|
|
1216
|
+
|
|
1217
|
+
class ConvergenceChecker:
|
|
1218
|
+
def __init__(self, config):
|
|
1219
|
+
self.config = config
|
|
1220
|
+
|
|
1221
|
+
@staticmethod
|
|
1222
|
+
def calculate_rms_safely(vector, threshold=1e-10):
|
|
1223
|
+
filtered_vector = vector[np.abs(vector) > threshold]
|
|
1224
|
+
if filtered_vector.size > 0:
|
|
1225
|
+
return np.sqrt((filtered_vector**2).mean())
|
|
432
1226
|
else:
|
|
433
|
-
|
|
1227
|
+
return 0.0
|
|
434
1228
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
'SP': self.SP, 'CMV': CMV, 'optimizer_instances': optimizer_instances,
|
|
439
|
-
'FIO': FIO, 'G': G, 'file_directory': file_directory,
|
|
440
|
-
'element_number_list': element_number_list, 'natom': natom,
|
|
441
|
-
'electric_charge_and_multiplicity': electric_charge_and_multiplicity,
|
|
442
|
-
'PC': PC, 'projection_constrain': projection_constrain,
|
|
443
|
-
'allactive_flag': allactive_flag, 'force_data': force_data, 'n_fix': n_fix,
|
|
444
|
-
'KA': KA
|
|
445
|
-
}
|
|
446
|
-
return tools
|
|
447
|
-
|
|
448
|
-
def check_negative_eigenvalues(self, geom_num_list, hessian):
|
|
449
|
-
# (This method is pure, no changes needed)
|
|
450
|
-
proj_hessian = Calculationtools().project_out_hess_tr_and_rot_for_coord(hessian, geom_num_list, geom_num_list, display_eigval=False)
|
|
451
|
-
if proj_hessian is not None:
|
|
452
|
-
eigvals = np.linalg.eigvalsh(proj_hessian)
|
|
453
|
-
if np.any(eigvals < -1e-10):
|
|
454
|
-
print("Notice: Negative eigenvalues detected.")
|
|
455
|
-
return True
|
|
456
|
-
return False
|
|
1229
|
+
def check_convergence(self, state, displacement_vector, optimizer_instances):
|
|
1230
|
+
max_force = np.abs(state.effective_gradient).max()
|
|
1231
|
+
rms_force = self.calculate_rms_safely(state.effective_gradient)
|
|
457
1232
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
with open(self.BPA_FOLDER_DIRECTORY+"no_negative_eigenvalues_detected.txt", "w") as f:
|
|
465
|
-
f.write("No negative eigenvalues detected while saddle_order > 0. Stopping optimization.")
|
|
466
|
-
return True
|
|
467
|
-
return False
|
|
1233
|
+
delta_max_force_threshold = max(
|
|
1234
|
+
0.0, self.config.MAX_FORCE_THRESHOLD - 1 * max_force
|
|
1235
|
+
)
|
|
1236
|
+
delta_rms_force_threshold = max(
|
|
1237
|
+
0.0, self.config.RMS_FORCE_THRESHOLD - 1 * rms_force
|
|
1238
|
+
)
|
|
468
1239
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
1240
|
+
max_displacement = np.abs(displacement_vector).max()
|
|
1241
|
+
rms_displacement = self.calculate_rms_safely(displacement_vector)
|
|
1242
|
+
|
|
1243
|
+
max_displacement_threshold = max(
|
|
1244
|
+
self.config.MAX_DISPLACEMENT_THRESHOLD,
|
|
1245
|
+
self.config.MAX_DISPLACEMENT_THRESHOLD + delta_max_force_threshold,
|
|
1246
|
+
)
|
|
1247
|
+
rms_displacement_threshold = max(
|
|
1248
|
+
self.config.RMS_DISPLACEMENT_THRESHOLD,
|
|
1249
|
+
self.config.RMS_DISPLACEMENT_THRESHOLD + delta_rms_force_threshold,
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
if (
|
|
1253
|
+
max_force < self.config.MAX_FORCE_THRESHOLD
|
|
1254
|
+
and rms_force < self.config.RMS_FORCE_THRESHOLD
|
|
1255
|
+
and max_displacement < max_displacement_threshold
|
|
1256
|
+
and rms_displacement < rms_displacement_threshold
|
|
1257
|
+
):
|
|
1258
|
+
return True, max_displacement_threshold, rms_displacement_threshold
|
|
478
1259
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
1260
|
+
for opt in optimizer_instances:
|
|
1261
|
+
if getattr(opt, 'proj_grad_converged', False):
|
|
1262
|
+
return True, max_displacement_threshold, rms_displacement_threshold
|
|
1263
|
+
|
|
482
1264
|
|
|
483
|
-
# 2. Initialize all other tools, passing FIO
|
|
484
|
-
force_data = force_data_parser(self.config.args)
|
|
485
|
-
tools = self._initialize_optimization_tools(FIO, force_data)
|
|
486
|
-
|
|
487
|
-
# 3. Unpack tools into local variables for the loop
|
|
488
|
-
# (This is better than the giant vars_dict at the end)
|
|
489
|
-
xtb_method = tools['xtb_method']
|
|
490
|
-
SP = tools['SP']
|
|
491
|
-
CMV = tools['CMV']
|
|
492
|
-
optimizer_instances = tools['optimizer_instances']
|
|
493
|
-
FIO = tools['FIO']
|
|
494
|
-
G = tools['G']
|
|
495
|
-
file_directory = tools['file_directory']
|
|
496
|
-
element_number_list = tools['element_number_list']
|
|
497
|
-
electric_charge_and_multiplicity = tools['electric_charge_and_multiplicity']
|
|
498
|
-
PC = tools['PC']
|
|
499
|
-
projection_constrain = tools['projection_constrain']
|
|
500
|
-
allactive_flag = tools['allactive_flag']
|
|
501
|
-
force_data = tools['force_data']
|
|
502
|
-
n_fix = tools['n_fix']
|
|
503
|
-
KA = tools['KA']
|
|
504
1265
|
|
|
505
|
-
|
|
506
|
-
for iter in range(self.config.NSTEP):
|
|
507
|
-
|
|
508
|
-
self.state.iter = iter
|
|
509
|
-
self.state.exit_flag = os.path.exists(self.BPA_FOLDER_DIRECTORY+"end.txt")
|
|
510
|
-
if self.state.exit_flag:
|
|
511
|
-
break
|
|
512
|
-
|
|
513
|
-
self.state.exit_flag = judge_shape_condition(self.state.geom_num_list, self.config.shape_conditions)
|
|
514
|
-
if self.state.exit_flag:
|
|
515
|
-
break
|
|
516
|
-
|
|
517
|
-
print(f"\n# ITR. {iter}\n")
|
|
518
|
-
|
|
519
|
-
# --- Perform Single Point Calculation ---
|
|
520
|
-
SP.Model_hess = copy.deepcopy(self.state.Model_hess)
|
|
521
|
-
e, g, geom_num_list, exit_flag = SP.single_point(file_directory, element_number_list, iter, electric_charge_and_multiplicity, xtb_method)
|
|
522
|
-
|
|
523
|
-
# Update state
|
|
524
|
-
self.state.e = e
|
|
525
|
-
self.state.g = g
|
|
526
|
-
self.state.geom_num_list = geom_num_list
|
|
527
|
-
self.state.exit_flag = exit_flag
|
|
528
|
-
self.state.Model_hess = copy.deepcopy(SP.Model_hess)
|
|
529
|
-
|
|
530
|
-
if self.state.exit_flag:
|
|
531
|
-
break
|
|
532
|
-
|
|
533
|
-
# --- Update Model Hessian (if needed) ---
|
|
534
|
-
if iter % self.config.mFC_COUNT == 0 and self.config.use_model_hessian is not None and self.config.FC_COUNT < 1:
|
|
535
|
-
SP.Model_hess = ApproxHessian().main(geom_num_list, self.element_list, g, self.config.use_model_hessian)
|
|
536
|
-
self.state.Model_hess = SP.Model_hess
|
|
537
|
-
|
|
538
|
-
if iter == 0:
|
|
539
|
-
initial_geom_num_list, pre_geom = self._save_init_geometry(geom_num_list, self.element_list, allactive_flag)
|
|
540
|
-
# Save initial geometry to state
|
|
541
|
-
self.state.pre_geom = pre_geom
|
|
542
|
-
|
|
543
|
-
# --- Bias Potential Calculation ---
|
|
544
|
-
_, B_e, B_g, BPA_hessian = self.CalcBiaspot.main(e, g, geom_num_list, self.element_list, force_data, self.state.pre_B_g, iter, initial_geom_num_list)
|
|
545
|
-
# Update state
|
|
546
|
-
self.state.B_e = B_e
|
|
547
|
-
self.state.B_g = B_g
|
|
548
|
-
|
|
549
|
-
# --- Check Eigenvalues (if first iter) ---
|
|
550
|
-
Hess = BPA_hessian + self.state.Model_hess
|
|
551
|
-
if iter == 0:
|
|
552
|
-
if self.judge_early_stop_due_to_no_negative_eigenvalues(geom_num_list, Hess):
|
|
553
|
-
break
|
|
554
|
-
|
|
555
|
-
# --- Constraints ---
|
|
556
|
-
PC = self._init_projection_constraint(PC, geom_num_list, iter, projection_constrain, hessian=Hess)
|
|
557
|
-
optimizer_instances = self._calc_eff_hess_for_fix_atoms_and_set_hess(allactive_flag, force_data, BPA_hessian, n_fix, optimizer_instances, geom_num_list, B_g, g, projection_constrain, PC)
|
|
558
|
-
|
|
559
|
-
if not allactive_flag:
|
|
560
|
-
B_g = copy.deepcopy(self.calc_fragement_grads(B_g, force_data["opt_fragment"]))
|
|
561
|
-
g = copy.deepcopy(self.calc_fragement_grads(g, force_data["opt_fragment"]))
|
|
562
|
-
|
|
563
|
-
self.save_tmp_energy_profiles(iter, e, g, B_g)
|
|
564
|
-
|
|
565
|
-
g, B_g, PC = self._apply_projection_constraints(projection_constrain, PC, geom_num_list, g, B_g)
|
|
566
|
-
g, B_g = self._zero_fixed_atom_gradients(allactive_flag, force_data, g, B_g)
|
|
1266
|
+
return False, max_displacement_threshold, rms_displacement_threshold
|
|
567
1267
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1268
|
+
def judge_early_stop_due_to_no_negative_eigenvalues(self, geom_num_list, hessian, saddle_order, FC_COUNT, detect_negative_eigenvalues, folder_dir):
|
|
1269
|
+
if detect_negative_eigenvalues and FC_COUNT > 0:
|
|
1270
|
+
proj_hessian = Calculationtools().project_out_hess_tr_and_rot_for_coord(
|
|
1271
|
+
hessian, geom_num_list, geom_num_list, display_eigval=False
|
|
1272
|
+
)
|
|
1273
|
+
if proj_hessian is not None:
|
|
1274
|
+
eigvals = np.linalg.eigvalsh(proj_hessian)
|
|
1275
|
+
if not np.any(eigvals < -1e-10) and saddle_order > 0:
|
|
1276
|
+
print("No negative eigenvalues detected while saddle_order > 0. Stopping optimization.")
|
|
1277
|
+
with open(
|
|
1278
|
+
folder_dir + "no_negative_eigenvalues_detected.txt",
|
|
1279
|
+
"w",
|
|
1280
|
+
) as f:
|
|
1281
|
+
f.write("No negative eigenvalues detected while saddle_order > 0. Stopping optimization.")
|
|
1282
|
+
return True
|
|
1283
|
+
return False
|
|
574
1284
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1285
|
+
|
|
1286
|
+
class HessianManager:
|
|
1287
|
+
def __init__(self, config):
|
|
1288
|
+
self.config = config
|
|
1289
|
+
|
|
1290
|
+
def calc_eff_hess_for_fix_atoms_and_set_hess(
|
|
1291
|
+
self,
|
|
1292
|
+
state,
|
|
1293
|
+
allactive_flag,
|
|
1294
|
+
force_data,
|
|
1295
|
+
BPA_hessian,
|
|
1296
|
+
n_fix,
|
|
1297
|
+
optimizer_instances,
|
|
1298
|
+
geom_num_list,
|
|
1299
|
+
B_g,
|
|
1300
|
+
g,
|
|
1301
|
+
projection_constrain,
|
|
1302
|
+
PC,
|
|
1303
|
+
):
|
|
1304
|
+
if not allactive_flag:
|
|
1305
|
+
fix_num = []
|
|
1306
|
+
for fnum in force_data["fix_atoms"]:
|
|
1307
|
+
fix_num.extend([3 * (fnum - 1) + 0, 3 * (fnum - 1) + 1, 3 * (fnum - 1) + 2])
|
|
1308
|
+
fix_num = np.array(fix_num, dtype="int64")
|
|
1309
|
+
tmp_fix_hess = state.Model_hess[np.ix_(fix_num, fix_num)] + np.eye(
|
|
1310
|
+
(3 * n_fix)
|
|
1311
|
+
) * 1e-10
|
|
1312
|
+
inv_tmp_fix_hess = np.linalg.pinv(tmp_fix_hess)
|
|
1313
|
+
tmp_fix_bias_hess = BPA_hessian[np.ix_(fix_num, fix_num)] + np.eye(
|
|
1314
|
+
(3 * n_fix)
|
|
1315
|
+
) * 1e-10
|
|
1316
|
+
inv_tmp_fix_bias_hess = np.linalg.pinv(tmp_fix_bias_hess)
|
|
1317
|
+
BPA_hessian -= np.dot(
|
|
1318
|
+
BPA_hessian[:, fix_num], np.dot(inv_tmp_fix_bias_hess, BPA_hessian[fix_num, :])
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
for inst in optimizer_instances:
|
|
1322
|
+
if projection_constrain:
|
|
1323
|
+
if np.all(np.abs(BPA_hessian) < 1e-20):
|
|
1324
|
+
proj_bpa_hess = PC.calc_project_out_hess(geom_num_list, B_g - g, BPA_hessian)
|
|
1325
|
+
else:
|
|
1326
|
+
proj_bpa_hess = BPA_hessian
|
|
1327
|
+
inst.set_bias_hessian(proj_bpa_hess)
|
|
593
1328
|
else:
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
# --- Check Convergence ---
|
|
597
|
-
converge_flag, max_displacement_threshold, rms_displacement_threshold = self._check_converge_criteria(B_g, displacement_vector)
|
|
598
|
-
self.print_info(e, B_e, B_g, displacement_vector, self.state.pre_e, self.state.pre_B_e, max_displacement_threshold, rms_displacement_threshold)
|
|
599
|
-
|
|
600
|
-
self.state.grad_list.append(self.calculate_rms_safely(g))
|
|
601
|
-
self.state.bias_grad_list.append(self.calculate_rms_safely(B_g))
|
|
602
|
-
|
|
603
|
-
new_geometry = self._reset_fixed_atom_positions(new_geometry, initial_geom_num_list, allactive_flag, force_data)
|
|
604
|
-
|
|
605
|
-
# --- Dissociation Check ---
|
|
606
|
-
DC_exit_flag = self.dissociation_check(new_geometry, self.element_list)
|
|
1329
|
+
inst.set_bias_hessian(BPA_hessian)
|
|
607
1330
|
|
|
608
|
-
if
|
|
609
|
-
|
|
610
|
-
|
|
1331
|
+
if state.iter % self.config.FC_COUNT == 0 or (
|
|
1332
|
+
self.config.use_model_hessian is not None
|
|
1333
|
+
and state.iter % self.config.mFC_COUNT == 0
|
|
1334
|
+
):
|
|
1335
|
+
if not allactive_flag:
|
|
1336
|
+
state.Model_hess -= np.dot(
|
|
1337
|
+
state.Model_hess[:, fix_num],
|
|
1338
|
+
np.dot(inv_tmp_fix_hess, state.Model_hess[fix_num, :]),
|
|
1339
|
+
)
|
|
1340
|
+
if projection_constrain:
|
|
1341
|
+
proj_model_hess = PC.calc_project_out_hess(
|
|
1342
|
+
geom_num_list, g, state.Model_hess
|
|
1343
|
+
)
|
|
1344
|
+
inst.set_hessian(proj_model_hess)
|
|
611
1345
|
else:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
print("converged!!!")
|
|
615
|
-
print("=====================================================")
|
|
616
|
-
break
|
|
617
|
-
|
|
618
|
-
if DC_exit_flag:
|
|
619
|
-
self.state.DC_check_flag = True
|
|
620
|
-
break
|
|
621
|
-
|
|
622
|
-
# --- Save State for Next Iteration ---
|
|
623
|
-
self.state.pre_B_e = B_e
|
|
624
|
-
self.state.pre_e = e
|
|
625
|
-
self.state.pre_B_g = B_g
|
|
626
|
-
self.state.pre_g = g
|
|
627
|
-
self.state.pre_geom = geom_num_list
|
|
628
|
-
self.state.pre_move_vector = move_vector
|
|
629
|
-
|
|
630
|
-
# --- Write Next Input File ---
|
|
631
|
-
geometry_list = FIO.print_geometry_list(new_geometry, self.element_list, electric_charge_and_multiplicity)
|
|
632
|
-
file_directory = FIO.make_psi4_input_file(geometry_list, iter+1)
|
|
633
|
-
|
|
634
|
-
else: # Loop ended (no break)
|
|
635
|
-
self.state.optimized_flag = False
|
|
636
|
-
print("Reached maximum number of iterations. This is not converged.")
|
|
637
|
-
with open(self.BPA_FOLDER_DIRECTORY+"not_converged.txt", "w") as f:
|
|
638
|
-
f.write("Reached maximum number of iterations. This is not converged.")
|
|
1346
|
+
inst.set_hessian(state.Model_hess)
|
|
1347
|
+
return optimizer_instances
|
|
639
1348
|
|
|
640
|
-
# --- 5. Post-Optimization Analysis ---
|
|
641
|
-
|
|
642
|
-
# Check if exact hessian is already computed.
|
|
643
|
-
if self.config.FC_COUNT == -1:
|
|
644
|
-
exact_hess_flag = False
|
|
645
|
-
elif self.state.iter % self.config.FC_COUNT == 0 and self.config.FC_COUNT > 0:
|
|
646
|
-
exact_hess_flag = True
|
|
647
|
-
else:
|
|
648
|
-
exact_hess_flag = False
|
|
649
|
-
|
|
650
|
-
if self.state.DC_check_flag:
|
|
651
|
-
print("Dissociation is detected. Optimization stopped.")
|
|
652
|
-
with open(self.BPA_FOLDER_DIRECTORY+"dissociation_is_detected.txt", "w") as f:
|
|
653
|
-
f.write("Dissociation is detected. Optimization stopped.")
|
|
654
1349
|
|
|
655
|
-
|
|
656
|
-
|
|
1350
|
+
class RunLogger:
|
|
1351
|
+
def __init__(self, config):
|
|
1352
|
+
self.config = config
|
|
657
1353
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1354
|
+
def log_dynamic_csv(self, state, folder_dir, convergence_checker):
|
|
1355
|
+
csv_path = os.path.join(folder_dir, "energy_profile.csv")
|
|
1356
|
+
keys = sorted(state.energies.keys())
|
|
1357
|
+
if state.iter == 0:
|
|
1358
|
+
with open(csv_path, "w") as f:
|
|
1359
|
+
f.write("iter," + ",".join(keys) + "\n")
|
|
1360
|
+
values = [str(state.energies[k]) for k in keys]
|
|
1361
|
+
with open(csv_path, "a") as f:
|
|
1362
|
+
f.write(f"{state.iter}," + ",".join(values) + "\n")
|
|
664
1363
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if exact_hess_flag:
|
|
673
|
-
g = np.zeros_like(geom_num_list, dtype="float64")
|
|
674
|
-
exit_flag = False
|
|
675
|
-
else:
|
|
676
|
-
print("Calculate exact Hessian...")
|
|
677
|
-
SP.hessian_flag = True
|
|
678
|
-
e, g, geom_num_list, exit_flag = SP.single_point(file_directory, element_list, iter, electric_charge_and_multiplicity, xtb_method)
|
|
679
|
-
SP.hessian_flag = False
|
|
680
|
-
|
|
681
|
-
if exit_flag:
|
|
682
|
-
print("Error: QM calculation failed.")
|
|
683
|
-
return
|
|
684
|
-
|
|
685
|
-
_, B_e, _, BPA_hessian = self.CalcBiaspot.main(e, g, geom_num_list, element_list, force_data, pre_B_g="", iter=iter, initial_geom_num_list="")
|
|
686
|
-
tmp_hess = copy.deepcopy(SP.Model_hess) # SP.Model_hess holds the latest hessian
|
|
687
|
-
tmp_hess += BPA_hessian
|
|
688
|
-
|
|
689
|
-
MV = MolecularVibrations(atoms=element_list, coordinates=geom_num_list, hessian=tmp_hess)
|
|
690
|
-
results = MV.calculate_thermochemistry(e_tot=B_e, temperature=self.config.thermo_temperature, pressure=self.config.thermo_pressure)
|
|
691
|
-
|
|
692
|
-
MV.print_thermochemistry(output_file=self.BPA_FOLDER_DIRECTORY+"/thermochemistry.txt")
|
|
693
|
-
MV.print_normal_modes(output_file=self.BPA_FOLDER_DIRECTORY+"/normal_modes.txt")
|
|
694
|
-
MV.create_vibration_animation(output_dir=self.BPA_FOLDER_DIRECTORY+"/vibration_animation")
|
|
695
|
-
|
|
696
|
-
if not self.state.optimized_flag:
|
|
697
|
-
print("Warning: Vibrational analysis was performed, but the optimization did not converge. The result of thermochemistry is useless.")
|
|
698
|
-
|
|
699
|
-
return
|
|
1364
|
+
grad_profile = os.path.join(folder_dir, "gradient_profile.csv")
|
|
1365
|
+
if state.iter == 0:
|
|
1366
|
+
with open(grad_profile, "w") as f:
|
|
1367
|
+
f.write("gradient (RMS) [hartree/Bohr] \n")
|
|
1368
|
+
with open(grad_profile, "a") as f:
|
|
1369
|
+
f.write(str(convergence_checker.calculate_rms_safely(state.raw_gradient)) + "\n")
|
|
700
1370
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
G.single_plot(self.state.NUM_LIST, bias_grad_list, file_directory, "", axis_name_2="bias gradient (RMS) [a.u.]", name="bias_gradient")
|
|
724
|
-
|
|
1371
|
+
bias_grad_profile = os.path.join(folder_dir, "bias_gradient_profile.csv")
|
|
1372
|
+
if state.iter == 0:
|
|
1373
|
+
with open(bias_grad_profile, "w") as f:
|
|
1374
|
+
f.write("bias gradient (RMS) [hartree/Bohr] \n")
|
|
1375
|
+
with open(bias_grad_profile, "a") as f:
|
|
1376
|
+
f.write(str(convergence_checker.calculate_rms_safely(state.bias_gradient)) + "\n")
|
|
1377
|
+
|
|
1378
|
+
def save_energy_profiles(self, state, folder_dir):
|
|
1379
|
+
with open(folder_dir + "energy_profile_kcalmol.csv", "w") as f:
|
|
1380
|
+
f.write("ITER.,energy[kcal/mol]\n")
|
|
1381
|
+
for i in range(len(state.ENERGY_LIST_FOR_PLOTTING)):
|
|
1382
|
+
f.write(
|
|
1383
|
+
str(i)
|
|
1384
|
+
+ ","
|
|
1385
|
+
+ str(
|
|
1386
|
+
state.ENERGY_LIST_FOR_PLOTTING[i]
|
|
1387
|
+
- state.ENERGY_LIST_FOR_PLOTTING[0]
|
|
1388
|
+
)
|
|
1389
|
+
+ "\n"
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
def geom_info_extract(self, state, force_data, file_directory, B_g, g, folder_dir):
|
|
725
1393
|
if len(force_data["geom_info"]) > 1:
|
|
1394
|
+
CSI = CalculationStructInfo()
|
|
1395
|
+
data_list, data_name_list = CSI.Data_extract(
|
|
1396
|
+
glob.glob(file_directory + "/*.xyz")[0], force_data["geom_info"]
|
|
1397
|
+
)
|
|
1398
|
+
|
|
726
1399
|
for num, i in enumerate(force_data["geom_info"]):
|
|
727
|
-
|
|
1400
|
+
cos = CSI.calculate_cos(B_g[i - 1] - g[i - 1], g[i - 1])
|
|
1401
|
+
state.cos_list[num].append(cos)
|
|
728
1402
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
FIO.argrelextrema_txt_save(grad_list, "local_min_grad", "min")
|
|
1403
|
+
if state.iter == 0:
|
|
1404
|
+
with open(folder_dir + "geometry_info.csv", "a") as f:
|
|
1405
|
+
f.write(",".join(data_name_list) + "\n")
|
|
733
1406
|
|
|
734
|
-
|
|
1407
|
+
with open(folder_dir + "geometry_info.csv", "a") as f:
|
|
1408
|
+
f.write(",".join(list(map(str, data_list))) + "\n")
|
|
735
1409
|
return
|
|
736
1410
|
|
|
737
|
-
def _copy_final_results_from_state(self):
|
|
738
|
-
"""Copy final results from the State object to the main Optimize object."""
|
|
739
|
-
if self.state:
|
|
740
|
-
self.final_file_directory = self.state.final_file_directory
|
|
741
|
-
self.final_geometry = self.state.final_geometry
|
|
742
|
-
self.final_energy = self.state.final_energy
|
|
743
|
-
self.final_bias_energy = self.state.final_bias_energy
|
|
744
|
-
self.symmetry = getattr(self.state, 'symmetry', None)
|
|
745
|
-
|
|
746
|
-
# These were not in the original _finalize, but probably should be
|
|
747
|
-
self.bias_pot_params_grad_list = self.state.bias_pot_params_grad_list
|
|
748
|
-
self.bias_pot_params_grad_name_list = self.state.bias_pot_params_grad_name_list
|
|
749
|
-
self.optimized_flag = self.state.optimized_flag
|
|
750
1411
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1412
|
+
class ResultPaths:
|
|
1413
|
+
@staticmethod
|
|
1414
|
+
def get_result_file_path(folder_dir, start_file):
|
|
1415
|
+
try:
|
|
1416
|
+
if folder_dir and start_file:
|
|
1417
|
+
base_name = os.path.splitext(os.path.basename(start_file))[0]
|
|
1418
|
+
optimized_filename = f"{base_name}_optimized.xyz"
|
|
1419
|
+
traj_filename = f"{base_name}_traj.xyz"
|
|
1420
|
+
|
|
1421
|
+
optimized_struct_file = os.path.abspath(
|
|
1422
|
+
os.path.join(folder_dir, optimized_filename)
|
|
1423
|
+
)
|
|
1424
|
+
traj_file = os.path.abspath(
|
|
1425
|
+
os.path.join(folder_dir, traj_filename)
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
print("Optimized structure file path:", optimized_struct_file)
|
|
1429
|
+
print("Trajectory file path:", traj_file)
|
|
1430
|
+
return optimized_struct_file, traj_file
|
|
1431
|
+
else:
|
|
1432
|
+
print(
|
|
1433
|
+
"Error: BPA_FOLDER_DIRECTORY or START_FILE is not set. Please run optimize() first."
|
|
1434
|
+
)
|
|
1435
|
+
return None, None
|
|
1436
|
+
except Exception as e:
|
|
1437
|
+
print(f"Error setting result file paths: {e}")
|
|
1438
|
+
return None, None
|
|
1439
|
+
|
|
1440
|
+
# =====================================================================================
|
|
1441
|
+
# 5. Optimize Runner
|
|
1442
|
+
# =====================================================================================
|
|
1443
|
+
class Optimize:
|
|
1444
|
+
"""
|
|
1445
|
+
Main runner orchestrating config, state, handler, and loop.
|
|
1446
|
+
"""
|
|
1447
|
+
|
|
1448
|
+
def __init__(self, args):
|
|
1449
|
+
self.config = OptimizationConfig(args)
|
|
1450
|
+
self.state = None
|
|
1451
|
+
self.handler = None
|
|
1452
|
+
self.file_io = None
|
|
1453
|
+
self.BPA_FOLDER_DIRECTORY = None
|
|
1454
|
+
self.START_FILE = None
|
|
1455
|
+
self.element_list = None
|
|
1456
|
+
self.SP = None
|
|
1457
|
+
self.final_file_directory = None
|
|
1458
|
+
self.final_geometry = None
|
|
1459
|
+
self.final_energy = None
|
|
1460
|
+
self.final_bias_energy = None
|
|
1461
|
+
self.symmetry = None
|
|
1462
|
+
self.irc_terminal_struct_paths = []
|
|
1463
|
+
self.optimized_struct_file = None
|
|
1464
|
+
self.traj_file = None
|
|
1465
|
+
self.bias_pot_params_grad_list = None
|
|
1466
|
+
self.bias_pot_params_grad_name_list = None
|
|
1467
|
+
|
|
1468
|
+
# Managers
|
|
1469
|
+
self.constraints = ConstraintManager(self.config)
|
|
1470
|
+
self.convergence = ConvergenceChecker(self.config)
|
|
1471
|
+
self.hessian_mgr = HessianManager(self.config)
|
|
1472
|
+
self.logger = RunLogger(self.config)
|
|
1473
|
+
self.result_paths = ResultPaths()
|
|
1474
|
+
|
|
1475
|
+
# ------------------------------------------------------------------
|
|
1476
|
+
# Setup helpers
|
|
1477
|
+
# ------------------------------------------------------------------
|
|
1478
|
+
def _setup_directory(self, input_file):
|
|
1479
|
+
self.START_FILE = input_file
|
|
1480
|
+
timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S_%f")[:-2]
|
|
1481
|
+
date = datetime.datetime.now().strftime("%Y_%m_%d")
|
|
1482
|
+
base_dir = f"{date}/{os.path.splitext(input_file)[0]}_OPT_"
|
|
1483
|
+
|
|
1484
|
+
if self.config.othersoft != "None":
|
|
1485
|
+
suffix = "ASE"
|
|
1486
|
+
elif self.config.sqm2:
|
|
1487
|
+
suffix = "SQM2"
|
|
1488
|
+
elif self.config.sqm1:
|
|
1489
|
+
suffix = "SQM1"
|
|
1490
|
+
elif self.config.args.usextb == "None" and self.config.args.usedxtb == "None":
|
|
1491
|
+
suffix = f"{self.config.FUNCTIONAL}_{self.config.BASIS_SET}"
|
|
1492
|
+
else:
|
|
1493
|
+
method = (
|
|
1494
|
+
self.config.args.usedxtb
|
|
1495
|
+
if self.config.args.usedxtb != "None"
|
|
1496
|
+
else self.config.args.usextb
|
|
1497
|
+
)
|
|
1498
|
+
suffix = method
|
|
1499
|
+
self.BPA_FOLDER_DIRECTORY = f"{base_dir}{suffix}_{timestamp}/"
|
|
1500
|
+
os.makedirs(self.BPA_FOLDER_DIRECTORY, exist_ok=True)
|
|
1501
|
+
|
|
1502
|
+
with open(os.path.join(self.BPA_FOLDER_DIRECTORY, "input.txt"), "w") as f:
|
|
1503
|
+
f.write(str(vars(self.config.args)))
|
|
1504
|
+
|
|
1505
|
+
def _init_calculation_module(self):
|
|
773
1506
|
xtb_method = None
|
|
774
1507
|
if self.config.args.pyscf:
|
|
775
1508
|
from multioptpy.Calculator.pyscf_calculation_tools import Calculation
|
|
@@ -784,14 +1517,17 @@ class Optimize:
|
|
|
784
1517
|
print("Use Lennard-Jones cluster potential.")
|
|
785
1518
|
elif self.config.othersoft.lower() == "emt":
|
|
786
1519
|
from multioptpy.Calculator.emt_calculation_tools import Calculation
|
|
787
|
-
print("Use
|
|
1520
|
+
print("Use EMT potential.")
|
|
788
1521
|
elif self.config.othersoft.lower() == "tersoff":
|
|
789
1522
|
from multioptpy.Calculator.tersoff_calculation_tools import Calculation
|
|
790
1523
|
print("Use Tersoff potential.")
|
|
791
1524
|
else:
|
|
792
1525
|
from multioptpy.Calculator.ase_calculation_tools import Calculation
|
|
793
1526
|
print("Use", self.config.othersoft)
|
|
794
|
-
with open(
|
|
1527
|
+
with open(
|
|
1528
|
+
self.BPA_FOLDER_DIRECTORY + "use_" + self.config.othersoft + ".txt",
|
|
1529
|
+
"w",
|
|
1530
|
+
) as f:
|
|
795
1531
|
f.write(self.config.othersoft + "\n")
|
|
796
1532
|
f.write(self.config.BASIS_SET + "\n")
|
|
797
1533
|
f.write(self.config.FUNCTIONAL + "\n")
|
|
@@ -804,24 +1540,18 @@ class Optimize:
|
|
|
804
1540
|
xtb_method = self.config.args.usextb
|
|
805
1541
|
else:
|
|
806
1542
|
from multioptpy.Calculator.psi4_calculation_tools import Calculation
|
|
807
|
-
|
|
808
1543
|
return Calculation, xtb_method
|
|
809
|
-
|
|
810
|
-
def
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
# This assumes the initial Model_hess (eye) is what's needed.
|
|
814
|
-
# This might be a flaw if SP needs the *current* Model_hess.
|
|
815
|
-
# Let's assume self.state.Model_hess is correct at time of call.
|
|
816
|
-
|
|
817
|
-
SP = Calculation(
|
|
1544
|
+
|
|
1545
|
+
def _create_calculation(self, Calculation, xtb_method, model_hess, override_dir=None):
|
|
1546
|
+
target_dir = override_dir if override_dir else self.BPA_FOLDER_DIRECTORY
|
|
1547
|
+
calc = Calculation(
|
|
818
1548
|
START_FILE=self.START_FILE,
|
|
819
1549
|
N_THREAD=self.config.N_THREAD,
|
|
820
1550
|
SET_MEMORY=self.config.SET_MEMORY,
|
|
821
1551
|
FUNCTIONAL=self.config.FUNCTIONAL,
|
|
822
1552
|
FC_COUNT=self.config.FC_COUNT,
|
|
823
|
-
BPA_FOLDER_DIRECTORY=
|
|
824
|
-
Model_hess=
|
|
1553
|
+
BPA_FOLDER_DIRECTORY=target_dir,
|
|
1554
|
+
Model_hess=model_hess,
|
|
825
1555
|
software_type=self.config.othersoft,
|
|
826
1556
|
unrestrict=self.config.unrestrict,
|
|
827
1557
|
SUB_BASIS_SET=self.config.SUB_BASIS_SET,
|
|
@@ -830,724 +1560,874 @@ class Optimize:
|
|
|
830
1560
|
electronic_charge=self.config.electronic_charge,
|
|
831
1561
|
excited_state=self.config.excited_state,
|
|
832
1562
|
dft_grid=self.config.dft_grid,
|
|
833
|
-
ECP
|
|
834
|
-
software_path_file
|
|
1563
|
+
ECP=self.config.ECP,
|
|
1564
|
+
software_path_file=self.config.software_path_file,
|
|
835
1565
|
)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
# which is a bit of a side-effect, but we'll keep it)
|
|
844
|
-
|
|
845
|
-
if os.path.splitext(FIO.START_FILE)[1] == ".gjf":
|
|
846
|
-
print("Gaussian input file (.gjf) detected.")
|
|
847
|
-
geometry_list, element_list, electric_charge_and_multiplicity = FIO.read_gjf_file(self.config.electric_charge_and_multiplicity)
|
|
848
|
-
elif os.path.splitext(FIO.START_FILE)[1] == ".inp":
|
|
849
|
-
print("GAMESS/Orca/Q-Chem input file (.inp) detected.")
|
|
850
|
-
geometry_list, element_list, electric_charge_and_multiplicity = FIO.read_gamess_inp_file(self.config.electric_charge_and_multiplicity)
|
|
851
|
-
elif os.path.splitext(FIO.START_FILE)[1] == ".mol":
|
|
852
|
-
print("MDL Molfile (.mol) detected.")
|
|
853
|
-
geometry_list, element_list, electric_charge_and_multiplicity = FIO.read_mol_file(self.config.electric_charge_and_multiplicity)
|
|
854
|
-
elif os.path.splitext(FIO.START_FILE)[1] == ".mol2":
|
|
855
|
-
print("MOL2 file (.mol2) detected.")
|
|
856
|
-
geometry_list, element_list, electric_charge_and_multiplicity = FIO.read_mol2_file(self.config.electric_charge_and_multiplicity)
|
|
857
|
-
else:
|
|
858
|
-
geometry_list, element_list, electric_charge_and_multiplicity = FIO.make_geometry_list(self.config.electric_charge_and_multiplicity)
|
|
859
|
-
|
|
860
|
-
file_directory = FIO.make_psi4_input_file(geometry_list, 0)
|
|
1566
|
+
calc.cpcm_solv_model = self.config.cpcm_solv_model
|
|
1567
|
+
calc.alpb_solv_model = self.config.alpb_solv_model
|
|
1568
|
+
calc.xtb_method = xtb_method
|
|
1569
|
+
return calc
|
|
1570
|
+
|
|
1571
|
+
def _initialize_handler(self, element_list, force_data):
|
|
1572
|
+
Calculation, xtb_method = self._init_calculation_module()
|
|
861
1573
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
self.element_list = element_list # Set self.element_list
|
|
866
|
-
# self.Model_hess = np.eye(len(element_list) * 3) # This is now done in OptimizationState
|
|
1574
|
+
|
|
1575
|
+
self.SP = self._create_calculation(Calculation, xtb_method, self.state.Model_hess)
|
|
867
1576
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if iter == 0:
|
|
873
|
-
with open(self.BPA_FOLDER_DIRECTORY+"energy_profile.csv","a") as f:
|
|
874
|
-
f.write("energy [hartree] \n")
|
|
875
|
-
with open(self.BPA_FOLDER_DIRECTORY+"energy_profile.csv","a") as f:
|
|
876
|
-
f.write(str(e)+"\n")
|
|
877
|
-
#-------------------gradient profile
|
|
878
|
-
if iter == 0:
|
|
879
|
-
with open(self.BPA_FOLDER_DIRECTORY+"gradient_profile.csv","a") as f:
|
|
880
|
-
f.write("gradient (RMS) [hartree/Bohr] \n")
|
|
881
|
-
with open(self.BPA_FOLDER_DIRECTORY+"gradient_profile.csv","a") as f:
|
|
882
|
-
f.write(str(self.calculate_rms_safely(g))+"\n")
|
|
883
|
-
#-------------------
|
|
884
|
-
if iter == 0:
|
|
885
|
-
with open(self.BPA_FOLDER_DIRECTORY+"bias_gradient_profile.csv","a") as f:
|
|
886
|
-
f.write("bias gradient (RMS) [hartree/Bohr] \n")
|
|
887
|
-
with open(self.BPA_FOLDER_DIRECTORY+"bias_gradient_profile.csv","a") as f:
|
|
888
|
-
f.write(str(self.calculate_rms_safely(B_g))+"\n")
|
|
889
|
-
#-------------------
|
|
890
|
-
return
|
|
891
|
-
|
|
892
|
-
def _save_energy_profiles(self):
|
|
893
|
-
# (Reads self.state)
|
|
894
|
-
with open(self.BPA_FOLDER_DIRECTORY+"energy_profile_kcalmol.csv","w") as f:
|
|
895
|
-
f.write("ITER.,energy[kcal/mol]\n")
|
|
896
|
-
for i in range(len(self.state.ENERGY_LIST_FOR_PLOTTING)):
|
|
897
|
-
f.write(str(i)+","+str(self.state.ENERGY_LIST_FOR_PLOTTING[i] - self.state.ENERGY_LIST_FOR_PLOTTING[0])+"\n")
|
|
898
|
-
return
|
|
899
|
-
|
|
900
|
-
def geom_info_extract(self, force_data, file_directory, B_g, g):
|
|
901
|
-
# (Writes to self.state.cos_list)
|
|
902
|
-
if len(force_data["geom_info"]) > 1:
|
|
903
|
-
CSI = CalculationStructInfo()
|
|
904
|
-
|
|
905
|
-
data_list, data_name_list = CSI.Data_extract(glob.glob(file_directory+"/*.xyz")[0], force_data["geom_info"])
|
|
906
|
-
|
|
907
|
-
for num, i in enumerate(force_data["geom_info"]):
|
|
908
|
-
cos = CSI.calculate_cos(B_g[i-1] - g[i-1], g[i-1])
|
|
909
|
-
self.state.cos_list[num].append(cos)
|
|
910
|
-
|
|
911
|
-
# Need to use self.state.iter to check
|
|
912
|
-
if self.state.iter == 0:
|
|
913
|
-
with open(self.BPA_FOLDER_DIRECTORY+"geometry_info.csv","a") as f:
|
|
914
|
-
f.write(",".join(data_name_list)+"\n")
|
|
915
|
-
|
|
916
|
-
with open(self.BPA_FOLDER_DIRECTORY+"geometry_info.csv","a") as f:
|
|
917
|
-
f.write(",".join(list(map(str,data_list)))+"\n")
|
|
918
|
-
return
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
def dissociation_check(self, new_geometry, element_list):
|
|
922
|
-
"""
|
|
923
|
-
Checks if the molecular geometry has dissociated into multiple fragments
|
|
924
|
-
based on a distance threshold.
|
|
925
|
-
"""
|
|
926
|
-
# (Reads self.config.DC_check_dist)
|
|
927
|
-
atom_label_list = list(range(len(new_geometry)))
|
|
928
|
-
fragm_atom_num_list = []
|
|
929
|
-
|
|
930
|
-
# 1. Identify all molecular fragments (connected components)
|
|
931
|
-
while len(atom_label_list) > 0:
|
|
932
|
-
tmp_fragm_list = Calculationtools().check_atom_connectivity(new_geometry, element_list, atom_label_list[0])
|
|
933
|
-
atom_label_list = list(set(atom_label_list) - set(tmp_fragm_list))
|
|
934
|
-
fragm_atom_num_list.append(tmp_fragm_list)
|
|
1577
|
+
self.state.element_list = element_list
|
|
1578
|
+
self.state.element_number_list = np.array(
|
|
1579
|
+
[element_number(elem) for elem in element_list], dtype="int"
|
|
1580
|
+
)
|
|
935
1581
|
|
|
936
|
-
#
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
#
|
|
944
|
-
|
|
1582
|
+
# --- EDEEL Mode ---
|
|
1583
|
+
# Assuming the parser sets an 'edeel' flag in args or force_data
|
|
1584
|
+
if hasattr(self.config.args, 'edeel') and self.config.args.edeel:
|
|
1585
|
+
print("Mode: EDEEL (Energy Decomposition and Extrapolation-based Electron Localization)")
|
|
1586
|
+
|
|
1587
|
+
# Note: The user handles the parser.
|
|
1588
|
+
# We assume 'ede_params', 'donor_atoms', and 'acceptor_atoms' are available in force_data.
|
|
1589
|
+
# Example structure for ede_params: {'complex': [0,1], 'd_ox': [0,1], ...}
|
|
1590
|
+
|
|
1591
|
+
ede_params = force_data.get('edeel_params')
|
|
1592
|
+
d_atoms = force_data.get('donor_atoms', [])
|
|
1593
|
+
a_atoms = force_data.get('acceptor_atoms', [])
|
|
1594
|
+
|
|
1595
|
+
if not ede_params or not d_atoms or not a_atoms:
|
|
1596
|
+
raise ValueError("EDEEL mode requires 'edeel_params', 'donor_atoms', and 'acceptor_atoms' in input.")
|
|
1597
|
+
|
|
1598
|
+
return EDEELHandler(
|
|
1599
|
+
self.SP,
|
|
1600
|
+
d_atoms,
|
|
1601
|
+
a_atoms,
|
|
1602
|
+
ede_params,
|
|
1603
|
+
self.config,
|
|
1604
|
+
self.file_io,
|
|
1605
|
+
self.BPA_FOLDER_DIRECTORY,
|
|
1606
|
+
force_data
|
|
1607
|
+
)
|
|
1608
|
+
# --- Model Function Mode (NEW) ---
|
|
1609
|
+
elif len(self.config.args.model_function) > 0:
|
|
1610
|
+
print("Mode: Model Function Optimization")
|
|
1611
|
+
Calculation, xtb_method = self._init_calculation_module()
|
|
1612
|
+
|
|
1613
|
+
# Create independent base directories for State 1 and State 2
|
|
1614
|
+
dir1 = os.path.join(self.BPA_FOLDER_DIRECTORY, "State1_base")
|
|
1615
|
+
dir2 = os.path.join(self.BPA_FOLDER_DIRECTORY, "State2_base")
|
|
1616
|
+
os.makedirs(dir1, exist_ok=True)
|
|
1617
|
+
os.makedirs(dir2, exist_ok=True)
|
|
1618
|
+
|
|
1619
|
+
# Initialize two independent calculators to prevent state contamination
|
|
1620
|
+
calc1 = self._create_calculation(Calculation, xtb_method, self.state.Model_hess, override_dir=dir1)
|
|
1621
|
+
calc2 = self._create_calculation(Calculation, xtb_method, self.state.Model_hess, override_dir=dir2)
|
|
1622
|
+
|
|
1623
|
+
handler = ModelFunctionHandler(
|
|
1624
|
+
calc1, calc2,
|
|
1625
|
+
self.config.args.model_function,
|
|
1626
|
+
self.config,
|
|
1627
|
+
self.file_io,
|
|
1628
|
+
self.BPA_FOLDER_DIRECTORY,
|
|
1629
|
+
force_data
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
# --- BITSS Mode ---
|
|
1633
|
+
if handler.is_bitss:
|
|
1634
|
+
print("BITSS Mode detected: Expanding state to 2N atoms.")
|
|
1635
|
+
geom1 = self.state.geometry
|
|
1636
|
+
geom2 = handler.bitss_ref_geom
|
|
945
1637
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
coords2 = geom_np[fragm_2_indices] # Shape (K, 3)
|
|
1638
|
+
if geom1.shape != geom2.shape:
|
|
1639
|
+
raise ValueError("BITSS: Input and Reference geometries must have the same dimensions.")
|
|
949
1640
|
|
|
950
|
-
#
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1641
|
+
# Expand geometry and gradients to 6N dimensions
|
|
1642
|
+
self.state.geometry = np.vstack((geom1, geom2))
|
|
1643
|
+
self.state.initial_geometry = copy.deepcopy(self.state.geometry)
|
|
1644
|
+
self.state.pre_geometry = copy.deepcopy(self.state.geometry)
|
|
954
1645
|
|
|
955
|
-
|
|
956
|
-
# This calculates the squared Euclidean distance for all pairs
|
|
957
|
-
# The result (sq_dist_matrix) will have shape (M, K)
|
|
958
|
-
sq_dist_matrix = np.sum(diff_matrix**2, axis=2)
|
|
1646
|
+
n_atoms = len(element_list) # Original N
|
|
959
1647
|
|
|
960
|
-
#
|
|
961
|
-
|
|
1648
|
+
# Current gradients
|
|
1649
|
+
self.state.raw_gradient = np.zeros((2 * n_atoms, 3))
|
|
1650
|
+
self.state.bias_gradient = np.zeros((2 * n_atoms, 3))
|
|
1651
|
+
self.state.effective_gradient = np.zeros((2 * n_atoms, 3))
|
|
962
1652
|
|
|
963
|
-
# Take the square root of only the minimum value to get the final distance
|
|
964
|
-
min_dist = np.sqrt(min_sq_dist)
|
|
965
|
-
|
|
966
|
-
fragm_dist_list.append(min_dist)
|
|
967
1653
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1654
|
+
self.state.pre_raw_gradient = np.zeros((2 * n_atoms, 3))
|
|
1655
|
+
self.state.pre_bias_gradient = np.zeros((2 * n_atoms, 3))
|
|
1656
|
+
self.state.pre_effective_gradient = np.zeros((2 * n_atoms, 3))
|
|
1657
|
+
self.state.pre_move_vector = np.zeros((2 * n_atoms, 3))
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
self.state.Model_hess = np.eye(2 * n_atoms * 3)
|
|
1661
|
+
|
|
1662
|
+
# Double the element lists
|
|
1663
|
+
self.state.element_list = element_list + element_list
|
|
1664
|
+
self.state.element_number_list = np.concatenate((self.state.element_number_list, self.state.element_number_list))
|
|
976
1665
|
else:
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1666
|
+
print(f"Standard Model Function Mode ({handler.method_name}): Using {len(element_list)} atoms.")
|
|
1667
|
+
|
|
1668
|
+
return handler
|
|
1669
|
+
|
|
1670
|
+
# --- ONIOM Mode ---
|
|
1671
|
+
elif len(self.config.args.oniom_flag) > 0:
|
|
1672
|
+
# ONIOM
|
|
1673
|
+
high_atoms = force_data["oniom_flag"][0]
|
|
1674
|
+
link_atoms = force_data["oniom_flag"][1]
|
|
981
1675
|
|
|
982
|
-
return DC_exit_flag
|
|
983
|
-
|
|
984
|
-
def calculate_rms_safely(self, vector, threshold=1e-10):
|
|
985
|
-
# (This method is pure, no changes needed)
|
|
986
|
-
filtered_vector = vector[np.abs(vector) > threshold]
|
|
987
|
-
if filtered_vector.size > 0:
|
|
988
|
-
return np.sqrt((filtered_vector**2).mean())
|
|
989
|
-
else:
|
|
990
|
-
return 0.0
|
|
991
1676
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
print("BIAS ENERGY : {:>15.12f} ".format(B_e))
|
|
1002
|
-
print("Maximum Force : {0:>15.12f} {1:>15.12f} ".format(max_B_g, self.config.MAX_FORCE_THRESHOLD))
|
|
1003
|
-
print("RMS Force : {0:>15.12f} {1:>15.12f} ".format(rms_force, self.config.RMS_FORCE_THRESHOLD))
|
|
1004
|
-
print("Maximum Displacement : {0:>15.12f} {1:>15.12f} ".format(max_displacement, max_displacement_threshold))
|
|
1005
|
-
print("RMS Displacement : {0:>15.12f} {1:>15.12f} ".format(rms_displacement, rms_displacement_threshold))
|
|
1006
|
-
print("ENERGY SHIFT : {:>15.12f} ".format(e - pre_e))
|
|
1007
|
-
print("BIAS ENERGY SHIFT : {:>15.12f} ".format(B_e - pre_B_e))
|
|
1008
|
-
return
|
|
1009
|
-
|
|
1010
|
-
def calc_fragement_grads(self, gradient, fragment_list):
|
|
1011
|
-
# (This method is pure, no changes needed)
|
|
1012
|
-
calced_gradient = gradient
|
|
1013
|
-
for fragment in fragment_list:
|
|
1014
|
-
tmp_grad = np.array([0.0, 0.0, 0.0], dtype="float64")
|
|
1015
|
-
for atom_num in fragment:
|
|
1016
|
-
tmp_grad += gradient[atom_num-1]
|
|
1017
|
-
tmp_grad /= len(fragment)
|
|
1677
|
+
hl_dir = os.path.join(self.BPA_FOLDER_DIRECTORY, "High_Layer")
|
|
1678
|
+
ll_dir = os.path.join(self.BPA_FOLDER_DIRECTORY, "Low_Layer")
|
|
1679
|
+
os.makedirs(hl_dir, exist_ok=True)
|
|
1680
|
+
os.makedirs(ll_dir, exist_ok=True)
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
high_calc = self._create_calculation(Calculation, xtb_method, self.state.Model_hess, override_dir=hl_dir)
|
|
1684
|
+
low_calc = self._create_calculation(Calculation, xtb_method, self.state.Model_hess, override_dir=ll_dir)
|
|
1685
|
+
|
|
1018
1686
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1687
|
+
return ONIOMHandler(
|
|
1688
|
+
high_calc,
|
|
1689
|
+
low_calc,
|
|
1690
|
+
high_atoms,
|
|
1691
|
+
link_atoms,
|
|
1692
|
+
self.config,
|
|
1693
|
+
self.file_io,
|
|
1694
|
+
self.BPA_FOLDER_DIRECTORY,
|
|
1695
|
+
force_data,
|
|
1696
|
+
)
|
|
1697
|
+
else:
|
|
1698
|
+
# Standard
|
|
1699
|
+
return StandardHandler(
|
|
1700
|
+
self.SP, self.config, self.file_io, self.BPA_FOLDER_DIRECTORY, force_data
|
|
1701
|
+
)
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
# ------------------------------------------------------------------
|
|
1705
|
+
# Geometry parsing helper
|
|
1706
|
+
# ------------------------------------------------------------------
|
|
1707
|
+
def _extract_geom_from_geometry_list(self, geometry_list):
|
|
1024
1708
|
"""
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1709
|
+
Extract geometry (Bohr) from geometry_list returned by FileIO.make_geometry_list.
|
|
1710
|
+
Assumes geometry_list[0][0] and [0][1] hold charge/multiplicity,
|
|
1711
|
+
and atom records start from geometry_list[0][2], each formatted as
|
|
1712
|
+
[element, x, y, z] in Angstrom.
|
|
1029
1713
|
"""
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
# 2. Write input files and create the State object
|
|
1039
|
-
geometry_list, element_list, electric_charge_and_multiplicity = self.write_input_files(FIO)
|
|
1040
|
-
self.element_list = element_list # Set on self for helpers
|
|
1041
|
-
|
|
1042
|
-
# Create the main State object for the "Real" system
|
|
1043
|
-
self.state = OptimizationState(element_list)
|
|
1044
|
-
self.state.cos_list = [[] for i in range(len(force_data["geom_info"]))]
|
|
1714
|
+
try:
|
|
1715
|
+
atom_entries = geometry_list[0][2:]
|
|
1716
|
+
coords_ang = np.array([atom[1:4] for atom in atom_entries], dtype=float)
|
|
1717
|
+
geom_bohr = coords_ang / self.config.bohr2angstroms
|
|
1718
|
+
return geom_bohr
|
|
1719
|
+
except Exception as e:
|
|
1720
|
+
raise ValueError(f"Failed to parse geometry_list: {geometry_list}") from e
|
|
1045
1721
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
# Save ONIOM configuration to file
|
|
1060
|
-
with open(self.BPA_FOLDER_DIRECTORY+"ONIOM2.txt", "w") as f:
|
|
1061
|
-
f.write("### Low layer ###\n")
|
|
1062
|
-
f.write(calc_method+"\n")
|
|
1063
|
-
f.write("### High layer ###\n")
|
|
1064
|
-
f.write(self.config.BASIS_SET+"\n")
|
|
1065
|
-
f.write(self.config.FUNCTIONAL+"\n")
|
|
1066
|
-
|
|
1067
|
-
# 4. Initialize geometries and ONIOM setup
|
|
1068
|
-
geom_num_list = []
|
|
1069
|
-
for i in range(2, len(geometry_list[0])):
|
|
1070
|
-
geom_num_list.append(geometry_list[0][i][1:4])
|
|
1071
|
-
geom_num_list = np.array(geom_num_list, dtype="float64") / self.config.bohr2angstroms
|
|
1072
|
-
self.state.geom_num_list = geom_num_list # Set initial geometry in state
|
|
1073
|
-
|
|
1074
|
-
linker_atom_pair_num = specify_link_atom_pairs(geom_num_list, element_list, high_layer_atom_num, link_atom_num)
|
|
1075
|
-
print("Boundary of high layer and low layer:", linker_atom_pair_num)
|
|
1076
|
-
|
|
1077
|
-
high_layer_geom_num_list, high_layer_element_list = separate_high_layer_and_low_layer(
|
|
1078
|
-
geom_num_list, linker_atom_pair_num, high_layer_atom_num, element_list)
|
|
1079
|
-
|
|
1080
|
-
real_2_highlayer_label_connect_dict, highlayer_2_real_label_connect_dict = link_number_high_layer_and_low_layer(high_layer_atom_num)
|
|
1081
|
-
|
|
1082
|
-
# 5. Initialize model Hessians (local state for ONIOM)
|
|
1083
|
-
LL_Model_hess = np.eye(len(element_list)*3)
|
|
1084
|
-
HL_Model_hess = np.eye((len(high_layer_element_list))*3)
|
|
1085
|
-
|
|
1086
|
-
# Create mask for high layer atoms
|
|
1087
|
-
bool_list = []
|
|
1088
|
-
for i in range(len(element_list)):
|
|
1089
|
-
if i in high_layer_atom_num:
|
|
1090
|
-
bool_list.extend([True, True, True])
|
|
1722
|
+
# ------------------------------------------------------------------
|
|
1723
|
+
# Main run
|
|
1724
|
+
# ------------------------------------------------------------------
|
|
1725
|
+
def run(self):
|
|
1726
|
+
input_list = (
|
|
1727
|
+
[self.config.args.INPUT]
|
|
1728
|
+
if isinstance(self.config.args.INPUT, str)
|
|
1729
|
+
else self.config.args.INPUT
|
|
1730
|
+
)
|
|
1731
|
+
job_file_list = []
|
|
1732
|
+
for job_file in input_list:
|
|
1733
|
+
if "*" in job_file:
|
|
1734
|
+
job_file_list.extend(glob.glob(job_file))
|
|
1091
1735
|
else:
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
# 6. Initialize bias potential calculators
|
|
1095
|
-
# (self.CalcBiaspot will be used for the "Real" system bias)
|
|
1096
|
-
LL_Calc_BiasPot = BiasPotentialCalculation(self.BPA_FOLDER_DIRECTORY)
|
|
1097
|
-
# HL_Calc_BiasPot = BiasPotentialCalculation(self.BPA_FOLDER_DIRECTORY) # Seems unused in original
|
|
1098
|
-
self.CalcBiaspot = BiasPotentialCalculation(self.BPA_FOLDER_DIRECTORY) # For main state
|
|
1736
|
+
job_file_list.append(job_file)
|
|
1099
1737
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1738
|
+
for file in job_file_list:
|
|
1739
|
+
self._run_single_job(file)
|
|
1740
|
+
|
|
1741
|
+
print("All calculations were completed.")
|
|
1742
|
+
self.get_result_file_path()
|
|
1743
|
+
return
|
|
1744
|
+
|
|
1745
|
+
# ------------------------------------------------------------------
|
|
1746
|
+
# Extracted single-job runner to simplify run()
|
|
1747
|
+
# ------------------------------------------------------------------
|
|
1748
|
+
def _run_single_job(self, file):
|
|
1749
|
+
print("********************************")
|
|
1750
|
+
print(file)
|
|
1751
|
+
print("********************************")
|
|
1752
|
+
if not os.path.exists(file):
|
|
1753
|
+
print(f"{file} does not exist.")
|
|
1754
|
+
return
|
|
1755
|
+
|
|
1756
|
+
self._setup_directory(file)
|
|
1757
|
+
self.file_io = FileIO(self.BPA_FOLDER_DIRECTORY, file)
|
|
1758
|
+
|
|
1759
|
+
# Read geometry
|
|
1760
|
+
geometry_list, element_list, chg_mult = self.file_io.make_geometry_list(
|
|
1761
|
+
self.config.electric_charge_and_multiplicity
|
|
1762
|
+
)
|
|
1763
|
+
geom = self._extract_geom_from_geometry_list(geometry_list)
|
|
1764
|
+
self.element_list = element_list
|
|
1765
|
+
|
|
1766
|
+
# Initialize state
|
|
1767
|
+
# NOTE: element_list is passed here, but self.state.element_list might be modified later (e.g. for BITSS)
|
|
1768
|
+
self.state = OptimizationState(element_list)
|
|
1769
|
+
self.state.geometry = copy.deepcopy(geom)
|
|
1770
|
+
self.state.initial_geometry = copy.deepcopy(geom)
|
|
1771
|
+
self.state.element_list = element_list
|
|
1772
|
+
self.state.element_number_list = np.array(
|
|
1773
|
+
[element_number(elem) for elem in element_list], dtype="int"
|
|
1774
|
+
)
|
|
1775
|
+
self.state.cos_list = [
|
|
1776
|
+
[] for _ in range(len(force_data_parser(self.config.args)["geom_info"]))
|
|
1777
|
+
]
|
|
1778
|
+
|
|
1779
|
+
force_data = force_data_parser(self.config.args)
|
|
1120
1780
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1781
|
+
# Initialize Handler (This may update self.state.element_list and self.state.geometry for BITSS)
|
|
1782
|
+
self.handler = self._initialize_handler(element_list, force_data)
|
|
1783
|
+
|
|
1784
|
+
# Constraint setup
|
|
1785
|
+
PC = ProjectOutConstrain(
|
|
1786
|
+
force_data["projection_constraint_condition_list"],
|
|
1787
|
+
force_data["projection_constraint_atoms"],
|
|
1788
|
+
force_data["projection_constraint_constant"],
|
|
1789
|
+
)
|
|
1790
|
+
projection_constrain, allactive_flag = self.constraints.constrain_flag_check(force_data)
|
|
1791
|
+
n_fix = len(force_data["fix_atoms"])
|
|
1792
|
+
|
|
1793
|
+
# Move vector and optimizer
|
|
1794
|
+
# FIX: Use self.state.element_list which handles the 2N size in BITSS mode
|
|
1795
|
+
CMV = CalculateMoveVector(
|
|
1796
|
+
self.config.DELTA,
|
|
1797
|
+
self.state.element_list,
|
|
1798
|
+
self.config.args.saddle_order,
|
|
1799
|
+
self.config.FC_COUNT,
|
|
1800
|
+
self.config.temperature,
|
|
1801
|
+
self.config.use_model_hessian,
|
|
1802
|
+
max_trust_radius=self.config.max_trust_radius,
|
|
1803
|
+
min_trust_radius=self.config.min_trust_radius,
|
|
1804
|
+
projection_constraint=PC
|
|
1805
|
+
)
|
|
1806
|
+
optimizer_instances = CMV.initialization(force_data["opt_method"])
|
|
1807
|
+
for opt in optimizer_instances:
|
|
1808
|
+
opt.set_hessian(self.state.Model_hess)
|
|
1123
1809
|
if self.config.DELTA != "x":
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
real_bias_grad_list = []
|
|
1158
|
-
|
|
1159
|
-
# 11. Main optimization loop
|
|
1160
|
-
for iter in range(self.config.NSTEP):
|
|
1161
|
-
self.state.iter = iter
|
|
1162
|
-
|
|
1163
|
-
exit_file_detect = os.path.exists(self.BPA_FOLDER_DIRECTORY+"end.txt")
|
|
1164
|
-
if exit_file_detect:
|
|
1165
|
-
self.state.exit_flag = True
|
|
1810
|
+
opt.DELTA = self.config.DELTA
|
|
1811
|
+
# Gracefully handle optimizers without newton_tag
|
|
1812
|
+
supports_exact_hess = getattr(opt, "newton_tag", True)
|
|
1813
|
+
if (not supports_exact_hess) and self.config.FC_COUNT > 0 and (
|
|
1814
|
+
"eigvec" not in force_data["projection_constraint_condition_list"]
|
|
1815
|
+
):
|
|
1816
|
+
print(
|
|
1817
|
+
"Error: This optimizer does not support exact Hessian calculations."
|
|
1818
|
+
)
|
|
1819
|
+
sys.exit(0)
|
|
1820
|
+
|
|
1821
|
+
# Koopman
|
|
1822
|
+
if self.config.koopman_analysis:
|
|
1823
|
+
# FIX: Use self.state.element_list
|
|
1824
|
+
KA = KoopmanAnalyzer(len(self.state.element_list), file_directory=self.BPA_FOLDER_DIRECTORY)
|
|
1825
|
+
else:
|
|
1826
|
+
KA = None
|
|
1827
|
+
|
|
1828
|
+
# Initial files
|
|
1829
|
+
# FIX: Use self.state.element_list
|
|
1830
|
+
self.file_io.print_geometry_list(
|
|
1831
|
+
self.state.geometry * self.config.bohr2angstroms,
|
|
1832
|
+
self.state.element_list,
|
|
1833
|
+
chg_mult,
|
|
1834
|
+
)
|
|
1835
|
+
|
|
1836
|
+
# Main loop
|
|
1837
|
+
for iter_idx in range(self.config.NSTEP):
|
|
1838
|
+
self.state.iter = iter_idx
|
|
1839
|
+
self.state.exit_flag = os.path.exists(
|
|
1840
|
+
self.BPA_FOLDER_DIRECTORY + "end.txt"
|
|
1841
|
+
)
|
|
1842
|
+
if self.state.exit_flag:
|
|
1166
1843
|
break
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
real_initial_geom_num_list = geom_num_list.copy() # Bohr
|
|
1174
|
-
real_pre_geom = real_initial_geom_num_list.copy() # Bohr
|
|
1175
|
-
|
|
1176
|
-
# --- Model Low Layer Calc ---
|
|
1177
|
-
print("Model low layer calculation")
|
|
1178
|
-
model_LL_e, model_LL_g, high_layer_geom_num_list, finish_frag = LLSP.single_point(
|
|
1179
|
-
file_directory, high_layer_element_list, iter, electric_charge_and_multiplicity,
|
|
1180
|
-
calc_method, geom_num_list=high_layer_geom_num_list*self.config.bohr2angstroms)
|
|
1181
|
-
|
|
1182
|
-
if finish_frag:
|
|
1183
|
-
self.state.exit_flag = True
|
|
1844
|
+
|
|
1845
|
+
# shape condition check
|
|
1846
|
+
self.state.exit_flag = judge_shape_condition(
|
|
1847
|
+
self.state.geometry, self.config.shape_conditions
|
|
1848
|
+
)
|
|
1849
|
+
if self.state.exit_flag:
|
|
1184
1850
|
break
|
|
1185
|
-
|
|
1186
|
-
# --- Microiterations ---
|
|
1187
|
-
print("Processing microiteration...")
|
|
1188
|
-
LL_CMV = CalculateMoveVector(self.config.DELTA, element_list, self.config.args.saddle_order, self.config.FC_COUNT, self.config.temperature)
|
|
1189
|
-
LL_optimizer_instances = LL_CMV.initialization(["fire"])
|
|
1190
|
-
LL_optimizer_instances[0].display_flag = False
|
|
1191
|
-
|
|
1192
|
-
low_layer_converged = False
|
|
1193
|
-
|
|
1194
|
-
# Use geom_num_list from main state
|
|
1195
|
-
current_geom_num_list = self.state.geom_num_list.copy()
|
|
1196
|
-
|
|
1197
|
-
for microiter in range(self.config.microiter_num):
|
|
1198
|
-
LLSP.Model_hess = LL_Model_hess
|
|
1199
|
-
|
|
1200
|
-
real_LL_e, real_LL_g, current_geom_num_list, finish_frag = LLSP.single_point(
|
|
1201
|
-
file_directory, element_list, microiter, electric_charge_and_multiplicity,
|
|
1202
|
-
calc_method, geom_num_list=current_geom_num_list*self.config.bohr2angstroms)
|
|
1203
|
-
|
|
1204
|
-
LL_Model_hess = LLSP.Model_hess
|
|
1205
|
-
|
|
1206
|
-
LL_Calc_BiasPot.Model_hess = LL_Model_hess
|
|
1207
|
-
_, real_LL_B_e, real_LL_B_g, LL_BPA_hessian = LL_Calc_BiasPot.main(
|
|
1208
|
-
real_LL_e, real_LL_g, current_geom_num_list, element_list,
|
|
1209
|
-
force_data, pre_real_LL_B_g, microiter, real_initial_geom_num_list)
|
|
1210
|
-
|
|
1211
|
-
for x in range(len(LL_optimizer_instances)):
|
|
1212
|
-
LL_optimizer_instances[x].set_bias_hessian(LL_BPA_hessian)
|
|
1213
|
-
if microiter % self.config.FC_COUNT == 0: # Using FC_COUNT, not mFC_COUNT
|
|
1214
|
-
LL_optimizer_instances[x].set_hessian(LL_Model_hess)
|
|
1215
|
-
|
|
1216
|
-
if len(force_data["opt_fragment"]) > 0:
|
|
1217
|
-
real_LL_B_g = copy.deepcopy(self.calc_fragement_grads(real_LL_B_g, force_data["opt_fragment"]))
|
|
1218
|
-
real_LL_g = copy.deepcopy(self.calc_fragement_grads(real_LL_g, force_data["opt_fragment"]))
|
|
1219
|
-
|
|
1220
|
-
prev_geom = current_geom_num_list.copy()
|
|
1221
|
-
|
|
1222
|
-
current_geom_num_list_ang, LL_move_vector, LL_optimizer_instances = LL_CMV.calc_move_vector(
|
|
1223
|
-
microiter, current_geom_num_list, real_LL_B_g, pre_real_LL_B_g,
|
|
1224
|
-
real_pre_geom, real_LL_B_e, pre_real_LL_B_e,
|
|
1225
|
-
pre_real_LL_move_vector, real_initial_geom_num_list,
|
|
1226
|
-
real_LL_g, pre_real_LL_g, LL_optimizer_instances, print_flag=False)
|
|
1227
|
-
|
|
1228
|
-
current_geom_num_list = current_geom_num_list_ang / self.config.bohr2angstroms
|
|
1229
1851
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
if len(force_data["fix_atoms"]) > 0:
|
|
1236
|
-
for j in force_data["fix_atoms"]:
|
|
1237
|
-
current_geom_num_list[j-1] = copy.deepcopy(real_initial_geom_num_list[j-1]) # Already in Bohr
|
|
1238
|
-
|
|
1239
|
-
displacement_vector = current_geom_num_list - prev_geom
|
|
1240
|
-
|
|
1241
|
-
# Calculate convergence metrics for low layer atoms only
|
|
1242
|
-
low_layer_grads = []
|
|
1243
|
-
low_layer_displacements = []
|
|
1244
|
-
for i in range(len(element_list)):
|
|
1245
|
-
if (i+1) not in high_layer_atom_num:
|
|
1246
|
-
low_layer_grads.append(real_LL_B_g[i])
|
|
1247
|
-
low_layer_displacements.append(displacement_vector[i])
|
|
1248
|
-
|
|
1249
|
-
low_layer_grads = np.array(low_layer_grads)
|
|
1250
|
-
low_layer_displacements = np.array(low_layer_displacements)
|
|
1251
|
-
|
|
1252
|
-
low_layer_rms_grad = self.calculate_rms_safely(low_layer_grads)
|
|
1253
|
-
max_displacement = np.abs(displacement_vector).max() if len(displacement_vector) > 0 else 0
|
|
1254
|
-
rms_displacement = self.calculate_rms_safely(displacement_vector)
|
|
1255
|
-
energy_shift = -1 * pre_real_LL_B_e + real_LL_B_e
|
|
1256
|
-
|
|
1257
|
-
if microiter % 10 == 0:
|
|
1258
|
-
print(f"M. ITR. {microiter}")
|
|
1259
|
-
print("Microiteration results:")
|
|
1260
|
-
print(f"LOW LAYER BIAS ENERGY : {float(real_LL_B_e):10.8f}")
|
|
1261
|
-
print(f"LOW LAYER ENERGY : {float(real_LL_e):10.8f}")
|
|
1262
|
-
print(f"LOW LAYER MAX GRADIENT: {float(low_layer_grads.max() if len(low_layer_grads) > 0 else 0):10.8f}")
|
|
1263
|
-
print(f"LOW LAYER RMS GRADIENT: {float(low_layer_rms_grad):10.8f}")
|
|
1264
|
-
print(f"MAX DISPLACEMENT : {float(max_displacement):10.8f}")
|
|
1265
|
-
print(f"RMS DISPLACEMENT : {float(rms_displacement):10.8f}")
|
|
1266
|
-
print(f"ENERGY SHIFT : {float(energy_shift):10.8f}")
|
|
1267
|
-
|
|
1268
|
-
# Check convergence (using hardcoded values from original)
|
|
1269
|
-
if (low_layer_rms_grad < 0.0003) and \
|
|
1270
|
-
(low_layer_grads.max() < 0.0006 if len(low_layer_grads) > 0 else True) and \
|
|
1271
|
-
(max_displacement < 0.003) and \
|
|
1272
|
-
(rms_displacement < 0.002):
|
|
1273
|
-
print("Low layer converged... (microiteration)")
|
|
1274
|
-
low_layer_converged = True
|
|
1275
|
-
break
|
|
1276
|
-
|
|
1277
|
-
# Update previous values for next microiteration
|
|
1278
|
-
pre_real_LL_B_e = real_LL_B_e
|
|
1279
|
-
pre_real_LL_g = real_LL_g
|
|
1280
|
-
pre_real_LL_B_g = real_LL_B_g
|
|
1281
|
-
pre_real_LL_move_vector = LL_move_vector
|
|
1282
|
-
|
|
1283
|
-
# End of microiteration loop
|
|
1284
|
-
if not low_layer_converged:
|
|
1285
|
-
print("Reached maximum number of microiterations.")
|
|
1286
|
-
print("Microiteration complete.")
|
|
1287
|
-
|
|
1288
|
-
# Update the main geometry state
|
|
1289
|
-
self.state.geom_num_list = current_geom_num_list
|
|
1290
|
-
geom_num_list = current_geom_num_list # Use for this iter
|
|
1291
|
-
|
|
1292
|
-
# --- Model High Layer Calc ---
|
|
1293
|
-
print("Model system (high layer)")
|
|
1294
|
-
HLSP.Model_hess = HL_Model_hess
|
|
1295
|
-
model_HL_e, model_HL_g, high_layer_geom_num_list, finish_frag = HLSP.single_point(
|
|
1296
|
-
file_directory, high_layer_element_list, iter, electric_charge_and_multiplicity,
|
|
1297
|
-
method="", geom_num_list=high_layer_geom_num_list*self.config.bohr2angstroms)
|
|
1298
|
-
|
|
1299
|
-
HL_Model_hess = HLSP.Model_hess
|
|
1300
|
-
|
|
1301
|
-
if finish_frag:
|
|
1302
|
-
self.state.exit_flag = True
|
|
1852
|
+
print(f"# ITR. {iter_idx}")
|
|
1853
|
+
|
|
1854
|
+
# Compute potentials
|
|
1855
|
+
self.state = self.handler.compute(self.state)
|
|
1856
|
+
if self.state.exit_flag:
|
|
1303
1857
|
break
|
|
1304
|
-
|
|
1305
|
-
#
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
high_layer_geom_num_list = high_layer_geom_num_list_ang / self.config.bohr2angstroms
|
|
1343
|
-
|
|
1344
|
-
# --- Update Full System Geometry ---
|
|
1345
|
-
for l in range(len(high_layer_geom_num_list) - len(linker_atom_pair_num)):
|
|
1346
|
-
geom_num_list[highlayer_2_real_label_connect_dict[l+1]-1] = copy.deepcopy(high_layer_geom_num_list[l])
|
|
1347
|
-
|
|
1348
|
-
# Project out translation and rotation
|
|
1349
|
-
geom_num_list -= Calculationtools().calc_center_of_mass(geom_num_list, element_list)
|
|
1350
|
-
geom_num_list, _ = Calculationtools().kabsch_algorithm(geom_num_list, real_pre_geom)
|
|
1351
|
-
|
|
1352
|
-
# Update high layer geometry after alignment
|
|
1353
|
-
high_layer_geom_num_list, high_layer_element_list = separate_high_layer_and_low_layer(
|
|
1354
|
-
geom_num_list, linker_atom_pair_num, high_layer_atom_num, element_list)
|
|
1355
|
-
|
|
1356
|
-
# --- Combine Energies and Gradients for REAL system ---
|
|
1357
|
-
real_e = real_LL_e + model_HL_e - model_LL_e
|
|
1358
|
-
real_B_e = real_LL_B_e + model_HL_B_e - model_LL_e # Original uses model_LL_e, not B_e
|
|
1359
|
-
real_g = real_LL_g + tmp_model_HL_g
|
|
1360
|
-
real_B_g = real_LL_B_g + tmp_model_HL_g
|
|
1361
|
-
|
|
1362
|
-
# --- Update Main State ---
|
|
1363
|
-
self.state.e = real_e
|
|
1364
|
-
self.state.B_e = real_B_e
|
|
1365
|
-
self.state.g = real_g
|
|
1366
|
-
self.state.B_g = real_B_g
|
|
1367
|
-
self.state.geom_num_list = geom_num_list
|
|
1368
|
-
|
|
1369
|
-
self.save_tmp_energy_profiles(iter, real_e, real_g, real_B_g)
|
|
1370
|
-
self.state.ENERGY_LIST_FOR_PLOTTING.append(real_e*self.config.hartree2kcalmol)
|
|
1371
|
-
self.state.BIAS_ENERGY_LIST_FOR_PLOTTING.append(real_B_e*self.config.hartree2kcalmol)
|
|
1372
|
-
self.state.NUM_LIST.append(iter)
|
|
1373
|
-
|
|
1374
|
-
self.geom_info_extract(force_data, file_directory, real_B_g, real_g)
|
|
1375
|
-
|
|
1376
|
-
if len(linker_atom_pair_num) > 0:
|
|
1377
|
-
tmp_real_B_g = model_HL_B_g[:-len(linker_atom_pair_num)].reshape(-1,1)
|
|
1378
|
-
else:
|
|
1379
|
-
tmp_real_B_g = model_HL_B_g.reshape(-1,1)
|
|
1380
|
-
|
|
1381
|
-
abjusted_high_layer_geom_num_list, _ = Calculationtools().kabsch_algorithm(high_layer_geom_num_list, pre_high_layer_geom_num_list)
|
|
1382
|
-
|
|
1383
|
-
if len(linker_atom_pair_num) > 0:
|
|
1384
|
-
tmp_displacement_vector = (abjusted_high_layer_geom_num_list - pre_high_layer_geom_num_list)[:-len(linker_atom_pair_num)].reshape(-1,1)
|
|
1385
|
-
else:
|
|
1386
|
-
tmp_displacement_vector = (abjusted_high_layer_geom_num_list - pre_high_layer_geom_num_list).reshape(-1,1)
|
|
1387
|
-
|
|
1388
|
-
# --- Check Convergence (on HL model) ---
|
|
1389
|
-
converge_flag, max_displacement_threshold, rms_displacement_threshold = self._check_converge_criteria(tmp_real_B_g, tmp_displacement_vector)
|
|
1390
|
-
|
|
1391
|
-
self.print_info(real_e, real_B_e, tmp_real_B_g, tmp_displacement_vector, self.state.pre_e, self.state.pre_B_e,
|
|
1392
|
-
max_displacement_threshold, rms_displacement_threshold)
|
|
1393
|
-
|
|
1394
|
-
real_grad_list.append(self.calculate_rms_safely(real_g))
|
|
1395
|
-
real_bias_grad_list.append(self.calculate_rms_safely(real_B_g))
|
|
1396
|
-
|
|
1397
|
-
# Update state lists
|
|
1398
|
-
self.state.grad_list = real_grad_list
|
|
1399
|
-
self.state.bias_grad_list = real_bias_grad_list
|
|
1400
|
-
|
|
1401
|
-
if converge_flag:
|
|
1402
|
-
self.state.optimized_flag = True
|
|
1403
|
-
print("\n=====================================================")
|
|
1404
|
-
print("converged!!!")
|
|
1405
|
-
print("=====================================================")
|
|
1858
|
+
|
|
1859
|
+
# Exact model Hessian update
|
|
1860
|
+
if (
|
|
1861
|
+
iter_idx % self.config.mFC_COUNT == 0
|
|
1862
|
+
and self.config.use_model_hessian is not None
|
|
1863
|
+
and self.config.FC_COUNT < 1
|
|
1864
|
+
):
|
|
1865
|
+
self.state.Model_hess = ApproxHessian().main(
|
|
1866
|
+
self.state.geometry,
|
|
1867
|
+
self.state.element_list, # FIX: Use self.state.element_list
|
|
1868
|
+
self.state.raw_gradient,
|
|
1869
|
+
self.config.use_model_hessian,
|
|
1870
|
+
)
|
|
1871
|
+
if isinstance(self.handler, StandardHandler):
|
|
1872
|
+
self.handler.calculator.Model_hess = self.state.Model_hess
|
|
1873
|
+
|
|
1874
|
+
# Initial geometry save
|
|
1875
|
+
if iter_idx == 0:
|
|
1876
|
+
# FIX: Use self.state.element_list
|
|
1877
|
+
initial_geom_num_list, pre_geom = self._save_init_geometry(
|
|
1878
|
+
self.state.geometry, self.state.element_list, allactive_flag
|
|
1879
|
+
)
|
|
1880
|
+
self.state.pre_geometry = pre_geom
|
|
1881
|
+
|
|
1882
|
+
# Build combined hessian
|
|
1883
|
+
Hess = (
|
|
1884
|
+
self.state.bias_hessian + self.state.Model_hess
|
|
1885
|
+
if hasattr(self.state, "bias_hessian")
|
|
1886
|
+
else self.state.Model_hess
|
|
1887
|
+
)
|
|
1888
|
+
if iter_idx == 0 and self.convergence.judge_early_stop_due_to_no_negative_eigenvalues(
|
|
1889
|
+
self.state.geometry,
|
|
1890
|
+
Hess,
|
|
1891
|
+
self.config.args.saddle_order,
|
|
1892
|
+
self.config.FC_COUNT,
|
|
1893
|
+
self.config.detect_negative_eigenvalues,
|
|
1894
|
+
self.BPA_FOLDER_DIRECTORY,
|
|
1895
|
+
):
|
|
1406
1896
|
break
|
|
1407
|
-
|
|
1408
|
-
#
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1897
|
+
|
|
1898
|
+
# Constraints / projection
|
|
1899
|
+
PC = self.constraints.init_projection_constraint(
|
|
1900
|
+
PC, self.state.geometry, iter_idx, projection_constrain, hessian=Hess
|
|
1901
|
+
)
|
|
1902
|
+
optimizer_instances = self.hessian_mgr.calc_eff_hess_for_fix_atoms_and_set_hess(
|
|
1903
|
+
self.state,
|
|
1904
|
+
allactive_flag,
|
|
1905
|
+
force_data,
|
|
1906
|
+
self.state.bias_hessian if hasattr(self.state, "bias_hessian") else np.zeros_like(Hess),
|
|
1907
|
+
n_fix,
|
|
1908
|
+
optimizer_instances,
|
|
1909
|
+
self.state.geometry,
|
|
1910
|
+
self.state.bias_gradient,
|
|
1911
|
+
self.state.raw_gradient,
|
|
1912
|
+
projection_constrain,
|
|
1913
|
+
PC,
|
|
1914
|
+
)
|
|
1915
|
+
|
|
1916
|
+
if not allactive_flag and len(force_data["opt_fragment"]) > 0:
|
|
1917
|
+
self.state.bias_gradient = self.constraints.calc_fragment_grads(
|
|
1918
|
+
self.state.bias_gradient, force_data["opt_fragment"]
|
|
1919
|
+
)
|
|
1920
|
+
self.state.raw_gradient = self.constraints.calc_fragment_grads(
|
|
1921
|
+
self.state.raw_gradient, force_data["opt_fragment"]
|
|
1922
|
+
)
|
|
1923
|
+
|
|
1924
|
+
# logging
|
|
1925
|
+
self.logger.log_dynamic_csv(self.state, self.BPA_FOLDER_DIRECTORY, self.convergence)
|
|
1926
|
+
self.state.ENERGY_LIST_FOR_PLOTTING.append(
|
|
1927
|
+
self.state.raw_energy * self.config.hartree2kcalmol
|
|
1928
|
+
)
|
|
1929
|
+
self.state.BIAS_ENERGY_LIST_FOR_PLOTTING.append(
|
|
1930
|
+
self.state.bias_energy * self.config.hartree2kcalmol
|
|
1931
|
+
)
|
|
1932
|
+
self.state.NUM_LIST.append(iter_idx)
|
|
1933
|
+
|
|
1934
|
+
# Geometry info extract
|
|
1935
|
+
# FIX: Use self.state.element_list
|
|
1936
|
+
self.logger.geom_info_extract(
|
|
1937
|
+
self.state,
|
|
1938
|
+
force_data,
|
|
1939
|
+
self.file_io.make_psi4_input_file(
|
|
1940
|
+
self.file_io.print_geometry_list(
|
|
1941
|
+
self.state.geometry * self.config.bohr2angstroms,
|
|
1942
|
+
self.state.element_list,
|
|
1943
|
+
chg_mult,
|
|
1944
|
+
display_flag=False
|
|
1945
|
+
),
|
|
1946
|
+
iter_idx,
|
|
1947
|
+
),
|
|
1948
|
+
self.state.bias_gradient,
|
|
1949
|
+
self.state.raw_gradient,
|
|
1950
|
+
self.BPA_FOLDER_DIRECTORY,
|
|
1951
|
+
)
|
|
1952
|
+
|
|
1953
|
+
# Apply constraints to gradients
|
|
1954
|
+
g = copy.deepcopy(self.state.raw_gradient)
|
|
1955
|
+
B_g = copy.deepcopy(self.state.bias_gradient)
|
|
1956
|
+
g, B_g, PC = self.constraints.apply_projection_constraints(
|
|
1957
|
+
projection_constrain, PC, self.state.geometry, g, B_g
|
|
1958
|
+
)
|
|
1959
|
+
g, B_g = self.constraints.zero_fixed_atom_gradients(allactive_flag, force_data, g, B_g)
|
|
1960
|
+
|
|
1961
|
+
self.state.raw_gradient = g
|
|
1962
|
+
self.state.bias_gradient = B_g
|
|
1963
|
+
self.state.effective_gradient = g + (B_g - g)
|
|
1964
|
+
|
|
1965
|
+
if self.config.koopman_analysis and KA is not None:
|
|
1966
|
+
# FIX: Use self.state.element_list
|
|
1967
|
+
_ = KA.run(iter_idx, self.state.geometry, B_g, self.state.element_list)
|
|
1968
|
+
|
|
1969
|
+
# Move vector
|
|
1970
|
+
new_geometry, move_vector, optimizer_instances = CMV.calc_move_vector(
|
|
1971
|
+
iter_idx,
|
|
1972
|
+
self.state.geometry,
|
|
1973
|
+
B_g,
|
|
1974
|
+
self.state.pre_bias_gradient,
|
|
1975
|
+
self.state.pre_geometry,
|
|
1976
|
+
self.state.bias_energy,
|
|
1977
|
+
self.state.pre_bias_energy,
|
|
1978
|
+
self.state.pre_move_vector,
|
|
1979
|
+
self.state.initial_geometry,
|
|
1980
|
+
g,
|
|
1981
|
+
self.state.pre_raw_gradient,
|
|
1982
|
+
optimizer_instances,
|
|
1983
|
+
projection_constrain,
|
|
1984
|
+
)
|
|
1985
|
+
|
|
1986
|
+
# Projection / alignment
|
|
1987
|
+
new_geometry = self.constraints.project_out_translation_rotation(
|
|
1988
|
+
new_geometry, self.state.geometry, allactive_flag
|
|
1989
|
+
)
|
|
1990
|
+
new_geometry, PC = self.constraints.apply_projection_constraints_to_geometry(
|
|
1991
|
+
projection_constrain, PC, new_geometry, hessian=Hess
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
displacement_vector = (
|
|
1995
|
+
move_vector
|
|
1996
|
+
if iter_idx == 0
|
|
1997
|
+
else new_geometry / self.config.bohr2angstroms - self.state.geometry
|
|
1998
|
+
)
|
|
1999
|
+
|
|
2000
|
+
# convergence
|
|
2001
|
+
converge_flag, max_disp_th, rms_disp_th = self.convergence.check_convergence(
|
|
2002
|
+
self.state, displacement_vector, optimizer_instances
|
|
2003
|
+
)
|
|
2004
|
+
|
|
2005
|
+
# track gradients
|
|
2006
|
+
self.state.grad_list.append(self.convergence.calculate_rms_safely(g))
|
|
2007
|
+
self.state.bias_grad_list.append(self.convergence.calculate_rms_safely(B_g))
|
|
2008
|
+
|
|
2009
|
+
# reset fixed atoms
|
|
2010
|
+
new_geometry = self.constraints.reset_fixed_atom_positions(
|
|
2011
|
+
new_geometry, self.state.initial_geometry, allactive_flag, force_data
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
# dissociation
|
|
2015
|
+
# FIX: Use self.state.element_list
|
|
2016
|
+
DC_exit_flag = self.dissociation_check(new_geometry, self.state.element_list)
|
|
2017
|
+
|
|
2018
|
+
# print info
|
|
2019
|
+
self._print_info(
|
|
2020
|
+
self.state.raw_energy,
|
|
2021
|
+
self.state.bias_energy,
|
|
2022
|
+
B_g,
|
|
2023
|
+
displacement_vector,
|
|
2024
|
+
self.state.pre_raw_energy,
|
|
2025
|
+
self.state.pre_bias_energy,
|
|
2026
|
+
max_disp_th,
|
|
2027
|
+
rms_disp_th,
|
|
2028
|
+
)
|
|
2029
|
+
|
|
2030
|
+
if converge_flag:
|
|
2031
|
+
if projection_constrain and iter_idx == 0:
|
|
2032
|
+
pass
|
|
2033
|
+
else:
|
|
2034
|
+
self.state.optimized_flag = True
|
|
2035
|
+
print("\n=====================================================")
|
|
2036
|
+
print("converged!!!")
|
|
2037
|
+
print("=====================================================")
|
|
2038
|
+
break
|
|
2039
|
+
|
|
1414
2040
|
if DC_exit_flag:
|
|
1415
2041
|
self.state.DC_check_flag = True
|
|
1416
2042
|
break
|
|
1417
|
-
|
|
1418
|
-
#
|
|
1419
|
-
self.state.
|
|
1420
|
-
self.state.
|
|
1421
|
-
self.state.
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
else:
|
|
2043
|
+
|
|
2044
|
+
# update previous
|
|
2045
|
+
self.state.pre_bias_energy = self.state.bias_energy
|
|
2046
|
+
self.state.pre_raw_energy = self.state.raw_energy
|
|
2047
|
+
self.state.pre_bias_gradient = B_g
|
|
2048
|
+
self.state.pre_raw_gradient = g
|
|
2049
|
+
self.state.pre_geometry = self.state.geometry
|
|
2050
|
+
self.state.pre_move_vector = move_vector
|
|
2051
|
+
self.state.geometry = new_geometry / self.config.bohr2angstroms
|
|
2052
|
+
|
|
2053
|
+
# write next input
|
|
2054
|
+
# FIX: Use self.state.element_list
|
|
2055
|
+
self.file_io.print_geometry_list(
|
|
2056
|
+
new_geometry, self.state.element_list, chg_mult, display_flag=False
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
else:
|
|
1434
2060
|
self.state.optimized_flag = False
|
|
1435
2061
|
print("Reached maximum number of iterations. This is not converged.")
|
|
1436
|
-
with open(
|
|
2062
|
+
with open(
|
|
2063
|
+
self.BPA_FOLDER_DIRECTORY + "not_converged.txt", "w"
|
|
2064
|
+
) as f:
|
|
1437
2065
|
f.write("Reached maximum number of iterations. This is not converged.")
|
|
1438
|
-
|
|
2066
|
+
|
|
2067
|
+
# Post steps
|
|
1439
2068
|
if self.state.DC_check_flag:
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
#
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
2069
|
+
print("Dissociation is detected. Optimization stopped.")
|
|
2070
|
+
with open(
|
|
2071
|
+
self.BPA_FOLDER_DIRECTORY + "dissociation_is_detected.txt", "w"
|
|
2072
|
+
) as f:
|
|
2073
|
+
f.write("Dissociation is detected. Optimization stopped.")
|
|
2074
|
+
|
|
2075
|
+
# Vibrational analysis
|
|
2076
|
+
if self.config.freq_analysis and not self.state.exit_flag and not self.state.DC_check_flag:
|
|
2077
|
+
self._perform_vibrational_analysis(
|
|
2078
|
+
self.SP,
|
|
2079
|
+
self.state.geometry,
|
|
2080
|
+
self.state.element_list, # FIX: Use self.state.element_list
|
|
2081
|
+
self.state.initial_geometry,
|
|
2082
|
+
force_data,
|
|
2083
|
+
self._is_exact_hessian(iter_idx),
|
|
2084
|
+
self.file_io.make_psi4_input_file(
|
|
2085
|
+
self.file_io.print_geometry_list(
|
|
2086
|
+
self.state.geometry * self.config.bohr2angstroms,
|
|
2087
|
+
self.state.element_list, # FIX: Use self.state.element_list
|
|
2088
|
+
chg_mult,
|
|
2089
|
+
),
|
|
2090
|
+
iter_idx,
|
|
2091
|
+
),
|
|
2092
|
+
iter_idx,
|
|
2093
|
+
chg_mult,
|
|
2094
|
+
self.SP.xtb_method if hasattr(self.SP, "xtb_method") else None,
|
|
2095
|
+
self.state.raw_energy,
|
|
2096
|
+
)
|
|
2097
|
+
|
|
2098
|
+
# Finalize
|
|
2099
|
+
# FIX: Use self.state.element_list
|
|
2100
|
+
self._finalize_optimization(
|
|
2101
|
+
self.file_io,
|
|
2102
|
+
Graph(self.BPA_FOLDER_DIRECTORY),
|
|
2103
|
+
self.state.grad_list,
|
|
2104
|
+
self.state.bias_grad_list,
|
|
2105
|
+
self.file_io.make_psi4_input_file(
|
|
2106
|
+
self.file_io.print_geometry_list(
|
|
2107
|
+
self.state.geometry * self.config.bohr2angstroms,
|
|
2108
|
+
self.state.element_list,
|
|
2109
|
+
chg_mult,
|
|
2110
|
+
),
|
|
2111
|
+
iter_idx,
|
|
2112
|
+
),
|
|
2113
|
+
force_data,
|
|
2114
|
+
self.state.geometry,
|
|
2115
|
+
self.state.raw_energy,
|
|
2116
|
+
self.state.bias_energy,
|
|
2117
|
+
self.SP,
|
|
2118
|
+
self.state.exit_flag,
|
|
2119
|
+
)
|
|
1450
2120
|
self._copy_final_results_from_state()
|
|
1451
|
-
return
|
|
1452
|
-
|
|
1453
|
-
def get_result_file_path(self):
|
|
1454
|
-
"""
|
|
1455
|
-
Sets the absolute file paths for optimization results.
|
|
1456
|
-
Relies on self.BPA_FOLDER_DIRECTORY and self.START_FILE
|
|
1457
|
-
which are set by _make_init_directory().
|
|
1458
|
-
"""
|
|
1459
|
-
try:
|
|
1460
|
-
if (hasattr(self, 'BPA_FOLDER_DIRECTORY') and self.BPA_FOLDER_DIRECTORY and
|
|
1461
|
-
hasattr(self, 'START_FILE') and self.START_FILE):
|
|
1462
|
-
|
|
1463
|
-
base_name = os.path.splitext(os.path.basename(self.START_FILE))[0]
|
|
1464
|
-
optimized_filename = f"{base_name}_optimized.xyz"
|
|
1465
|
-
traj_filename = f"{base_name}_traj.xyz"
|
|
1466
2121
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
2122
|
+
# Analyses
|
|
2123
|
+
if self.state and self.config.CMDS:
|
|
2124
|
+
CMDPA = CMDSPathAnalysis(
|
|
2125
|
+
self.BPA_FOLDER_DIRECTORY,
|
|
2126
|
+
self.state.ENERGY_LIST_FOR_PLOTTING,
|
|
2127
|
+
self.state.BIAS_ENERGY_LIST_FOR_PLOTTING,
|
|
2128
|
+
)
|
|
2129
|
+
CMDPA.main()
|
|
2130
|
+
if self.state and self.config.PCA:
|
|
2131
|
+
PCAPA = PCAPathAnalysis(
|
|
2132
|
+
self.BPA_FOLDER_DIRECTORY,
|
|
2133
|
+
self.state.ENERGY_LIST_FOR_PLOTTING,
|
|
2134
|
+
self.state.BIAS_ENERGY_LIST_FOR_PLOTTING,
|
|
2135
|
+
)
|
|
2136
|
+
PCAPA.main()
|
|
2137
|
+
|
|
2138
|
+
if self.state and len(self.config.irc) > 0:
|
|
2139
|
+
if self.config.args.usextb != "None":
|
|
2140
|
+
xtb_method = self.config.args.usextb
|
|
1473
2141
|
else:
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
2142
|
+
xtb_method = "None"
|
|
2143
|
+
if self.state.iter % self.config.FC_COUNT == 0:
|
|
2144
|
+
hessian = self.state.Model_hess
|
|
2145
|
+
else:
|
|
2146
|
+
hessian = None
|
|
2147
|
+
|
|
2148
|
+
# NOTE: IRC logic for BITSS might need special handling, but keeping standard
|
|
2149
|
+
EXEC_IRC = IRC(
|
|
2150
|
+
self.BPA_FOLDER_DIRECTORY,
|
|
2151
|
+
self.state.final_file_directory,
|
|
2152
|
+
self.config.irc,
|
|
2153
|
+
self.SP,
|
|
2154
|
+
self.state.element_list, # FIX: Use self.state.element_list
|
|
2155
|
+
self.config.electric_charge_and_multiplicity,
|
|
2156
|
+
force_data_parser(self.config.args),
|
|
2157
|
+
xtb_method,
|
|
2158
|
+
FC_count=int(self.config.FC_COUNT),
|
|
2159
|
+
hessian=hessian,
|
|
2160
|
+
)
|
|
2161
|
+
EXEC_IRC.run()
|
|
2162
|
+
self.irc_terminal_struct_paths = EXEC_IRC.terminal_struct_paths
|
|
2163
|
+
else:
|
|
2164
|
+
self.irc_terminal_struct_paths = []
|
|
1482
2165
|
|
|
1483
|
-
|
|
2166
|
+
print(f"Trial of geometry optimization ({file}) was completed.")
|
|
2167
|
+
|
|
2168
|
+
# ------------------------------------------------------------------
|
|
2169
|
+
# Secondary helpers reused from legacy
|
|
2170
|
+
# ------------------------------------------------------------------
|
|
2171
|
+
def _save_init_geometry(self, geom_num_list, element_list, allactive_flag):
|
|
2172
|
+
if allactive_flag:
|
|
2173
|
+
initial_geom_num_list = geom_num_list - Calculationtools().calc_center(
|
|
2174
|
+
geom_num_list, element_list
|
|
2175
|
+
)
|
|
2176
|
+
pre_geom = initial_geom_num_list - Calculationtools().calc_center(
|
|
2177
|
+
geom_num_list, element_list
|
|
2178
|
+
)
|
|
2179
|
+
else:
|
|
2180
|
+
initial_geom_num_list = geom_num_list
|
|
2181
|
+
pre_geom = initial_geom_num_list
|
|
2182
|
+
return initial_geom_num_list, pre_geom
|
|
1484
2183
|
|
|
1485
|
-
def
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
2184
|
+
def dissociation_check(self, new_geometry, element_list):
|
|
2185
|
+
atom_label_list = list(range(len(new_geometry)))
|
|
2186
|
+
fragm_atom_num_list = []
|
|
2187
|
+
|
|
2188
|
+
while len(atom_label_list) > 0:
|
|
2189
|
+
tmp_fragm_list = Calculationtools().check_atom_connectivity(
|
|
2190
|
+
new_geometry, element_list, atom_label_list[0]
|
|
2191
|
+
)
|
|
2192
|
+
atom_label_list = list(set(atom_label_list) - set(tmp_fragm_list))
|
|
2193
|
+
fragm_atom_num_list.append(tmp_fragm_list)
|
|
2194
|
+
|
|
2195
|
+
if len(fragm_atom_num_list) > 1:
|
|
2196
|
+
fragm_dist_list = []
|
|
2197
|
+
geom_np = np.asarray(new_geometry)
|
|
2198
|
+
for fragm_1_indices, fragm_2_indices in itertools.combinations(
|
|
2199
|
+
fragm_atom_num_list, 2
|
|
2200
|
+
):
|
|
2201
|
+
coords1 = geom_np[fragm_1_indices]
|
|
2202
|
+
coords2 = geom_np[fragm_2_indices]
|
|
2203
|
+
diff_matrix = coords1[:, np.newaxis, :] - coords2[np.newaxis, :, :]
|
|
2204
|
+
sq_dist_matrix = np.sum(diff_matrix ** 2, axis=2)
|
|
2205
|
+
min_sq_dist = np.min(sq_dist_matrix)
|
|
2206
|
+
min_dist = np.sqrt(min_sq_dist)
|
|
2207
|
+
fragm_dist_list.append(min_dist)
|
|
2208
|
+
|
|
2209
|
+
min_interfragment_dist = min(fragm_dist_list)
|
|
2210
|
+
if min_interfragment_dist > self.config.DC_check_dist:
|
|
2211
|
+
print(
|
|
2212
|
+
f"Minimum fragment distance (ang.) {min_interfragment_dist:.4f} > {self.config.DC_check_dist}"
|
|
2213
|
+
)
|
|
2214
|
+
print("These molecules are dissociated.")
|
|
2215
|
+
return True
|
|
2216
|
+
return False
|
|
2217
|
+
|
|
2218
|
+
def _perform_vibrational_analysis(
|
|
2219
|
+
self,
|
|
2220
|
+
SP,
|
|
2221
|
+
geom_num_list,
|
|
2222
|
+
element_list,
|
|
2223
|
+
initial_geom_num_list,
|
|
2224
|
+
force_data,
|
|
2225
|
+
exact_hess_flag,
|
|
2226
|
+
file_directory,
|
|
2227
|
+
iter_idx,
|
|
2228
|
+
electric_charge_and_multiplicity,
|
|
2229
|
+
xtb_method,
|
|
2230
|
+
e,
|
|
2231
|
+
):
|
|
2232
|
+
print("\n====================================================")
|
|
2233
|
+
print("Performing vibrational analysis...")
|
|
2234
|
+
print("====================================================\n")
|
|
2235
|
+
print("Is Exact Hessian calculated? : ", exact_hess_flag)
|
|
2236
|
+
|
|
2237
|
+
if exact_hess_flag:
|
|
2238
|
+
g = np.zeros_like(geom_num_list, dtype="float64")
|
|
2239
|
+
exit_flag = False
|
|
1489
2240
|
else:
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2241
|
+
print("Calculate exact Hessian...")
|
|
2242
|
+
SP.hessian_flag = True
|
|
2243
|
+
e, g, geom_num_list, exit_flag = SP.single_point(
|
|
2244
|
+
file_directory, element_list, iter_idx, electric_charge_and_multiplicity, xtb_method
|
|
2245
|
+
)
|
|
2246
|
+
SP.hessian_flag = False
|
|
2247
|
+
|
|
2248
|
+
if exit_flag:
|
|
2249
|
+
print("Error: QM calculation failed.")
|
|
2250
|
+
return
|
|
2251
|
+
|
|
2252
|
+
_, B_e, _, BPA_hessian = self.handler.bias_pot_calc.main(
|
|
2253
|
+
e,
|
|
2254
|
+
g,
|
|
2255
|
+
geom_num_list,
|
|
2256
|
+
element_list,
|
|
2257
|
+
force_data,
|
|
2258
|
+
pre_B_g="",
|
|
2259
|
+
iter=iter_idx,
|
|
2260
|
+
initial_geom_num_list="",
|
|
2261
|
+
)
|
|
2262
|
+
tmp_hess = copy.deepcopy(SP.Model_hess)
|
|
2263
|
+
tmp_hess += BPA_hessian
|
|
2264
|
+
|
|
2265
|
+
MV = MolecularVibrations(
|
|
2266
|
+
atoms=element_list, coordinates=geom_num_list, hessian=tmp_hess
|
|
2267
|
+
)
|
|
2268
|
+
MV.calculate_thermochemistry(
|
|
2269
|
+
e_tot=B_e,
|
|
2270
|
+
temperature=self.config.thermo_temperature,
|
|
2271
|
+
pressure=self.config.thermo_pressure,
|
|
2272
|
+
)
|
|
2273
|
+
MV.print_thermochemistry(
|
|
2274
|
+
output_file=self.BPA_FOLDER_DIRECTORY + "/thermochemistry.txt"
|
|
2275
|
+
)
|
|
2276
|
+
MV.print_normal_modes(
|
|
2277
|
+
output_file=self.BPA_FOLDER_DIRECTORY + "/normal_modes.txt"
|
|
2278
|
+
)
|
|
2279
|
+
MV.create_vibration_animation(
|
|
2280
|
+
output_dir=self.BPA_FOLDER_DIRECTORY + "/vibration_animation"
|
|
2281
|
+
)
|
|
2282
|
+
|
|
2283
|
+
if not self.state.optimized_flag:
|
|
2284
|
+
print(
|
|
2285
|
+
"Warning: Vibrational analysis was performed, but the optimization did not converge. The result of thermochemistry is unreliable."
|
|
2286
|
+
)
|
|
2287
|
+
|
|
2288
|
+
def _is_exact_hessian(self, iter_idx):
|
|
2289
|
+
if self.config.FC_COUNT == -1:
|
|
2290
|
+
return False
|
|
2291
|
+
elif iter_idx % self.config.FC_COUNT == 0 and self.config.FC_COUNT > 0:
|
|
2292
|
+
return True
|
|
2293
|
+
return False
|
|
2294
|
+
|
|
2295
|
+
def _finalize_optimization(
|
|
2296
|
+
self,
|
|
2297
|
+
FIO,
|
|
2298
|
+
G,
|
|
2299
|
+
grad_list,
|
|
2300
|
+
bias_grad_list,
|
|
2301
|
+
file_directory,
|
|
2302
|
+
force_data,
|
|
2303
|
+
geom_num_list,
|
|
2304
|
+
e,
|
|
2305
|
+
B_e,
|
|
2306
|
+
SP,
|
|
2307
|
+
exit_flag,
|
|
2308
|
+
):
|
|
2309
|
+
|
|
2310
|
+
G.double_plot(
|
|
2311
|
+
self.state.NUM_LIST,
|
|
2312
|
+
self.state.ENERGY_LIST_FOR_PLOTTING,
|
|
2313
|
+
self.state.BIAS_ENERGY_LIST_FOR_PLOTTING,
|
|
2314
|
+
)
|
|
2315
|
+
G.single_plot(
|
|
2316
|
+
self.state.NUM_LIST,
|
|
2317
|
+
grad_list,
|
|
2318
|
+
file_directory,
|
|
2319
|
+
"",
|
|
2320
|
+
axis_name_2="gradient (RMS) [a.u.]",
|
|
2321
|
+
name="gradient",
|
|
2322
|
+
)
|
|
2323
|
+
G.single_plot(
|
|
2324
|
+
self.state.NUM_LIST,
|
|
2325
|
+
bias_grad_list,
|
|
2326
|
+
file_directory,
|
|
2327
|
+
"",
|
|
2328
|
+
axis_name_2="bias gradient (RMS) [a.u.]",
|
|
2329
|
+
name="bias_gradient",
|
|
2330
|
+
)
|
|
2331
|
+
|
|
2332
|
+
if len(force_data["geom_info"]) > 1:
|
|
2333
|
+
for num, i in enumerate(force_data["geom_info"]):
|
|
2334
|
+
G.single_plot(self.state.NUM_LIST, self.state.cos_list[num], file_directory, i)
|
|
2335
|
+
|
|
2336
|
+
FIO.make_traj_file()
|
|
2337
|
+
FIO.argrelextrema_txt_save(self.state.ENERGY_LIST_FOR_PLOTTING, "approx_TS", "max")
|
|
2338
|
+
FIO.argrelextrema_txt_save(self.state.ENERGY_LIST_FOR_PLOTTING, "approx_EQ", "min")
|
|
2339
|
+
FIO.argrelextrema_txt_save(grad_list, "local_min_grad", "min")
|
|
2340
|
+
|
|
2341
|
+
self.logger.save_energy_profiles(self.state, self.BPA_FOLDER_DIRECTORY)
|
|
2342
|
+
|
|
2343
|
+
self.state.bias_pot_params_grad_list = self.handler.bias_pot_calc.bias_pot_params_grad_list
|
|
2344
|
+
self.state.bias_pot_params_grad_name_list = self.handler.bias_pot_calc.bias_pot_params_grad_name_list
|
|
2345
|
+
self.state.final_file_directory = file_directory
|
|
2346
|
+
self.state.final_geometry = geom_num_list
|
|
2347
|
+
self.state.final_energy = e
|
|
2348
|
+
self.state.final_bias_energy = B_e
|
|
2349
|
+
|
|
2350
|
+
if not exit_flag:
|
|
2351
|
+
self.symmetry = analyze_symmetry(self.element_list, self.state.final_geometry)
|
|
2352
|
+
self.state.symmetry = self.symmetry
|
|
2353
|
+
with open(self.BPA_FOLDER_DIRECTORY + "symmetry.txt", "w") as f:
|
|
2354
|
+
f.write(f"Symmetry of final structure: {self.symmetry}")
|
|
2355
|
+
print(f"Symmetry: {self.symmetry}")
|
|
2356
|
+
|
|
2357
|
+
if isinstance(self.handler, ModelFunctionHandler) and self.handler.is_bitss:
|
|
2358
|
+
# We need original single-N element list for writing single frames
|
|
2359
|
+
# But state.element_list is doubled.
|
|
2360
|
+
# Use cached or slice.
|
|
2361
|
+
# The handler.finalize_bitss_trajectory needs access to single element list
|
|
2362
|
+
single_elem_len = len(self.state.element_list) // 2
|
|
2363
|
+
real_elems = self.state.element_list[:single_elem_len]
|
|
2364
|
+
self.config.args.element_list_cache = real_elems # Ensure correct list is used
|
|
1547
2365
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
self.
|
|
1553
|
-
|
|
2366
|
+
|
|
2367
|
+
self.handler.finalize_bitss_trajectory()
|
|
2368
|
+
|
|
2369
|
+
def _copy_final_results_from_state(self):
|
|
2370
|
+
if self.state:
|
|
2371
|
+
self.final_file_directory = self.state.final_file_directory
|
|
2372
|
+
self.final_geometry = self.state.final_geometry
|
|
2373
|
+
self.final_energy = self.state.final_energy
|
|
2374
|
+
self.final_bias_energy = self.state.final_bias_energy
|
|
2375
|
+
self.symmetry = getattr(self.state, "symmetry", None)
|
|
2376
|
+
self.bias_pot_params_grad_list = self.state.bias_pot_params_grad_list
|
|
2377
|
+
self.bias_pot_params_grad_name_list = self.state.bias_pot_params_grad_name_list
|
|
2378
|
+
self.optimized_flag = self.state.optimized_flag
|
|
2379
|
+
|
|
2380
|
+
def geom_info_extract(self, force_data, file_directory, B_g, g):
|
|
2381
|
+
# kept for backward compatibility; delegate to logger
|
|
2382
|
+
return self.logger.geom_info_extract(self.state, force_data, file_directory, B_g, g, self.BPA_FOLDER_DIRECTORY)
|
|
2383
|
+
|
|
2384
|
+
def _print_info(
|
|
2385
|
+
self,
|
|
2386
|
+
e,
|
|
2387
|
+
B_e,
|
|
2388
|
+
B_g,
|
|
2389
|
+
displacement_vector,
|
|
2390
|
+
pre_e,
|
|
2391
|
+
pre_B_e,
|
|
2392
|
+
max_displacement_threshold,
|
|
2393
|
+
rms_displacement_threshold,
|
|
2394
|
+
):
|
|
2395
|
+
rms_force = self.convergence.calculate_rms_safely(np.abs(B_g))
|
|
2396
|
+
rms_displacement = self.convergence.calculate_rms_safely(np.abs(displacement_vector))
|
|
2397
|
+
max_B_g = np.abs(B_g).max()
|
|
2398
|
+
max_displacement = np.abs(displacement_vector).max()
|
|
2399
|
+
print("caluculation results (unit a.u.):")
|
|
2400
|
+
print(" Value Threshold ")
|
|
2401
|
+
print("ENERGY : {:>15.12f} ".format(e))
|
|
2402
|
+
print("BIAS ENERGY : {:>15.12f} ".format(B_e))
|
|
2403
|
+
print(
|
|
2404
|
+
"Maximum Force : {0:>15.12f} {1:>15.12f} ".format(
|
|
2405
|
+
max_B_g, self.config.MAX_FORCE_THRESHOLD
|
|
2406
|
+
)
|
|
2407
|
+
)
|
|
2408
|
+
print(
|
|
2409
|
+
"RMS Force : {0:>15.12f} {1:>15.12f} ".format(
|
|
2410
|
+
rms_force, self.config.RMS_FORCE_THRESHOLD
|
|
2411
|
+
)
|
|
2412
|
+
)
|
|
2413
|
+
print(
|
|
2414
|
+
"Maximum Displacement : {0:>15.12f} {1:>15.12f} ".format(
|
|
2415
|
+
max_displacement, max_displacement_threshold
|
|
2416
|
+
)
|
|
2417
|
+
)
|
|
2418
|
+
print(
|
|
2419
|
+
"RMS Displacement : {0:>15.12f} {1:>15.12f} ".format(
|
|
2420
|
+
rms_displacement, rms_displacement_threshold
|
|
2421
|
+
)
|
|
2422
|
+
)
|
|
2423
|
+
print("ENERGY SHIFT : {:>15.12f} ".format(e - pre_e))
|
|
2424
|
+
print("BIAS ENERGY SHIFT : {:>15.12f} ".format(B_e - pre_B_e))
|
|
2425
|
+
|
|
2426
|
+
def get_result_file_path(self):
|
|
2427
|
+
self.optimized_struct_file, self.traj_file = self.result_paths.get_result_file_path(
|
|
2428
|
+
self.BPA_FOLDER_DIRECTORY, self.START_FILE
|
|
2429
|
+
)
|
|
2430
|
+
|
|
2431
|
+
if __name__ == "__main__":
|
|
2432
|
+
# The actual CLI interface is expected to supply args.
|
|
2433
|
+
pass
|