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,203 @@
1
+ import numpy as np
2
+ from .gdiis import GDIIS
3
+ from .ediis import EDIIS
4
+
5
+ class GEDIIS:
6
+ """
7
+ Combined GDIIS and EDIIS optimization method.
8
+
9
+ This class implements a combined gradient- and energy-based DIIS approach
10
+ that adaptively blends the two methods based on their performance.
11
+ """
12
+
13
+ def __init__(self):
14
+ # Create individual optimizers
15
+ self.gdiis = GDIIS()
16
+ self.ediis = EDIIS()
17
+
18
+ # Configuration parameters
19
+ self.gediis_mode = 'adaptive' # 'adaptive', 'sequential', or 'blend'
20
+ self.gediis_ediis_weight = 0.6 # Initial EDIIS weight in blend mode
21
+ self.gediis_auto_switch = True # Automatically switch based on performance
22
+
23
+ # Tracking variables
24
+ self.energy_history = []
25
+ self.grad_rms_history = []
26
+ self.gdiis_success_count = 0
27
+ self.ediis_success_count = 0
28
+ self.iter = 0
29
+
30
+ # For sequential mode
31
+ self.gediis_phase = 'ediis' # Start with EDIIS ('ediis' or 'gdiis')
32
+ self.gediis_phase_steps = 0
33
+ self.gediis_phase_switch = 3 # Switch phases every N steps
34
+ return
35
+
36
+ def _evaluate_performance(self):
37
+ """
38
+ Evaluate the relative performance of GDIIS and EDIIS
39
+
40
+ Returns:
41
+ --------
42
+ tuple
43
+ (ediis_weight, gdiis_weight) - Relative weights for the two methods
44
+ """
45
+ # With limited history, favor EDIIS early and GDIIS later
46
+ if len(self.energy_history) < 3:
47
+ if self.iter < 10:
48
+ # Early iterations: favor EDIIS (better for large steps)
49
+ return 0.7, 0.3
50
+ else:
51
+ # Later iterations: favor GDIIS (better for fine convergence)
52
+ return 0.3, 0.7
53
+
54
+ # Look at recent energy and gradient trends
55
+ energy_decreasing = self.energy_history[-1] < self.energy_history[-2]
56
+ grad_decreasing = self.grad_rms_history[-1] < self.grad_rms_history[-2]
57
+
58
+ # Adjust success counters
59
+ if energy_decreasing:
60
+ self.ediis_success_count += 1
61
+ else:
62
+ self.ediis_success_count = max(0, self.ediis_success_count - 1)
63
+
64
+ if grad_decreasing:
65
+ self.gdiis_success_count += 1
66
+ else:
67
+ self.gdiis_success_count = max(0, self.gdiis_success_count - 1)
68
+
69
+ # Calculate weights based on success counts and iteration phase
70
+ total_success = self.ediis_success_count + self.gdiis_success_count + 1 # +1 to avoid division by zero
71
+ ediis_raw_weight = self.ediis_success_count / total_success
72
+
73
+ # Adjust based on optimization phase (EDIIS early, GDIIS late)
74
+ phase_factor = max(0.0, min(1.0, (20 - self.iter) / 20)) # 1.0→0.0 over 20 iterations
75
+ ediis_weight = 0.3 + ediis_raw_weight * 0.4 + phase_factor * 0.3
76
+
77
+ # Ensure weights are in reasonable range
78
+ ediis_weight = max(0.2, min(0.8, ediis_weight))
79
+ gdiis_weight = 1.0 - ediis_weight
80
+
81
+ return ediis_weight, gdiis_weight
82
+
83
+ def run(self, geom_num_list, energy, B_g, pre_B_g, original_move_vector):
84
+ """
85
+ Run combined GEDIIS optimization
86
+
87
+ Parameters:
88
+ -----------
89
+ geom_num_list : numpy.ndarray
90
+ Current geometry
91
+ energy : float
92
+ Current energy value
93
+ B_g : numpy.ndarray
94
+ Current gradient
95
+ pre_B_g : numpy.ndarray
96
+ Previous gradient
97
+ original_move_vector : numpy.ndarray
98
+ Original optimization step
99
+
100
+ Returns:
101
+ --------
102
+ numpy.ndarray
103
+ Optimized step vector
104
+ """
105
+ print("GEDIIS method (combined GDIIS and EDIIS)")
106
+ n_coords = len(geom_num_list)
107
+ grad_rms = np.sqrt(np.mean(B_g ** 2))
108
+
109
+ # Update history
110
+ self.energy_history.append(energy)
111
+ self.grad_rms_history.append(grad_rms)
112
+
113
+ # Always run both methods to update their histories
114
+ gdiis_step = self.gdiis.run(geom_num_list, B_g, pre_B_g, original_move_vector)
115
+ ediis_step = self.ediis.run(geom_num_list, energy, B_g, original_move_vector)
116
+
117
+ # Determine mode of operation
118
+ if self.gediis_mode == 'sequential':
119
+ # Sequential mode switches between methods periodically
120
+ self.gediis_phase_steps += 1
121
+ if self.gediis_phase_steps >= self.gediis_phase_switch:
122
+ self.gediis_phase = 'ediis' if self.gediis_phase == 'gdiis' else 'gdiis'
123
+ self.gediis_phase_steps = 0
124
+ print(f"Switching to {self.gediis_phase.upper()} phase")
125
+
126
+ if self.gediis_phase == 'ediis':
127
+ print("Using EDIIS step (sequential mode)")
128
+ step = ediis_step
129
+ else:
130
+ print("Using GDIIS step (sequential mode)")
131
+ step = gdiis_step
132
+
133
+ elif self.gediis_mode == 'adaptive':
134
+ # Adaptive mode chooses the best method based on performance
135
+ if self.gediis_auto_switch and len(self.energy_history) > 1:
136
+ ediis_weight, gdiis_weight = self._evaluate_performance()
137
+
138
+ # Blend with emphasis on better-performing method
139
+ if ediis_weight > 0.7:
140
+ print(f"Using EDIIS step (adaptive mode, {ediis_weight:.2f})")
141
+ step = ediis_step
142
+ elif gdiis_weight > 0.7:
143
+ print(f"Using GDIIS step (adaptive mode, {gdiis_weight:.2f})")
144
+ step = gdiis_step
145
+ else:
146
+ # Blend the steps
147
+ step = ediis_weight * ediis_step + gdiis_weight * gdiis_step
148
+ print(f"Using blended step: {ediis_weight:.2f}*EDIIS + {gdiis_weight:.2f}*GDIIS")
149
+ else:
150
+ # Default behavior early in optimization
151
+ if self.iter < 5:
152
+ print("Using EDIIS step (early iterations)")
153
+ step = ediis_step
154
+ else:
155
+ print("Using GDIIS step (later iterations)")
156
+ step = gdiis_step
157
+
158
+ else: # blend mode
159
+ # Blend mode always combines both methods with fixed weights
160
+ ediis_weight = self.gediis_ediis_weight
161
+ gdiis_weight = 1.0 - ediis_weight
162
+ step = ediis_weight * ediis_step + gdiis_weight * gdiis_step
163
+ print(f"Using blended step: {ediis_weight:.2f}*EDIIS + {gdiis_weight:.2f}*GDIIS")
164
+
165
+ # Final safety checks
166
+ step_norm = np.linalg.norm(step)
167
+ orig_norm = np.linalg.norm(original_move_vector)
168
+
169
+ # Check for problematic steps
170
+ if np.any(np.isnan(step)) or np.any(np.isinf(step)) or step_norm < 1e-10:
171
+ print("Warning: Invalid GEDIIS step, falling back to original step")
172
+ step = original_move_vector
173
+ elif step_norm > 3.0 * orig_norm and orig_norm > 1e-10:
174
+ # Cap step size to avoid large jumps
175
+ print(f"Warning: GEDIIS step too large ({step_norm:.4f} > {3.0 * orig_norm:.4f}), scaling down")
176
+ scale_factor = 3.0 * orig_norm / step_norm
177
+ step = scale_factor * step
178
+
179
+ # Monitor convergence behavior
180
+ if len(self.energy_history) > 3:
181
+ # Check if optimization is oscillating
182
+ oscillating = False
183
+ energy_diffs = np.diff(self.energy_history[-4:])
184
+ if np.all(np.abs(energy_diffs) > 0): # All non-zero differences
185
+ signs = np.sign(energy_diffs)
186
+ if np.sum(np.abs(np.diff(signs))) >= 2: # Sign changes indicate oscillation
187
+ oscillating = True
188
+
189
+ if oscillating:
190
+ print("Warning: Possible oscillatory behavior detected")
191
+ # Switch mode or adjust parameters to stabilize
192
+ if self.gediis_mode == 'adaptive':
193
+ # Increase EDIIS weight for stability
194
+ ediis_weight = max(0.7, ediis_weight)
195
+ gdiis_weight = 1.0 - ediis_weight
196
+ # Recalculate step with higher EDIIS weight
197
+ step = ediis_weight * ediis_step + gdiis_weight * gdiis_step
198
+ print(f"Stabilizing oscillation: {ediis_weight:.2f}*EDIIS + {gdiis_weight:.2f}*GDIIS")
199
+
200
+ # Update iteration counter
201
+ self.iter += 1
202
+
203
+ return step
@@ -0,0 +1,433 @@
1
+ import numpy as np
2
+ from scipy.integrate import solve_ivp
3
+ import logging
4
+
5
+ from multioptpy.Parameters.parameter import covalent_radii_lib
6
+
7
+ class GeodesicStepper:
8
+ """
9
+ Geodesic step calculation for geometry optimization based on
10
+ J. Chem. Phys. 155, 094105 (2021) "Geometry optimization speedup through a geodesic coordinate system"
11
+
12
+ This class implements geodesic-based step calculation to improve optimization convergence
13
+ by following the intrinsic curved geometry of the internal coordinate space.
14
+ """
15
+ def __init__(self, element_list):
16
+ """
17
+ Initialize the GeodesicStep optimizer
18
+
19
+ Parameters:
20
+ -----------
21
+ element_list : list
22
+ List of element symbols as strings
23
+ """
24
+ self.element_list = element_list
25
+ self.natoms = len(element_list)
26
+ self.ndim = 3 * self.natoms
27
+ self.logger = logging.getLogger(__name__)
28
+ self.bond_scale = 1.5 # Scaling factor for covalent radii
29
+
30
+ def determine_bonds(self):
31
+ """
32
+ Determine bonded atoms based on covalent radii
33
+ Only considers bond lengths as internal coordinates
34
+
35
+ Returns:
36
+ --------
37
+ list
38
+ List of (i,j) tuples representing bonded atoms
39
+ """
40
+
41
+
42
+ # Get covalent radii for all atoms
43
+ radii = [covalent_radii_lib(elem) for elem in self.element_list]
44
+
45
+ bonds = []
46
+ for i in range(self.natoms):
47
+ for j in range(i+1, self.natoms):
48
+ # Calculate distance threshold based on covalent radii with scaling
49
+ threshold = (radii[i] + radii[j]) * self.bond_scale
50
+ # Store bond if it exists
51
+ bonds.append((i, j, threshold))
52
+
53
+ return bonds
54
+
55
+ def calculate_internal_coordinates(self, geometry):
56
+ """
57
+ Calculate internal coordinates (bond lengths) from Cartesian coordinates
58
+
59
+ Parameters:
60
+ -----------
61
+ geometry : numpy.ndarray
62
+ Cartesian coordinates (natoms×3)
63
+
64
+ Returns:
65
+ --------
66
+ tuple
67
+ (bond_values, bond_pairs, bond_thresholds)
68
+ """
69
+ # Get bonds
70
+ bonds = self.determine_bonds()
71
+
72
+ # Calculate bond lengths and filter by threshold
73
+ bond_values = []
74
+ bond_pairs = []
75
+ bond_thresholds = []
76
+
77
+ for i, j, threshold in bonds:
78
+ r_ij = np.linalg.norm(geometry[i] - geometry[j])
79
+ if r_ij < threshold:
80
+ bond_values.append(r_ij)
81
+ bond_pairs.append((i, j))
82
+ bond_thresholds.append(threshold)
83
+
84
+ return np.array(bond_values), bond_pairs, bond_thresholds
85
+
86
+ def calculate_b_matrix(self, geometry, bond_pairs):
87
+ """
88
+ Calculate Wilson's B-matrix (derivatives of internal coordinates w.r.t. Cartesian)
89
+
90
+ Parameters:
91
+ -----------
92
+ geometry : numpy.ndarray
93
+ Cartesian coordinates (natoms×3)
94
+ bond_pairs : list
95
+ List of (i,j) tuples representing bonded atoms
96
+
97
+ Returns:
98
+ --------
99
+ numpy.ndarray
100
+ B matrix
101
+ """
102
+ ncoords = len(bond_pairs)
103
+ ndim = self.ndim
104
+ B = np.zeros((ncoords, ndim))
105
+
106
+ # Calculate B matrix elements
107
+ for idx, (i, j) in enumerate(bond_pairs):
108
+ # Vector pointing from atom j to atom i
109
+ rij = geometry[i] - geometry[j]
110
+ # Bond length
111
+ r = np.linalg.norm(rij)
112
+ # Unit vector
113
+ if r > 1e-10: # Avoid division by zero
114
+ unit_vec = rij / r
115
+ else:
116
+ unit_vec = np.zeros(3)
117
+
118
+ # Fill B matrix elements
119
+ B[idx, 3*i:3*i+3] = unit_vec
120
+ B[idx, 3*j:3*j+3] = -unit_vec
121
+
122
+ return B
123
+
124
+ def calculate_b_derivatives(self, geometry, bond_pairs):
125
+ """
126
+ Calculate derivatives of the B-matrix w.r.t. Cartesian coordinates
127
+ Using vectorized operations
128
+
129
+ Parameters:
130
+ -----------
131
+ geometry : numpy.ndarray
132
+ Cartesian coordinates (natoms×3)
133
+ bond_pairs : list
134
+ List of (i,j) tuples representing bonded atoms
135
+
136
+ Returns:
137
+ --------
138
+ numpy.ndarray
139
+ Derivatives of B matrix
140
+ """
141
+ ncoords = len(bond_pairs)
142
+ ndim = self.ndim
143
+ dB = np.zeros((ncoords, ndim, ndim))
144
+
145
+ coords = geometry.reshape(-1, 3)
146
+
147
+ # Vectorized calculation for all bonds
148
+ for idx, (i, j) in enumerate(bond_pairs):
149
+ # Vector pointing from atom j to atom i
150
+ rij = coords[i] - coords[j]
151
+ r = np.linalg.norm(rij)
152
+
153
+ if r < 1e-10: # Avoid numerical issues
154
+ continue
155
+
156
+ # Precompute terms
157
+ r3 = r**3
158
+
159
+ # Create unit matrices for i and j blocks
160
+ eye3 = np.eye(3)
161
+ outer_prod = np.outer(rij, rij) / r3
162
+
163
+ # Term for each 3x3 block is: δ_ab/r - r_a*r_b/r^3
164
+ block = eye3 / r - outer_prod
165
+
166
+ # Assign to the 4 blocks efficiently
167
+ ii_idx = slice(3*i, 3*i+3)
168
+ jj_idx = slice(3*j, 3*j+3)
169
+
170
+ dB[idx, ii_idx, ii_idx] = block
171
+ dB[idx, jj_idx, jj_idx] = block
172
+ dB[idx, ii_idx, jj_idx] = -block
173
+ dB[idx, jj_idx, ii_idx] = -block
174
+
175
+ return dB
176
+
177
+ def calculate_metric_tensor(self, B):
178
+ """
179
+ Calculate metric tensor G = B·Bᵀ
180
+
181
+ Parameters:
182
+ -----------
183
+ B : numpy.ndarray
184
+ Wilson's B matrix
185
+
186
+ Returns:
187
+ --------
188
+ numpy.ndarray
189
+ Metric tensor G
190
+ """
191
+ G = np.dot(B, B.T)
192
+ return G
193
+
194
+ def calculate_christoffel_symbols(self, B, dB, G_inv):
195
+ """
196
+ Calculate Christoffel symbols of the second kind using vectorized operations
197
+
198
+ Parameters:
199
+ -----------
200
+ B : numpy.ndarray
201
+ Wilson's B matrix
202
+ dB : numpy.ndarray
203
+ Derivatives of B matrix
204
+ G_inv : numpy.ndarray
205
+ Inverse of G matrix
206
+
207
+ Returns:
208
+ --------
209
+ gamma : numpy.ndarray
210
+ Christoffel symbols
211
+ """
212
+ ncoords = B.shape[0]
213
+
214
+ # Initialize Christoffel symbols tensor
215
+ gamma = np.zeros((ncoords, ncoords, ncoords))
216
+
217
+ # Step 1: Calculate partial_j B_ia for all combinations
218
+ partial_j_B_i_a = np.zeros((ncoords, ncoords, self.ndim))
219
+ for i in range(ncoords):
220
+ for j in range(ncoords):
221
+ for a in range(self.ndim):
222
+ partial_j_B_i_a[i, j, a] = np.sum(dB[i, a, :] * B[j, :])
223
+
224
+ # Step 2: Sum over dimension a and contract with G_inv
225
+ for i in range(ncoords):
226
+ for j in range(ncoords):
227
+ for k in range(ncoords):
228
+ gamma[i, j, k] = G_inv[i, k] * np.sum(partial_j_B_i_a[i, j, :])
229
+
230
+ # Step 3: Symmetrize over j,k indices
231
+ for i in range(ncoords):
232
+ for j in range(ncoords):
233
+ for k in range(j+1, ncoords):
234
+ avg = (gamma[i, j, k] + gamma[i, k, j]) / 2
235
+ gamma[i, j, k] = avg
236
+ gamma[i, k, j] = avg
237
+
238
+ return gamma
239
+
240
+ def geodesic_equation(self, t, y, gamma, ncoords):
241
+ """
242
+ Vectorized implementation of the geodesic equation ODE function
243
+
244
+ Parameters:
245
+ -----------
246
+ t : float
247
+ Integration parameter (0 to 1)
248
+ y : numpy.ndarray
249
+ State vector [q, q_dot], where q are internal coordinates
250
+ gamma : numpy.ndarray
251
+ Christoffel symbols
252
+ ncoords : int
253
+ Number of internal coordinates
254
+
255
+ Returns:
256
+ --------
257
+ dydt : numpy.ndarray
258
+ Derivative of state vector
259
+ """
260
+ # Extract q and q_dot from state vector
261
+ q = y[:ncoords]
262
+ q_dot = y[ncoords:]
263
+
264
+ # Calculate acceleration using vectorized operations
265
+ # Compute the contraction of Christoffel symbols with velocities
266
+ q_ddot = np.zeros(ncoords)
267
+ for i in range(ncoords):
268
+ for j in range(ncoords):
269
+ for k in range(ncoords):
270
+ q_ddot[i] -= gamma[i, j, k] * q_dot[j] * q_dot[k]
271
+
272
+ # Return combined derivative [q_dot, q_ddot]
273
+ return np.concatenate([q_dot, q_ddot])
274
+
275
+ def parallel_transport(self, v0, B0, G0_inv, B1, G1_inv):
276
+ """
277
+ Parallel transport a vector from one point to another
278
+
279
+ Parameters:
280
+ -----------
281
+ v0 : numpy.ndarray
282
+ Vector in Cartesian coordinates at the initial point
283
+ B0, B1 : numpy.ndarray
284
+ B matrices at initial and final points
285
+ G0_inv, G1_inv : numpy.ndarray
286
+ Inverse metric tensors at initial and final points
287
+
288
+ Returns:
289
+ --------
290
+ numpy.ndarray
291
+ Transported vector in Cartesian coordinates
292
+ """
293
+ # Transform to internal coordinates
294
+ v0_int = np.dot(B0, v0)
295
+
296
+ # Parallel transport in internal coordinates (simplified)
297
+ # In this implementation, we use a direct transformation approach
298
+ # For more accuracy, one should solve the parallel transport equations
299
+
300
+ # Transform back to Cartesian coordinates
301
+ v1 = np.dot(B1.T, np.dot(G1_inv, np.dot(G0_inv, v0_int)))
302
+
303
+ return v1
304
+
305
+ def solve_geodesic(self, q0, v0, gamma, ncoords):
306
+ """
307
+ Solve the geodesic equation using LSODA method
308
+
309
+ Parameters:
310
+ -----------
311
+ q0 : numpy.ndarray
312
+ Initial internal coordinates
313
+ v0 : numpy.ndarray
314
+ Initial velocity in internal coordinates
315
+ gamma : numpy.ndarray
316
+ Christoffel symbols
317
+ ncoords : int
318
+ Number of internal coordinates
319
+
320
+ Returns:
321
+ --------
322
+ numpy.ndarray
323
+ Final internal coordinates after following the geodesic
324
+ """
325
+ # Initial state [q0, v0]
326
+ y0 = np.concatenate([q0, v0])
327
+
328
+ # ODE solver with LSODA method
329
+ solution = solve_ivp(
330
+ lambda t, y: self.geodesic_equation(t, y, gamma, ncoords),
331
+ [0, 1], # Integration from t=0 to t=1
332
+ y0,
333
+ method='LSODA',
334
+ rtol=1e-6,
335
+ atol=1e-8
336
+ )
337
+
338
+ # Return final coordinates
339
+ q_final = solution.y[:ncoords, -1]
340
+ return q_final
341
+
342
+ def transform_to_cartesian(self, geometry, q_final, B, bond_pairs):
343
+ """
344
+ Transform internal coordinate step to Cartesian coordinates
345
+
346
+ Parameters:
347
+ -----------
348
+ geometry : numpy.ndarray
349
+ Current Cartesian geometry
350
+ q_final : numpy.ndarray
351
+ Final internal coordinates
352
+ B : numpy.ndarray
353
+ Wilson's B matrix
354
+ bond_pairs : list
355
+ List of (i,j) tuples for bonds
356
+
357
+ Returns:
358
+ --------
359
+ numpy.ndarray
360
+ Step vector in Cartesian coordinates
361
+ """
362
+ # Calculate current internal coordinates
363
+ q_current = np.array([np.linalg.norm(geometry[i] - geometry[j])
364
+ for i, j in bond_pairs])
365
+
366
+ # Calculate change in internal coordinates
367
+ dq = q_final - q_current
368
+
369
+ # Transform to Cartesian step using the pseudo-inverse of B
370
+ B_pinv = np.linalg.pinv(B)
371
+ cartesian_step = np.dot(B_pinv, dq)
372
+
373
+ return cartesian_step
374
+
375
+ def run(self, geometry, original_move_vector):
376
+ """
377
+ Run geodesic step calculation
378
+
379
+ Parameters:
380
+ -----------
381
+ geometry : numpy.ndarray
382
+ Current geometry in Cartesian coordinates
383
+ original_move_vector : numpy.ndarray
384
+ Original step vector from optimizer
385
+
386
+ Returns:
387
+ --------
388
+ numpy.ndarray
389
+ Modified step vector following geodesic path
390
+ """
391
+ # Reshape inputs if needed
392
+ geom = geometry.reshape(self.natoms, 3)
393
+ move_vec = original_move_vector.reshape(-1)
394
+
395
+ # Calculate internal coordinates
396
+ q, bond_pairs, _ = self.calculate_internal_coordinates(geom)
397
+ ncoords = len(q)
398
+
399
+ if ncoords == 0:
400
+ self.logger.warning("No internal coordinates found. Using original step.")
401
+ return original_move_vector
402
+
403
+ # Calculate Wilson's B-matrix
404
+ B = self.calculate_b_matrix(geom, bond_pairs)
405
+
406
+ # Calculate metric tensor and its inverse
407
+ G = self.calculate_metric_tensor(B)
408
+
409
+ try:
410
+ G_inv = np.linalg.inv(G)
411
+ except np.linalg.LinAlgError:
412
+ self.logger.warning("Singular G matrix. Using pseudo-inverse instead.")
413
+ G_inv = np.linalg.pinv(G)
414
+
415
+ # Calculate derivatives of B-matrix
416
+ dB = self.calculate_b_derivatives(geom, bond_pairs)
417
+
418
+ # Calculate Christoffel symbols
419
+ gamma = self.calculate_christoffel_symbols(B, dB, G_inv)
420
+
421
+ # Transform original move vector to internal coordinates
422
+ v0_int = np.dot(B, move_vec)
423
+
424
+ # Solve geodesic equation
425
+ q_final = self.solve_geodesic(q, v0_int, gamma, ncoords)
426
+
427
+ # Transform back to Cartesian coordinates
428
+ geodesic_step = self.transform_to_cartesian(geom, q_final, B, bond_pairs)
429
+ print(f"Geodesic step norm: {np.linalg.norm(geodesic_step):<.6f}")
430
+ print(f"Original step norm: {np.linalg.norm(move_vec):<.6f}")
431
+ print(f"Ratio (geodesic/original): {np.linalg.norm(geodesic_step)/np.linalg.norm(move_vec):<.6f}")
432
+ # Reshape to match input format
433
+ return geodesic_step.reshape(original_move_vector.shape)