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.
@@ -1,11 +1,10 @@
1
- import sys
2
1
  import os
3
- import copy
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 separate_high_layer_and_low_layer, specify_link_atom_pairs, link_number_high_layer_and_low_layer
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
- # Responsibility 1: Holds the "Configuration" (immutable)
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
- Holds all "settings" that do not change during the run.
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
- # Port the logic from _set_convergence_criteria
55
+ # Base args
56
+ self.args = args
44
57
  self._set_convergence_criteria(args)
45
58
 
46
- # Port all "immutable" settings from _initialize_variables
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.temperature = 0.0
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
- # Port the logic from _check_sub_basisset
73
+
74
+ # Sub-basis and ECP
62
75
  self._check_sub_basisset(args)
63
-
64
- self.mFC_COUNT = args.calc_model_hess
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
- self.electric_charge_and_multiplicity = [int(args.electronic_charge), int(args.spin_multiplicity)]
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[2 * j + 1]
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 += "assign " + args.sub_basisset[2 * j] + " " + args.sub_basisset[2 * j + 1] + "\n"
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[args.effective_core_potential[2*j]] = args.effective_core_potential[2*j+1]
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
- # Responsibility 2: Manages the "State" (mutable)
187
+ # =====================================================================================
188
+ # 2. State (Mutable Data)
189
+ # =====================================================================================
145
190
  class OptimizationState:
146
191
  """
147
- Holds all "state" variables that change during the optimization loop.
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
- # Previous step state
163
- self.pre_e = 0.0
164
- self.pre_B_e = 0.0
165
- self.pre_geom = np.zeros((natom, 3), dtype="float64")
166
- self.pre_g = np.zeros((natom, 3), dtype="float64")
167
- self.pre_B_g = np.zeros((natom, 3), dtype="float64")
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
- # Plotting / result lists
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 = [] # Initialized properly in Optimize class
240
+ self.cos_list = []
177
241
 
178
- # Final result placeholders
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
- # Responsibility 3: Performs the "Execution" (main logic)
192
- class Optimize:
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
- Main execution (Runner) class.
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
- # 3. Helper instances and job-specific variables
206
- self.BPA_FOLDER_DIRECTORY = None
207
- self.START_FILE = None
208
- self.element_list = None # Will be set in run()
209
- self.CalcBiaspot = None # Shared helper
210
- self.SP = None # Shared helper
211
-
212
- # 4. Final results (for external access, mirrors original design)
213
- self.final_file_directory = None
214
- self.final_geometry = None
215
- self.final_energy = None
216
- self.final_bias_energy = None
217
- self.irc_terminal_struct_paths = []
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
- # --- Helper Methods ---
223
- # (Ported from the original class)
224
- # These methods must now read from self.config
225
- # and read/write from self.state.
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 _make_init_directory(self, file):
228
- """
229
- Create initial directory for optimization results.
230
- Uses self.config to build the path.
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
- if self.config.othersoft != "None":
238
- self.BPA_FOLDER_DIRECTORY = f"{base_dir}ASE_{timestamp}/"
239
- elif self.config.sqm2:
240
- self.BPA_FOLDER_DIRECTORY = f"{base_dir}SQM2_{timestamp}/"
241
- elif self.config.sqm1:
242
- self.BPA_FOLDER_DIRECTORY = f"{base_dir}SQM1_{timestamp}/"
243
- elif self.config.args.usextb == "None" and self.config.args.usedxtb == "None":
244
- self.BPA_FOLDER_DIRECTORY = f"{base_dir}{self.config.FUNCTIONAL}_{self.config.BASIS_SET}_{timestamp}/"
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
- method = self.config.args.usedxtb if self.config.args.usedxtb != "None" else self.config.args.usextb
247
- self.BPA_FOLDER_DIRECTORY = f"{base_dir}{method}_{timestamp}/"
248
-
249
- os.makedirs(self.BPA_FOLDER_DIRECTORY, exist_ok=True)
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
- def _save_input_data(self):
252
- with open(self.BPA_FOLDER_DIRECTORY+"input.txt", "w") as f:
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
- projection_constrain = False
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
- allactive_flag = False
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
- if "x" in force_data["projection_constraint_condition_list"] or "y" in force_data["projection_constraint_condition_list"] or "z" in force_data["projection_constraint_condition_list"]:
269
- allactive_flag = False
270
-
271
- return projection_constrain, allactive_flag
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
- def _init_projection_constraint(self, PC, geom_num_list, iter, projection_constrain, hessian=None):
274
- # (This method is pure, no changes needed)
275
- if iter == 0:
276
- if projection_constrain:
277
- PC.initialize(geom_num_list, hessian=hessian)
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
- pass
280
- return PC
281
- else:
282
- return PC
851
+ bool_list.extend([False, False, False])
283
852
 
284
- def _save_init_geometry(self, geom_num_list, element_list, allactive_flag):
285
- # (This method is pure, no changes needed)
286
- if allactive_flag:
287
- initial_geom_num_list = geom_num_list - Calculationtools().calc_center(geom_num_list, element_list)
288
- pre_geom = initial_geom_num_list - Calculationtools().calc_center(geom_num_list, element_list)
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
- initial_geom_num_list = geom_num_list
291
- pre_geom = initial_geom_num_list
292
-
293
- return initial_geom_num_list, pre_geom
959
+ return 0.0
294
960
 
295
- def _calc_eff_hess_for_fix_atoms_and_set_hess(self, allactive_flag, force_data, BPA_hessian, n_fix, optimizer_instances, geom_num_list, B_g, g, projection_constrain, PC):
296
- # (Reads self.state.Model_hess, self.config.FC_COUNT, etc.)
297
- if not allactive_flag:
298
- fix_num = []
299
- for fnum in force_data["fix_atoms"]:
300
- fix_num.extend([3*(fnum-1)+0, 3*(fnum-1)+1, 3*(fnum-1)+2])
301
- fix_num = np.array(fix_num, dtype="int64")
302
- #effective hessian
303
- tmp_fix_hess = self.state.Model_hess[np.ix_(fix_num, fix_num)] + np.eye((3*n_fix)) * 1e-10
304
- inv_tmp_fix_hess = np.linalg.pinv(tmp_fix_hess)
305
- tmp_fix_bias_hess = BPA_hessian[np.ix_(fix_num, fix_num)] + np.eye((3*n_fix)) * 1e-10
306
- inv_tmp_fix_bias_hess = np.linalg.pinv(tmp_fix_bias_hess)
307
- BPA_hessian -= np.dot(BPA_hessian[:, fix_num], np.dot(inv_tmp_fix_bias_hess, BPA_hessian[fix_num, :]))
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
- for i in range(len(optimizer_instances)):
310
-
311
- if projection_constrain:
312
- if np.all(np.abs(BPA_hessian) < 1e-20):
313
- proj_bpa_hess = PC.calc_project_out_hess(geom_num_list, B_g - g, BPA_hessian)
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
- if not allactive_flag:
323
- self.state.Model_hess -= np.dot(self.state.Model_hess[:, fix_num], np.dot(inv_tmp_fix_hess, self.state.Model_hess[fix_num, :]))
324
-
325
-
326
- if projection_constrain:
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
- return optimizer_instances
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
- def _apply_projection_constraints(self, projection_constrain, PC, geom_num_list, g, B_g):
335
- # (This method is pure, no changes needed)
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 _zero_fixed_atom_gradients(self, allactive_flag, force_data, g, B_g):
344
- # (This method is pure, no changes needed)
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 _project_out_translation_rotation(self, new_geometry, geom_num_list, allactive_flag):
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 _apply_projection_constraints_to_geometry(self, projection_constrain, PC, new_geometry, hessian=None):
365
- # (Reads self.config.bohr2angstroms)
366
- if projection_constrain:
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(initial_geom_num_list[j-1]*self.config.bohr2angstroms)
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
- def _initialize_optimization_tools(self, FIO, force_data):
382
- """
383
- Initializes all tools needed for the optimization loop.
384
- This replaces the old _initialize_optimization_variables.
385
- It assumes self.state is already created.
386
- """
387
- # Load modules
388
- Calculation, xtb_method = self._import_calculation_module()
389
- self._save_input_data() # Save input.txt
390
- G = Graph(self.BPA_FOLDER_DIRECTORY)
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
- # Bias potential and calculation setup
408
- self.CalcBiaspot = BiasPotentialCalculation(self.BPA_FOLDER_DIRECTORY)
409
- self.SP = self.setup_calculation(Calculation) # SP is self.SP
410
-
411
- # Move vector calculation
412
- CMV = CalculateMoveVector(self.config.DELTA, element_list, self.config.args.saddle_order,
413
- self.config.FC_COUNT, self.config.temperature, self.config.use_model_hessian,
414
- max_trust_radius=self.config.max_trust_radius, min_trust_radius=self.config.min_trust_radius)
415
- optimizer_instances = CMV.initialization(force_data["opt_method"])
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
- KA = None
1227
+ return 0.0
434
1228
 
435
- # Pack and return all initialized tools
436
- tools = {
437
- 'Calculation': Calculation, 'xtb_method': xtb_method,
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
- def judge_early_stop_due_to_no_negative_eigenvalues(self, geom_num_list, hessian):
459
- # (Reads self.config)
460
- if self.config.detect_negative_eigenvalues and self.config.FC_COUNT > 0:
461
- negative_eigenvalues_detected = self.check_negative_eigenvalues(geom_num_list, hessian)
462
- if not negative_eigenvalues_detected and self.config.args.saddle_order > 0:
463
- print("No negative eigenvalues detected while saddle_order > 0. Stopping optimization.")
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
- def optimize(self):
470
- # 1. Initialize State.
471
- # write_input_files needs FIO, FIO needs BPA_FOLDER_DIRECTORY.
472
- # This is complex. Let's initialize FIO and element_list first.
473
- FIO = FileIO(self.BPA_FOLDER_DIRECTORY, self.START_FILE)
474
-
475
- # This will read the file and set self.element_list
476
- file_directory, electric_charge_and_multiplicity, element_list = self.write_input_files(FIO)
477
- self.element_list = element_list
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
- # Now we can create the State
480
- self.state = OptimizationState(element_list)
481
- self.state.cos_list = [[] for i in range(len(force_data_parser(self.config.args)["geom_info"]))] # Init cos_list
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
- # 4. Main Optimization Loop
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
- # Update state with final gradients for this step
569
- self.state.g = g
570
- self.state.B_g = B_g
571
-
572
- if self.config.koopman_analysis:
573
- _ = KA.run(iter, geom_num_list, B_g, self.element_list)
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
- # --- Calculate Move Vector ---
576
- new_geometry, move_vector, optimizer_instances = CMV.calc_move_vector(
577
- iter, geom_num_list, B_g, self.state.pre_B_g, self.state.pre_geom, B_e, self.state.pre_B_e,
578
- self.state.pre_move_vector, initial_geom_num_list, g, self.state.pre_g, optimizer_instances, projection_constrain)
579
-
580
- # --- Post-step Geometry Adjustments ---
581
- new_geometry = self._project_out_translation_rotation(new_geometry, geom_num_list, allactive_flag)
582
- new_geometry, PC = self._apply_projection_constraints_to_geometry(projection_constrain, PC, new_geometry, hessian=Hess)
583
-
584
- # --- Update State Lists ---
585
- self.state.ENERGY_LIST_FOR_PLOTTING.append(e * self.config.hartree2kcalmol)
586
- self.state.BIAS_ENERGY_LIST_FOR_PLOTTING.append(B_e * self.config.hartree2kcalmol)
587
- self.state.NUM_LIST.append(int(iter))
588
-
589
- self.geom_info_extract(force_data, file_directory, B_g, g) # This updates self.state.cos_list
590
-
591
- if self.state.iter == 0:
592
- displacement_vector = move_vector
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
- displacement_vector = new_geometry / self.config.bohr2angstroms - geom_num_list
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 converge_flag:
609
- if projection_constrain and iter == 0:
610
- pass
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
- self.state.optimized_flag = True
613
- print("\n=====================================================")
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
- if self.config.freq_analysis and not self.state.exit_flag and not self.state.DC_check_flag:
656
- self._perform_vibrational_analysis(SP, geom_num_list, self.element_list, initial_geom_num_list, force_data, exact_hess_flag, file_directory, iter, electric_charge_and_multiplicity, xtb_method, e)
1350
+ class RunLogger:
1351
+ def __init__(self, config):
1352
+ self.config = config
657
1353
 
658
- # --- 6. Finalize and Save Results ---
659
- self._finalize_optimization(FIO, G, self.state.grad_list, self.state.bias_grad_list, file_directory, force_data, geom_num_list, e, B_e, SP, self.state.exit_flag)
660
-
661
- # Copy final results from state to self
662
- self._copy_final_results_from_state()
663
- return
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
- def _perform_vibrational_analysis(self, SP, geom_num_list, element_list, initial_geom_num_list, force_data, exact_hess_flag, file_directory, iter, electric_charge_and_multiplicity, xtb_method, e):
666
- # (Reads self.state, self.config)
667
- print("\n====================================================")
668
- print("Performing vibrational analysis...")
669
- print("====================================================\n")
670
- print("Is Exact Hessian calculated? : ", exact_hess_flag)
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
- def _finalize_optimization(self, FIO, G, grad_list, bias_grad_list, file_directory, force_data, geom_num_list, e, B_e, SP, exit_flag):
702
- # (Writes to self.state)
703
- self._save_opt_results(FIO, G, grad_list, bias_grad_list, file_directory, force_data)
704
-
705
- self.state.bias_pot_params_grad_list = self.CalcBiaspot.bias_pot_params_grad_list
706
- self.state.bias_pot_params_grad_name_list = self.CalcBiaspot.bias_pot_params_grad_name_list
707
- self.state.final_file_directory = file_directory
708
- self.state.final_geometry = geom_num_list # Bohr
709
- self.state.final_energy = e # Hartree
710
- self.state.final_bias_energy = B_e # Hartree
711
-
712
- if not exit_flag:
713
- self.symmetry = analyze_symmetry(self.element_list, self.state.final_geometry)
714
- self.state.symmetry = self.symmetry # Save to state too
715
- with open(self.BPA_FOLDER_DIRECTORY+"symmetry.txt", "w") as f:
716
- f.write(f"Symmetry of final structure: {self.symmetry}")
717
- print(f"Symmetry: {self.symmetry}")
718
-
719
- def _save_opt_results(self, FIO, G, grad_list, bias_grad_list, file_directory, force_data):
720
- # (Reads self.state)
721
- G.double_plot(self.state.NUM_LIST, self.state.ENERGY_LIST_FOR_PLOTTING, self.state.BIAS_ENERGY_LIST_FOR_PLOTTING)
722
- G.single_plot(self.state.NUM_LIST, grad_list, file_directory, "", axis_name_2="gradient (RMS) [a.u.]", name="gradient")
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
- G.single_plot(self.state.NUM_LIST, self.state.cos_list[num], file_directory, i)
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
- FIO.make_traj_file()
730
- FIO.argrelextrema_txt_save(self.state.ENERGY_LIST_FOR_PLOTTING, "approx_TS", "max")
731
- FIO.argrelextrema_txt_save(self.state.ENERGY_LIST_FOR_PLOTTING, "approx_EQ", "min")
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
- self._save_energy_profiles()
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
- def _check_converge_criteria(self, B_g, displacement_vector):
752
- # (Reads self.config)
753
- max_force = np.abs(B_g).max()
754
- max_force_threshold = self.config.MAX_FORCE_THRESHOLD
755
-
756
- rms_force = self.calculate_rms_safely(B_g)
757
- rms_force_threshold = self.config.RMS_FORCE_THRESHOLD
758
-
759
- delta_max_force_threshold = max(0.0, max_force_threshold -1 * max_force)
760
- delta_rms_force_threshold = max(0.0, rms_force_threshold -1 * rms_force)
761
-
762
- max_displacement = np.abs(displacement_vector).max()
763
- max_displacement_threshold = max(self.config.MAX_DISPLACEMENT_THRESHOLD, self.config.MAX_DISPLACEMENT_THRESHOLD + delta_max_force_threshold)
764
- rms_displacement = self.calculate_rms_safely(displacement_vector)
765
- rms_displacement_threshold = max(self.config.RMS_DISPLACEMENT_THRESHOLD, self.config.RMS_DISPLACEMENT_THRESHOLD + delta_rms_force_threshold)
766
-
767
- if max_force < max_force_threshold and rms_force < rms_force_threshold and max_displacement < max_displacement_threshold and rms_displacement < rms_displacement_threshold:
768
- return True, max_displacement_threshold, rms_displacement_threshold
769
- return False, max_displacement_threshold, rms_displacement_threshold
770
-
771
- def _import_calculation_module(self):
772
- # (Reads self.config)
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 ETM potential.")
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(self.BPA_FOLDER_DIRECTORY + "use_" + self.config.othersoft + ".txt", "w") as f:
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 setup_calculation(self, Calculation):
811
- # (Reads self.config, self.state)
812
- # Note: Model_hess is passed from state, but SP is re-created per job.
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=self.BPA_FOLDER_DIRECTORY,
824
- Model_hess=self.state.Model_hess, # Reads from state
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 = self.config.ECP,
834
- software_path_file = self.config.software_path_file
1563
+ ECP=self.config.ECP,
1564
+ software_path_file=self.config.software_path_file,
835
1565
  )
836
- SP.cpcm_solv_model = self.config.cpcm_solv_model
837
- SP.alpb_solv_model = self.config.alpb_solv_model
838
- return SP
839
-
840
- def write_input_files(self, FIO):
841
- # (Reads self.config)
842
- # (This method sets self.element_list and self.state.Model_hess,
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
- if self.config.args.pyscf:
863
- electric_charge_and_multiplicity = self.config.electric_charge_and_multiplicity
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
- return file_directory, electric_charge_and_multiplicity, element_list
869
-
870
- def save_tmp_energy_profiles(self, iter, e, g, B_g):
871
- # (This method is pure, no changes needed, writes to files)
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
- # 2. Check distances only if there is more than one fragment
937
- if len(fragm_atom_num_list) > 1:
938
- fragm_dist_list = []
939
-
940
- # Ensure geometry is a NumPy array for efficient slicing
941
- geom_np = np.asarray(new_geometry)
942
-
943
- # Iterate through all unique pairs of fragments
944
- for fragm_1_indices, fragm_2_indices in itertools.combinations(fragm_atom_num_list, 2):
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
- # Get the coordinates for all atoms in each fragment
947
- coords1 = geom_np[fragm_1_indices] # Shape (M, 3)
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
- # Reshape coords1 to (M, 1, 3) and coords2 to (1, K, 3)
951
- # This allows NumPy broadcasting to create all pairs of differences
952
- # The result (diff_matrix) will have shape (M, K, 3)
953
- diff_matrix = coords1[:, np.newaxis, :] - coords2[np.newaxis, :, :]
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
- # Square the differences and sum along the last axis (axis=2)
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
- # Find the minimum value in the squared distance matrix
961
- min_sq_dist = np.min(sq_dist_matrix)
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
- # 3. Check if the closest distance between any two fragments
969
- # is greater than the dissociation threshold.
970
- min_interfragment_dist = min(fragm_dist_list)
971
-
972
- if min_interfragment_dist > self.config.DC_check_dist:
973
- print(f"Minimum fragment distance (ang.) {min_interfragment_dist:.4f} > {self.config.DC_check_dist}")
974
- print("These molecules are dissociated.")
975
- DC_exit_flag = True
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
- DC_exit_flag = False
978
- else:
979
- # Only one fragment, so it's not dissociated
980
- DC_exit_flag = False
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
- def print_info(self, e, B_e, B_g, displacement_vector, pre_e, pre_B_e, max_displacement_threshold, rms_displacement_threshold):
993
- # (Reads self.config)
994
- rms_force = self.calculate_rms_safely(np.abs(B_g))
995
- rms_displacement = self.calculate_rms_safely(np.abs(displacement_vector))
996
- max_B_g = np.abs(B_g).max()
997
- max_displacement = np.abs(displacement_vector).max()
998
- print("caluculation results (unit a.u.):")
999
- print(" Value Threshold ")
1000
- print("ENERGY : {:>15.12f} ".format(e))
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
- for atom_num in fragment:
1020
- calced_gradient[atom_num-1] = copy.deepcopy(tmp_grad)
1021
- return calced_gradient
1022
-
1023
- def optimize_oniom(self):
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
- Perform ONIOM optimization using a high-level QM method for a subset of atoms
1026
- and a low-level method for the entire system.
1027
-
1028
- Refactored to use self.config and self.state.
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
- # 1. Parse input parameters and initialize file IO
1031
- force_data = force_data_parser(self.config.args)
1032
- high_layer_atom_num = force_data["oniom_flag"][0]
1033
- link_atom_num = force_data["oniom_flag"][1]
1034
- calc_method = force_data["oniom_flag"][2]
1035
-
1036
- FIO = FileIO(self.BPA_FOLDER_DIRECTORY, self.START_FILE)
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
- file_directory = FIO.make_psi4_input_file(geometry_list, 0)
1047
-
1048
- # 3. Import appropriate calculation modules
1049
- if self.config.args.pyscf:
1050
- from multioptpy.Calculator.pyscf_calculation_tools import Calculation as HL_Calculation
1051
- else:
1052
- from multioptpy.Calculator.psi4_calculation_tools import Calculation as HL_Calculation
1053
-
1054
- if calc_method in ["GFN2-xTB", "GFN1-xTB", "IPEA1-xTB"]:
1055
- from multioptpy.Calculator.tblite_calculation_tools import Calculation as LL_Calculation
1056
- else:
1057
- from multioptpy.Calculator.ase_calculation_tools import Calculation as LL_Calculation
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
- bool_list.extend([False, False, False])
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
- with open(self.BPA_FOLDER_DIRECTORY+"input.txt", "w") as f:
1101
- f.write(str(vars(self.config.args)))
1102
-
1103
- # 7. Initialize ONIOM-specific previous-step variables (as local vars)
1104
- pre_model_HL_B_e = 0.0
1105
- pre_model_HL_B_g = np.zeros((len(high_layer_element_list), 3))
1106
- pre_model_HL_g = np.zeros((len(high_layer_element_list), 3))
1107
- # pre_model_LL_B_g = np.zeros((len(high_layer_element_list), 3)) # Seems unused
1108
- pre_real_LL_B_e = 0.0
1109
- pre_real_LL_e = 0.0
1110
- pre_real_LL_B_g = np.zeros((len(element_list), 3))
1111
- pre_real_LL_g = np.zeros((len(element_list), 3))
1112
- pre_real_LL_move_vector = np.zeros((len(element_list), 3))
1113
- pre_model_HL_move_vector = np.zeros((len(high_layer_element_list), 3))
1114
-
1115
- # 8. Initialize HL optimizer
1116
- HL_CMV = CalculateMoveVector(self.config.DELTA, high_layer_element_list[:len(high_layer_atom_num)],
1117
- self.config.args.saddle_order, self.config.FC_COUNT, self.config.temperature,
1118
- max_trust_radius=self.config.max_trust_radius, min_trust_radius=self.config.min_trust_radius)
1119
- HL_optimizer_instances = HL_CMV.initialization(force_data["opt_method"])
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
- for i in range(len(HL_optimizer_instances)):
1122
- HL_optimizer_instances[i].set_hessian(HL_Model_hess[:len(high_layer_atom_num)*3, :len(high_layer_atom_num)*3])
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
- HL_optimizer_instances[i].DELTA = self.config.DELTA
1125
-
1126
- # 9. Initialize calculation instances
1127
- HLSP = HL_Calculation(START_FILE=self.START_FILE,
1128
- SUB_BASIS_SET=self.config.SUB_BASIS_SET,
1129
- BASIS_SET=self.config.BASIS_SET,
1130
- N_THREAD=self.config.N_THREAD,
1131
- SET_MEMORY=self.config.SET_MEMORY,
1132
- FUNCTIONAL=self.config.FUNCTIONAL,
1133
- FC_COUNT=self.config.FC_COUNT,
1134
- BPA_FOLDER_DIRECTORY=self.BPA_FOLDER_DIRECTORY,
1135
- Model_hess=HL_Model_hess[:len(high_layer_atom_num)*3, :len(high_layer_atom_num)*3],
1136
- unrestrict=self.config.unrestrict,
1137
- excited_state=self.config.excited_state,
1138
- electronic_charge=self.config.electronic_charge,
1139
- spin_multiplicity=self.config.spin_multiplicity
1140
- )
1141
-
1142
- LLSP = LL_Calculation(START_FILE=self.START_FILE,
1143
- SUB_BASIS_SET=self.config.SUB_BASIS_SET,
1144
- BASIS_SET=self.config.BASIS_SET,
1145
- N_THREAD=self.config.N_THREAD,
1146
- SET_MEMORY=self.config.SET_MEMORY,
1147
- FUNCTIONAL=self.config.FUNCTIONAL,
1148
- FC_COUNT=self.config.FC_COUNT,
1149
- BPA_FOLDER_DIRECTORY=self.BPA_FOLDER_DIRECTORY,
1150
- Model_hess=LL_Model_hess,
1151
- unrestrict=self.config.unrestrict,
1152
- software_type=calc_method,
1153
- excited_state=self.config.excited_state)
1154
-
1155
- # 10. Initialize result tracking (uses self.state)
1156
- real_grad_list = []
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
- print(f"\n# ITR. {iter}\n")
1169
-
1170
- if iter == 0:
1171
- high_layer_initial_geom_num_list = high_layer_geom_num_list.copy() # Bohr
1172
- high_layer_pre_geom = high_layer_initial_geom_num_list.copy() # Bohr
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
- # Fix high layer atoms
1231
- for key, value in highlayer_2_real_label_connect_dict.items():
1232
- current_geom_num_list[value-1] = copy.deepcopy(high_layer_geom_num_list[key-1]) # Already in Bohr
1233
-
1234
- # Fix user-specified atoms
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
- # --- Combine Gradients ---
1306
- # Use LL_Calc_BiasPot to get bias gradient on "Real" system
1307
- _, tmp_model_HL_B_e, tmp_model_HL_B_g, LL_BPA_hessian = LL_Calc_BiasPot.main(
1308
- 0.0, real_LL_g*0.0, geom_num_list, element_list, force_data, pre_real_LL_B_g*0.0, iter, real_initial_geom_num_list)
1309
-
1310
- tmp_model_HL_g = tmp_model_HL_B_g * 0.0
1311
-
1312
- for key, value in real_2_highlayer_label_connect_dict.items():
1313
- tmp_model_HL_B_g[key-1] += model_HL_g[value-1] - model_LL_g[value-1]
1314
- tmp_model_HL_g[key-1] += model_HL_g[value-1] - model_LL_g[value-1]
1315
-
1316
- HL_BPA_hessian = LL_BPA_hessian[np.ix_(bool_list, bool_list)]
1317
-
1318
- for i in range(len(HL_optimizer_instances)):
1319
- HL_optimizer_instances[i].set_bias_hessian(HL_BPA_hessian)
1320
- if iter % self.config.FC_COUNT == 0:
1321
- HL_optimizer_instances[i].set_hessian(HL_Model_hess[:len(high_layer_atom_num)*3, :len(high_layer_atom_num)*3])
1322
-
1323
- if len(force_data["opt_fragment"]) > 0:
1324
- tmp_model_HL_B_g = copy.deepcopy(self.calc_fragement_grads(tmp_model_HL_B_g, force_data["opt_fragment"]))
1325
- tmp_model_HL_g = copy.deepcopy(self.calc_fragement_grads(tmp_model_HL_g, force_data["opt_fragment"]))
1326
-
1327
- model_HL_B_g = copy.deepcopy(model_HL_g)
1328
- model_HL_B_e = model_HL_e + tmp_model_HL_B_e
1329
-
1330
- for key, value in real_2_highlayer_label_connect_dict.items():
1331
- model_HL_B_g[value-1] += tmp_model_HL_B_g[key-1] # This seems incorrect logic, but mirrors original
1332
-
1333
- pre_high_layer_geom_num_list = high_layer_geom_num_list
1334
-
1335
- # --- Calculate HL Move Vector ---
1336
- high_layer_geom_num_list_ang, move_vector, HL_optimizer_instances = HL_CMV.calc_move_vector(
1337
- iter, high_layer_geom_num_list[:len(high_layer_atom_num)], model_HL_B_g[:len(high_layer_atom_num)], pre_model_HL_B_g[:len(high_layer_atom_num)],
1338
- pre_high_layer_geom_num_list[:len(high_layer_atom_num)], model_HL_B_e, pre_model_HL_B_e,
1339
- pre_model_HL_move_vector[:len(high_layer_atom_num)], high_layer_pre_geom[:len(high_layer_atom_num)],
1340
- model_HL_g[:len(high_layer_atom_num)], pre_model_HL_g[:len(high_layer_atom_num)], HL_optimizer_instances)
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
- # Fix user-specified atoms
1409
- if len(force_data["fix_atoms"]) > 0:
1410
- for j in force_data["fix_atoms"]:
1411
- geom_num_list[j-1] = copy.deepcopy(real_initial_geom_num_list[j-1]) # Bohr
1412
-
1413
- DC_exit_flag = self.dissociation_check(geom_num_list, element_list)
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
- # --- Update Previous State Variables ---
1419
- self.state.pre_B_e = real_B_e
1420
- self.state.pre_e = real_e
1421
- self.state.pre_geom = geom_num_list
1422
- real_pre_geom = geom_num_list # Update local var
1423
-
1424
- pre_model_HL_B_g = model_HL_B_g
1425
- pre_model_HL_g = model_HL_g
1426
- pre_model_HL_B_e = model_HL_B_e
1427
- pre_model_HL_move_vector = move_vector
1428
-
1429
- # Create input for next iteration
1430
- geometry_list = FIO.print_geometry_list(geom_num_list*self.config.bohr2angstroms, element_list, electric_charge_and_multiplicity)
1431
- file_directory = FIO.make_psi4_input_file(geometry_list, iter+1)
1432
-
1433
- else: # Loop finished without break
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(self.BPA_FOLDER_DIRECTORY+"not_converged.txt", "w") as f:
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
- with open(self.BPA_FOLDER_DIRECTORY+"dissociation_is_detected.txt", "w") as f:
1441
- f.write("These molecules are dissociated.")
1442
-
1443
- # --- 12. Finalize and Save Results ---
1444
- G = Graph(self.BPA_FOLDER_DIRECTORY)
1445
-
1446
- # Finalize plots and save results (using self.state lists)
1447
- self._finalize_optimization(FIO, G, self.state.grad_list, self.state.bias_grad_list, file_directory, force_data, geom_num_list, e, B_e, SP, self.state.exit_flag) # Pass LLSP as dummy
1448
-
1449
- # Copy final results from state to self
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
- self.optimized_struct_file = os.path.abspath(os.path.join(self.BPA_FOLDER_DIRECTORY, optimized_filename))
1468
- self.traj_file = os.path.abspath(os.path.join(self.BPA_FOLDER_DIRECTORY, traj_filename))
1469
-
1470
- print("Optimized structure file path:", self.optimized_struct_file)
1471
- print("Trajectory file path:", self.traj_file)
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
- print("Error: BPA_FOLDER_DIRECTORY or START_FILE is not set. Please run optimize() or optimize_oniom() first.")
1475
- self.optimized_struct_file = None
1476
- self.traj_file = None
1477
-
1478
- except Exception as e:
1479
- print(f"Error setting result file paths: {e}")
1480
- self.optimized_struct_file = None
1481
- self.traj_file = None
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
- return
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 run(self):
1486
- # (Reads self.config)
1487
- if type(self.config.args.INPUT) is str:
1488
- START_FILE_LIST = [self.config.args.INPUT]
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
- START_FILE_LIST = self.config.args.INPUT
1491
-
1492
- job_file_list = []
1493
-
1494
- for job_file in START_FILE_LIST:
1495
- print()
1496
- if "*" in job_file:
1497
- result_list = glob.glob(job_file)
1498
- job_file_list = job_file_list + result_list
1499
- else:
1500
- job_file_list = job_file_list + [job_file]
1501
-
1502
- for file in job_file_list:
1503
- print("********************************")
1504
- print(file)
1505
- print("********************************")
1506
- if os.path.exists(file) == False:
1507
- print(f"{file} does not exist.")
1508
- continue
1509
-
1510
- # This creates the directory and sets self.START_FILE
1511
- self._make_init_directory(file)
1512
-
1513
- # Run the main optimization, which will create its own state
1514
- if len(self.config.args.oniom_flag) > 0:
1515
- self.optimize_oniom()
1516
- else:
1517
- self.optimize()
1518
-
1519
- # Post-processing (relies on self.state being set by optimize())
1520
- if self.state and self.config.CMDS:
1521
- CMDPA = CMDSPathAnalysis(self.BPA_FOLDER_DIRECTORY, self.state.ENERGY_LIST_FOR_PLOTTING, self.state.BIAS_ENERGY_LIST_FOR_PLOTTING)
1522
- CMDPA.main()
1523
- if self.state and self.config.PCA:
1524
- PCAPA = PCAPathAnalysis(self.BPA_FOLDER_DIRECTORY, self.state.ENERGY_LIST_FOR_PLOTTING, self.state.BIAS_ENERGY_LIST_FOR_PLOTTING)
1525
- PCAPA.main()
1526
-
1527
- if self.state and len(self.config.irc) > 0:
1528
- if self.config.args.usextb != "None":
1529
- xtb_method = self.config.args.usextb
1530
- else:
1531
- xtb_method = "None"
1532
-
1533
- if self.state.iter % self.config.FC_COUNT == 0:
1534
- hessian = self.state.Model_hess
1535
- else:
1536
- hessian = None
1537
-
1538
- EXEC_IRC = IRC(self.BPA_FOLDER_DIRECTORY, self.state.final_file_directory,
1539
- self.config.irc, self.SP, self.element_list,
1540
- self.config.electric_charge_and_multiplicity, # This was from config
1541
- force_data_parser(self.config.args), # Re-parse force_data
1542
- xtb_method, FC_count=int(self.config.FC_COUNT), hessian=hessian)
1543
- EXEC_IRC.run()
1544
- self.irc_terminal_struct_paths = EXEC_IRC.terminal_struct_paths
1545
- else:
1546
- self.irc_terminal_struct_paths = []
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
- print(f"Trial of geometry optimization ({file}) was completed.")
1549
-
1550
- print("All calculations were completed.")
1551
-
1552
- self.get_result_file_path()
1553
- return
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