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,345 @@
1
+ import numpy as np
2
+ from numpy.linalg import norm
3
+
4
+ from multioptpy.Parameters.parameter import UnitValueLib
5
+
6
+ class ConjugateGradientNEB:
7
+ """Nonlinear Conjugate Gradient optimizer for Nudged Elastic Band calculations.
8
+
9
+ This implementation provides multiple conjugate gradient update formulas:
10
+ - Fletcher-Reeves (FR): Numerically stable but slower convergence
11
+ - Polak-Ribière (PR): Faster convergence but can be unstable without safeguards
12
+ - Hestenes-Stiefel (HS): Often performs better than FR and PR for nonlinear problems
13
+ - Dai-Yuan (DY): Good global convergence properties and descent direction guarantee
14
+ - Hager-Zhang (HZ): Modern, efficient update with strong theoretical properties
15
+
16
+ All methods include automatic restart strategies for enhanced stability and performance.
17
+ """
18
+
19
+ def __init__(self, **config):
20
+ # Configuration parameters
21
+ self.config = config
22
+
23
+ # Initialize flags
24
+ self.Initialization = True
25
+ self.iter = 0
26
+
27
+ # Set default parameters
28
+ self.maxstep = config.get("maxstep", 0.1) # Maximum step size
29
+
30
+ # Line search parameters
31
+ self.initial_step_size = config.get("initial_step_size", 0.1) # Initial step size for line search
32
+ self.min_step_size = config.get("min_step_size", 0.01) # Minimum step size
33
+ self.max_step_size = config.get("max_step_size", 0.1) # Maximum step size
34
+ self.step_descent_factor = config.get("step_descent_factor", 0.5) # Step size reduction factor
35
+
36
+ # CG parameters
37
+ # Valid options: "FR", "PR", "HS", "DY", "HZ"
38
+ self.cg_method = config.get("cg_method", "HS") # Conjugate gradient method
39
+ self.restart_cycles = config.get("restart_cycles", 10) # Restart CG every n iterations
40
+ self.restart_threshold = config.get("restart_threshold", 0.2) # Restart if orthogonality drops below this
41
+
42
+ # Storage for previous step data
43
+ self.prev_forces = None # Previous forces for all nodes
44
+ self.prev_directions = None # Previous CG directions for all nodes
45
+ self.prev_positions = None # Previous positions for all nodes
46
+ self.prev_energies = None # Previous energies for all nodes
47
+
48
+ # Node-specific step sizes (adaptive)
49
+ self.node_step_sizes = None
50
+
51
+ # Unit conversion
52
+ self.bohr2angstroms = UnitValueLib().bohr2angstroms
53
+ if 'bohr2angstroms' in config:
54
+ self.bohr2angstroms = config['bohr2angstroms']
55
+
56
+ # Compatibility with the original code
57
+ self.TR_NEB = config.get("TR_NEB", None)
58
+
59
+ # Print initialization message based on selected method
60
+ print(f"Initialized Conjugate Gradient NEB optimizer with method={self.cg_method}, maxstep={self.maxstep}")
61
+ if self.cg_method == "FR":
62
+ print("Using Fletcher-Reeves method with periodic restart")
63
+ elif self.cg_method == "PR":
64
+ print("Using Polak-Ribière method with automatic restart")
65
+ elif self.cg_method == "HS":
66
+ print("Using Hestenes-Stiefel method with automatic restart")
67
+ elif self.cg_method == "DY":
68
+ print("Using Dai-Yuan method with strong convergence properties")
69
+ elif self.cg_method == "HZ":
70
+ print("Using Hager-Zhang method with enhanced numerical stability")
71
+ else:
72
+ print(f"Warning: Unknown CG method '{self.cg_method}', falling back to Fletcher-Reeves")
73
+ self.cg_method = "FR"
74
+
75
+ def initialize_data(self, num_nodes):
76
+ """Initialize data structures for the optimization.
77
+
78
+ Parameters:
79
+ ----------
80
+ num_nodes : int
81
+ Number of nodes in the NEB calculation
82
+ """
83
+ # Initialize storage for previous data
84
+ self.prev_forces = [None] * num_nodes
85
+ self.prev_directions = [None] * num_nodes
86
+ self.prev_positions = [None] * num_nodes
87
+ self.prev_energies = [None] * num_nodes
88
+
89
+ # Initialize node-specific step sizes
90
+ self.node_step_sizes = np.ones(num_nodes) * self.initial_step_size
91
+
92
+ print(f"Initialized CG storage for {num_nodes} nodes")
93
+
94
+ def compute_cg_direction(self, force, node_idx):
95
+ """Compute the conjugate gradient direction for an node.
96
+
97
+ Parameters:
98
+ ----------
99
+ force : ndarray
100
+ Current force vector for the node
101
+ node_idx : int
102
+ Index of the node in the NEB chain
103
+
104
+ Returns:
105
+ -------
106
+ ndarray
107
+ Conjugate gradient direction
108
+ """
109
+ # If first iteration or no history, return the force direction (steepest descent)
110
+ if self.prev_forces[node_idx] is None:
111
+ return force.copy()
112
+
113
+ # Get previous force and direction
114
+ prev_force = self.prev_forces[node_idx]
115
+ prev_direction = self.prev_directions[node_idx]
116
+
117
+ # Compute change in force (negative gradient)
118
+ force_change = force - prev_force
119
+
120
+ # Flatten vectors for easier dot product calculations
121
+ g = force.flatten()
122
+ g_prev = prev_force.flatten()
123
+ d_prev = prev_direction.flatten()
124
+ y = force_change.flatten()
125
+
126
+ # Compute beta coefficient according to selected method
127
+ beta = 0.0 # Default value
128
+
129
+ # Compute dot products that will be reused
130
+ g_dot_g = np.dot(g, g)
131
+ g_prev_dot_g_prev = np.dot(g_prev, g_prev)
132
+ y_dot_d_prev = np.dot(y, d_prev)
133
+ g_dot_y = np.dot(g, y)
134
+
135
+ # Small constant to avoid division by zero
136
+ epsilon = 1e-10
137
+
138
+ if self.cg_method == "FR": # Fletcher-Reeves
139
+ beta = g_dot_g / max(epsilon, g_prev_dot_g_prev)
140
+
141
+ elif self.cg_method == "PR": # Polak-Ribière
142
+ beta = g_dot_y / max(epsilon, g_prev_dot_g_prev)
143
+ # Apply Polak-Ribière+ modification: max(beta, 0)
144
+ beta = max(0.0, beta)
145
+
146
+ elif self.cg_method == "HS": # Hestenes-Stiefel
147
+ beta = g_dot_y / max(epsilon, y_dot_d_prev)
148
+ # Apply HS+ modification for guaranteed descent direction
149
+ beta = max(0.0, beta)
150
+
151
+ elif self.cg_method == "DY": # Dai-Yuan
152
+ beta = g_dot_g / max(epsilon, y_dot_d_prev)
153
+ # DY method always yields descent directions
154
+
155
+ elif self.cg_method == "HZ": # Hager-Zhang
156
+ y_dot_y = np.dot(y, y)
157
+ g_prev_dot_y = np.dot(g_prev, y)
158
+
159
+ # Hager-Zhang formula
160
+ beta = (g_dot_y - 2 * np.dot(g, y) * np.dot(y, d_prev) / max(epsilon, y_dot_y)) / max(epsilon, y_dot_d_prev)
161
+
162
+ # Apply HZ+ modification
163
+ eta = 0.4 # Parameter from the HZ paper
164
+ beta = max(-eta * g_prev_dot_g_prev / max(epsilon, np.dot(d_prev, d_prev)), beta)
165
+
166
+ # Check orthogonality for restart
167
+ orthogonality = np.abs(np.dot(g, g_prev)) / (norm(g) * norm(g_prev) + epsilon)
168
+
169
+ # Reset beta to zero (restart CG) if:
170
+ # 1. We're at a restart cycle iteration
171
+ # 2. Orthogonality is high (gradient directions are too similar)
172
+ # 3. Beta is negative (not an issue with PR+, HS+, HZ+, but added as safety)
173
+ if (self.iter % self.restart_cycles == 0) or (orthogonality > (1.0 - self.restart_threshold)):
174
+ print(f"Restarting CG for node {node_idx} (orthogonality: {orthogonality:.3f})")
175
+ beta = 0.0
176
+
177
+ # Compute new conjugate direction
178
+ new_direction = force + beta * prev_direction
179
+
180
+ # Ensure we're moving in a descent direction (dot product with force should be positive)
181
+ if np.dot(new_direction.flatten(), g) < 0:
182
+ print(f"Warning: CG direction not a descent direction for node {node_idx}, resetting to steepest descent")
183
+ new_direction = force.copy()
184
+
185
+ return new_direction
186
+
187
+ def adjust_step_sizes(self, energies, prev_energies, forces):
188
+ """Adjust step sizes for each node based on energy changes.
189
+
190
+ Parameters:
191
+ ----------
192
+ energies : list of float
193
+ Current energies for all nodes
194
+ prev_energies : list of float
195
+ Previous energies for all nodes
196
+ forces : list of ndarray
197
+ Current forces for all nodes
198
+ """
199
+ if prev_energies is None or None in prev_energies:
200
+ return
201
+
202
+ for i, (curr_e, prev_e) in enumerate(zip(energies, prev_energies)):
203
+ if prev_e is None:
204
+ continue
205
+
206
+ # Calculate energy change
207
+ energy_change = prev_e - curr_e
208
+ force_magnitude = norm(forces[i].flatten())
209
+
210
+ # Adjust step size based on energy change
211
+ if energy_change > 0: # Energy decreased (good)
212
+ # Increase step size, but more conservatively for larger forces
213
+ increase_factor = 1.2 * np.exp(-0.1 * force_magnitude)
214
+ self.node_step_sizes[i] = min(
215
+ self.node_step_sizes[i] * (1.0 + increase_factor),
216
+ self.max_step_size
217
+ )
218
+ print(f"node {i}: Energy decreased by {energy_change:.6e}, increasing step size to {self.node_step_sizes[i]:.4f}")
219
+ else: # Energy increased (bad)
220
+ # Decrease step size more aggressively for larger energy increases
221
+ decrease_factor = self.step_descent_factor * (1.0 + 0.5 * abs(energy_change))
222
+ self.node_step_sizes[i] = max(
223
+ self.node_step_sizes[i] * decrease_factor,
224
+ self.min_step_size
225
+ )
226
+ print(f"node {i}: Energy increased by {abs(energy_change):.6e}, decreasing step size to {self.node_step_sizes[i]:.4f}")
227
+
228
+ def determine_step(self, directions, step_sizes):
229
+ """Determine step vectors from directions and step sizes, constrained by maxstep.
230
+
231
+ Parameters:
232
+ ----------
233
+ directions : list of ndarray
234
+ Direction vectors for all nodes
235
+ step_sizes : ndarray
236
+ Step sizes for all nodes
237
+
238
+ Returns:
239
+ -------
240
+ list of ndarray
241
+ Step vectors for all nodes
242
+ """
243
+ steps = []
244
+
245
+ for i, direction in enumerate(directions):
246
+ # Normalize direction
247
+ direction_norm = norm(direction.flatten())
248
+ if direction_norm < 1e-10: # Avoid division by zero
249
+ steps.append(np.zeros_like(direction))
250
+ continue
251
+
252
+ normalized_direction = direction / direction_norm
253
+
254
+ # Apply step size
255
+ step = normalized_direction * step_sizes[i]
256
+
257
+ # Apply maxstep constraint if needed
258
+ step_length = norm(step.flatten())
259
+ if step_length > self.maxstep:
260
+ step = step * (self.maxstep / step_length)
261
+ print(f"node {i}: Step constrained by maxstep={self.maxstep}")
262
+
263
+ steps.append(step)
264
+
265
+ return steps
266
+
267
+ def CG_NEB_calc(self, geometry_num_list, total_force_list, pre_total_velocity,
268
+ optimize_num, total_velocity, cos_list,
269
+ biased_energy_list, pre_biased_energy_list, pre_geom):
270
+ """Calculate step using Conjugate Gradient for NEB.
271
+
272
+ Parameters:
273
+ ----------
274
+ geometry_num_list : ndarray
275
+ Current geometry coordinates for all nodes
276
+ total_force_list : ndarray
277
+ Current forces for all nodes
278
+ pre_total_velocity : ndarray
279
+ Previous velocities for all nodes
280
+ optimize_num : int
281
+ Current optimization iteration number
282
+ total_velocity : ndarray
283
+ Current velocities for all nodes
284
+ cos_list : ndarray
285
+ Cosines between adjacent nodes
286
+ biased_energy_list : ndarray
287
+ Current energy for all nodes
288
+ pre_biased_energy_list : ndarray
289
+ Previous energy for all nodes
290
+ pre_geom : ndarray
291
+ Previous geometry coordinates for all nodes
292
+
293
+ Returns:
294
+ -------
295
+ ndarray
296
+ Updated geometry coordinates for all nodes
297
+ """
298
+ print(f"\n{'='*50}\nNEB-CG Iteration {self.iter}\n{'='*50}")
299
+
300
+ # Get number of nodes
301
+ num_nodes = len(geometry_num_list)
302
+
303
+ # Initialize data structures if first iteration
304
+ if self.Initialization:
305
+ self.initialize_data(num_nodes)
306
+ self.Initialization = False
307
+ print("First iteration - using steepest descent")
308
+
309
+ # Adjust step sizes based on energy changes (if not first iteration)
310
+ if not self.Initialization and self.iter > 0 and self.prev_energies[0] is not None:
311
+ self.adjust_step_sizes(biased_energy_list, self.prev_energies, total_force_list)
312
+
313
+ # Compute conjugate gradient directions for each node
314
+ cg_directions = []
315
+ for i, force in enumerate(total_force_list):
316
+ direction = self.compute_cg_direction(force, i)
317
+ cg_directions.append(direction)
318
+
319
+ # Determine step vectors from directions and step sizes
320
+ move_vectors = self.determine_step(cg_directions, self.node_step_sizes)
321
+
322
+ # Store current data for next iteration
323
+ for i in range(num_nodes):
324
+ self.prev_forces[i] = total_force_list[i].copy()
325
+ self.prev_directions[i] = cg_directions[i].copy()
326
+ self.prev_positions[i] = geometry_num_list[i].copy()
327
+ self.prev_energies[i] = biased_energy_list[i]
328
+
329
+
330
+ if self.TR_NEB:
331
+ move_vectors = self.TR_NEB.TR_calc(geometry_num_list, total_force_list, move_vectors,
332
+ biased_energy_list, pre_biased_energy_list, pre_geom)
333
+
334
+ # Update geometry using move vectors
335
+ new_geometry = []
336
+ for i, (geom, move) in enumerate(zip(geometry_num_list, move_vectors)):
337
+ new_geom = geom + move
338
+ new_geometry.append(new_geom)
339
+
340
+ # Convert to numpy array and apply unit conversion
341
+ new_geometry = np.array(new_geometry)
342
+ new_geometry *= self.bohr2angstroms
343
+
344
+ self.iter += 1
345
+ return new_geometry