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,253 @@
1
+ from .linesearch import LineSearch
2
+ import numpy as np
3
+
4
+ class LBFGS:
5
+ """Limited-memory BFGS optimizer.
6
+
7
+ A limited memory version of the BFGS algorithm that approximates the inverse Hessian
8
+ matrix using a limited amount of memory. Unlike the standard BFGS algorithm,
9
+ LBFGS does not explicitly store the Hessian matrix, but instead builds it implicitly
10
+ from previous gradients and positions.
11
+ """
12
+
13
+ def __init__(self, **config):
14
+ # Configuration parameters
15
+ self.config = config
16
+
17
+ # Initialize flags
18
+ self.Initialization = True
19
+ self.linesearchflag = False
20
+ self.optimal_step_flag = False
21
+
22
+ # Set default parameters
23
+ self.DELTA = config.get("delta", 1.0) # Step size scaling factor
24
+ self.FC_COUNT = config.get("fc_count", -1) # Frequency of computing full Hessian
25
+ self.saddle_order = 0
26
+ self.iter = 0
27
+ self.memory = config.get("memory", 30) # Number of previous steps to remember
28
+ self.damping = config.get("damping", 0.75) # Damping factor for step size
29
+ self.alpha = config.get("alpha", 10.0) # Initial Hessian scaling
30
+ self.beta = config.get("beta", 0.1) # Momentum factor
31
+
32
+ # Storage for L-BFGS vectors
33
+ self.s = [] # Position differences
34
+ self.y = [] # Gradient differences
35
+ self.rho = [] # 1 / (y_k^T s_k)
36
+
37
+ # Initialize Hessian related variables
38
+ self.hessian = None # Not explicitly stored in L-BFGS
39
+ self.bias_hessian = None # For additional Hessian terms
40
+ self.H0 = 1.0 / self.alpha # Initial approximation of inverse Hessian
41
+
42
+ # For line search
43
+ self.prev_move_vector = None
44
+
45
+ return
46
+
47
+ def project_out_hess_tr_and_rot_for_coord(self, hessian, geometry):
48
+ """Project out translation and rotation from Hessian.
49
+
50
+ This is only used for line search in this implementation.
51
+ """
52
+ natoms = len(geometry)
53
+
54
+ geometry -= self.calc_center(geometry)
55
+
56
+ tr_x = (np.tile(np.array([1, 0, 0]), natoms)).reshape(-1, 3)
57
+ tr_y = (np.tile(np.array([0, 1, 0]), natoms)).reshape(-1, 3)
58
+ tr_z = (np.tile(np.array([0, 0, 1]), natoms)).reshape(-1, 3)
59
+
60
+ rot_x = np.cross(geometry, tr_x).flatten()
61
+ rot_y = np.cross(geometry, tr_y).flatten()
62
+ rot_z = np.cross(geometry, tr_z).flatten()
63
+ tr_x = tr_x.flatten()
64
+ tr_y = tr_y.flatten()
65
+ tr_z = tr_z.flatten()
66
+
67
+ TR_vectors = np.vstack([tr_x, tr_y, tr_z, rot_x, rot_y, rot_z])
68
+
69
+ Q, R = np.linalg.qr(TR_vectors.T)
70
+ keep_indices = ~np.isclose(np.diag(R), 0, atol=1e-6, rtol=0)
71
+ TR_vectors = Q.T[keep_indices]
72
+ n_tr = len(TR_vectors)
73
+
74
+ P = np.identity(natoms * 3)
75
+ for vector in TR_vectors:
76
+ P -= np.outer(vector, vector)
77
+
78
+ hess_proj = np.dot(np.dot(P.T, hessian), P)
79
+
80
+ return hess_proj
81
+
82
+ def calc_center(self, geometry, element_list=[]):
83
+ """Calculate center of geometry."""
84
+ center = np.array([0.0, 0.0, 0.0], dtype="float64")
85
+ for i in range(len(geometry)):
86
+ center += geometry[i]
87
+ center /= float(len(geometry))
88
+
89
+ return center
90
+
91
+ def set_hessian(self, hessian):
92
+ """Set explicit Hessian matrix (not used in LBFGS)."""
93
+ self.hessian = hessian
94
+ return
95
+
96
+ def set_bias_hessian(self, bias_hessian):
97
+ """Set bias Hessian matrix."""
98
+ self.bias_hessian = bias_hessian
99
+ return
100
+
101
+ def get_hessian(self):
102
+ """Get Hessian matrix.
103
+
104
+ Note: In LBFGS, the Hessian is not explicitly stored,
105
+ but this method is provided for compatibility.
106
+ """
107
+ return self.hessian
108
+
109
+ def get_bias_hessian(self):
110
+ """Get bias Hessian matrix."""
111
+ return self.bias_hessian
112
+
113
+ def update_vectors(self, displacement, delta_grad):
114
+ """Update the vectors used for the L-BFGS approximation."""
115
+ # Flatten vectors
116
+ s = displacement.flatten()
117
+ y = delta_grad.flatten()
118
+
119
+ # Calculate rho = 1 / (y^T * s)
120
+ dot_product = np.dot(y, s)
121
+ if abs(dot_product) < 1e-10:
122
+ # Avoid division by very small numbers
123
+ rho = 1000.0
124
+ else:
125
+ rho = 1.0 / dot_product
126
+
127
+ # Add to history
128
+ self.s.append(s)
129
+ self.y.append(y)
130
+ self.rho.append(rho)
131
+
132
+ # Remove oldest vectors if exceeding memory limit
133
+ if len(self.s) > self.memory:
134
+ self.s.pop(0)
135
+ self.y.pop(0)
136
+ self.rho.pop(0)
137
+
138
+ def compute_lbfgs_direction(self, gradient):
139
+ """Compute the search direction using the L-BFGS algorithm."""
140
+ # Flatten gradient
141
+ q = gradient.flatten()
142
+
143
+ # Number of vectors to use
144
+ loopmax = min(self.memory, len(self.s))
145
+ a = np.empty((loopmax,), dtype=np.float64)
146
+
147
+ # First loop: compute alpha_i = rho_i * s_i^T * q
148
+ for i in range(loopmax - 1, -1, -1):
149
+ a[i] = self.rho[i] * np.dot(self.s[i], q)
150
+ q = q - a[i] * self.y[i]
151
+
152
+ # Apply initial Hessian approximation: z = H_0 * q
153
+ z = self.H0 * q
154
+
155
+ # Second loop: compute search direction
156
+ for i in range(loopmax):
157
+ b = self.rho[i] * np.dot(self.y[i], z)
158
+ z = z + self.s[i] * (a[i] - b)
159
+
160
+ # Reshape to original gradient shape
161
+ z = z.reshape(gradient.shape)
162
+
163
+ return z
164
+
165
+ def determine_step(self, dr):
166
+ """Determine step to take according to maxstep."""
167
+ if self.config.get("maxstep") is None:
168
+ return dr
169
+
170
+ # Get maxstep from config
171
+ maxstep = self.config.get("maxstep")
172
+
173
+ # Calculate step lengths
174
+ dr_reshaped = dr.reshape(-1, 3) if dr.size % 3 == 0 else dr.reshape(-1, dr.size)
175
+ steplengths = np.sqrt((dr_reshaped**2).sum(axis=1))
176
+ longest_step = np.max(steplengths)
177
+
178
+ # Scale step if necessary
179
+ if longest_step > maxstep:
180
+ dr = dr * (maxstep / longest_step)
181
+
182
+ return dr
183
+
184
+ def normal(self, geom_num_list, B_g, pre_B_g, pre_geom, B_e, pre_B_e, pre_g, g):
185
+ """Normal L-BFGS optimization step."""
186
+
187
+ if self.linesearchflag:
188
+ print("linesearch mode")
189
+ else:
190
+ print("normal mode")
191
+
192
+ # First iteration - just return scaled gradient
193
+ if self.Initialization:
194
+ self.Initialization = False
195
+ return self.DELTA * B_g
196
+
197
+ # Calculate displacement and gradient difference
198
+ delta_grad = (g - pre_g).reshape(len(geom_num_list), 1)
199
+ displacement = (geom_num_list - pre_geom).reshape(len(geom_num_list), 1)
200
+
201
+ # Update L-BFGS vectors
202
+ self.update_vectors(displacement, delta_grad)
203
+
204
+ # Compute L-BFGS search direction
205
+ move_vector = self.compute_lbfgs_direction(B_g)
206
+
207
+ # Scale the step
208
+ move_vector = self.DELTA * move_vector
209
+
210
+ # Apply step size constraints
211
+ move_vector = self.determine_step(move_vector)
212
+
213
+ # Handle line search if enabled
214
+ if self.iter > 1 and self.linesearchflag:
215
+ # For line search, we need an explicit Hessian approximation
216
+ if self.hessian is not None:
217
+ tmp_hess = self.project_out_hess_tr_and_rot_for_coord(
218
+ self.hessian, geom_num_list.reshape(-1, 3)
219
+ )
220
+ else:
221
+ tmp_hess = None
222
+
223
+ if self.optimal_step_flag or self.iter == 2:
224
+ self.LS = LineSearch(
225
+ self.prev_move_vector, move_vector,
226
+ B_g, pre_B_g, B_e, pre_B_e, tmp_hess
227
+ )
228
+
229
+ new_move_vector, self.optimal_step_flag = self.LS.linesearch(
230
+ self.prev_move_vector, move_vector,
231
+ B_g, pre_B_g, B_e, pre_B_e, tmp_hess
232
+ )
233
+ move_vector = new_move_vector
234
+
235
+ if self.optimal_step_flag or self.iter == 1:
236
+ self.prev_move_vector = move_vector
237
+ else:
238
+ self.optimal_step_flag = True
239
+
240
+ print("step size: ", self.DELTA, "\n")
241
+
242
+ self.iter += 1
243
+ return move_vector
244
+
245
+
246
+ 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):
247
+ """Run the optimization step."""
248
+ method = self.config.get("method", "lbfgs").lower()
249
+
250
+
251
+ move_vector = self.normal(geom_num_list, B_g, pre_B_g, pre_geom, B_e, pre_B_e, pre_g, g)
252
+
253
+ return move_vector
@@ -0,0 +1,355 @@
1
+ import numpy as np
2
+ from numpy.linalg import norm
3
+
4
+ from multioptpy.Parameters.parameter import UnitValueLib
5
+
6
+ class LBFGS_NEB:
7
+ """Limited-memory BFGS optimizer for Nudged Elastic Band calculations.
8
+
9
+ This implementation uses standard L-BFGS algorithm to approximate the inverse Hessian
10
+ matrix using a limited amount of memory, maintaining separate approximations
11
+ for each image in the NEB chain.
12
+
13
+ Step length is controlled through direct scaling rather than trust regions.
14
+ """
15
+
16
+ def __init__(self, **config):
17
+ # Configuration parameters
18
+ self.config = config
19
+
20
+ # Initialize flags
21
+ self.Initialization = True
22
+ self.iter = 0
23
+
24
+ # Set default parameters
25
+ self.memory = config.get("memory", 40) # Number of previous steps to remember
26
+ self.maxstep = config.get("maxstep", 0.1) # Maximum step size
27
+
28
+ # Scaling parameters
29
+ self.initial_step_scale = config.get("initial_step_scale", 1.0)
30
+ self.step_scale = self.initial_step_scale # Current step scaling factor
31
+
32
+ # Storage for L-BFGS history per image
33
+ self.s_images = [] # Position differences per image
34
+ self.y_images = [] # Force differences per image
35
+ self.rho_images = [] # 1 / (y_k^T s_k) per image
36
+ self.gamma_images = [] # Scaling factors per image
37
+
38
+ # Unit conversion
39
+ self.bohr2angstroms = UnitValueLib().bohr2angstroms
40
+ if 'bohr2angstroms' in config:
41
+ self.bohr2angstroms = config['bohr2angstroms']
42
+
43
+ self.TR_NEB = config.get("TR_NEB", None) # Keep this for compatibility
44
+
45
+ print(f"Initialized LBFGS_NEB optimizer with memory={self.memory}, "
46
+ f"maxstep={self.maxstep}")
47
+
48
+ def initialize_per_node_data(self, num_images):
49
+ """Initialize data structures for per-node L-BFGS history management.
50
+
51
+ Parameters:
52
+ ----------
53
+ num_images : int
54
+ Number of images in the NEB calculation
55
+ """
56
+ # Initialize L-BFGS history storage for each image
57
+ self.s_images = [[] for _ in range(num_images)]
58
+ self.y_images = [[] for _ in range(num_images)]
59
+ self.rho_images = [[] for _ in range(num_images)]
60
+ self.gamma_images = [1.0] * num_images # Initial gamma value for each image
61
+
62
+ # Previous data for updates
63
+ self.prev_forces = [None] * num_images
64
+ self.prev_positions = [None] * num_images
65
+ self.prev_energies = [None] * num_images
66
+
67
+ print(f"Initialized L-BFGS history storage for {num_images} images")
68
+
69
+ def update_vectors(self, displacements, delta_forces):
70
+ """Update the vectors used for the L-BFGS approximation for each image.
71
+
72
+ Parameters:
73
+ ----------
74
+ displacements : list of ndarray
75
+ Position displacement vectors for each image
76
+ delta_forces : list of ndarray
77
+ Force difference vectors for each image
78
+ """
79
+ # Initialize storage if this is the first update
80
+ if not self.s_images:
81
+ self.initialize_per_node_data(len(displacements))
82
+
83
+ # Update vectors for each image
84
+ for i, (s, y) in enumerate(zip(displacements, delta_forces)):
85
+ # Flatten vectors
86
+ s_flat = s.flatten()
87
+ y_flat = y.flatten()
88
+
89
+ # Calculate rho = 1 / (y^T * s)
90
+ dot_product = np.dot(y_flat, s_flat)
91
+ if abs(dot_product) < 1e-10:
92
+ # Avoid division by very small numbers
93
+ rho = 1000.0
94
+ print(f"Warning: Image {i} has very small y^T·s value ({dot_product:.2e})")
95
+ else:
96
+ rho = 1.0 / dot_product
97
+
98
+ # Update gamma (scaling factor for initial Hessian approximation)
99
+ y_norm_squared = np.dot(y_flat, y_flat)
100
+ if y_norm_squared > 1e-10:
101
+ self.gamma_images[i] = y_norm_squared / dot_product
102
+ print(f"Updated gamma for image {i} to {self.gamma_images[i]:.4f}")
103
+
104
+ # Add to history
105
+ self.s_images[i].append(s_flat)
106
+ self.y_images[i].append(y_flat)
107
+ self.rho_images[i].append(rho)
108
+
109
+ # Remove oldest vectors if exceeding memory limit
110
+ if len(self.s_images[i]) > self.memory:
111
+ self.s_images[i].pop(0)
112
+ self.y_images[i].pop(0)
113
+ self.rho_images[i].pop(0)
114
+
115
+ def compute_lbfgs_step(self, force, image_idx):
116
+ """Compute the L-BFGS step direction for a single image.
117
+
118
+ Parameters:
119
+ ----------
120
+ force : ndarray
121
+ Current force vector for the image
122
+ image_idx : int
123
+ Index of the image in the NEB chain
124
+
125
+ Returns:
126
+ -------
127
+ ndarray
128
+ Step direction
129
+ """
130
+ # Flatten force vector
131
+ f = force.flatten()
132
+ orig_shape = force.shape
133
+
134
+ # If we have no history yet for this image, just return force direction
135
+ if not self.s_images or image_idx >= len(self.s_images) or len(self.s_images[image_idx]) == 0:
136
+ return f.reshape(orig_shape)
137
+
138
+ # Get history for this image
139
+ s_list = self.s_images[image_idx]
140
+ y_list = self.y_images[image_idx]
141
+ rho_list = self.rho_images[image_idx]
142
+ gamma = self.gamma_images[image_idx]
143
+
144
+ # Apply two-loop recursion algorithm for L-BFGS
145
+ q = -f # Negative gradient (negative of negative force)
146
+ k = len(s_list)
147
+ alpha = np.zeros(k)
148
+
149
+ try:
150
+ # First loop
151
+ for i in range(k-1, -1, -1):
152
+ alpha[i] = rho_list[i] * np.dot(s_list[i], q)
153
+ q = q - alpha[i] * y_list[i]
154
+
155
+ # Initial Hessian approximation
156
+ r = gamma * q
157
+
158
+ # Second loop
159
+ for i in range(k):
160
+ beta = rho_list[i] * np.dot(y_list[i], r)
161
+ r = r + s_list[i] * (alpha[i] - beta)
162
+
163
+ # r is now the product B^(-1) * g where B is the Hessian approximation
164
+ return r.reshape(orig_shape)
165
+ except (ValueError, np.linalg.LinAlgError) as e:
166
+ print(f"Error in L-BFGS calculation for image {image_idx}: {e}")
167
+ print("Falling back to steepest descent")
168
+ # Fallback to steepest descent
169
+ return f.reshape(orig_shape)
170
+
171
+ def determine_step(self, dr):
172
+ """Determine step to take according to maxstep.
173
+
174
+ Parameters:
175
+ ----------
176
+ dr : ndarray or list of ndarray
177
+ Step vector(s)
178
+
179
+ Returns:
180
+ -------
181
+ ndarray or list of ndarray
182
+ Step vector(s) constrained by maxstep if necessary
183
+ """
184
+ if self.maxstep is None:
185
+ return dr
186
+
187
+ # Handle single step vector or list of step vectors
188
+ if isinstance(dr, list):
189
+ result = []
190
+ for step in dr:
191
+ # Calculate step lengths
192
+ step_reshaped = step.reshape(-1, 3) if step.size % 3 == 0 else step
193
+ steplengths = np.sqrt((step_reshaped**2).sum(axis=1))
194
+ longest_step = np.max(steplengths)
195
+
196
+ # Scale step if necessary
197
+ if longest_step > self.maxstep:
198
+ result.append(step * (self.maxstep / longest_step))
199
+ else:
200
+ result.append(step)
201
+ return result
202
+ else:
203
+ # Single step vector
204
+ step_reshaped = dr.reshape(-1, 3) if dr.size % 3 == 0 else dr
205
+ steplengths = np.sqrt((step_reshaped**2).sum(axis=1))
206
+ longest_step = np.max(steplengths)
207
+
208
+ # Scale step if necessary
209
+ if longest_step > self.maxstep:
210
+ return dr * (self.maxstep / longest_step)
211
+ return dr
212
+
213
+ def adjust_step_scale(self, energies, prev_energies):
214
+ """Adjust the global step scaling factor based on energy changes.
215
+
216
+ Parameters:
217
+ ----------
218
+ energies : list of float
219
+ Current energies for all images
220
+ prev_energies : list of float
221
+ Previous energies for all images
222
+ """
223
+ if prev_energies is None or None in prev_energies:
224
+ return
225
+
226
+ # Calculate energy changes
227
+ energy_changes = [prev - curr for prev, curr in zip(prev_energies, energies) if prev is not None and curr is not None]
228
+
229
+ if not energy_changes:
230
+ return
231
+
232
+ # Count improvements and deteriorations
233
+ improvements = sum(1 for change in energy_changes if change > 0)
234
+ deteriorations = sum(1 for change in energy_changes if change < 0)
235
+
236
+ # Adjust scale based on overall improvement ratio
237
+ if improvements > deteriorations:
238
+ # More improvements than deteriorations - increase step scale
239
+ self.step_scale = min(self.step_scale * 1.1, 2.0)
240
+ print(f"Energy improved for {improvements} images, increasing step scale to {self.step_scale:.4f}")
241
+ elif deteriorations > improvements:
242
+ # More deteriorations than improvements - decrease step scale
243
+ self.step_scale = max(self.step_scale * 0.5, 0.1)
244
+ print(f"Energy deteriorated for {deteriorations} images, decreasing step scale to {self.step_scale:.4f}")
245
+ else:
246
+ # Equal number - maintain current scale
247
+ print(f"Maintaining current step scale at {self.step_scale:.4f}")
248
+
249
+ def LBFGS_NEB_calc(self, geometry_num_list, total_force_list, pre_total_velocity,
250
+ optimize_num, total_velocity, cos_list,
251
+ biased_energy_list, pre_biased_energy_list, pre_geom):
252
+ """Calculate step using standard L-BFGS for NEB.
253
+
254
+ Parameters:
255
+ ----------
256
+ geometry_num_list : ndarray
257
+ Current geometry coordinates for all images
258
+ total_force_list : ndarray
259
+ Current forces for all images
260
+ pre_total_velocity : ndarray
261
+ Previous velocities for all images
262
+ optimize_num : int
263
+ Current optimization iteration number
264
+ total_velocity : ndarray
265
+ Current velocities for all images
266
+ cos_list : ndarray
267
+ Cosines between adjacent images
268
+ biased_energy_list : ndarray
269
+ Current energy for all images
270
+ pre_biased_energy_list : ndarray
271
+ Previous energy for all images
272
+ pre_geom : ndarray
273
+ Previous geometry coordinates for all images
274
+
275
+ Returns:
276
+ -------
277
+ ndarray
278
+ Updated geometry coordinates for all images
279
+ """
280
+ print(f"\n{'='*50}\nNEB-LBFGS Iteration {self.iter}\n{'='*50}")
281
+
282
+ # Get number of images
283
+ num_images = len(geometry_num_list)
284
+
285
+ # Initialize data structures if first iteration
286
+ if self.Initialization:
287
+ self.initialize_per_node_data(num_images)
288
+ self.Initialization = False
289
+ print("First iteration")
290
+
291
+ # Adjust step scale based on energy changes (if not first iteration)
292
+ if not self.Initialization and self.iter > 0 and self.prev_energies[0] is not None:
293
+ self.adjust_step_scale(biased_energy_list, self.prev_energies)
294
+
295
+ # Store current energies for next iteration
296
+ self.prev_energies = biased_energy_list.copy()
297
+
298
+ # List to store move vectors for each image
299
+ move_vectors = []
300
+
301
+ # Process each image
302
+ for i, force in enumerate(total_force_list):
303
+ # Compute L-BFGS step direction
304
+ step_direction = self.compute_lbfgs_step(force, i)
305
+
306
+ # Scale step by the current step scaling factor
307
+ step = self.step_scale * step_direction
308
+
309
+ move_vectors.append(step)
310
+
311
+ # Apply maxstep constraint if needed
312
+ move_vectors = self.determine_step(move_vectors)
313
+
314
+ # If this is not the first iteration, update L-BFGS vectors
315
+ if optimize_num > 0:
316
+ # Calculate displacement and force difference for each image
317
+ displacements = []
318
+ delta_forces = []
319
+
320
+ for i in range(num_images):
321
+ if pre_geom is not None and pre_total_velocity is not None and len(pre_total_velocity) > 0:
322
+ # Get previous force if available
323
+ if len(pre_total_velocity) > i:
324
+ prev_force = pre_total_velocity[i]
325
+ curr_force = total_force_list[i]
326
+ delta_force = curr_force - prev_force
327
+
328
+ # Get displacement
329
+ curr_geom = geometry_num_list[i]
330
+ prev_geom = pre_geom[i]
331
+ displacement = curr_geom - prev_geom
332
+
333
+ displacements.append(displacement)
334
+ delta_forces.append(delta_force)
335
+
336
+ if displacements and delta_forces:
337
+ self.update_vectors(displacements, delta_forces)
338
+
339
+ # Apply trust region correction if TR_NEB is provided for compatibility
340
+ if self.TR_NEB:
341
+ move_vectors = self.TR_NEB.TR_calc(geometry_num_list, total_force_list, move_vectors,
342
+ biased_energy_list, pre_biased_energy_list, pre_geom)
343
+
344
+ # Update geometry using move vectors
345
+ new_geometry = []
346
+ for i, (geom, move) in enumerate(zip(geometry_num_list, move_vectors)):
347
+ new_geom = geom + move
348
+ new_geometry.append(new_geom)
349
+
350
+ # Convert to numpy array and apply unit conversion
351
+ new_geometry = np.array(new_geometry)
352
+ new_geometry *= self.bohr2angstroms
353
+
354
+ self.iter += 1
355
+ return new_geometry