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,1015 @@
1
+ import numpy as np
2
+ from scipy.optimize import brentq
3
+ from multioptpy.Optimizer.hessian_update import ModelHessianUpdate
4
+ from multioptpy.Optimizer.block_hessian_update import BlockHessianUpdate
5
+ import sys
6
+
7
+
8
+ class InternalCoordinates:
9
+ """
10
+ Handles the construction and transformation of Delocalized Internal Coordinates (DIC)
11
+ as described by Baker, Kessi, and Delley (1996).
12
+ """
13
+ def __init__(self, **config):
14
+ self.log_func = config.get("log_func", print)
15
+ self.g_tol = config.get("g_tol", 1e-6)
16
+ # B, U, Lambda_inv are calculated once and stored in the instance
17
+ self.B_prim = None # Primitive B-Matrix (n x 3N)
18
+ self.U = None # Active Eigenvectors (n x k)
19
+ self.Lambda_inv = None # Inverse Eigenvalues (k x k)
20
+ self.k = 0 # Number of active coordinates
21
+
22
+ def log(self, message, force=False):
23
+ if self.log_func and force: self.log_func(message, force=True)
24
+ elif self.log_func: self.log_func(message)
25
+
26
+ def _build_primitive_B_stretches(self, geom_cart_3N):
27
+ """ (Unchanged) Build B-matrix for all stretches (N*(N-1)/2) """
28
+ geom_cart_N3 = geom_cart_3N.reshape(-1, 3); N = geom_cart_N3.shape[0]; M = N * (N - 1) // 2
29
+ if M <= 0: return np.zeros((0, 3*N))
30
+ B_prim_ic = np.zeros((M, 3 * N)); row_idx = 0
31
+ for i in range(N):
32
+ for j in range(i + 1, N):
33
+ ri = geom_cart_N3[i, :]; rj = geom_cart_N3[j, :]
34
+ rij_vec = ri - rj; rij_norm = np.linalg.norm(rij_vec)
35
+ s_vec = np.zeros(3)
36
+ if rij_norm > 1e-8: s_vec = rij_vec / rij_norm
37
+ B_prim_ic[row_idx, 3*i : 3*i+3] = s_vec
38
+ B_prim_ic[row_idx, 3*j : 3*j+3] = -s_vec
39
+ row_idx += 1
40
+ return B_prim_ic
41
+
42
+ def _build_primitive_B_bends(self, geom_cart_3N):
43
+ """
44
+ 【TODO】Build B-matrix for all planar bends
45
+ Calculate B-matrix for all i-j-k tuples based on molecular connectivity.
46
+ """
47
+ N_atoms = len(geom_cart_3N) // 3
48
+ self.log("Warning: _build_primitive_B_bends is not implemented.", force=True)
49
+ # Placeholder:
50
+ return np.zeros((0, 3 * N_atoms))
51
+
52
+ def _build_primitive_B_torsions(self, geom_cart_3N):
53
+ """
54
+ 【TODO】Build B-matrix for all proper torsions
55
+ Calculate B-matrix for all i-j-k-l tuples based on molecular connectivity.
56
+ """
57
+ N_atoms = len(geom_cart_3N) // 3
58
+ self.log("Warning: _build_primitive_B_torsions is not implemented.", force=True)
59
+ # Placeholder:
60
+ return np.zeros((0, 3 * N_atoms))
61
+
62
+ def build_active_subspace(self, geom_cart_3N):
63
+ """
64
+ Build B-matrix from stretches, bends, torsions based on the paper.
65
+ Removed Cartesian coordinates (B_CC).
66
+ """
67
+ N_atoms = len(geom_cart_3N) // 3
68
+ if N_atoms == 0:
69
+ self.log("Warning: No atoms.", force=True); self.k=0; self.B_prim=np.zeros((0,0)); self.U=np.zeros((0,0)); self.Lambda_inv=np.zeros((0,0)); return 0
70
+
71
+ # Stack primitive internal coordinates according to the paper
72
+ B_Stretches = self._build_primitive_B_stretches(geom_cart_3N)
73
+ B_Bends = self._build_primitive_B_bends(geom_cart_3N)
74
+ B_Torsions = self._build_primitive_B_torsions(geom_cart_3N)
75
+
76
+ # Remove B_CC (Cartesian) and construct B_prim using only internal coordinates
77
+ self.B_prim = np.vstack((B_Stretches, B_Bends, B_Torsions))
78
+
79
+ if self.B_prim.shape[0] == 0:
80
+ self.log("FATAL: Primitive B-Matrix is empty. (Bends/Torsions not implemented?)", force=True)
81
+ # Fallback to Cartesian (differs from the paper's intent)
82
+ # self.B_prim = np.eye(3 * N_atoms)
83
+ raise ValueError("Primitive B-Matrix is empty. Implement Bends and Torsions.")
84
+
85
+ M_total, N_cart = self.B_prim.shape
86
+ self.log(f"Building G-Matrix ({M_total} x {M_total}) from {M_total} primitives...")
87
+
88
+ # G = B * B.T
89
+ G = np.dot(self.B_prim, self.B_prim.T); G = 0.5 * (G + G.T)
90
+
91
+ self.log("Diagonalizing G-Matrix...")
92
+ try:
93
+ eigvals_g, U_g = np.linalg.eigh(G)
94
+ except np.linalg.LinAlgError as e:
95
+ self.log(f"FATAL: G-matrix eigh failed: {e}. Check geom.", force=True); raise
96
+
97
+ active_indices = eigvals_g > self.g_tol; k = np.sum(active_indices)
98
+
99
+ # According to the paper, k should be 3N-6 (linear) or 3N-5 (non-linear)
100
+ expected_k = 3*N_atoms
101
+ if N_atoms == 1: expected_k = 3
102
+ elif N_atoms > 1: expected_k = 3*N_atoms - 6 # (Assuming non-linear for simplicity)
103
+ if expected_k < 1: expected_k = 1
104
+
105
+ if k == 0:
106
+ self.log("No active coords! Check g_tol/geom. Forcing k=1.", force=True); k = 1
107
+ active_indices = np.array([G.shape[0] - 1])
108
+
109
+ self.log(f"DIC: Found {k} active coordinates (Expected ~{expected_k})")
110
+
111
+ self.k = k
112
+ self.U = U_g[:, active_indices] # (n x k)
113
+ active_eigvals = eigvals_g[active_indices]
114
+
115
+ if np.any(active_eigvals <= 0):
116
+ num_neg = np.sum(active_eigvals <= 0); self.log(f"Warning: {num_neg} non-positive active eigvals. Clamping.", force=True)
117
+ active_eigvals[active_eigvals <= 0] = 1e-12
118
+
119
+ self.Lambda_inv = np.diag(1.0 / active_eigvals) # (k x k)
120
+ return k
121
+
122
+ # ===================================================================
123
+ # Coordinate Transformation Methods
124
+ # ===================================================================
125
+
126
+ def project_cart_to_dic(self, vec_cart_3N):
127
+ """
128
+ Projects a Cartesian vector (3N,) to a DIC vector (k,).
129
+ g_q = T @ g_x = (Lambda_inv @ U.T @ B_prim) @ g_x
130
+ """
131
+ if self.B_prim is None: raise ValueError("Coordinate system not built.")
132
+ # 1. B_prim @ g_x
133
+ vec_prim = np.dot(self.B_prim, vec_cart_3N)
134
+ # 2. U.T @ (B_prim @ g_x)
135
+ vec_dic_temp = np.dot(self.U.T, vec_prim)
136
+ # 3. Lambda_inv @ (U.T @ B_prim @ g_x)
137
+ vec_dic = np.dot(self.Lambda_inv, vec_dic_temp)
138
+ return vec_dic
139
+
140
+ def back_transform_dic_to_cart(self, vec_dic_k):
141
+ """
142
+ Back-transforms a DIC vector (k,) to a Cartesian vector (3N,).
143
+ dx = T_dagger @ dq = (B_prim.T @ U @ Lambda_inv) @ dq
144
+ """
145
+ if self.B_prim is None: raise ValueError("Coordinate system not built.")
146
+ # 1. Lambda_inv @ dq
147
+ tmp_vec = np.dot(self.Lambda_inv, vec_dic_k)
148
+ # 2. U @ (Lambda_inv @ dq)
149
+ vec_prim = np.dot(self.U, tmp_vec)
150
+ # 3. B_prim.T @ (U @ Lambda_inv @ dq)
151
+ vec_cart_3N = np.dot(self.B_prim.T, vec_prim)
152
+ return vec_cart_3N
153
+
154
+ def transform_hessian_cart_to_dic(self, H_cart_3N):
155
+ """
156
+ Transforms a Cartesian Hessian (3N x 3N) to a DIC Hessian (k x k).
157
+ H_q = T @ H_x @ T_dagger = (Lambda_inv @ U.T @ B_prim) @ H_x @ (B_prim.T @ U @ Lambda_inv)
158
+ """
159
+ if self.B_prim is None: raise ValueError("Coordinate system not built.")
160
+ k = self.k
161
+
162
+ if H_cart_3N is None:
163
+ self.log("No Cartesian Hessian provided, initializing DIC Hessian as Identity.", force=True)
164
+ return np.eye(k)
165
+
166
+ dim_cart = H_cart_3N.shape[0]
167
+ if dim_cart != self.B_prim.shape[1]:
168
+ self.log(f"ERROR: Cartesian Hessian dimension ({dim_cart}) mismatch with B_prim ({self.B_prim.shape[1]}). Using Identity.", force=True)
169
+ return np.eye(k)
170
+
171
+ self.log("Transforming Cartesian Hessian to DIC...", force=True)
172
+ try:
173
+ # T = Lambda_inv @ U.T @ B_prim
174
+ T_part1 = np.dot(self.U.T, self.B_prim) # (k x 3N)
175
+ T = np.dot(self.Lambda_inv, T_part1) # (k x 3N)
176
+
177
+ # T_dagger = B_prim.T @ U @ Lambda_inv
178
+ T_dagger_part1 = np.dot(self.B_prim.T, self.U) # (3N x k)
179
+ T_dagger = np.dot(T_dagger_part1, self.Lambda_inv) # (3N x k)
180
+
181
+ # H_q = T @ H_cart @ T_dagger
182
+ H_q_temp = np.dot(T, H_cart_3N) # (k x 3N)
183
+ H_q = np.dot(H_q_temp, T_dagger) # (k x k)
184
+
185
+ H_q = 0.5 * (H_q + H_q.T) # Ensure symmetry
186
+ self.log("Transformation complete.", force=True)
187
+ except Exception as e:
188
+ self.log(f"WARNING: Cartesian Hessian to DIC transformation failed: {e}. Using Identity.", force=True)
189
+ H_q = np.eye(k)
190
+
191
+ return H_q
192
+
193
+ # ===================================================================
194
+ # Modified DIC_RSIRFO Class
195
+ # ===================================================================
196
+
197
+ class DIC_RSIRFO:
198
+ def __init__(self, **config):
199
+ """
200
+ Delocalized Internal Coordinates (DIC) RS-I-RFO Optimizer.
201
+ """
202
+ # --- Common RSIRFO configuration ---
203
+ self.alpha0 = config.get("alpha0", 1.0)
204
+ self.max_micro_cycles = config.get("max_micro_cycles", 40)
205
+ self.saddle_order = config.get("saddle_order", 1)
206
+ self.hessian_update_method = config.get("method", "auto")
207
+ self.small_eigval_thresh = config.get("small_eigval_thresh", 1e-6)
208
+ self.alpha_max = config.get("alpha_max", 1e6)
209
+ self.alpha_step_max = config.get("alpha_step_max", 10.0)
210
+ if self.saddle_order == 0:
211
+ self.trust_radius_initial = config.get("trust_radius", 0.5)
212
+ self.trust_radius_max = config.get("trust_radius_max", 0.5)
213
+ else:
214
+ self.trust_radius_initial = config.get("trust_radius", 0.1)
215
+ self.trust_radius_max = config.get("trust_radius_max", 0.1)
216
+ self.trust_radius = self.trust_radius_initial
217
+ self.trust_radius_min = config.get("trust_radius_min", 0.01)
218
+ self.good_step_threshold = config.get("good_step_threshold", 0.75)
219
+ self.poor_step_threshold = config.get("poor_step_threshold", 0.25)
220
+ self.trust_radius_increase_factor = config.get("trust_radius_increase_factor", 1.2)
221
+ self.trust_radius_decrease_factor = config.get("trust_radius_decrease_factor", 0.5)
222
+ self.energy_change_threshold = config.get("energy_change_threshold", 1e-6)
223
+ self.gradient_norm_threshold = config.get("gradient_norm_threshold", 1e-4)
224
+ self.step_norm_tolerance = config.get("step_norm_tolerance", 1e-3)
225
+ self.debug_mode = config.get("debug_mode", False)
226
+ self.display_flag = config.get("display_flag", True)
227
+ self.Initialization = True
228
+
229
+ # --- Hessian storage (Cartesian) ---
230
+ self.hessian = None # Stores the INITIAL/CURRENT Cartesian Hessian (3N x 3N)
231
+ self.bias_hessian = None # Stores the Cartesian Bias Hessian (3N x 3N)
232
+
233
+ # --- DIC-specific storage ---
234
+ self.dic_hessian = None # The operational Hessian in DIC space (k x k)
235
+ self.dic_bias_hessian = None # The operational Bias Hessian in DIC space (k x k)
236
+
237
+ # --- State variables ---
238
+ self.prev_eigvec_min = None
239
+ self.prev_eigvec_size = None
240
+ self.predicted_energy_changes = []
241
+ self.actual_energy_changes = []
242
+ self.prev_geometry = None # Cartesian
243
+ self.prev_gradient = None # Cartesian
244
+ self.prev_energy = None
245
+ self.converged = False
246
+ self.iteration = 0
247
+ self.roots = list(range(self.saddle_order))
248
+
249
+ # --- Updaters and Helpers ---
250
+ self.hessian_updater = ModelHessianUpdate()
251
+ self.block_hessian_updater = BlockHessianUpdate()
252
+ self.alpha_init_values = [0.001 + (10.0 - 0.001) * i / 14 for i in range(15)]
253
+ self.NEB_mode = False
254
+
255
+ # --- DIC coordinate system ---
256
+ config["log_func"] = self.log
257
+ self.coord_system = InternalCoordinates(**config) # Instance to hold the coordinate system
258
+
259
+ def log(self, message, force=False):
260
+ display = getattr(self, 'display_flag', True)
261
+ debug = getattr(self, 'debug_mode', False)
262
+ if display and (force or debug):
263
+ print(message)
264
+
265
+ # === Main Methods ===
266
+
267
+ def run(self, geom_num_list, B_g, pre_B_g=[], pre_geom=[], B_e=0.0, pre_B_e=0.0, pre_move_vector=[], initial_geom_num_list=[], g=[], pre_g=[]):
268
+ self.log(f"\n{'='*50}\nDIC-RS-I-RFO Iteration {self.iteration}\n{'='*50}", force=True)
269
+
270
+ geom_cart_3N = np.asarray(geom_num_list).ravel()
271
+ g_cart_for_step = np.asarray(B_g).ravel()
272
+
273
+ # --- 1. Build Coordinate System (Once) ---
274
+ try:
275
+ # Build coordinate system only on the first step
276
+ if self.coord_system.B_prim is None or self.Initialization:
277
+ self.log("Building DIC coordinate system (first step)...", force=True)
278
+ k = self.coord_system.build_active_subspace(geom_cart_3N)
279
+ if k <= 0: raise ValueError("Invalid number of active coordinates (k<=0).")
280
+
281
+ # Initialize DIC Hessian
282
+ self.dic_hessian = self.coord_system.transform_hessian_cart_to_dic(self.hessian)
283
+
284
+ self.Initialization = False
285
+ self.predicted_energy_changes = []; self.actual_energy_changes = []
286
+ self.converged = False; self.iteration = 0
287
+ else:
288
+ k = self.coord_system.k # Use existing coordinate system
289
+
290
+ # --- 2. Hessian Update (Subsequent steps) ---
291
+ if self.prev_geometry is not None and len(pre_g) > 0 and len(pre_geom) > 0:
292
+ g_cart_for_update = np.asarray(g).ravel()
293
+ pre_g_cart_for_update = np.asarray(pre_g).ravel()
294
+
295
+ self.update_hessian(geom_cart_3N, g_cart_for_update,
296
+ np.asarray(pre_geom).ravel(), pre_g_cart_for_update)
297
+
298
+ # --- Trust Radius Update ---
299
+ if self.prev_energy is not None:
300
+ actual_energy_change = B_e - self.prev_energy
301
+ if len(self.actual_energy_changes) >= 3: self.actual_energy_changes.pop(0)
302
+ self.actual_energy_changes.append(actual_energy_change)
303
+ if self.predicted_energy_changes:
304
+ self.adjust_trust_radius(actual_energy_change, self.predicted_energy_changes[-1])
305
+
306
+ except Exception as e:
307
+ self.log(f"FATAL: DIC coordinate generation/update failed: {e}", force=True)
308
+ self.log("Aborting optimization.", force=True); self.converged = True
309
+ return np.zeros_like(geom_num_list).reshape(-1, 1)
310
+
311
+ # --- Bias Hessian Prep ---
312
+ if self.bias_hessian is not None and self.dic_bias_hessian is None:
313
+ self.log("Transforming Cartesian Bias Hessian to DIC...")
314
+ self.dic_bias_hessian = self.coord_system.transform_hessian_cart_to_dic(self.bias_hessian)
315
+ if self.dic_bias_hessian.shape[0] != k:
316
+ self.log(f"Warn: Transformed Bias shape != k. Ignoring.", force=True)
317
+ self.dic_bias_hessian = None
318
+ elif self.bias_hessian is None:
319
+ self.dic_bias_hessian = None
320
+
321
+ # --- 3. Convergence Check (Cartesian) ---
322
+ gradient_norm = np.linalg.norm(g_cart_for_step)
323
+ self.log(f"Gradient norm (Cartesian): {gradient_norm:.6f}", force=True)
324
+ if gradient_norm < self.gradient_norm_threshold:
325
+ self.log(f"Converged: Gradient norm {gradient_norm:.6f} < {self.gradient_norm_threshold:.6f}", force=True)
326
+ self.converged = True
327
+
328
+ if self.actual_energy_changes:
329
+ last_energy_change = abs(self.actual_energy_changes[-1])
330
+ if last_energy_change < self.energy_change_threshold:
331
+ self.log(f"Converged: Energy change {last_energy_change:.6f} < {self.energy_change_threshold:.6f}", force=True)
332
+ self.converged = True
333
+
334
+ if self.converged:
335
+ return np.zeros_like(geom_num_list).reshape(-1, 1)
336
+
337
+ # --- 4. RFO Step (in DIC space) ---
338
+
339
+ # g_q = T @ g_x
340
+ g_q = self.coord_system.project_cart_to_dic(g_cart_for_step)
341
+
342
+ H_q = self.dic_hessian
343
+
344
+ if self.dic_bias_hessian is not None:
345
+ if H_q.shape == self.dic_bias_hessian.shape:
346
+ H_q = H_q + self.dic_bias_hessian
347
+ else:
348
+ self.log(f"Warn: DIC Bias shape mismatch. Ignoring.", force=True)
349
+
350
+ H_q = 0.5 * (H_q + H_q.T)
351
+
352
+ try:
353
+ eigvals_q, eigvecs_q = np.linalg.eigh(H_q)
354
+ except np.linalg.LinAlgError:
355
+ self.log("FATAL: DIC Hessian diagonalization failed. Using Identity.", force=True)
356
+ H_q = np.eye(k); self.dic_hessian = H_q
357
+ eigvals_q, eigvecs_q = np.linalg.eigh(H_q)
358
+
359
+
360
+ if self.display_flag:
361
+ self.log(f"--- DIC Hessian Eigenvalues (k={k}) ---", force=True)
362
+ chunk_size = 6
363
+ for i in range(0, k, chunk_size):
364
+ chunk = eigvals_q[i:i + chunk_size]
365
+ line_str = " ".join([f"{val:10.6f}" for val in chunk])
366
+ self.log(f" {line_str}", force=True)
367
+ self.log(f"----------------------------------------", force=True)
368
+
369
+
370
+
371
+ neg_eigvals = np.sum(eigvals_q < -1e-10)
372
+ self.log(f"Found {neg_eigvals} negative eigenvalues in DIC Hessian (target: {self.saddle_order})", force=True)
373
+
374
+
375
+
376
+ # (RFO/Image Projection logic - Unchanged)
377
+ P_q = np.eye(k)
378
+ root_num = 0; i = 0
379
+ while root_num < len(self.roots) and i < k:
380
+ if i < len(eigvals_q) and np.abs(eigvals_q[i]) > 1e-10:
381
+ trans_vec_q = eigvecs_q[:, i]
382
+ if self.NEB_mode: P_q -= np.outer(trans_vec_q, trans_vec_q)
383
+ else: P_q -= 2 * np.outer(trans_vec_q, trans_vec_q)
384
+ root_num += 1
385
+ elif i >= len(eigvals_q): self.log(f"Warn: Index i={i} out of bounds for eigvals_q ({len(eigvals_q)}).", force=True); break
386
+ i += 1
387
+
388
+ H_q_star = np.dot(P_q, H_q); H_q_star = 0.5 * (H_q_star + H_q_star.T)
389
+ g_q_star = np.dot(P_q, g_q)
390
+ eigvals_q_star, eigvecs_q_star = np.linalg.eigh(H_q_star)
391
+ eigvals_q_star_filt, eigvecs_q_star_filt = self.filter_small_eigvals(eigvals_q_star, eigvecs_q_star)
392
+
393
+ current_eigvec_size = eigvecs_q_star_filt.shape[1]
394
+ if current_eigvec_size == 0:
395
+ self.log("ERROR: No eigenvalues after filtering. Using steepest descent.", force=True)
396
+ step_q = -g_q
397
+ step_norm_q = np.linalg.norm(step_q)
398
+ if step_norm_q > self.trust_radius: step_q *= self.trust_radius / step_norm_q
399
+ else:
400
+ self.log(f"Using {current_eigvec_size} eigenvalues/vectors after filtering")
401
+ if self.prev_eigvec_size is not None and self.prev_eigvec_size != current_eigvec_size:
402
+ self.log(f"Resetting prev eigvec info (dim change: {self.prev_eigvec_size} -> {current_eigvec_size})")
403
+ self.prev_eigvec_min = None
404
+
405
+ g_q_star_in_filt_basis = np.dot(eigvecs_q_star_filt.T, g_q_star)
406
+ step_q_filt_trans = self.get_rs_step(eigvals_q_star_filt, g_q_star_in_filt_basis)
407
+ step_q = np.dot(eigvecs_q_star_filt, step_q_filt_trans)
408
+ self.prev_eigvec_size = current_eigvec_size
409
+
410
+ # --- 5. Back-transform & Save State ---
411
+
412
+ # dx = T_dagger @ dq
413
+ move_vector_cart_3N = self.coord_system.back_transform_dic_to_cart(step_q)
414
+
415
+ step_norm_cart = np.linalg.norm(move_vector_cart_3N)
416
+ max_step_allowed = 2.0 * self.trust_radius_max
417
+ if step_norm_cart > max_step_allowed:
418
+ self.log(f"Warn: Cart step norm {step_norm_cart:.4f} > limit {max_step_allowed:.4f}. Scaling.", force=True)
419
+ move_vector_cart_3N *= max_step_allowed / step_norm_cart
420
+
421
+ predicted_energy_change = self.rfo_model(g_q, H_q, step_q)
422
+
423
+ if len(self.predicted_energy_changes) >= 3: self.predicted_energy_changes.pop(0)
424
+ self.predicted_energy_changes.append(predicted_energy_change)
425
+ self.log(f"Predicted energy change (DIC): {predicted_energy_change:.6f}", force=True)
426
+
427
+ self.prev_geometry = geom_cart_3N
428
+ self.prev_gradient = g_cart_for_step
429
+ self.prev_energy = B_e
430
+ self.iteration += 1
431
+
432
+ return -1 * move_vector_cart_3N.reshape(-1, 1)
433
+
434
+ def update_hessian(self, current_geom_cart, current_grad_cart, previous_geom_cart, previous_grad_cart):
435
+ """
436
+ Updates the DIC Hessian (self.dic_hessian).
437
+ Uses the *fixed* coordinate system (self.coord_system).
438
+ """
439
+
440
+ # Check if coordinate system is built
441
+ if self.coord_system.B_prim is None or self.dic_hessian is None:
442
+ self.log("Warning: Coordinate system or DIC Hessian not ready. Skipping Hessian update.")
443
+ return
444
+
445
+ displacement_cart = np.asarray(current_geom_cart - previous_geom_cart).ravel()
446
+ delta_grad_cart = np.asarray(current_grad_cart - previous_grad_cart).ravel()
447
+
448
+ # 【P2 Fix】Use correct projection (T @ dx)
449
+ displacement_q = self.coord_system.project_cart_to_dic(displacement_cart)
450
+ delta_grad_q = self.coord_system.project_cart_to_dic(delta_grad_cart)
451
+
452
+ disp_norm = np.linalg.norm(displacement_q)
453
+ grad_diff_norm = np.linalg.norm(delta_grad_q)
454
+ if disp_norm < 1e-10 or grad_diff_norm < 1e-10:
455
+ self.log("Skipping Hessian update (DIC changes too small)")
456
+ return
457
+
458
+ dot_product_q = np.dot(displacement_q, delta_grad_q)
459
+ if dot_product_q <= 1e-8:
460
+ self.log(f"Skipping Hessian update (DIC poor alignment: dot={dot_product_q:.2e})")
461
+ return
462
+
463
+ self.log(f"Hessian update (DIC): disp_norm={disp_norm:.6f}, grad_diff_norm={grad_diff_norm:.6f}, dot={dot_product_q:.6f}")
464
+
465
+ displacement_q = displacement_q.reshape(-1, 1)
466
+ delta_grad_q = delta_grad_q.reshape(-1, 1)
467
+
468
+ try:
469
+ # --- (This part is unchanged) Select and call the update method ---
470
+ method_name = self.hessian_update_method.lower()
471
+ delta_hess = None
472
+
473
+ if "flowchart" in method_name:
474
+ # ... (Logic below is same as original update_hessian) ...
475
+ self.log(f"Hessian update method: flowchart")
476
+ delta_hess = self.hessian_updater.flowchart_hessian_update(
477
+ self.dic_hessian, displacement_q, delta_grad_q, "auto"
478
+ )
479
+ elif "block_cfd_fsb" in method_name:
480
+ self.log(f"Hessian update method: block_cfd_fsb")
481
+ delta_hess = self.block_hessian_updater.block_CFD_FSB_hessian_update(
482
+ self.dic_hessian, displacement_q, delta_grad_q
483
+ )
484
+ elif "block_cfd_bofill" in method_name:
485
+ self.log(f"Hessian update method: block_cfd_bofill")
486
+ delta_hess = self.block_hessian_updater.block_CFD_Bofill_hessian_update(
487
+ self.dic_hessian, displacement_q, delta_grad_q
488
+ )
489
+ elif "block_bfgs" in method_name:
490
+ self.log(f"Hessian update method: block_bfgs")
491
+ delta_hess = self.block_hessian_updater.block_BFGS_hessian_update(
492
+ self.dic_hessian, displacement_q, delta_grad_q
493
+ )
494
+ elif "block_fsb" in method_name:
495
+ self.log(f"Hessian update method: block_fsb")
496
+ delta_hess = self.block_hessian_updater.block_FSB_hessian_update(
497
+ self.dic_hessian, displacement_q, delta_grad_q
498
+ )
499
+ elif "block_bofill" in method_name:
500
+ self.log(f"Hessian update method: block_bofill")
501
+ delta_hess = self.block_hessian_updater.block_Bofill_hessian_update(
502
+ self.dic_hessian, displacement_q, delta_grad_q
503
+ )
504
+ elif "bfgs" in method_name:
505
+ self.log(f"Hessian update method: bfgs")
506
+ delta_hess = self.hessian_updater.BFGS_hessian_update(
507
+ self.dic_hessian, displacement_q, delta_grad_q
508
+ )
509
+ elif "sr1" in method_name:
510
+ self.log(f"Hessian update method: sr1")
511
+ delta_hess = self.hessian_updater.SR1_hessian_update(
512
+ self.dic_hessian, displacement_q, delta_grad_q
513
+ )
514
+ elif "pcfd_bofill" in method_name:
515
+ self.log(f"Hessian update method: pcfd_bofill")
516
+ delta_hess = self.hessian_updater.pCFD_Bofill_hessian_update(
517
+ self.dic_hessian, displacement_q, delta_grad_q
518
+ )
519
+ elif "cfd_fsb" in method_name:
520
+ self.log(f"Hessian update method: cfd_fsb")
521
+ delta_hess = self.hessian_updater.CFD_FSB_hessian_update(
522
+ self.dic_hessian, displacement_q, delta_grad_q
523
+ )
524
+ elif "cfd_bofill" in method_name:
525
+ self.log(f"Hessian update method: cfd_bofill")
526
+ delta_hess = self.hessian_updater.CFD_Bofill_hessian_update(
527
+ self.dic_hessian, displacement_q, delta_grad_q
528
+ )
529
+ elif "fsb" in method_name:
530
+ self.log(f"Hessian update method: fsb")
531
+ delta_hess = self.hessian_updater.FSB_hessian_update(
532
+ self.dic_hessian, displacement_q, delta_grad_q
533
+ )
534
+ elif "bofill" in method_name:
535
+ self.log(f"Hessian update method: bofill")
536
+ delta_hess = self.hessian_updater.Bofill_hessian_update(
537
+ self.dic_hessian, displacement_q, delta_grad_q
538
+ )
539
+ elif "psb" in method_name:
540
+ self.log(f"Hessian update method: psb")
541
+ delta_hess = self.hessian_updater.PSB_hessian_update(
542
+ self.dic_hessian, displacement_q, delta_grad_q
543
+ )
544
+ elif "msp" in method_name:
545
+ self.log(f"Hessian update method: msp")
546
+ delta_hess = self.hessian_updater.MSP_hessian_update(
547
+ self.dic_hessian, displacement_q, delta_grad_q
548
+ )
549
+ else: # Default fallback
550
+ self.log(f"Unknown Hessian update method: '{self.hessian_update_method}'. Using flowchart/auto.")
551
+ delta_hess = self.hessian_updater.flowchart_hessian_update(
552
+ self.dic_hessian, displacement_q, delta_grad_q, "auto"
553
+ )
554
+
555
+ # --- Apply the update ---
556
+ if delta_hess is None:
557
+ self.log("Error: delta_hess is None. Skipping update.", force=True)
558
+ elif not np.all(np.isfinite(delta_hess)):
559
+ self.log("Warning: Hessian update resulted in non-finite values. Skipping update.", force=True)
560
+ else:
561
+ self.dic_hessian += delta_hess
562
+ self.dic_hessian = 0.5 * (self.dic_hessian + self.dic_hessian.T)
563
+
564
+ except Exception as e:
565
+ self.log(f"Error during Hessian update method call for '{method_name}': {e}. Skipping update.")
566
+ if self.debug_mode: raise e
567
+
568
+ # --- (Helper methods below are unchanged) ---
569
+ # (Unchanged helper methods are omitted for brevity)
570
+ # (Please copy from the original code)
571
+
572
+ def set_hessian(self, hessian_cart):
573
+ self.log("Cartesian Hessian received.")
574
+ if hessian_cart is not None:
575
+ self.hessian = np.asarray(hessian_cart)
576
+ else:
577
+ self.hessian = None
578
+ # DIC Hessian will be transformed in run() (if needed)
579
+ self.dic_hessian = None
580
+ # Reset coordinate system as well
581
+ self.coord_system = InternalCoordinates(**self.coord_system.__dict__)
582
+ self.Initialization = True
583
+ return
584
+
585
+ def get_hessian(self):
586
+ return self.hessian
587
+
588
+ def set_bias_hessian(self, bias_hessian_cart):
589
+ self.log("Cartesian Bias Hessian received.")
590
+ if bias_hessian_cart is not None:
591
+ self.bias_hessian = np.asarray(bias_hessian_cart)
592
+ else:
593
+ self.bias_hessian = None
594
+ self.dic_bias_hessian = None # Recalculated in run()
595
+ return
596
+
597
+ def get_bias_hessian(self):
598
+ return self.bias_hessian
599
+
600
+ # (Copy from original code)
601
+ def switch_NEB_mode(self):
602
+ if self.NEB_mode: self.NEB_mode = False
603
+ else: self.NEB_mode = True
604
+
605
+ def filter_small_eigvals(self, eigvals, eigvecs, mask=False):
606
+ small_inds = np.abs(eigvals) < self.small_eigval_thresh
607
+ small_num = np.sum(small_inds)
608
+
609
+ if small_num > 0:
610
+ self.log(f"Found {small_num} small eigenvalues. Removed corresponding.")
611
+
612
+ filtered_eigvals = eigvals[~small_inds]
613
+ filtered_eigvecs = eigvecs[:, ~small_inds]
614
+
615
+ if small_num > 6 and eigvals.shape[0] > 10: # Only warn if DoF is large
616
+ self.log(f"Warning: Found {small_num} small eigenvalues (>6).", force=True)
617
+
618
+ if mask:
619
+ return filtered_eigvals, filtered_eigvecs, small_inds
620
+ else:
621
+ return filtered_eigvals, filtered_eigvecs
622
+
623
+ def adjust_trust_radius(self, actual_change, predicted_change):
624
+ if abs(predicted_change) < 1e-10:
625
+ self.log("Skipping trust radius update: predicted change too small")
626
+ return
627
+ # Avoid division by zero if actual_change is also tiny
628
+ if abs(actual_change) < 1e-10:
629
+ ratio = 1.0 # Treat as perfect agreement
630
+ else:
631
+ ratio = actual_change / predicted_change
632
+
633
+ self.log(f"Energy change: actual={actual_change:.6f}, predicted={predicted_change:.6f}, ratio={ratio:.3f}", force=True)
634
+ old_trust_radius = self.trust_radius
635
+
636
+ if ratio > self.good_step_threshold:
637
+ self.trust_radius = min(self.trust_radius * self.trust_radius_increase_factor,
638
+ self.trust_radius_max)
639
+ if self.trust_radius != old_trust_radius:
640
+ self.log(f"Good step (ratio={ratio:.3f}), increasing trust radius to {self.trust_radius:.6f}", force=True)
641
+ elif ratio < self.poor_step_threshold:
642
+ self.trust_radius = max(self.trust_radius * self.trust_radius_decrease_factor,
643
+ self.trust_radius_min)
644
+ if self.trust_radius != old_trust_radius:
645
+ self.log(f"Poor step (ratio={ratio:.3f}), decreasing trust radius to {self.trust_radius:.6f}", force=True)
646
+ else:
647
+ self.log(f"Acceptable step (ratio={ratio:.3f}), keeping trust radius at {self.trust_radius:.6f}", force=True)
648
+
649
+ def evaluate_step_quality(self):
650
+ if len(self.predicted_energy_changes) < 2 or len(self.actual_energy_changes) < 2:
651
+ return "unknown"
652
+ ratios = []
653
+ for actual, predicted in zip(self.actual_energy_changes[-2:], self.predicted_energy_changes[-2:]):
654
+ if abs(predicted) > 1e-10:
655
+ if abs(actual) < 1e-10: # Handle tiny actual change
656
+ ratios.append(1.0)
657
+ else:
658
+ ratios.append(actual / predicted)
659
+ if not ratios: return "unknown"
660
+ avg_ratio = sum(ratios) / len(ratios)
661
+ # Check if steps generally decrease energy (allow small positive actual changes)
662
+ generally_downhill = all(a < 1e-6 or (a > 0 and abs(a/p) < 0.1)
663
+ for a, p in zip(self.actual_energy_changes[-2:], self.predicted_energy_changes[-2:]) if abs(p) > 1e-10)
664
+
665
+ if 0.8 < avg_ratio < 1.2 and generally_downhill: quality = "good"
666
+ elif 0.5 < avg_ratio < 1.5 and generally_downhill: quality = "acceptable"
667
+ else: quality = "poor"
668
+ self.log(f"Step quality assessment: {quality} (avg ratio: {avg_ratio:.3f})", force=True)
669
+ return quality
670
+
671
+ def get_rs_step(self, eigvals, gradient_trans):
672
+ try:
673
+ initial_step, _, _, _ = self.solve_rfo(eigvals, gradient_trans, self.alpha0)
674
+ initial_step_norm = np.linalg.norm(initial_step)
675
+ self.log(f"Initial step with alpha={self.alpha0:.6f} has norm={initial_step_norm:.6f}", force=True)
676
+
677
+ if initial_step_norm <= self.trust_radius:
678
+ self.log(f"Initial step is within trust radius ({self.trust_radius:.6f}), using it directly", force=True)
679
+ return initial_step # Return the step in the eigenvector basis
680
+
681
+ self.log(f"Initial step exceeds trust radius, optimizing alpha...", force=True)
682
+ except Exception as e:
683
+ self.log(f"Error calculating initial step: {str(e)}", force=True)
684
+
685
+ best_overall_step = None
686
+ best_overall_norm_diff = float('inf')
687
+ best_alpha_value = None
688
+
689
+ self.log(f"Trying {len(self.alpha_init_values)} different initial alpha values:", force=True)
690
+
691
+ for trial_idx, alpha_init in enumerate(self.alpha_init_values):
692
+ if self.debug_mode:
693
+ self.log(f"\n--- Alpha Trial {trial_idx+1}/{len(self.alpha_init_values)}: alpha_init = {alpha_init:.6f} ---")
694
+
695
+ try:
696
+ step_, step_norm, alpha_final = self.compute_rsprfo_step(
697
+ eigvals, gradient_trans, alpha_init
698
+ )
699
+ norm_diff = abs(step_norm - self.trust_radius)
700
+
701
+ if self.debug_mode:
702
+ self.log(f"Alpha trial {trial_idx+1}: ... step_norm={step_norm:.6f}, diff={norm_diff:.6f}")
703
+
704
+ is_better = False
705
+ if best_overall_step is None: is_better = True
706
+ elif step_norm <= self.trust_radius and best_overall_norm_diff > self.trust_radius: is_better = True
707
+ elif (step_norm <= self.trust_radius) == (best_overall_norm_diff <= self.trust_radius):
708
+ if norm_diff < best_overall_norm_diff: is_better = True
709
+
710
+ if is_better:
711
+ best_overall_step = step_
712
+ best_overall_norm_diff = norm_diff
713
+ best_alpha_value = alpha_init
714
+
715
+ except Exception as e:
716
+ if self.debug_mode:
717
+ self.log(f"Error in alpha trial {trial_idx+1}: {str(e)}")
718
+
719
+ if best_overall_step is None:
720
+ self.log("All alpha trials failed, using steepest descent step as fallback", force=True)
721
+ sd_step = -gradient_trans
722
+ sd_norm = np.linalg.norm(sd_step)
723
+ best_overall_step = sd_step / sd_norm * self.trust_radius if sd_norm > self.trust_radius else sd_step
724
+ else:
725
+ self.log(f"Selected alpha value: {best_alpha_value:.6f}", force=True)
726
+
727
+ step = best_overall_step
728
+ step_norm = np.linalg.norm(step)
729
+ self.log(f"Final norm(step)={step_norm:.6f}", force=True)
730
+
731
+ return step # Return the step in the eigenvector basis
732
+
733
+ def compute_rsprfo_step(self, eigvals, gradient_trans, alpha_init):
734
+ def calculate_step(alpha):
735
+ try:
736
+ step, eigval_min, _, _ = self.solve_rfo(eigvals, gradient_trans, alpha)
737
+ return step, eigval_min
738
+ except Exception as e:
739
+ self.log(f"Error in step calculation: {str(e)}")
740
+ raise
741
+ def step_norm_squared(alpha):
742
+ step, _ = calculate_step(alpha)
743
+ return np.dot(step, step)
744
+ def objective_function(alpha):
745
+ return step_norm_squared(alpha) - self.trust_radius**2
746
+
747
+ alpha_lo, alpha_hi = 1e-6, self.alpha_max
748
+ try:
749
+ step_lo, _ = calculate_step(alpha_lo)
750
+ obj_lo = np.dot(step_lo, step_lo) - self.trust_radius**2
751
+ step_hi, _ = calculate_step(alpha_hi)
752
+ obj_hi = np.dot(step_hi, step_hi) - self.trust_radius**2
753
+
754
+ self.log(f"Bracket search: alpha_lo={alpha_lo:.6e}, obj={obj_lo:.6e}")
755
+ self.log(f"Bracket search: alpha_hi={alpha_hi:.6e}, obj={obj_hi:.6e}")
756
+
757
+ if obj_lo * obj_hi >= 0:
758
+ self.log("Could not establish bracket, proceeding with Newton")
759
+ alpha = alpha_init
760
+ else:
761
+ self.log("Bracket established, using Brent's method")
762
+ try:
763
+ alpha = brentq(objective_function, alpha_lo, alpha_hi,
764
+ xtol=1e-6, rtol=1e-6, maxiter=50)
765
+ self.log(f"Brent's method converged to alpha={alpha:.6e}")
766
+ except Exception as e:
767
+ self.log(f"Brent's method failed: {str(e)}, using initial alpha")
768
+ alpha = alpha_init
769
+ except Exception as e:
770
+ self.log(f"Error establishing bracket: {str(e)}, using initial alpha")
771
+ alpha = alpha_init
772
+
773
+ alpha = alpha_init if 'alpha' not in locals() else alpha
774
+ step_norm_history = np.zeros(self.max_micro_cycles)
775
+ history_count = 0
776
+ best_step = None
777
+ best_step_norm_diff = float('inf')
778
+ alpha_left, alpha_right = None, None
779
+
780
+ for mu in range(self.max_micro_cycles):
781
+ self.log(f"RS-I-RFO micro cycle {mu:02d}, alpha={alpha:.6f}")
782
+ try:
783
+ step, eigval_min = calculate_step(alpha)
784
+ step_norm = np.linalg.norm(step)
785
+ self.log(f"norm(step)={step_norm:.6f}")
786
+
787
+ norm_diff = abs(step_norm - self.trust_radius)
788
+ if norm_diff < best_step_norm_diff:
789
+ if best_step is None:
790
+ best_step = step.copy()
791
+ else:
792
+ best_step = np.copyto(best_step, step)
793
+ best_step_norm_diff = norm_diff
794
+
795
+ objval = step_norm**2 - self.trust_radius**2
796
+ self.log(f"U(a)={objval:.6e}")
797
+
798
+ if objval < 0 and (alpha_left is None or alpha > alpha_left): alpha_left = alpha
799
+ elif objval > 0 and (alpha_right is None or alpha < alpha_right): alpha_right = alpha
800
+
801
+ if abs(objval) < 1e-8 or norm_diff < self.step_norm_tolerance:
802
+ self.log(f"Step norm {step_norm:.6f} is sufficiently close to trust radius")
803
+ if mu >= 1: break
804
+
805
+ if history_count < self.max_micro_cycles:
806
+ step_norm_history[history_count] = step_norm
807
+ history_count += 1
808
+
809
+ dstep2_dalpha = self.get_step_derivative(alpha, eigvals, gradient_trans,
810
+ step=step, eigval_min=eigval_min)
811
+ self.log(f"d(||step||^2)/dα={dstep2_dalpha:.6e}")
812
+
813
+ if abs(dstep2_dalpha) < 1e-10:
814
+ if alpha_left is not None and alpha_right is not None:
815
+ alpha_new = (alpha_left + alpha_right) / 2
816
+ self.log(f"Small derivative, using bisection")
817
+ else:
818
+ alpha_new = max(alpha / 2, 1e-6) if objval > 0 else min(alpha * 2, self.alpha_max)
819
+ self.log(f"Small derivative, no bracket, using heuristic")
820
+ else:
821
+ alpha_step_raw = -objval / dstep2_dalpha
822
+ alpha_step = np.clip(alpha_step_raw, -self.alpha_step_max, self.alpha_step_max)
823
+ if abs(alpha_step) != abs(alpha_step_raw):
824
+ self.log(f"Limited alpha step from {alpha_step_raw:.6f} to {alpha_step:.6f}")
825
+ alpha_new = alpha + alpha_step
826
+ if alpha_left is not None and alpha_right is not None:
827
+ alpha_new = max(min(alpha_new, alpha_right * 0.99), alpha_left * 1.01)
828
+
829
+ old_alpha = alpha
830
+ alpha = min(max(alpha_new, 1e-6), self.alpha_max)
831
+ self.log(f"Updated alpha: {old_alpha:.6f} -> {alpha:.6f}")
832
+
833
+ if alpha == self.alpha_max or alpha == 1e-6:
834
+ self.log(f"Alpha hit boundary at {alpha:.6e}, stopping iterations")
835
+ break
836
+
837
+ if history_count >= 3:
838
+ idx = history_count - 1
839
+ changes = [abs(step_norm_history[idx] - step_norm_history[idx-1]),
840
+ abs(step_norm_history[idx-1] - step_norm_history[idx-2])]
841
+ if all(c < 1e-6 for c in changes):
842
+ self.log("Step norm not changing significantly, stopping iterations")
843
+ break
844
+ except Exception as e:
845
+ self.log(f"Error in micro-cycle {mu}: {str(e)}")
846
+ if best_step is not None:
847
+ self.log("Using best step found so far due to error")
848
+ step, step_norm = best_step, np.linalg.norm(best_step)
849
+ break
850
+ else:
851
+ self.log("Falling back to steepest descent due to errors")
852
+ step = -gradient_trans
853
+ step_norm = np.linalg.norm(step)
854
+ if step_norm > self.trust_radius:
855
+ step = step / step_norm * self.trust_radius
856
+ step_norm = self.trust_radius
857
+ break
858
+ else:
859
+ self.log(f"RS-I-RFO did not converge in {self.max_micro_cycles} cycles")
860
+ if best_step is not None:
861
+ self.log("Using best step found during iterations")
862
+ step, step_norm = best_step, np.linalg.norm(best_step)
863
+
864
+ return step, step_norm, alpha
865
+
866
+ def get_step_derivative(self, alpha, eigvals, gradient_trans, step=None, eigval_min=None):
867
+ if step is None or eigval_min is None:
868
+ try:
869
+ step, eigval_min, _, _ = self.solve_rfo(eigvals, gradient_trans, alpha)
870
+ except Exception as e:
871
+ self.log(f"Error in step calculation for derivative: {str(e)}")
872
+ return 1e-8
873
+
874
+ try:
875
+ denominators = eigvals - eigval_min * alpha
876
+ small_denoms = np.abs(denominators) < 1e-8
877
+ if np.any(small_denoms):
878
+ safe_denoms = denominators.copy()
879
+ safe_denoms[small_denoms] = np.sign(safe_denoms[small_denoms]) * np.maximum(1e-8, np.abs(safe_denoms[small_denoms]))
880
+ zero_mask = safe_denoms[small_denoms] == 0
881
+ if np.any(zero_mask): safe_denoms[small_denoms][zero_mask] = 1e-8
882
+ denominators = safe_denoms
883
+
884
+ numerator = gradient_trans**2
885
+ denominator = denominators**3
886
+ valid_indices = np.abs(denominator) > 1e-10
887
+ if not np.any(valid_indices): return 1e-8
888
+
889
+ sum_terms = np.zeros_like(numerator)
890
+ sum_terms[valid_indices] = numerator[valid_indices] / denominator[valid_indices]
891
+
892
+ max_magnitude = 1e20
893
+ large_values = np.abs(sum_terms) > max_magnitude
894
+ if np.any(large_values):
895
+ sum_terms[large_values] = np.sign(sum_terms[large_values]) * max_magnitude
896
+ sum_term = np.sum(sum_terms)
897
+
898
+ dstep2_dalpha = 2.0 * eigval_min * sum_term
899
+
900
+ if not np.isfinite(dstep2_dalpha) or abs(dstep2_dalpha) > max_magnitude:
901
+ dstep2_dalpha = np.sign(dstep2_dalpha) * max_magnitude if dstep2_dalpha != 0 else 1e-8
902
+
903
+ return dstep2_dalpha
904
+
905
+ except Exception as e:
906
+ self.log(f"Error in derivative calculation: {str(e)}")
907
+ return 1e-8
908
+
909
+ def _solve_secular_equation(self, eigvals, grad_comps, alpha):
910
+ # 1. Prepare scaled values
911
+ eigvals_prime = eigvals / alpha
912
+ grad_comps_prime = grad_comps / alpha
913
+ grad_comps_prime_sq = grad_comps_prime**2
914
+
915
+ # 2. Define the secular function f(lambda)
916
+ def f(lambda_aug):
917
+ denoms = eigvals_prime - lambda_aug
918
+ # Strictly avoid division by zero
919
+ denoms[np.abs(denoms) < 1e-30] = np.sign(denoms[np.abs(denoms) < 1e-30]) * 1e-30
920
+ terms = grad_comps_prime_sq / denoms
921
+ return lambda_aug + np.sum(terms)
922
+
923
+ # --- 3. Robust bracket (asymptote) search ---
924
+
925
+ # Sort eigenvalues and rearrange corresponding gradients
926
+ sort_indices = np.argsort(eigvals_prime)
927
+ eigvals_sorted = eigvals_prime[sort_indices]
928
+ grad_comps_sorted_sq = grad_comps_prime_sq[sort_indices]
929
+
930
+ b_upper = None
931
+ min_eig_val_overall = eigvals_sorted[0] # Fallback value
932
+
933
+ # Find the "first" asymptote where the gradient is non-zero
934
+ for i in range(len(eigvals_sorted)):
935
+ if grad_comps_sorted_sq[i] > 1e-20: # Gradient is non-zero
936
+ # This is the first asymptote
937
+ b_upper = eigvals_sorted[i] - 1e-10
938
+ break
939
+
940
+ if b_upper is None:
941
+ # All gradient components are zero (already at a stationary point)
942
+ self.log("All gradient components in RFO space are zero.", force=True)
943
+ return 0.0 # Step will be zero
944
+
945
+ # --- 4. Set the lower bracket bound (b_lower) ---
946
+ g_norm_sq = np.sum(grad_comps_prime_sq)
947
+ # Add small constant to avoid b_lower == b_upper if g_norm_sq is tiny
948
+ b_lower = b_upper - (1e6 + g_norm_sq) # A robust heuristic lower bound
949
+
950
+ # --- 5. Check bracket validity ---
951
+ try:
952
+ f_upper = f(b_upper)
953
+ f_lower = f(b_lower)
954
+ except Exception as e:
955
+ self.log(f"f(lambda) calculation failed: {e}. Using fallback.", force=True)
956
+ return min_eig_val_overall - 1e-6 # Worst-case fallback
957
+
958
+ if f_lower * f_upper >= 0:
959
+ # Bracket is invalid (meaning f(b_upper) did not go to +inf or b_lower not low enough)
960
+ self.log(f"brentq bracket invalid: f(lower)={f_lower:.2e}, f(upper)={f_upper:.2e}", force=True)
961
+
962
+ # Try a much lower b_lower
963
+ b_lower = b_upper - 1e12 # Even lower
964
+ try:
965
+ f_lower = f(b_lower)
966
+ except Exception as e:
967
+ self.log(f"f(lambda) calculation failed for lower fallback: {e}. Using fallback.", force=True)
968
+ return min_eig_val_overall - 1e-6
969
+
970
+ if f_lower * f_upper >= 0:
971
+ self.log("FATAL: Could not find valid bracket. Using fallback.", force=True)
972
+ return min_eig_val_overall - 1e-6 # Worst-case fallback
973
+
974
+ # --- 6. Root finding ---
975
+ try:
976
+ root = brentq(f, b_lower, b_upper, xtol=1e-10, rtol=1e-10, maxiter=100)
977
+ return root
978
+ except Exception as e:
979
+ self.log(f"brentq failed: {e}. Using fallback.", force=True)
980
+ return min_eig_val_overall - 1e-6
981
+
982
+ def solve_rfo(self, eigvals, gradient_components, alpha, mode="min"):
983
+ if mode != "min":
984
+ raise NotImplementedError("Secular equation solver is only implemented for RFO minimization (mode='min')")
985
+
986
+ eigval_min = self._solve_secular_equation(eigvals, gradient_components, alpha)
987
+ denominators = (eigvals / alpha) - eigval_min
988
+
989
+ safe_denoms = denominators
990
+ small_denoms = np.abs(safe_denoms) < 1e-10
991
+ if np.any(small_denoms):
992
+ # Use copy() to avoid modifying original denominators if it's passed by reference elsewhere
993
+ safe_denoms = safe_denoms.copy()
994
+ safe_denoms[small_denoms] = np.sign(safe_denoms[small_denoms]) * np.maximum(1e-10, np.abs(safe_denoms[small_denoms]))
995
+ zero_mask = safe_denoms[small_denoms] == 0
996
+ if np.any(zero_mask): safe_denoms[small_denoms][zero_mask] = 1e-10
997
+
998
+ step = -(gradient_components / alpha) / safe_denoms
999
+ return step, eigval_min, 1.0, None
1000
+
1001
+ def rfo_model(self, gradient, hessian, step):
1002
+ return np.dot(gradient, step) + 0.5 * np.dot(step, np.dot(hessian, step))
1003
+
1004
+ def is_converged(self):
1005
+ return self.converged
1006
+
1007
+ def get_predicted_energy_changes(self):
1008
+ return self.predicted_energy_changes
1009
+
1010
+ def get_actual_energy_changes(self):
1011
+ return self.actual_energy_changes
1012
+
1013
+ def reset_trust_radius(self):
1014
+ self.trust_radius = self.trust_radius_initial
1015
+ self.log(f"Trust radius reset to initial value: {self.trust_radius:.6f}", force=True)