MultiOptPy 1.20.2__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.
Files changed (246) hide show
  1. multioptpy/Calculator/__init__.py +0 -0
  2. multioptpy/Calculator/ase_calculation_tools.py +424 -0
  3. multioptpy/Calculator/ase_tools/__init__.py +0 -0
  4. multioptpy/Calculator/ase_tools/fairchem.py +28 -0
  5. multioptpy/Calculator/ase_tools/gamess.py +19 -0
  6. multioptpy/Calculator/ase_tools/gaussian.py +165 -0
  7. multioptpy/Calculator/ase_tools/mace.py +28 -0
  8. multioptpy/Calculator/ase_tools/mopac.py +19 -0
  9. multioptpy/Calculator/ase_tools/nwchem.py +31 -0
  10. multioptpy/Calculator/ase_tools/orca.py +22 -0
  11. multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
  12. multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
  13. multioptpy/Calculator/emt_calculation_tools.py +458 -0
  14. multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
  15. multioptpy/Calculator/lj_calculation_tools.py +314 -0
  16. multioptpy/Calculator/psi4_calculation_tools.py +334 -0
  17. multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
  18. multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
  19. multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
  20. multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
  21. multioptpy/Calculator/tblite_calculation_tools.py +352 -0
  22. multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
  23. multioptpy/Constraint/__init__.py +0 -0
  24. multioptpy/Constraint/constraint_condition.py +834 -0
  25. multioptpy/Coordinate/__init__.py +0 -0
  26. multioptpy/Coordinate/polar_coordinate.py +199 -0
  27. multioptpy/Coordinate/redundant_coordinate.py +638 -0
  28. multioptpy/IRC/__init__.py +0 -0
  29. multioptpy/IRC/converge_criteria.py +28 -0
  30. multioptpy/IRC/dvv.py +544 -0
  31. multioptpy/IRC/euler.py +439 -0
  32. multioptpy/IRC/hpc.py +564 -0
  33. multioptpy/IRC/lqa.py +540 -0
  34. multioptpy/IRC/modekill.py +662 -0
  35. multioptpy/IRC/rk4.py +579 -0
  36. multioptpy/Interpolation/__init__.py +0 -0
  37. multioptpy/Interpolation/adaptive_interpolation.py +283 -0
  38. multioptpy/Interpolation/binomial_interpolation.py +179 -0
  39. multioptpy/Interpolation/geodesic_interpolation.py +785 -0
  40. multioptpy/Interpolation/interpolation.py +156 -0
  41. multioptpy/Interpolation/linear_interpolation.py +473 -0
  42. multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
  43. multioptpy/Interpolation/spline_interpolation.py +353 -0
  44. multioptpy/MD/__init__.py +0 -0
  45. multioptpy/MD/thermostat.py +185 -0
  46. multioptpy/MEP/__init__.py +0 -0
  47. multioptpy/MEP/pathopt_bneb_force.py +443 -0
  48. multioptpy/MEP/pathopt_dmf_force.py +448 -0
  49. multioptpy/MEP/pathopt_dneb_force.py +130 -0
  50. multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
  51. multioptpy/MEP/pathopt_gpneb_force.py +512 -0
  52. multioptpy/MEP/pathopt_lup_force.py +113 -0
  53. multioptpy/MEP/pathopt_neb_force.py +225 -0
  54. multioptpy/MEP/pathopt_nesb_force.py +205 -0
  55. multioptpy/MEP/pathopt_om_force.py +153 -0
  56. multioptpy/MEP/pathopt_qsm_force.py +174 -0
  57. multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
  58. multioptpy/ModelFunction/__init__.py +7 -0
  59. multioptpy/ModelFunction/avoiding_model_function.py +29 -0
  60. multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
  61. multioptpy/ModelFunction/conical_model_function.py +26 -0
  62. multioptpy/ModelFunction/opt_meci.py +50 -0
  63. multioptpy/ModelFunction/opt_mesx.py +47 -0
  64. multioptpy/ModelFunction/opt_mesx_2.py +49 -0
  65. multioptpy/ModelFunction/seam_model_function.py +27 -0
  66. multioptpy/ModelHessian/__init__.py +0 -0
  67. multioptpy/ModelHessian/approx_hessian.py +147 -0
  68. multioptpy/ModelHessian/calc_params.py +227 -0
  69. multioptpy/ModelHessian/fischer.py +236 -0
  70. multioptpy/ModelHessian/fischerd3.py +360 -0
  71. multioptpy/ModelHessian/fischerd4.py +398 -0
  72. multioptpy/ModelHessian/gfn0xtb.py +633 -0
  73. multioptpy/ModelHessian/gfnff.py +709 -0
  74. multioptpy/ModelHessian/lindh.py +165 -0
  75. multioptpy/ModelHessian/lindh2007d2.py +707 -0
  76. multioptpy/ModelHessian/lindh2007d3.py +822 -0
  77. multioptpy/ModelHessian/lindh2007d4.py +1030 -0
  78. multioptpy/ModelHessian/morse.py +106 -0
  79. multioptpy/ModelHessian/schlegel.py +144 -0
  80. multioptpy/ModelHessian/schlegeld3.py +322 -0
  81. multioptpy/ModelHessian/schlegeld4.py +559 -0
  82. multioptpy/ModelHessian/shortrange.py +346 -0
  83. multioptpy/ModelHessian/swartd2.py +496 -0
  84. multioptpy/ModelHessian/swartd3.py +706 -0
  85. multioptpy/ModelHessian/swartd4.py +918 -0
  86. multioptpy/ModelHessian/tshess.py +40 -0
  87. multioptpy/Optimizer/QHAdam.py +61 -0
  88. multioptpy/Optimizer/__init__.py +0 -0
  89. multioptpy/Optimizer/abc_fire.py +83 -0
  90. multioptpy/Optimizer/adabelief.py +58 -0
  91. multioptpy/Optimizer/adabound.py +68 -0
  92. multioptpy/Optimizer/adadelta.py +65 -0
  93. multioptpy/Optimizer/adaderivative.py +56 -0
  94. multioptpy/Optimizer/adadiff.py +68 -0
  95. multioptpy/Optimizer/adafactor.py +70 -0
  96. multioptpy/Optimizer/adam.py +65 -0
  97. multioptpy/Optimizer/adamax.py +62 -0
  98. multioptpy/Optimizer/adamod.py +83 -0
  99. multioptpy/Optimizer/adamw.py +65 -0
  100. multioptpy/Optimizer/adiis.py +523 -0
  101. multioptpy/Optimizer/afire_neb.py +282 -0
  102. multioptpy/Optimizer/block_hessian_update.py +709 -0
  103. multioptpy/Optimizer/c2diis.py +491 -0
  104. multioptpy/Optimizer/component_wise_scaling.py +405 -0
  105. multioptpy/Optimizer/conjugate_gradient.py +82 -0
  106. multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
  107. multioptpy/Optimizer/coordinate_locking.py +405 -0
  108. multioptpy/Optimizer/dic_rsirfo.py +1015 -0
  109. multioptpy/Optimizer/ediis.py +417 -0
  110. multioptpy/Optimizer/eve.py +76 -0
  111. multioptpy/Optimizer/fastadabelief.py +61 -0
  112. multioptpy/Optimizer/fire.py +77 -0
  113. multioptpy/Optimizer/fire2.py +249 -0
  114. multioptpy/Optimizer/fire_neb.py +92 -0
  115. multioptpy/Optimizer/gan_step.py +486 -0
  116. multioptpy/Optimizer/gdiis.py +609 -0
  117. multioptpy/Optimizer/gediis.py +203 -0
  118. multioptpy/Optimizer/geodesic_step.py +433 -0
  119. multioptpy/Optimizer/gpmin.py +633 -0
  120. multioptpy/Optimizer/gpr_step.py +364 -0
  121. multioptpy/Optimizer/gradientdescent.py +78 -0
  122. multioptpy/Optimizer/gradientdescent_neb.py +52 -0
  123. multioptpy/Optimizer/hessian_update.py +433 -0
  124. multioptpy/Optimizer/hybrid_rfo.py +998 -0
  125. multioptpy/Optimizer/kdiis.py +625 -0
  126. multioptpy/Optimizer/lars.py +21 -0
  127. multioptpy/Optimizer/lbfgs.py +253 -0
  128. multioptpy/Optimizer/lbfgs_neb.py +355 -0
  129. multioptpy/Optimizer/linesearch.py +236 -0
  130. multioptpy/Optimizer/lookahead.py +40 -0
  131. multioptpy/Optimizer/nadam.py +64 -0
  132. multioptpy/Optimizer/newton.py +200 -0
  133. multioptpy/Optimizer/prodigy.py +70 -0
  134. multioptpy/Optimizer/purtubation.py +16 -0
  135. multioptpy/Optimizer/quickmin_neb.py +245 -0
  136. multioptpy/Optimizer/radam.py +75 -0
  137. multioptpy/Optimizer/rfo_neb.py +302 -0
  138. multioptpy/Optimizer/ric_rfo.py +842 -0
  139. multioptpy/Optimizer/rl_step.py +627 -0
  140. multioptpy/Optimizer/rmspropgrave.py +65 -0
  141. multioptpy/Optimizer/rsirfo.py +1647 -0
  142. multioptpy/Optimizer/rsprfo.py +1056 -0
  143. multioptpy/Optimizer/sadam.py +60 -0
  144. multioptpy/Optimizer/samsgrad.py +63 -0
  145. multioptpy/Optimizer/tr_lbfgs.py +678 -0
  146. multioptpy/Optimizer/trim.py +273 -0
  147. multioptpy/Optimizer/trust_radius.py +207 -0
  148. multioptpy/Optimizer/trust_radius_neb.py +121 -0
  149. multioptpy/Optimizer/yogi.py +60 -0
  150. multioptpy/OtherMethod/__init__.py +0 -0
  151. multioptpy/OtherMethod/addf.py +1150 -0
  152. multioptpy/OtherMethod/dimer.py +895 -0
  153. multioptpy/OtherMethod/elastic_image_pair.py +629 -0
  154. multioptpy/OtherMethod/modelfunction.py +456 -0
  155. multioptpy/OtherMethod/newton_traj.py +454 -0
  156. multioptpy/OtherMethod/twopshs.py +1095 -0
  157. multioptpy/PESAnalyzer/__init__.py +0 -0
  158. multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
  159. multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
  160. multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
  161. multioptpy/PESAnalyzer/pca_analysis.py +314 -0
  162. multioptpy/Parameters/__init__.py +0 -0
  163. multioptpy/Parameters/atomic_mass.py +20 -0
  164. multioptpy/Parameters/atomic_number.py +22 -0
  165. multioptpy/Parameters/covalent_radii.py +44 -0
  166. multioptpy/Parameters/d2.py +61 -0
  167. multioptpy/Parameters/d3.py +63 -0
  168. multioptpy/Parameters/d4.py +103 -0
  169. multioptpy/Parameters/dreiding.py +34 -0
  170. multioptpy/Parameters/gfn0xtb_param.py +137 -0
  171. multioptpy/Parameters/gfnff_param.py +315 -0
  172. multioptpy/Parameters/gnb.py +104 -0
  173. multioptpy/Parameters/parameter.py +22 -0
  174. multioptpy/Parameters/uff.py +72 -0
  175. multioptpy/Parameters/unit_values.py +20 -0
  176. multioptpy/Potential/AFIR_potential.py +55 -0
  177. multioptpy/Potential/LJ_repulsive_potential.py +345 -0
  178. multioptpy/Potential/__init__.py +0 -0
  179. multioptpy/Potential/anharmonic_keep_potential.py +28 -0
  180. multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
  181. multioptpy/Potential/electrostatic_potential.py +69 -0
  182. multioptpy/Potential/flux_potential.py +30 -0
  183. multioptpy/Potential/gaussian_potential.py +101 -0
  184. multioptpy/Potential/idpp.py +516 -0
  185. multioptpy/Potential/keep_angle_potential.py +146 -0
  186. multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
  187. multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
  188. multioptpy/Potential/keep_potential.py +99 -0
  189. multioptpy/Potential/mechano_force_potential.py +74 -0
  190. multioptpy/Potential/nanoreactor_potential.py +52 -0
  191. multioptpy/Potential/potential.py +896 -0
  192. multioptpy/Potential/spacer_model_potential.py +221 -0
  193. multioptpy/Potential/switching_potential.py +258 -0
  194. multioptpy/Potential/universal_potential.py +34 -0
  195. multioptpy/Potential/value_range_potential.py +36 -0
  196. multioptpy/Potential/void_point_potential.py +25 -0
  197. multioptpy/SQM/__init__.py +0 -0
  198. multioptpy/SQM/sqm1/__init__.py +0 -0
  199. multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
  200. multioptpy/SQM/sqm2/__init__.py +0 -0
  201. multioptpy/SQM/sqm2/calc_tools.py +95 -0
  202. multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
  203. multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
  204. multioptpy/SQM/sqm2/sqm2_core.py +303 -0
  205. multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
  206. multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
  207. multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
  208. multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
  209. multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
  210. multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
  211. multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
  212. multioptpy/Thermo/__init__.py +0 -0
  213. multioptpy/Thermo/normal_mode_analyzer.py +865 -0
  214. multioptpy/Utils/__init__.py +0 -0
  215. multioptpy/Utils/bond_connectivity.py +264 -0
  216. multioptpy/Utils/calc_tools.py +884 -0
  217. multioptpy/Utils/oniom.py +96 -0
  218. multioptpy/Utils/pbc.py +48 -0
  219. multioptpy/Utils/riemann_curvature.py +208 -0
  220. multioptpy/Utils/symmetry_analyzer.py +482 -0
  221. multioptpy/Visualization/__init__.py +0 -0
  222. multioptpy/Visualization/visualization.py +156 -0
  223. multioptpy/WFAnalyzer/MO_analysis.py +104 -0
  224. multioptpy/WFAnalyzer/__init__.py +0 -0
  225. multioptpy/Wrapper/__init__.py +0 -0
  226. multioptpy/Wrapper/autots.py +1239 -0
  227. multioptpy/Wrapper/ieip_wrapper.py +93 -0
  228. multioptpy/Wrapper/md_wrapper.py +92 -0
  229. multioptpy/Wrapper/neb_wrapper.py +94 -0
  230. multioptpy/Wrapper/optimize_wrapper.py +76 -0
  231. multioptpy/__init__.py +5 -0
  232. multioptpy/entrypoints.py +916 -0
  233. multioptpy/fileio.py +660 -0
  234. multioptpy/ieip.py +340 -0
  235. multioptpy/interface.py +1086 -0
  236. multioptpy/irc.py +529 -0
  237. multioptpy/moleculardynamics.py +432 -0
  238. multioptpy/neb.py +1267 -0
  239. multioptpy/optimization.py +1553 -0
  240. multioptpy/optimizer.py +709 -0
  241. multioptpy-1.20.2.dist-info/METADATA +438 -0
  242. multioptpy-1.20.2.dist-info/RECORD +246 -0
  243. multioptpy-1.20.2.dist-info/WHEEL +5 -0
  244. multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
  245. multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
  246. multioptpy-1.20.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1553 @@
1
+ import sys
2
+ import os
3
+ import copy
4
+ import glob
5
+ import itertools
6
+ import datetime
7
+
8
+
9
+ import numpy as np
10
+
11
+ from multioptpy.optimizer import CalculateMoveVector
12
+ from multioptpy.Visualization.visualization import Graph
13
+ from multioptpy.fileio import FileIO
14
+ from multioptpy.Parameters.parameter import UnitValueLib, element_number
15
+ from multioptpy.interface import force_data_parser
16
+ from multioptpy.ModelHessian.approx_hessian import ApproxHessian
17
+ from multioptpy.PESAnalyzer.cmds_analysis import CMDSPathAnalysis
18
+ from multioptpy.PESAnalyzer.pca_analysis import PCAPathAnalysis
19
+ from multioptpy.PESAnalyzer.koopman_analysis import KoopmanAnalyzer
20
+ from multioptpy.Potential.potential import BiasPotentialCalculation
21
+ from multioptpy.Utils.calc_tools import CalculationStructInfo, Calculationtools
22
+ from multioptpy.Constraint.constraint_condition import ProjectOutConstrain
23
+ from multioptpy.irc import IRC
24
+ 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
26
+ from multioptpy.Utils.symmetry_analyzer import analyze_symmetry
27
+ from multioptpy.Thermo.normal_mode_analyzer import MolecularVibrations
28
+
29
+ # Responsibility 1: Holds the "Configuration" (immutable)
30
+ class OptimizationConfig:
31
+ """
32
+ Holds all "settings" that do not change during the run.
33
+ Initialized from 'args'.
34
+ """
35
+ def __init__(self, args):
36
+ # Constants like UVL
37
+ UVL = UnitValueLib()
38
+ np.set_printoptions(precision=12, floatmode="fixed", suppress=True)
39
+ self.hartree2kcalmol = UVL.hartree2kcalmol
40
+ self.bohr2angstroms = UVL.bohr2angstroms
41
+ self.hartree2kjmol = UVL.hartree2kjmol
42
+
43
+ # Port the logic from _set_convergence_criteria
44
+ self._set_convergence_criteria(args)
45
+
46
+ # Port all "immutable" settings from _initialize_variables
47
+ self.microiter_num = 100
48
+ self.args = args # Keep a reference to args
49
+ self.FC_COUNT = args.calc_exact_hess
50
+ self.temperature = 0.0
51
+ self.CMDS = args.cmds
52
+ self.PCA = args.pca
53
+ self.DELTA = "x" if args.DELTA == "x" else float(args.DELTA)
54
+ self.N_THREAD = args.N_THREAD
55
+ self.SET_MEMORY = args.SET_MEMORY
56
+ self.NSTEP = args.NSTEP
57
+ self.BASIS_SET = args.basisset
58
+ self.FUNCTIONAL = args.functional
59
+ self.excited_state = args.excited_state
60
+
61
+ # Port the logic from _check_sub_basisset
62
+ self._check_sub_basisset(args)
63
+
64
+ self.mFC_COUNT = args.calc_model_hess
65
+ self.DC_check_dist = float(args.dissociate_check)
66
+ self.unrestrict = args.unrestrict
67
+ self.irc = args.intrinsic_reaction_coordinates
68
+ self.othersoft = args.othersoft
69
+ self.cpcm_solv_model = args.cpcm_solv_model
70
+ self.alpb_solv_model = args.alpb_solv_model
71
+ self.shape_conditions = args.shape_conditions
72
+ self.oniom = args.oniom_flag
73
+ self.use_model_hessian = args.use_model_hessian
74
+ self.sqm1 = args.sqm1
75
+ self.sqm2 = args.sqm2
76
+ self.freq_analysis = args.frequency_analysis
77
+ self.thermo_temperature = float(args.temperature)
78
+ self.thermo_pressure = float(args.pressure)
79
+ self.dft_grid = int(args.dft_grid)
80
+ self.max_trust_radius = args.max_trust_radius
81
+ self.min_trust_radius = args.min_trust_radius
82
+ self.software_path_file = args.software_path_file
83
+ self.koopman_analysis = args.koopman
84
+ self.detect_negative_eigenvalues = args.detect_negative_eigenvalues
85
+
86
+ def _set_convergence_criteria(self, args):
87
+ # Original _set_convergence_criteria method code
88
+ if args.tight_convergence_criteria and not args.loose_convergence_criteria:
89
+ self.MAX_FORCE_THRESHOLD = 0.000015
90
+ self.RMS_FORCE_THRESHOLD = 0.000010
91
+ self.MAX_DISPLACEMENT_THRESHOLD = 0.000060
92
+ self.RMS_DISPLACEMENT_THRESHOLD = 0.000040
93
+ elif not args.tight_convergence_criteria and args.loose_convergence_criteria:
94
+ self.MAX_FORCE_THRESHOLD = 0.0030
95
+ self.RMS_FORCE_THRESHOLD = 0.0020
96
+ self.MAX_DISPLACEMENT_THRESHOLD = 0.0100
97
+ self.RMS_DISPLACEMENT_THRESHOLD = 0.0070
98
+ else:
99
+ self.MAX_FORCE_THRESHOLD = 0.0003
100
+ self.RMS_FORCE_THRESHOLD = 0.0002
101
+ self.MAX_DISPLACEMENT_THRESHOLD = 0.0015
102
+ self.RMS_DISPLACEMENT_THRESHOLD = 0.0010
103
+
104
+ def _check_sub_basisset(self, args):
105
+ # Original _check_sub_basisset method code
106
+ if len(args.sub_basisset) % 2 != 0:
107
+ print("invalid input (-sub_bs)")
108
+ sys.exit(0)
109
+ self.electric_charge_and_multiplicity = [int(args.electronic_charge), int(args.spin_multiplicity)]
110
+ self.electronic_charge = args.electronic_charge
111
+ self.spin_multiplicity = args.spin_multiplicity
112
+
113
+ if args.pyscf:
114
+ self.SUB_BASIS_SET = {}
115
+ if len(args.sub_basisset) > 0:
116
+ self.SUB_BASIS_SET["default"] = str(self.BASIS_SET)
117
+ 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]
119
+ print("Basis Sets defined by User are detected.")
120
+ print(self.SUB_BASIS_SET)
121
+ else:
122
+ self.SUB_BASIS_SET = {"default": self.BASIS_SET}
123
+ else:
124
+ self.SUB_BASIS_SET = ""
125
+ if len(args.sub_basisset) > 0:
126
+ self.SUB_BASIS_SET += "\nassign " + str(self.BASIS_SET) + "\n"
127
+ 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"
129
+ print("Basis Sets defined by User are detected.")
130
+ print(self.SUB_BASIS_SET)
131
+
132
+ if len(args.effective_core_potential) % 2 != 0:
133
+ print("invaild input (-ecp)")
134
+ sys.exit(0)
135
+
136
+ if args.pyscf:
137
+ self.ECP = {}
138
+ 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]
141
+ else:
142
+ self.ECP = ""
143
+
144
+ # Responsibility 2: Manages the "State" (mutable)
145
+ class OptimizationState:
146
+ """
147
+ Holds all "state" variables that change during the optimization loop.
148
+ """
149
+ def __init__(self, element_list):
150
+ natom = len(element_list)
151
+
152
+ # Current step state
153
+ 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
+
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")
168
+ self.pre_move_vector = np.zeros((natom, 3), dtype="float64")
169
+
170
+ # Plotting / result lists
171
+ self.ENERGY_LIST_FOR_PLOTTING = []
172
+ self.BIAS_ENERGY_LIST_FOR_PLOTTING = []
173
+ self.NUM_LIST = []
174
+ self.grad_list = []
175
+ self.bias_grad_list = []
176
+ self.cos_list = [] # Initialized properly in Optimize class
177
+
178
+ # Final result placeholders
179
+ self.final_file_directory = None
180
+ self.final_geometry = None
181
+ self.final_energy = None
182
+ self.final_bias_energy = None
183
+ self.bias_pot_params_grad_list = None
184
+ self.bias_pot_params_grad_name_list = None
185
+
186
+ # Flags
187
+ self.DC_check_flag = False
188
+ self.optimized_flag = False
189
+ self.exit_flag = False
190
+
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
204
+
205
+ # 3. Helper instances and job-specific variables
206
+ self.BPA_FOLDER_DIRECTORY = None
207
+ self.START_FILE = None
208
+ self.element_list = None # Will be set in run()
209
+ self.CalcBiaspot = None # Shared helper
210
+ self.SP = None # Shared helper
211
+
212
+ # 4. Final results (for external access, mirrors original design)
213
+ self.final_file_directory = None
214
+ self.final_geometry = None
215
+ self.final_energy = None
216
+ self.final_bias_energy = None
217
+ self.irc_terminal_struct_paths = []
218
+ self.optimized_struct_file = None
219
+ self.traj_file = None
220
+ self.symmetry = None
221
+
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
+
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_"
236
+
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}/"
245
+ else:
246
+ method = self.config.args.usedxtb if self.config.args.usedxtb != "None" else self.config.args.usextb
247
+ self.BPA_FOLDER_DIRECTORY = f"{base_dir}{method}_{timestamp}/"
248
+
249
+ os.makedirs(self.BPA_FOLDER_DIRECTORY, exist_ok=True)
250
+
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
255
+
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
+ else:
261
+ projection_constrain = False
262
+
263
+ if len(force_data["fix_atoms"]) == 0:
264
+ allactive_flag = True
265
+ else:
266
+ allactive_flag = False
267
+
268
+ if "x" in force_data["projection_constraint_condition_list"] or "y" in force_data["projection_constraint_condition_list"] or "z" in force_data["projection_constraint_condition_list"]:
269
+ allactive_flag = False
270
+
271
+ return projection_constrain, allactive_flag
272
+
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)
278
+ else:
279
+ pass
280
+ return PC
281
+ else:
282
+ return PC
283
+
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)
289
+ 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
294
+
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, :]))
308
+
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):
321
+
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)
331
+
332
+ return optimizer_instances
333
+
334
+ def _apply_projection_constraints(self, projection_constrain, PC, geom_num_list, g, B_g):
335
+ # (This method is pure, no changes needed)
336
+ if projection_constrain:
337
+ g = copy.deepcopy(PC.calc_project_out_grad(geom_num_list, g))
338
+ proj_d_B_g = copy.deepcopy(PC.calc_project_out_grad(geom_num_list, B_g - g))
339
+ B_g = copy.deepcopy(g + proj_d_B_g)
340
+
341
+ return g, B_g, PC
342
+
343
+ def _zero_fixed_atom_gradients(self, allactive_flag, force_data, g, B_g):
344
+ # (This method is pure, no changes needed)
345
+ if not allactive_flag:
346
+ 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
+
350
+ return g, B_g
351
+
352
+ def _project_out_translation_rotation(self, new_geometry, geom_num_list, allactive_flag):
353
+ # (Reads self.config.bohr2angstroms)
354
+ if allactive_flag:
355
+ # Convert to Bohr, apply Kabsch alignment algorithm, then convert back
356
+ aligned_geometry, _ = Calculationtools().kabsch_algorithm(
357
+ new_geometry/self.config.bohr2angstroms, geom_num_list)
358
+ aligned_geometry *= self.config.bohr2angstroms
359
+ return aligned_geometry
360
+ else:
361
+ # If not all atoms are active, return the original geometry
362
+ return new_geometry
363
+
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)
375
+ if not allactive_flag:
376
+ for j in force_data["fix_atoms"]:
377
+ new_geometry[j-1] = copy.deepcopy(initial_geom_num_list[j-1]*self.config.bohr2angstroms)
378
+
379
+ return new_geometry
380
+
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"])
406
+
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)
432
+ else:
433
+ KA = None
434
+
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
457
+
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
468
+
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
478
+
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
482
+
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
+
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)
567
+
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)
574
+
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
593
+ 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)
607
+
608
+ if converge_flag:
609
+ if projection_constrain and iter == 0:
610
+ pass
611
+ 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.")
639
+
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
+
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)
657
+
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
664
+
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
700
+
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
+
725
+ if len(force_data["geom_info"]) > 1:
726
+ 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)
728
+
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")
733
+
734
+ self._save_energy_profiles()
735
+ return
736
+
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
+
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)
773
+ xtb_method = None
774
+ if self.config.args.pyscf:
775
+ from multioptpy.Calculator.pyscf_calculation_tools import Calculation
776
+ elif self.config.sqm2:
777
+ from multioptpy.Calculator.sqm2_calculation_tools import Calculation
778
+ print("Use SQM2 potential.")
779
+ elif self.config.sqm1:
780
+ from multioptpy.Calculator.sqm1_calculation_tools import Calculation
781
+ elif self.config.othersoft and self.config.othersoft != "None":
782
+ if self.config.othersoft.lower() == "lj":
783
+ from multioptpy.Calculator.lj_calculation_tools import Calculation
784
+ print("Use Lennard-Jones cluster potential.")
785
+ elif self.config.othersoft.lower() == "emt":
786
+ from multioptpy.Calculator.emt_calculation_tools import Calculation
787
+ print("Use ETM potential.")
788
+ elif self.config.othersoft.lower() == "tersoff":
789
+ from multioptpy.Calculator.tersoff_calculation_tools import Calculation
790
+ print("Use Tersoff potential.")
791
+ else:
792
+ from multioptpy.Calculator.ase_calculation_tools import Calculation
793
+ print("Use", self.config.othersoft)
794
+ with open(self.BPA_FOLDER_DIRECTORY + "use_" + self.config.othersoft + ".txt", "w") as f:
795
+ f.write(self.config.othersoft + "\n")
796
+ f.write(self.config.BASIS_SET + "\n")
797
+ f.write(self.config.FUNCTIONAL + "\n")
798
+ else:
799
+ if self.config.args.usedxtb and self.config.args.usedxtb != "None":
800
+ from multioptpy.Calculator.dxtb_calculation_tools import Calculation
801
+ xtb_method = self.config.args.usedxtb
802
+ elif self.config.args.usextb and self.config.args.usextb != "None":
803
+ from multioptpy.Calculator.tblite_calculation_tools import Calculation
804
+ xtb_method = self.config.args.usextb
805
+ else:
806
+ from multioptpy.Calculator.psi4_calculation_tools import Calculation
807
+
808
+ 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(
818
+ START_FILE=self.START_FILE,
819
+ N_THREAD=self.config.N_THREAD,
820
+ SET_MEMORY=self.config.SET_MEMORY,
821
+ FUNCTIONAL=self.config.FUNCTIONAL,
822
+ FC_COUNT=self.config.FC_COUNT,
823
+ BPA_FOLDER_DIRECTORY=self.BPA_FOLDER_DIRECTORY,
824
+ Model_hess=self.state.Model_hess, # Reads from state
825
+ software_type=self.config.othersoft,
826
+ unrestrict=self.config.unrestrict,
827
+ SUB_BASIS_SET=self.config.SUB_BASIS_SET,
828
+ BASIS_SET=self.config.BASIS_SET,
829
+ spin_multiplicity=self.config.spin_multiplicity,
830
+ electronic_charge=self.config.electronic_charge,
831
+ excited_state=self.config.excited_state,
832
+ dft_grid=self.config.dft_grid,
833
+ ECP = self.config.ECP,
834
+ software_path_file = self.config.software_path_file
835
+ )
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)
861
+
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
867
+
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)
935
+
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):
945
+
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)
949
+
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, :, :]
954
+
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)
959
+
960
+ # Find the minimum value in the squared distance matrix
961
+ min_sq_dist = np.min(sq_dist_matrix)
962
+
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
+
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
976
+ else:
977
+ DC_exit_flag = False
978
+ else:
979
+ # Only one fragment, so it's not dissociated
980
+ DC_exit_flag = False
981
+
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
+
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
1022
+
1023
+ def optimize_oniom(self):
1024
+ """
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.
1029
+ """
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"]))]
1045
+
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])
1091
+ 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
1099
+
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"])
1120
+
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])
1123
+ 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
1166
+ 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
1184
+ 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
+
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
1303
+ 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("=====================================================")
1406
+ 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)
1414
+ if DC_exit_flag:
1415
+ self.state.DC_check_flag = True
1416
+ 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
1434
+ self.state.optimized_flag = False
1435
+ print("Reached maximum number of iterations. This is not converged.")
1436
+ with open(self.BPA_FOLDER_DIRECTORY+"not_converged.txt", "w") as f:
1437
+ f.write("Reached maximum number of iterations. This is not converged.")
1438
+
1439
+ 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
1450
+ 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
+
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
+
1473
+ 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
1482
+
1483
+ return
1484
+
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]
1489
+ 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 = []
1547
+
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