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