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,998 @@
1
+ import numpy as np
2
+ from scipy import linalg
3
+ from multioptpy.Parameters.parameter import UnitValueLib, covalent_radii_lib
4
+ from .hessian_update import ModelHessianUpdate
5
+
6
+ """
7
+ Hybrid Internal-Cartesian Coordinate RFO implementation
8
+ Implementation combining redundant internal coordinates and Cartesian coordinates
9
+ """
10
+
11
+ class HybridRFO:
12
+ def __init__(self, **config):
13
+ self.config = config
14
+ self.hess_update = ModelHessianUpdate()
15
+
16
+ # Initialize specific parameters
17
+ self.Initialization = True
18
+ self.saddle_order = self.config.get("saddle_order", 0)
19
+ self.trust_radius = float(self.config.get("trust_radius", 0.1))
20
+ self.trust_radius /= UnitValueLib().bohr2angstroms
21
+ self.DELTA = 0.1 # Step scale factor
22
+
23
+ # For coordinate handling
24
+ self.B_matrix = None # Wilson B matrix (transformation matrix)
25
+ self.G_matrix = None # G matrix (metric matrix)
26
+ self.primitive_coords = None
27
+
28
+ # Hybrid specific parameters
29
+ self.redundant_thresh = 1e-8 # Threshold for numerical stability
30
+ self.coord_type = self.config.get("coord_type", "hybrid")
31
+ self.iter = 0
32
+ self.max_backsteps = 20
33
+ self.max_micro_cycles = 100
34
+ self.gradient_rms_threshold = 1e-4
35
+ self.FC_COUNT = self.config.get("FC_COUNT", -1)
36
+ self.backconv_method = self.config.get("backconv_method", "scf").lower()
37
+ # Initial weighting between coordinate systems
38
+ self.internal_weight = self.config.get("internal_weight", 0.5) # Default weight for internal coords
39
+ self.cartesian_weight = 1.0 - self.internal_weight # Weight for Cartesian coords
40
+
41
+ # Tracking variables
42
+ self.prev_cartesian = None
43
+ self.prev_gradient = None
44
+ self.internal_hessian = None
45
+ self.cartesian_hessian = None
46
+ self.hybrid_hessian = None
47
+ self.hessian = None
48
+ self.bias_hessian = None
49
+ self.prev_connectivity = None
50
+ self.last_backtransform_error = 0.0
51
+
52
+ self.element_list = self.config.get("element_list", None)
53
+
54
+
55
+ def define_internal_coordinates(self, geometry):
56
+ """
57
+ Define primitive internal coordinates based on molecular connectivity
58
+ Returns the connectivity table and primitive internal coordinates
59
+ Using only bonds and angles (no dihedrals as requested)
60
+ """
61
+ atomic_numbers = self.element_list
62
+ # Convert geometry from (3N, 1) to (N, 3) format
63
+ natoms = len(atomic_numbers)
64
+ geom_reshaped = geometry.reshape(natoms, 3)
65
+
66
+ primitive_coords = []
67
+ connectivity = np.zeros((natoms, natoms), dtype=int)
68
+
69
+ # Build connectivity based on covalent radii
70
+ for i in range(natoms):
71
+ for j in range(i+1, natoms):
72
+ # Calculate distance between atoms
73
+ dist = np.linalg.norm(geom_reshaped[i] - geom_reshaped[j])
74
+
75
+ # Get covalent radii based on atomic numbers
76
+ r_i = covalent_radii_lib(atomic_numbers[i])
77
+ r_j = covalent_radii_lib(atomic_numbers[j])
78
+
79
+ # Check if atoms are bonded (using a bond tolerance factor of 1.2)
80
+ if dist < 1.2 * (r_i + r_j):
81
+ connectivity[i, j] = 1
82
+ connectivity[j, i] = 1
83
+
84
+ # Add bond as primitive coordinate
85
+ primitive_coords.append({
86
+ 'type': 'bond',
87
+ 'atoms': [i, j]
88
+ })
89
+
90
+ # Add angle coordinates
91
+ for j in range(natoms):
92
+ bonded_to_j = np.where(connectivity[j] > 0)[0]
93
+ for i in bonded_to_j:
94
+ for k in bonded_to_j:
95
+ if i < k: # Avoid duplicates
96
+ primitive_coords.append({
97
+ 'type': 'angle',
98
+ 'atoms': [i, j, k]
99
+ })
100
+
101
+ # Note: As requested, no dihedral angles are added
102
+
103
+ self.primitive_coords = primitive_coords
104
+ return connectivity, primitive_coords
105
+
106
+ def calc_center(self, geometry):
107
+ """Calculate center of mass of the geometry"""
108
+ n_atoms = len(geometry) // 3
109
+ return np.mean(geometry.reshape(n_atoms, 3), axis=0)
110
+
111
+ def build_B_matrix(self, geometry, primitive_coords=None):
112
+ """
113
+ Build Wilson B matrix (∂q/∂x) for coordinate transformation
114
+ q: internal coordinates
115
+ x: Cartesian coordinates
116
+
117
+ The geometry is expected in (3N, 1) format
118
+ """
119
+ if primitive_coords is None:
120
+ primitive_coords = self.primitive_coords
121
+
122
+ natoms = len(geometry) // 3
123
+ geom_reshaped = geometry.reshape(natoms, 3)
124
+ ncoords = len(primitive_coords)
125
+
126
+ B = np.zeros((ncoords, 3*natoms))
127
+
128
+ for i, coord in enumerate(primitive_coords):
129
+ if coord['type'] == 'bond':
130
+ a1, a2 = coord['atoms']
131
+ B[i] = self._bond_B_elements(geom_reshaped, a1, a2)
132
+ elif coord['type'] == 'angle':
133
+ a1, a2, a3 = coord['atoms']
134
+ B[i] = self._angle_B_elements(geom_reshaped, a1, a2, a3)
135
+
136
+ return B
137
+
138
+ def _bond_B_elements(self, geometry, a1, a2):
139
+ """Calculate B matrix elements for bond stretching"""
140
+ r1 = geometry[a1]
141
+ r2 = geometry[a2]
142
+
143
+ # Vector pointing from atom a1 to atom a2
144
+ vec = r2 - r1
145
+
146
+ # Distance between the atoms
147
+ distance = np.linalg.norm(vec)
148
+
149
+ if distance < 1e-10:
150
+ unit_vec = np.zeros(3)
151
+ else:
152
+ unit_vec = vec / distance
153
+
154
+ # B matrix elements (derivatives of the bond distance w.r.t. Cartesian coordinates)
155
+ B_elements = np.zeros(3 * len(geometry))
156
+
157
+ # For atom a1
158
+ B_elements[3*a1:3*a1+3] = -unit_vec
159
+
160
+ # For atom a2
161
+ B_elements[3*a2:3*a2+3] = unit_vec
162
+
163
+ return B_elements
164
+
165
+ def _angle_B_elements(self, geometry, a1, a2, a3):
166
+ """Calculate B matrix elements for angle bending"""
167
+ r1 = geometry[a1]
168
+ r2 = geometry[a2]
169
+ r3 = geometry[a3]
170
+
171
+ # Vectors from central atom to the other two atoms
172
+ v1 = r1 - r2
173
+ v3 = r3 - r2
174
+
175
+ # Normalize vectors
176
+ d1 = np.linalg.norm(v1)
177
+ d3 = np.linalg.norm(v3)
178
+
179
+ if d1 < 1e-10 or d3 < 1e-10:
180
+ return np.zeros(3 * len(geometry))
181
+
182
+ v1_normalized = v1 / d1
183
+ v3_normalized = v3 / d3
184
+
185
+ # Cosine of the angle
186
+ cos_angle = np.dot(v1_normalized, v3_normalized)
187
+
188
+ # Ensure numerical stability
189
+ if cos_angle > 1.0:
190
+ cos_angle = 1.0
191
+ elif cos_angle < -1.0:
192
+ cos_angle = -1.0
193
+
194
+ sin_angle = np.sqrt(1.0 - cos_angle**2)
195
+
196
+ if sin_angle < 1e-10:
197
+ return np.zeros(3 * len(geometry))
198
+
199
+ # Calculate derivatives
200
+ term1 = 1.0 / (d1 * sin_angle)
201
+ term3 = 1.0 / (d3 * sin_angle)
202
+
203
+ # Cross products
204
+ cp1 = np.cross(v1_normalized, v3_normalized)
205
+ cp3 = np.cross(v3_normalized, v1_normalized)
206
+
207
+ # B matrix elements
208
+ B_elements = np.zeros(3 * len(geometry))
209
+
210
+ # For atom a1
211
+ B_elements[3*a1:3*a1+3] = term1 * cp1
212
+
213
+ # For atom a3
214
+ B_elements[3*a3:3*a3+3] = term3 * cp3
215
+
216
+ # For central atom a2
217
+ B_elements[3*a2:3*a2+3] = -(B_elements[3*a1:3*a1+3] + B_elements[3*a3:3*a3+3])
218
+
219
+ return B_elements
220
+
221
+ def build_G_matrix(self, B_matrix):
222
+ """
223
+ Build G matrix (metric matrix) from Wilson B matrix
224
+ G = B·B^T
225
+ """
226
+ G = np.dot(B_matrix, B_matrix.T)
227
+ return G
228
+
229
+ def check_connectivity_change(self, geometry):
230
+ """
231
+ Check if the molecular connectivity has changed significantly
232
+ """
233
+ if self.prev_connectivity is None:
234
+ # First call, just store the current connectivity
235
+ connectivity, _ = self.define_internal_coordinates(geometry)
236
+ self.prev_connectivity = connectivity
237
+ return False
238
+
239
+ # Check current connectivity
240
+ current_connectivity, _ = self.define_internal_coordinates(geometry)
241
+
242
+ # Compare with previous connectivity matrix
243
+ diff = np.sum(np.abs(current_connectivity - self.prev_connectivity))
244
+
245
+ # If there are changes in connectivity, reset coordinate system
246
+ if diff > 0:
247
+ print(f"WARNING: Detected {diff} changes in molecular connectivity")
248
+ print("Resetting internal coordinates due to bond rearrangement")
249
+ self.prev_connectivity = current_connectivity
250
+
251
+ # Adjust weights to favor Cartesian coordinates during bond rearrangements
252
+ self.internal_weight = max(0.5, self.internal_weight * 0.6)
253
+ self.cartesian_weight = 1.0 - self.internal_weight
254
+ print(f"Adjusted coordinate weights: Internal={self.internal_weight:.2f}, Cartesian={self.cartesian_weight:.2f}")
255
+
256
+ return True
257
+
258
+ return False
259
+
260
+ def update_coordinates(self, geometry):
261
+ """
262
+ Update the internal coordinates for the current geometry
263
+ """
264
+ # Build B matrix for current geometry
265
+ B = self.build_B_matrix(geometry)
266
+ self.B_matrix = B
267
+
268
+ # Build G matrix
269
+ G = self.build_G_matrix(B)
270
+ self.G_matrix = G
271
+
272
+ return B
273
+
274
+ def build_hybrid_system(self, geometry, cart_gradient=None):
275
+ """
276
+ Build the hybrid coordinate system combining internal and Cartesian
277
+ """
278
+ n_cart = len(geometry)
279
+
280
+ # Build or update internal coordinate system
281
+ B = self.update_coordinates(geometry)
282
+ n_int = B.shape[0]
283
+
284
+ # Create transformation matrices for the hybrid system
285
+ cart_identity = np.eye(n_cart)
286
+
287
+ # Calculate weights for the hybrid system
288
+ w_int = np.sqrt(self.internal_weight)
289
+ w_cart = np.sqrt(self.cartesian_weight)
290
+
291
+ # Create hybrid transformation matrix: [w_int*B; w_cart*I]
292
+ hybrid_transform = np.vstack([
293
+ w_int * B,
294
+ w_cart * cart_identity
295
+ ])
296
+
297
+ # Store dimensions for later use
298
+ self.n_cart = n_cart
299
+ self.n_int = n_int
300
+ self.hybrid_transform = hybrid_transform
301
+
302
+ return hybrid_transform, n_int + n_cart
303
+
304
+ def transform_hybrid_gradient(self, cart_gradient):
305
+ """
306
+ Transform Cartesian gradient to hybrid coordinate gradient
307
+ Returns: [w_int*B*g_cart; w_cart*g_cart]
308
+ """
309
+ # Transform gradient for internal coordinates: B*g_cart
310
+ int_gradient = np.dot(self.B_matrix, cart_gradient)
311
+
312
+ # Apply weights
313
+ w_int = np.sqrt(self.internal_weight)
314
+ w_cart = np.sqrt(self.cartesian_weight)
315
+
316
+ # Create hybrid gradient vector
317
+ hybrid_gradient = np.vstack([
318
+ w_int * int_gradient,
319
+ w_cart * cart_gradient
320
+ ])
321
+
322
+ return hybrid_gradient
323
+
324
+ def transform_hybrid_hessian(self, cart_hessian):
325
+ """
326
+ Transform Cartesian Hessian to hybrid coordinate Hessian
327
+ with proper dimension checking
328
+ """
329
+ B = self.B_matrix
330
+ n_int = B.shape[0]
331
+ n_cart = cart_hessian.shape[0]
332
+
333
+ # Check dimensions
334
+ if n_cart != self.n_cart:
335
+ print(f"Warning: Bias Hessian dimension ({n_cart}) doesn't match current system ({self.n_cart})")
336
+
337
+ return np.zeros((n_int + self.n_cart, n_int + self.n_cart))
338
+
339
+ # Internal coordinate part: B*H_cart*B^T
340
+ int_hessian = np.dot(B, np.dot(cart_hessian, B.T))
341
+
342
+ # Apply weights
343
+ w_int = np.sqrt(self.internal_weight)
344
+ w_cart = np.sqrt(self.cartesian_weight)
345
+
346
+ # Create hybrid Hessian with block structure
347
+ hybrid_hessian = np.zeros((n_int + n_cart, n_int + n_cart))
348
+
349
+ # Internal block: w_int^2 * B*H_cart*B^T
350
+ hybrid_hessian[:n_int, :n_int] = w_int**2 * int_hessian
351
+
352
+ # Cross terms: w_int*w_cart * B*H_cart and w_cart*w_int * H_cart*B^T
353
+ hybrid_hessian[:n_int, n_int:] = w_int * w_cart * np.dot(B, cart_hessian)
354
+ hybrid_hessian[n_int:, :n_int] = w_int * w_cart * np.dot(cart_hessian, B.T)
355
+
356
+ # Cartesian block: w_cart^2 * H_cart
357
+ hybrid_hessian[n_int:, n_int:] = w_cart**2 * cart_hessian
358
+
359
+ return hybrid_hessian
360
+
361
+ def hybrid_to_cartesian(self, step_hybrid, geometry):
362
+ """
363
+ Convert a step in hybrid coordinates back to Cartesian coordinates
364
+ The hybrid step contains both internal and Cartesian contributions
365
+ """
366
+ n_int = self.n_int
367
+ n_cart = self.n_cart
368
+
369
+ # Extract and unweight the internal and Cartesian components
370
+ w_int = np.sqrt(self.internal_weight)
371
+ w_cart = np.sqrt(self.cartesian_weight)
372
+
373
+ step_int = step_hybrid[:n_int] / w_int
374
+ step_cart_direct = step_hybrid[n_int:] / w_cart
375
+
376
+ # Step in Cartesian coordinates via internal coordinates
377
+ cart_step_from_int = self.internal_to_cartesian(step_int, geometry)
378
+
379
+ # Combine the two steps with proper weighting
380
+ total_cart_step = self.internal_weight * cart_step_from_int + self.cartesian_weight * step_cart_direct
381
+
382
+ # Print information about the contributions
383
+ int_norm = np.linalg.norm(cart_step_from_int)
384
+ cart_norm = np.linalg.norm(step_cart_direct)
385
+ total_norm = np.linalg.norm(total_cart_step)
386
+
387
+ print(f"Step norms - Internal: {int_norm:.6f}, Cartesian: {cart_norm:.6f}, Combined: {total_norm:.6f}")
388
+
389
+ return total_cart_step
390
+
391
+
392
+ def _evaluate_error(self, geom, reference_geom, target_step):
393
+ """
394
+ Helper function to evaluate the error for a given geometry
395
+ """
396
+ B = self.build_B_matrix(geom)
397
+ current_q = np.dot(B, geom - reference_geom)
398
+ delta_q = target_step - current_q
399
+ return np.linalg.norm(delta_q)
400
+
401
+ def _evaluate_guess_error(self, geom, step_int, reference_geom):
402
+ """
403
+ Helper function to evaluate the quality of an initial guess
404
+
405
+ Parameters:
406
+ -----------
407
+ geom : ndarray
408
+ Geometry to evaluate
409
+ step_int : ndarray
410
+ Target step in internal coordinates
411
+ reference_geom : ndarray
412
+ Reference geometry
413
+
414
+ Returns:
415
+ --------
416
+ float
417
+ RMS error of the guess
418
+ """
419
+ B = self.build_B_matrix(geom)
420
+ q = np.dot(B, geom - reference_geom)
421
+ delta_q = step_int - q
422
+ error = np.linalg.norm(delta_q)
423
+ rms_error = error / np.sqrt(len(delta_q))
424
+ return rms_error
425
+
426
+ def internal_to_cartesian_scf(self, step_int, geometry,
427
+ max_iterations=100,
428
+ convergence_threshold=1e-8):
429
+ """
430
+ SCF-inspired algorithm for internal to Cartesian coordinate conversion.
431
+ Uses techniques from electronic structure theory: line search when far from
432
+ convergence and Newton-Raphson near convergence, with DIIS acceleration.
433
+
434
+ Parameters:
435
+ -----------
436
+ step_int : ndarray
437
+ Target step in internal coordinates
438
+ geometry : ndarray
439
+ Starting geometry in Cartesian coordinates
440
+ max_iterations : int
441
+ Maximum number of iterations
442
+ convergence_threshold : float
443
+ Convergence criterion for RMS error
444
+
445
+ Returns:
446
+ --------
447
+ ndarray
448
+ Step in Cartesian coordinates
449
+ """
450
+ # Initialize variables
451
+ reference_geom = geometry.copy()
452
+ current_geom = geometry.copy()
453
+ best_geom = current_geom.copy()
454
+ best_error = float('inf')
455
+
456
+ # DIIS parameters
457
+ diis_start = 3
458
+ diis_max = 8
459
+ diis_errors = []
460
+ diis_geometries = []
461
+
462
+ # Dynamic algorithm control
463
+ use_newton = False # Start with line search, switch to Newton later
464
+ damping_factor = 0.7 # Initial damping
465
+ level_shift = 0.0 # Level shifting parameter
466
+
467
+ # Last iteration data for backtracks
468
+ prev_error = float('inf')
469
+ prev_geom = None
470
+
471
+ # Calculate initial values
472
+ B = self.build_B_matrix(current_geom)
473
+ current_q = np.dot(B, current_geom - reference_geom)
474
+ delta_q = step_int - current_q
475
+ error = np.linalg.norm(delta_q)
476
+ rms_error = error / np.sqrt(len(delta_q))
477
+
478
+ print(f"Starting SCF-like optimization: initial RMS error = {rms_error:.3e}")
479
+
480
+ # Main iteration loop
481
+ for iteration in range(max_iterations):
482
+ # Save best solution
483
+ if error < best_error:
484
+ best_error = error
485
+ best_geom = current_geom.copy()
486
+
487
+ # Report progress
488
+ if iteration % 20 == 0 or iteration < 2:
489
+ print(f"Iteration {iteration}: RMS error = {rms_error:.3e}, "
490
+ f"{'Newton' if use_newton else 'LineSearch'}, damping={damping_factor:.2f}")
491
+
492
+ # Check convergence
493
+ if rms_error < convergence_threshold:
494
+ print(f"Conversion converged in {iteration+1} iterations")
495
+ break
496
+
497
+ # Prepare for this iteration
498
+ B = self.build_B_matrix(current_geom)
499
+
500
+ # Calculate step based on current method
501
+ if use_newton:
502
+ # Calculate B+ (pseudoinverse of B) using SVD with level shifting
503
+ try:
504
+ U, s, Vh = np.linalg.svd(B, full_matrices=False)
505
+
506
+ # Apply level shifting to singular values (similar to SCF level shifting)
507
+ s_inv = np.where(s > 1e-7, 1.0 / (s + level_shift), 0.0)
508
+
509
+ B_pinv = np.dot(Vh.T * s_inv, U.T)
510
+
511
+ # Calculate Newton step
512
+ step = np.dot(B_pinv, delta_q)
513
+ except np.linalg.LinAlgError:
514
+ # Fall back to a more stable approach
515
+ print("SVD failed, using pinv with increased regularization")
516
+ B_pinv = np.linalg.pinv(B, rcond=1e-6)
517
+ step = np.dot(B_pinv, delta_q)
518
+ else:
519
+ # Line search along steepest descent direction
520
+ gradient = np.dot(B.T, delta_q)
521
+ grad_norm = np.linalg.norm(gradient)
522
+
523
+ if grad_norm < 1e-10:
524
+ # Gradient too small, try Newton step
525
+ use_newton = True
526
+ B_pinv = np.linalg.pinv(B, rcond=1e-7)
527
+ step = np.dot(B_pinv, delta_q)
528
+ else:
529
+ # Normalize gradient and scale by dynamic step size
530
+ step_dir = gradient / grad_norm
531
+
532
+ # Determine step size (larger when further from convergence)
533
+ # Similar to trust radius in SCF
534
+ step_size = min(0.2, 0.1 * (1.0 + 10.0 * rms_error))
535
+
536
+ step = step_size * step_dir
537
+
538
+ # Apply DIIS acceleration if we have enough iterations
539
+ diis_step = None
540
+ if iteration >= diis_start:
541
+ # Store current error and geometry for DIIS
542
+ diis_errors.append(delta_q.flatten())
543
+ diis_geometries.append(current_geom.copy())
544
+
545
+ # Limit DIIS vector storage
546
+ if len(diis_errors) > diis_max:
547
+ diis_errors.pop(0)
548
+ diis_geometries.pop(0)
549
+
550
+ # Apply DIIS if we have at least 2 vectors
551
+ if len(diis_errors) >= 2:
552
+ try:
553
+ diis_step = self._compute_diis_solution(diis_errors, diis_geometries)
554
+ except Exception as e:
555
+ print(f"DIIS failed: {e}")
556
+ diis_step = None
557
+
558
+ # Try step with current damping
559
+ damped_step = step * damping_factor
560
+ trial_geom = current_geom + damped_step
561
+
562
+ # Evaluate trial geometry
563
+ trial_B = self.build_B_matrix(trial_geom)
564
+ trial_q = np.dot(trial_B, trial_geom - reference_geom)
565
+ trial_delta_q = step_int - trial_q
566
+ trial_error = np.linalg.norm(trial_delta_q)
567
+ trial_rms = trial_error / np.sqrt(len(trial_delta_q))
568
+
569
+ # If we have a DIIS solution, evaluate it too
570
+ if diis_step is not None:
571
+ diis_B = self.build_B_matrix(diis_step)
572
+ diis_q = np.dot(diis_B, diis_step - reference_geom)
573
+ diis_delta_q = step_int - diis_q
574
+ diis_error = np.linalg.norm(diis_delta_q)
575
+ diis_rms = diis_error / np.sqrt(len(diis_delta_q))
576
+
577
+ # Use DIIS if it's better
578
+ if diis_error < trial_error and diis_error < error:
579
+ #print(f"DIIS improvement: {trial_rms:.3e} -> {diis_rms:.3e}")
580
+ current_geom = diis_step
581
+ delta_q = diis_delta_q
582
+ error = diis_error
583
+ rms_error = diis_rms
584
+
585
+ # DIIS was successful, try to reduce level shifting
586
+ if level_shift > 0:
587
+ level_shift = max(0, level_shift * 0.5)
588
+ continue # Skip to next iteration
589
+
590
+ # Dynamically adjust the algorithm based on progress
591
+ if trial_error < error:
592
+ # Step is good, accept it
593
+ current_geom = trial_geom
594
+ delta_q = trial_delta_q
595
+ prev_error = error
596
+ error = trial_error
597
+ rms_error = trial_rms
598
+
599
+ # Increase damping for more aggressive steps
600
+ damping_factor = min(1.0, damping_factor * 1.2)
601
+
602
+ # Switch to Newton method when close enough to solution
603
+ if not use_newton and rms_error < 0.1:
604
+ #print(f"Switching to Newton-Raphson at iteration {iteration+1}")
605
+ use_newton = True
606
+
607
+ # Reduce level shifting since things are going well
608
+ if level_shift > 0:
609
+ level_shift = max(0, level_shift * 0.5)
610
+
611
+ else:
612
+ # Step is bad, adjust strategy
613
+ if use_newton:
614
+ # Newton step made things worse
615
+ if level_shift == 0:
616
+ # Start with modest level shifting
617
+ level_shift = 0.1
618
+ else:
619
+ # Increase level shifting (similar to SCF when oscillating)
620
+ level_shift = min(1.0, level_shift * 2.0)
621
+
622
+ #print(f"Increasing level shift to {level_shift:.3e}")
623
+
624
+ # If level shift is getting too high, try line search
625
+ if level_shift > 0.5:
626
+ #print("Switching to line search due to unstable Newton steps")
627
+ use_newton = False
628
+
629
+ # Reduce damping for more conservative steps
630
+ damping_factor = max(0.2, damping_factor * 0.5)
631
+
632
+ # If we have previous good geometry, backtrack halfway
633
+ if prev_geom is not None:
634
+ #print(f"Backtracking: {error:.3e} -> {prev_error:.3e}")
635
+ current_geom = 0.5 * (current_geom + prev_geom)
636
+
637
+ # Recalculate at backtracked position
638
+ B = self.build_B_matrix(current_geom)
639
+ current_q = np.dot(B, current_geom - reference_geom)
640
+ delta_q = step_int - current_q
641
+ error = np.linalg.norm(delta_q)
642
+ rms_error = error / np.sqrt(len(delta_q))
643
+
644
+ # Save current position for potential backtracking
645
+ prev_geom = current_geom.copy()
646
+
647
+ # Handle non-convergence
648
+ if iteration == max_iterations - 1 and rms_error > convergence_threshold:
649
+ print(f"Warning: Conversion did not converge. Best RMS error = {best_error/np.sqrt(len(step_int)):.3e}")
650
+ current_geom = best_geom
651
+
652
+ # Return the Cartesian step
653
+ return current_geom - reference_geom
654
+
655
+ def _compute_diis_solution(self, error_vectors, geometries):
656
+ """
657
+ Compute DIIS extrapolated solution using Pulay's method
658
+
659
+ Parameters:
660
+ -----------
661
+ error_vectors : list
662
+ List of error vectors (flattened)
663
+ geometries : list
664
+ List of corresponding geometries
665
+
666
+ Returns:
667
+ --------
668
+ ndarray
669
+ DIIS extrapolated geometry
670
+ """
671
+ n_vecs = len(error_vectors)
672
+
673
+ # Build DIIS B matrix for Pulay method
674
+ B_diis = np.zeros((n_vecs + 1, n_vecs + 1))
675
+
676
+ # Fill error vector dot products
677
+ for i in range(n_vecs):
678
+ for j in range(n_vecs):
679
+ B_diis[i, j] = np.dot(error_vectors[i], error_vectors[j])
680
+
681
+ # Add constraint rows/columns
682
+ B_diis[n_vecs, :n_vecs] = 1.0
683
+ B_diis[:n_vecs, n_vecs] = 1.0
684
+ B_diis[n_vecs, n_vecs] = 0.0
685
+
686
+ # RHS vector [0,0,...,0,1]
687
+ rhs = np.zeros(n_vecs + 1)
688
+ rhs[n_vecs] = 1.0
689
+
690
+ # Add small regularization to diagonal for numerical stability
691
+ for i in range(n_vecs):
692
+ B_diis[i, i] += 1e-8 * (1.0 + abs(B_diis[i, i]))
693
+
694
+ # Solve DIIS equations
695
+ try:
696
+ c = np.linalg.solve(B_diis, rhs)
697
+ except np.linalg.LinAlgError:
698
+ # Use SVD-based solution if direct solve fails
699
+ c = np.linalg.lstsq(B_diis, rhs, rcond=1e-10)[0]
700
+
701
+ # Construct DIIS solution
702
+ diis_geom = np.zeros_like(geometries[0])
703
+ for i in range(n_vecs):
704
+ diis_geom += c[i] * geometries[i]
705
+
706
+ return diis_geom
707
+
708
+
709
+
710
+ def internal_to_cartesian(self, step_int, geometry):
711
+ """
712
+ Transform step in internal coordinates to Cartesian coordinates
713
+ using enhanced iterative methods
714
+ """
715
+ # Choose the algorithm based on configuration or problem characteristics
716
+
717
+ if self.backconv_method == "scf":
718
+ return self.internal_to_cartesian_scf(step_int, geometry)
719
+ else:
720
+ return self.internal_to_cartesian_scf(step_int, geometry)
721
+
722
+ def get_cleaned_hessian(self, hessian):
723
+ """Ensure the Hessian is clean and well-conditioned"""
724
+ # Ensure symmetry
725
+ hessian = 0.5 * (hessian + hessian.T)
726
+
727
+ try:
728
+ # Use more stable eigenvalue decomposition
729
+ eigval, eigvec = linalg.eigh(hessian)
730
+ except np.linalg.LinAlgError:
731
+ # Fallback to more robust algorithm if standard fails
732
+ print("Warning: Using more robust eigenvalue decomposition")
733
+ eigval, eigvec = linalg.eigh(hessian, driver='evr')
734
+
735
+ # Find valid eigenvalues (|λ| > 1e-7)
736
+ valid_mask = np.abs(eigval) > 1e-7
737
+ n_removed = np.sum(~valid_mask)
738
+
739
+ # Create diagonal matrix with only valid eigenvalues
740
+ # Replace small eigenvalues with small positive values
741
+ cleaned_eigval = np.where(valid_mask, eigval, 1e-7)
742
+
743
+ # Reconstruct Hessian using only valid components
744
+ # H = U Λ U^T where Λ contains only valid eigenvalues
745
+ cleaned_hessian = np.dot(np.dot(eigvec, np.diag(cleaned_eigval)), eigvec.T)
746
+
747
+ # Ensure symmetry of final result
748
+ cleaned_hessian = 0.5 * (cleaned_hessian + cleaned_hessian.T)
749
+
750
+ return cleaned_hessian, n_removed
751
+
752
+ def run_hybrid_rfo_step(self, hybrid_gradient, hybrid_hessian):
753
+ """
754
+ Calculate the RFO step in hybrid coordinates
755
+ """
756
+ n_coords = len(hybrid_gradient)
757
+
758
+ # Ensure symmetry and clean the Hessian
759
+ new_hess = 0.5 * (hybrid_hessian + hybrid_hessian.T)
760
+ new_hess, _ = self.get_cleaned_hessian(new_hess)
761
+
762
+ # Construct RFO matrix
763
+ matrix_for_RFO = np.block([
764
+ [new_hess, hybrid_gradient.reshape(n_coords, 1)],
765
+ [hybrid_gradient.reshape(1, n_coords), np.zeros((1, 1))]
766
+ ])
767
+
768
+ # Get eigenvalues of the RFO matrix
769
+ try:
770
+ RFO_eigenvalues, RFO_eigenvectors = linalg.eigh(matrix_for_RFO)
771
+ except np.linalg.LinAlgError:
772
+ print("Warning: Using more robust eigenvalue algorithm")
773
+ RFO_eigenvalues, RFO_eigenvectors = linalg.eigh(matrix_for_RFO, driver='evr')
774
+
775
+ # Sort eigenvalues
776
+ idx = np.argsort(RFO_eigenvalues)
777
+ RFO_eigenvalues = RFO_eigenvalues[idx]
778
+ RFO_eigenvectors = RFO_eigenvectors[:, idx]
779
+
780
+ # Select appropriate eigenvalue based on saddle order
781
+ lambda_for_calc = float(RFO_eigenvalues[self.saddle_order])
782
+
783
+ # Calculate step using direct RFO approach
784
+ shifted_hessian = new_hess - lambda_for_calc * np.eye(n_coords)
785
+
786
+ # Solve the RFO equations
787
+ try:
788
+ # LU decomposition for stable solving
789
+ move_vector = -np.linalg.solve(shifted_hessian, hybrid_gradient)
790
+ except np.linalg.LinAlgError:
791
+ print("Warning: Linear solve failed, using pseudoinverse")
792
+ # Use pseudoinverse as fallback
793
+ shifted_hessian_inv = np.linalg.pinv(shifted_hessian, rcond=1e-10)
794
+ move_vector = -np.dot(shifted_hessian_inv, hybrid_gradient)
795
+
796
+ print(f"Lambda for RFO step: {lambda_for_calc}")
797
+ print(f"Gradient RMS: {np.sqrt(np.mean(hybrid_gradient**2))}")
798
+ print(f"Step RMS: {np.sqrt(np.mean(move_vector**2))}")
799
+
800
+ # Limit step size if it exceeds trust radius
801
+ step_norm = np.linalg.norm(move_vector)
802
+ if step_norm > self.trust_radius:
803
+ scale_factor = self.trust_radius / step_norm
804
+ move_vector *= scale_factor
805
+ print(f"Step scaled by {scale_factor} to meet trust radius")
806
+
807
+ return move_vector.reshape(-1, 1)
808
+
809
+ def reset_system(self, geometry):
810
+ """
811
+ Reset internal coordinates when molecular structure changes significantly
812
+ """
813
+ print("Resetting hybrid coordinate system")
814
+
815
+ # Clear previous coordinates
816
+ self.primitive_coords = None
817
+
818
+ # Define new internal coordinates
819
+ _, self.primitive_coords = self.define_internal_coordinates(geometry)
820
+ print(f"Defined {len(self.primitive_coords)} new primitive coordinates")
821
+
822
+ # Update B matrix
823
+ B = self.update_coordinates(geometry)
824
+
825
+ # Reset hybrid system dimensions
826
+ self.n_int = len(self.primitive_coords)
827
+ self.n_cart = len(geometry)
828
+
829
+ # Reset Hessian
830
+ self.internal_hessian = np.eye(self.n_int)
831
+ self.cartesian_hessian = np.eye(self.n_cart)
832
+ self.hybrid_hessian = np.eye(self.n_int + self.n_cart)
833
+
834
+ # Reset flags
835
+ self.Initialization = False
836
+
837
+ return B
838
+
839
+ def update_weights(self, geometry, cart_gradient, cart_step=None):
840
+ """
841
+ Adaptively update weights for internal and Cartesian coordinates
842
+ based on optimization progress
843
+ """
844
+ # Skip if no previous step
845
+ if self.iter < 2 or cart_step is None:
846
+ return
847
+
848
+ # Update weights based on backtransformation error and step norm
849
+ if self.last_backtransform_error > 0.1:
850
+ # Increase weight of Cartesian coords if transformation is unstable
851
+ self.internal_weight = max(0.2, self.internal_weight * 0.9)
852
+ elif self.last_backtransform_error < 0.01 and self.internal_weight < 0.8:
853
+ # Increase weight of internal if transformation is reliable
854
+ self.internal_weight = min(0.5, self.internal_weight * 1.1)
855
+
856
+ # Update Cartesian weight accordingly
857
+ self.cartesian_weight = 1.0 - self.internal_weight
858
+
859
+ print(f"Updated coordinate weights: Internal={self.internal_weight:.2f}, Cartesian={self.cartesian_weight:.2f}")
860
+
861
+ def run(self, geom_num_list, B_g, pre_B_g, pre_geom, B_e, pre_B_e, pre_move_vector, initial_geom_num_list, g, pre_g):
862
+ """
863
+ Main optimization step function using Hybrid Cartesian-Internal Coordinates
864
+ """
865
+ print(f"======= Hybrid RFO Iteration {self.iter} =======")
866
+
867
+ # Define internal coordinates if not already defined
868
+ if self.primitive_coords is None:
869
+ _, self.primitive_coords = self.define_internal_coordinates(geom_num_list)
870
+ print(f"Defined {len(self.primitive_coords)} primitive internal coordinates")
871
+
872
+ # Check if molecular connectivity has changed
873
+ coords_reset = False
874
+ if pre_geom is not None:
875
+ coords_reset = self.check_connectivity_change(geom_num_list)
876
+
877
+ if coords_reset:
878
+ # Reset coordinate system if connectivity changed
879
+ self.reset_system(geom_num_list)
880
+
881
+ # Build hybrid coordinate system
882
+ hybrid_transform, n_hybrid = self.build_hybrid_system(geom_num_list, B_g)
883
+
884
+ # Transform gradient to hybrid coordinates
885
+ hybrid_gradient = self.transform_hybrid_gradient(B_g)
886
+
887
+ # Initialize or update the Hessian
888
+ if self.Initialization or self.hybrid_hessian is None:
889
+ # Start with identity Hessian in hybrid coordinates
890
+ self.hybrid_hessian = np.eye(n_hybrid)
891
+ self.internal_hessian = np.eye(self.n_int)
892
+ self.cartesian_hessian = np.eye(self.n_cart)
893
+ self.Initialization = False
894
+ else:
895
+ # Update the hybrid Hessian if we have previous geometry and gradient
896
+ if pre_geom is not None and pre_B_g is not None and not coords_reset:
897
+ # Get previous hybrid transform and gradient
898
+ prev_B = self.build_B_matrix(pre_geom)
899
+ prev_hybrid_gradient = self.transform_hybrid_gradient(pre_B_g)
900
+
901
+ # Calculate displacement in hybrid coordinates
902
+ cart_displacement = (geom_num_list - pre_geom).reshape(-1, 1)
903
+ hybrid_displacement = np.dot(hybrid_transform, cart_displacement)
904
+
905
+ # Delta gradient in hybrid coordinates
906
+ delta_grad = (hybrid_gradient - prev_hybrid_gradient).reshape(-1, 1)
907
+
908
+ # Update Hessian using the appropriate update method
909
+ if "msp" in self.config.get("method", "").lower():
910
+ print("Hybrid RFO: Using MSP Hessian update")
911
+ delta_hess = self.hess_update.MSP_hessian_update(
912
+ self.hybrid_hessian, hybrid_displacement, delta_grad
913
+ )
914
+ elif "bfgs" in self.config.get("method", "").lower():
915
+ print("Hybrid RFO: Using BFGS Hessian update")
916
+ delta_hess = self.hess_update.BFGS_hessian_update(
917
+ self.hybrid_hessian, hybrid_displacement, delta_grad
918
+ )
919
+ elif "fsb" in self.config.get("method", "").lower():
920
+ print("Hybrid RFO: Using FSB Hessian update")
921
+ delta_hess = self.hess_update.FSB_hessian_update(
922
+ self.hybrid_hessian, hybrid_displacement, delta_grad
923
+ )
924
+ elif "bofill" in self.config.get("method", "").lower():
925
+ print("Hybrid RFO: Using Bofill Hessian update")
926
+ delta_hess = self.hess_update.Bofill_hessian_update(
927
+ self.hybrid_hessian, hybrid_displacement, delta_grad
928
+ )
929
+ elif "sr1" in self.config.get("method", "").lower():
930
+ print("Hybrid RFO: Using SR1 Hessian update")
931
+ delta_hess = self.hess_update.SR1_hessian_update(
932
+ self.hybrid_hessian, hybrid_displacement, delta_grad
933
+ )
934
+ elif "psb" in self.config.get("method", "").lower():
935
+ print("Hybrid RFO: Using PSB Hessian update")
936
+ delta_hess = self.hess_update.PSB_hessian_update(
937
+ self.hybrid_hessian, hybrid_displacement, delta_grad
938
+ )
939
+ elif "flowchart" in self.config.get("method", "").lower():
940
+ print("Hybrid RFO: Using flowchart Hessian update")
941
+ delta_hess = self.hess_update.flowchart_hessian_update(
942
+ self.hybrid_hessian, hybrid_displacement, delta_grad, self.config["method"]
943
+ )
944
+ else:
945
+ # Default to BFGS if no method is specified
946
+ print("Hybrid RFO: Using BFGS Hessian update (default)")
947
+ delta_hess = self.hess_update.BFGS_hessian_update(
948
+ self.hybrid_hessian, hybrid_displacement, delta_grad
949
+ )
950
+
951
+ # Apply the Hessian update
952
+ self.hybrid_hessian += delta_hess
953
+
954
+ # Ensure the Hessian is symmetric
955
+ self.hybrid_hessian = 0.5 * (self.hybrid_hessian + self.hybrid_hessian.T)
956
+
957
+ # Apply bias Hessian if provided
958
+ working_hessian = self.hybrid_hessian.copy()
959
+ if self.bias_hessian is not None:
960
+ hybrid_bias_hessian = self.transform_hybrid_hessian(self.bias_hessian)
961
+ working_hessian += hybrid_bias_hessian
962
+
963
+ # Calculate RFO step in hybrid coordinates
964
+ hybrid_step = self.run_hybrid_rfo_step(hybrid_gradient, working_hessian)
965
+
966
+ # Transform step back to Cartesian coordinates
967
+ cart_step = self.hybrid_to_cartesian(hybrid_step, geom_num_list)
968
+
969
+ # Update weights for next iteration based on this step
970
+ self.update_weights(geom_num_list, B_g, cart_step)
971
+
972
+ # Store current state for next iteration
973
+ self.prev_cartesian = geom_num_list.copy()
974
+ self.prev_gradient = B_g.copy()
975
+
976
+ # Increment iteration counter
977
+ self.iter += 1
978
+
979
+ # Apply DELTA scaling and reshape
980
+ return -1 * self.DELTA * cart_step.reshape(-1, 1)
981
+
982
+ def set_hessian(self, hessian):
983
+ """Set Cartesian Hessian"""
984
+ self.hessian = hessian.copy()
985
+ if self.B_matrix is not None:
986
+ # Transform to internal coordinates
987
+ self.internal_hessian = self.transform_hybrid_hessian(hessian)
988
+
989
+ def set_bias_hessian(self, bias_hessian):
990
+ """Set bias Hessian (in Cartesian coordinates)"""
991
+ self.bias_hessian = bias_hessian.copy()
992
+
993
+ def get_hessian(self):
994
+ """Return current Hessian (in Cartesian coordinates)"""
995
+ return self.hessian if self.hessian is not None else None
996
+
997
+ def get_bias_hessian(self):
998
+ return self.bias_hessian