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,156 @@
1
+ import numpy as np
2
+
3
+
4
+ def compute_natural_spline_coefficients(x, y):
5
+ """
6
+ Computes the coefficients for the natural cubic spline interpolation for data points (x, y).
7
+ Returns:
8
+ a: array of a coefficients (equals y values for each interval start)
9
+ b: array of b coefficients for each interval
10
+ c: array of c coefficients for each interval (from 0 to n-2)
11
+ d: array of d coefficients for each interval
12
+ h: array of interval lengths, where h[i] = x[i+1] - x[i]
13
+ """
14
+ n = len(x)
15
+ a = np.array(y, dtype=float)
16
+ h = np.diff(x)
17
+
18
+ # Build the tri-diagonal system for c: second derivative coefficients.
19
+ A = np.zeros((n, n))
20
+ rhs = np.zeros(n)
21
+
22
+ # Natural spline boundary conditions: second derivatives at endpoints are 0.
23
+ A[0, 0] = 1.0
24
+ A[-1, -1] = 1.0
25
+ for i in range(1, n-1):
26
+ A[i, i-1] = h[i-1]
27
+ A[i, i] = 2*(h[i-1] + h[i])
28
+ A[i, i+1] = h[i]
29
+ rhs[i] = 3 * ((a[i+1]-a[i])/h[i] - (a[i]-a[i-1])/h[i-1])
30
+
31
+ # Solve for c (second derivatives at data points)
32
+ c_full = np.linalg.solve(A, rhs)
33
+
34
+ # Compute b and d for each segment
35
+ b = np.zeros(n-1)
36
+ d = np.zeros(n-1)
37
+ # For each interval [x[i], x[i+1]]
38
+ for i in range(n-1):
39
+ b[i] = (a[i+1]-a[i])/h[i] - h[i]*(2*c_full[i] + c_full[i+1])/3
40
+ d[i] = (c_full[i+1]-c_full[i])/(3*h[i])
41
+
42
+ # We will use c coefficients per segment as c[i] (from point i)
43
+ # Note: For evaluating spline on interval i, c_full[i] is used.
44
+ return a, b, c_full, d, h
45
+
46
+ def evaluate_spline(x, a, b, c_full, d, xi):
47
+ """
48
+ Evaluate the natural cubic spline S(x) at a given point xi.
49
+ Identifies the appropriate interval and computes:
50
+ S(x) = a[i] + b[i]*(xi - x[i]) + c_full[i]*(xi-x[i])^2 + d[i]*(xi-x[i])^3
51
+ """
52
+ n = len(x)
53
+ # Find the appropriate interval
54
+ if xi <= x[0]:
55
+ i = 0
56
+ elif xi >= x[-1]:
57
+ i = n - 2
58
+ else:
59
+ # find i such that x[i] <= xi < x[i+1]
60
+ i = np.searchsorted(x, xi) - 1
61
+ dx = xi - x[i]
62
+ return a[i] + b[i]*dx + c_full[i]*dx**2 + d[i]*dx**3
63
+
64
+ def evaluate_spline_deriv(x, a, b, c_full, d, xi):
65
+ """
66
+ Evaluate the first derivative S'(x) of the spline at xi.
67
+ S'(x) = b[i] + 2*c_full[i]*(xi-x[i]) + 3*d[i]*(xi-x[i])^2
68
+ """
69
+ n = len(x)
70
+ if xi <= x[0]:
71
+ i = 0
72
+ elif xi >= x[-1]:
73
+ i = n - 2
74
+ else:
75
+ i = np.searchsorted(x, xi) - 1
76
+ dx = xi - x[i]
77
+ return b[i] + 2*c_full[i]*dx + 3*d[i]*dx**2
78
+
79
+ def evaluate_spline_second_deriv(x, c_full, d, xi):
80
+ """
81
+ Evaluate the second derivative S''(x) of the spline at xi.
82
+ S''(x) = 2*c_full[i] + 6*d[i]*(xi-x[i])
83
+ """
84
+ n = len(x)
85
+ if xi <= x[0]:
86
+ i = 0
87
+ elif xi >= x[-1]:
88
+ i = n - 2
89
+ else:
90
+ i = np.searchsorted(x, xi) - 1
91
+ dx = xi - x[i]
92
+ return 2*c_full[i] + 6*d[i]*dx
93
+
94
+ def newton_method(f, df, x0, tol=1e-10, max_iter=500):
95
+ """
96
+ Apply Newton's method to solve f(x)=0.
97
+ Starts from initial guess x0, and iterates until convergence.
98
+ """
99
+ x_current = x0
100
+ for _ in range(max_iter):
101
+ f_val = f(x_current)
102
+ df_val = df(x_current)
103
+ if abs(df_val) < 1e-12:
104
+ break
105
+ x_new = x_current - f_val/df_val
106
+ if abs(x_new - x_current) < tol:
107
+ return x_new
108
+ x_current = x_new
109
+ return x_current
110
+
111
+ def find_extrema(x, a, b, c_full, d):
112
+ """
113
+ Find local extrema for the natural spline interpolation.
114
+ For each spline segment [x[i], x[i+1]], we apply Newton's method to solve:
115
+ S'_i(x) = 0,
116
+ starting from the midpoint of the interval.
117
+
118
+ We then classify the extremum using the second derivative:
119
+ - If S''(x) < 0, it's a local maximum.
120
+ - If S''(x) > 0, it's a local minimum.
121
+
122
+ Returns two lists: local_maxima and local_minima.
123
+ Each element is a tuple (xi, S(xi)).
124
+ """
125
+ local_maxima = []
126
+ local_minima = []
127
+ n = len(x)
128
+
129
+ for i in range(n-1):
130
+ def f(xi):
131
+ return b[i] + 2*c_full[i]*(xi - x[i]) + 3*d[i]*(xi - x[i])**2
132
+ def df(xi):
133
+ return 2*c_full[i] + 6*d[i]*(xi - x[i])
134
+
135
+
136
+ x0 = (x[i] + x[i+1]) / 2
137
+ root = newton_method(f, df, x0)
138
+
139
+ if root >= x[i] - 1e-10 and root <= x[i+1] + 1e-10:
140
+ S_value = a[i] + b[i]*(root - x[i]) + c_full[i]*(root - x[i])**2 + d[i]*(root - x[i])**3
141
+ second_deriv = 2*c_full[i] + 6*d[i]*(root - x[i])
142
+ if second_deriv < 0:
143
+ local_maxima.append((root, S_value))
144
+ elif second_deriv > 0:
145
+ local_minima.append((root, S_value))
146
+ return local_maxima, local_minima
147
+
148
+ def spline_interpolation(x_data, y_data):
149
+ resolution = 100000
150
+ a, b, c_full, d, h = compute_natural_spline_coefficients(x_data, y_data)
151
+ t_fine = np.linspace(x_data[0], x_data[-1], resolution)
152
+ S_values = [evaluate_spline(x_data, a, b, c_full, d, t) for t in t_fine]
153
+ local_maxima, local_minima = find_extrema(x_data, a, b, c_full, d)
154
+ # distance, energy = zip(*local_maxima or *local_minima)
155
+ return local_maxima, local_minima
156
+
@@ -0,0 +1,473 @@
1
+ import numpy as np
2
+ from multioptpy.Utils.calc_tools import calc_path_length_list
3
+
4
+
5
+ def _cumulative_path_length(geom):
6
+ """Calculates cumulative path length along the string."""
7
+ diffs = np.diff(geom, axis=0)
8
+ seg_lengths = np.linalg.norm(diffs, axis=1)
9
+ seg_lengths = np.nan_to_num(seg_lengths, nan=0.0)
10
+ return np.concatenate([[0.0], np.cumsum(seg_lengths)])
11
+
12
+ def _estimate_curvature_and_tangents(gradients, geom):
13
+ """
14
+ Calculates curvature and tangent vectors at each node.
15
+ Returns:
16
+ curvatures: np.array (N,) - Second derivative along path
17
+ tangents: np.array (N, atoms*3) - Normalized tangent vectors
18
+ """
19
+ n = len(geom)
20
+ gradients = gradients.reshape(n, -1)
21
+ geom = geom.reshape(n, -1)
22
+
23
+ # 1. Calculate Tangents (Central Difference)
24
+ tangents = np.zeros_like(geom)
25
+
26
+ # Internal nodes: T_i ~ (x_{i+1} - x_{i-1})
27
+ if n > 2:
28
+ vecs = geom[2:] - geom[:-2]
29
+ norms = np.linalg.norm(vecs, axis=1)
30
+ norms = np.maximum(norms, 1e-12)
31
+ tangents[1:-1] = vecs / norms[:, None]
32
+
33
+ # Endpoints: Forward/Backward difference
34
+ t_start = geom[1] - geom[0]
35
+ tangents[0] = t_start / max(np.linalg.norm(t_start), 1e-12)
36
+
37
+ t_end = geom[-1] - geom[-2]
38
+ tangents[-1] = t_end / max(np.linalg.norm(t_end), 1e-12)
39
+
40
+ # 2. Calculate Gradient Projections (Slope)
41
+ grad_along = np.sum(gradients * tangents, axis=1)
42
+
43
+ # 3. Calculate Curvature (Numerical differentiation of slope)
44
+ curvature = np.zeros(n)
45
+ dists = np.linalg.norm(geom[1:] - geom[:-1], axis=1) # Segment lengths
46
+
47
+ for k in range(1, n - 1):
48
+ ds_prev = dists[k-1]
49
+ ds_next = dists[k]
50
+ total_ds = ds_prev + ds_next
51
+
52
+ if total_ds > 1e-10:
53
+ # Centered finite difference of the first derivative
54
+ curvature[k] = (grad_along[k+1] - grad_along[k-1]) / total_ds
55
+
56
+ curvature[0] = curvature[1]
57
+ curvature[-1] = curvature[-2]
58
+
59
+ return curvature, tangents, grad_along
60
+
61
+ def _solve_polynomial_max(s_vals, E_vals, g_vals, gamma_vals=None):
62
+ """
63
+ Fits a polynomial to 3 points and finds the local maximum.
64
+ Calculates 3rd derivative to estimate asymmetry.
65
+
66
+ Args:
67
+ s_vals: [s_prev, 0, s_next] (relative arc lengths)
68
+ E_vals: [E_prev, E_curr, E_next]
69
+ g_vals: [g_prev, g_curr, g_next] (projected gradients)
70
+ gamma_vals: [c_prev, c_curr, c_next] (curvatures, optional)
71
+
72
+ Returns:
73
+ s_max: Predicted arc length of TS (relative to center), or None if invalid.
74
+ """
75
+ # Coordinate scaling to avoid numerical issues with small s
76
+ scale = max(abs(s_vals[0]), abs(s_vals[2]))
77
+ if scale < 1e-12: return None
78
+
79
+ s = np.array(s_vals) / scale
80
+ E = np.array(E_vals)
81
+ g = np.array(g_vals) * scale # Derivative scales with 1/scale
82
+
83
+ # Construct Linear System: Ac = b
84
+ # E(s) = sum(c_k * s^k)
85
+
86
+ rows = []
87
+ rhs = []
88
+
89
+ # Degree of polynomial
90
+ # If gamma is provided: 3 points * 3 constraints = 9 constraints -> Degree 8 (Octic)
91
+ # If gamma is None: 3 points * 2 constraints = 6 constraints -> Degree 5 (Quintic)
92
+ use_curvature = (gamma_vals is not None)
93
+ degree = 8 if use_curvature else 5
94
+
95
+ if use_curvature:
96
+ gamma = np.array(gamma_vals) * (scale**2) # 2nd deriv scales with 1/scale^2
97
+
98
+ for i in range(3):
99
+ si = s[i]
100
+ # Energy constraint: E(si)
101
+ rows.append([si**k for k in range(degree + 1)])
102
+ rhs.append(E[i])
103
+
104
+ # Gradient constraint: E'(si)
105
+ row_g = [0.0] * (degree + 1)
106
+ for k in range(1, degree + 1):
107
+ row_g[k] = k * si**(k-1)
108
+ rows.append(row_g)
109
+ rhs.append(g[i])
110
+
111
+ # Curvature constraint: E''(si)
112
+ if use_curvature:
113
+ row_c = [0.0] * (degree + 1)
114
+ for k in range(2, degree + 1):
115
+ row_c[k] = k * (k-1) * si**(k-2)
116
+ rows.append(row_c)
117
+ rhs.append(gamma[i])
118
+
119
+ try:
120
+ # Solve for coefficients
121
+ coeffs = np.linalg.solve(np.array(rows), np.array(rhs))
122
+ except np.linalg.LinAlgError:
123
+ return None
124
+
125
+ # Find roots of the derivative (E'(s) = 0)
126
+ deriv_coeffs = [k * coeffs[k] for k in range(1, degree + 1)]
127
+ roots = np.roots(deriv_coeffs[::-1])
128
+
129
+ # Filter real roots within the interval
130
+ valid_roots = []
131
+ s_min, s_max = s[0], s[2]
132
+
133
+ for r in roots:
134
+ if np.isreal(r):
135
+ r_real = r.real
136
+ # Allow slightly outside for prediction, but clamp tightly for safety
137
+ if s_min * 1.1 <= r_real <= s_max * 1.1:
138
+ # Check curvature (E''(s) < 0 for maximum)
139
+ # Calculate 2nd derivative value at this root
140
+ curvature_val = 0.0
141
+ for k in range(2, degree + 1):
142
+ curvature_val += k * (k-1) * coeffs[k] * (r_real**(k-2))
143
+
144
+ if curvature_val < -1e-5: # Must be concave (maximum)
145
+ # --- Added: Calculate 3rd Derivative for Asymmetry Check ---
146
+ deriv3_val = 0.0
147
+ for k in range(3, degree + 1):
148
+ deriv3_val += k * (k-1) * (k-2) * coeffs[k] * (r_real**(k-3))
149
+
150
+ # Scale back to physical units
151
+ # 3rd derivative scales with 1/scale^3
152
+ true_deriv3 = deriv3_val / (scale**3)
153
+ true_curvature = curvature_val / (scale**2)
154
+
155
+ fit_type = "Octic" if use_curvature else "Quintic"
156
+ print(f" [{fit_type}] TS candidate found at s={r_real*scale:.4f}. "
157
+ f"Curvature={true_curvature:.4e}, 3rd Deriv={true_deriv3:.4e}")
158
+
159
+ # Calculate Energy at this root
160
+ energy_val = np.polynomial.polynomial.polyval(r_real, coeffs)
161
+ valid_roots.append((r_real, energy_val))
162
+
163
+ if not valid_roots:
164
+ return None
165
+
166
+ # Return the root with the highest energy
167
+ best_s = max(valid_roots, key=lambda x: x[1])[0]
168
+
169
+ return best_s * scale
170
+
171
+ def distribute_geometry_by_predicted_energy(
172
+ geometry_list,
173
+ energy_list,
174
+ gradient_list,
175
+ n_points=None,
176
+ method="octic" # Options: 'quintic', 'octic'
177
+ ):
178
+ """
179
+ Redistributes geometry nodes to align with predicted Transition State (TS).
180
+ """
181
+ geom = np.asarray(geometry_list, dtype=float)
182
+ energies = np.asarray(energy_list, dtype=float)
183
+ n_old = len(geom)
184
+
185
+ if n_points is None:
186
+ n_points = n_old
187
+
188
+ natom = geom.shape[1]
189
+ geom_flat = geom.reshape(n_old, -1)
190
+ gradients = np.asarray(gradient_list, dtype=float).reshape(n_old, -1)
191
+
192
+ # 1. Path length calculation
193
+ s_cum = _cumulative_path_length(geom_flat)
194
+ total_length = s_cum[-1]
195
+
196
+ if total_length < 1e-12 or n_old < 3:
197
+ return geom.copy()
198
+
199
+ # 2. Calculate properties along the path
200
+ curvatures, _, projected_gradients = _estimate_curvature_and_tangents(gradients, geom)
201
+
202
+ # 3. Identify Anchors
203
+ anchors = [(0, 0.0), (n_points - 1, total_length)]
204
+
205
+ # Scan for local maxima
206
+ for i in range(1, n_old - 1):
207
+ if energies[i] > energies[i-1] and energies[i] > energies[i+1]:
208
+
209
+ s_prev = s_cum[i-1] - s_cum[i]
210
+ s_curr = 0.0
211
+ s_next = s_cum[i+1] - s_cum[i]
212
+
213
+ s_vals = [s_prev, s_curr, s_next]
214
+ E_vals = [energies[i-1], energies[i], energies[i+1]]
215
+ g_vals = [projected_gradients[i-1], projected_gradients[i], projected_gradients[i+1]]
216
+ c_vals = [curvatures[i-1], curvatures[i], curvatures[i+1]]
217
+
218
+ s_ts_local = None
219
+ print(f"NODE {i}: Detected local maximum at s={s_cum[i]:.4f}, E={energies[i]:.4f}")
220
+ # --- Attempt Method 2: Octic (9-point) ---
221
+ if method == "octic":
222
+ s_ts_local = _solve_polynomial_max(s_vals, E_vals, g_vals, c_vals)
223
+
224
+ # --- Fallback/Method 1: Quintic (6-point) ---
225
+ if s_ts_local is None:
226
+ # print(f"Node {i}: Fallback to Quintic fitting.")
227
+ s_ts_local = _solve_polynomial_max(s_vals, E_vals, g_vals, None)
228
+
229
+ # --- Finalize Position ---
230
+ if s_ts_local is not None:
231
+ s_ts_global = s_cum[i] + s_ts_local
232
+
233
+ # Map old index 'i' to new index 'j'
234
+ j = int(round(i * (n_points - 1) / (n_old - 1)))
235
+
236
+ if 0 < j < n_points - 1:
237
+ anchors.append((j, s_ts_global))
238
+
239
+ # 4. Process Anchors & Interpolate
240
+ anchors.sort(key=lambda x: x[0])
241
+
242
+ unique_anchors = [anchors[0]]
243
+ for k in range(1, len(anchors)):
244
+ curr_idx, curr_s = anchors[k]
245
+ prev_idx, prev_s = unique_anchors[-1]
246
+
247
+ if curr_idx > prev_idx:
248
+ if curr_s <= prev_s:
249
+ curr_s = prev_s + 1e-6
250
+ unique_anchors.append((curr_idx, curr_s))
251
+
252
+ if unique_anchors[-1][0] != n_points - 1:
253
+ unique_anchors.append((n_points - 1, total_length))
254
+
255
+ # Construct Target Grid
256
+ target_s = np.zeros(n_points)
257
+ for k in range(len(unique_anchors) - 1):
258
+ idx_start, s_start = unique_anchors[k]
259
+ idx_end, s_end = unique_anchors[k+1]
260
+ count = idx_end - idx_start + 1
261
+ target_s[idx_start : idx_end + 1] = np.linspace(s_start, s_end, count)
262
+
263
+ # Interpolate Geometry
264
+ new_flat_geom = np.zeros((n_points, geom_flat.shape[1]))
265
+ for dim in range(geom_flat.shape[1]):
266
+ new_flat_geom[:, dim] = np.interp(target_s, s_cum, geom_flat[:, dim])
267
+
268
+ new_geom = new_flat_geom.reshape(n_points, natom, 3)
269
+ new_geom[0] = geom[0]
270
+ new_geom[-1] = geom[-1]
271
+
272
+ return new_geom
273
+
274
+ def distribute_geometry_by_length(geometry_list, angstrom_spacing):
275
+ """Distribute geometries by specified distance spacing"""
276
+ path_length_list = calc_path_length_list(geometry_list)
277
+ total_length = path_length_list[-1]
278
+ new_geometry_list = []
279
+
280
+ # Avoid zero length
281
+ if total_length < 1e-8:
282
+ return [geometry_list[0]]
283
+
284
+ max_steps = int(total_length // angstrom_spacing)
285
+ # Start point
286
+ new_geometry_list.append(geometry_list[0])
287
+
288
+ for i in range(1, max_steps + 1):
289
+ dist = i * angstrom_spacing
290
+ if dist >= total_length:
291
+ break
292
+
293
+ for j in range(len(path_length_list) - 1):
294
+ if path_length_list[j] <= dist <= path_length_list[j+1]:
295
+ delta_t = (dist - path_length_list[j]) / (path_length_list[j+1] - path_length_list[j])
296
+ new_geometry = geometry_list[j] + (geometry_list[j+1] - geometry_list[j]) * delta_t
297
+ new_geometry_list.append(new_geometry)
298
+ break
299
+
300
+ # Add the last point if it is far enough from the previous one,
301
+ # following the original specification of appending the final geometry.
302
+ if len(new_geometry_list) == 0 or np.linalg.norm(new_geometry_list[-1] - geometry_list[-1]) > 1e-4:
303
+ new_geometry_list.append(geometry_list[-1])
304
+
305
+ return new_geometry_list
306
+
307
+
308
+ def distribute_geometry(geometry_list):
309
+ """Distribute geometries evenly along the path"""
310
+ nnode = len(geometry_list)
311
+ path_length_list = calc_path_length_list(geometry_list)
312
+ total_length = path_length_list[-1]
313
+
314
+ if total_length < 1e-8:
315
+ return list(geometry_list)
316
+
317
+ node_dist = total_length / (nnode-1)
318
+
319
+ new_geometry_list = [geometry_list[0]]
320
+ for i in range(1, nnode-1):
321
+ dist = i * node_dist
322
+ # Search logic
323
+ found = False
324
+ for j in range(len(path_length_list)-1):
325
+ if path_length_list[j] <= dist <= path_length_list[j+1]:
326
+ delta_t = (dist - path_length_list[j]) / (path_length_list[j+1] - path_length_list[j])
327
+ new_geometry = geometry_list[j] + (geometry_list[j+1] - geometry_list[j]) * delta_t
328
+ new_geometry_list.append(new_geometry)
329
+ found = True
330
+ break
331
+ if not found:
332
+ # Safeguard for cases out of range due to numerical error
333
+ new_geometry_list.append(geometry_list[-1])
334
+
335
+ new_geometry_list.append(geometry_list[-1])
336
+ return new_geometry_list
337
+
338
+ def distribute_geometry_by_energy(geometry_list, energy_list, gradient_list=None, n_points=None, smoothing=0.1):
339
+ """
340
+ Distribute geometries concentrating on ALL high-energy regions (Multiple Peaks)
341
+ using Linear Interpolation.
342
+
343
+ Improvements:
344
+ - Uses 'gradient_list' (if provided) to calculate Energy Curvature for precise Peak Detection.
345
+ - Concentrates nodes on secondary transition states as well as the global maximum.
346
+
347
+ Args:
348
+ geometry_list: list of np.ndarray
349
+ energy_list: list of float
350
+ gradient_list: list of np.ndarray (dE/dx). Optional.
351
+ n_points: int, number of output nodes (default: same as input)
352
+ smoothing: float, weighting factor for low energy regions
353
+
354
+ Returns:
355
+ new_geometry_list: list of np.ndarray
356
+ """
357
+ if len(geometry_list) != len(energy_list):
358
+ raise ValueError("Length of geometry_list and energy_list must be the same.")
359
+
360
+ if n_points is None:
361
+ n_points = len(geometry_list)
362
+
363
+ path_length_list = calc_path_length_list(geometry_list)
364
+ total_length = path_length_list[-1]
365
+
366
+ if total_length < 1e-8:
367
+ return list(geometry_list)
368
+
369
+ geometry_arr = np.array(geometry_list)
370
+ energies = np.array(energy_list)
371
+ n_nodes = len(energies)
372
+
373
+ # --- 1. Calculate Global Energy Weights (Height) ---
374
+ E_min = np.min(energies)
375
+ E_max = np.max(energies)
376
+
377
+ if E_max - E_min < 1e-6:
378
+ w_global = np.zeros_like(energies)
379
+ else:
380
+ w_global = (energies - E_min) / (E_max - E_min)
381
+
382
+ # --- 2. Calculate Local Peak Weights (Convexity) ---
383
+ w_local = np.zeros_like(energies)
384
+
385
+ if n_nodes > 2:
386
+ # Identify Hills (Energy Higher than Neighbors)
387
+ E_center = energies[1:-1]
388
+ E_neighbors = (energies[:-2] + energies[2:]) / 2.0
389
+ # Boolean mask for hill regions
390
+ is_hill = E_center > E_neighbors
391
+
392
+ if gradient_list is not None:
393
+ # --- A. Use Gradients for Precision ---
394
+ grad_arr = np.array(gradient_list)
395
+
396
+ # Calculate Tangents
397
+ vecs = geometry_arr[1:] - geometry_arr[:-1]
398
+ vec_norms = np.linalg.norm(vecs, axis=(1,2))
399
+ valid = vec_norms > 1e-10
400
+ tangents = np.zeros_like(geometry_arr)
401
+ tangents[:-1][valid] = vecs[valid] / vec_norms[valid][:, np.newaxis, np.newaxis]
402
+ tangents[-1] = tangents[-2]
403
+
404
+ # Project Gradient -> Slope (dE/ds)
405
+ slopes = np.sum(grad_arr * tangents, axis=(1,2))
406
+
407
+ # Curvature ~ Change in Slope
408
+ slope_change = np.zeros_like(slopes)
409
+ slope_change[1:-1] = slopes[2:] - slopes[:-2]
410
+
411
+ # Assign weight based on curvature magnitude ONLY in Hill regions
412
+ peak_metric = np.abs(slope_change[1:-1])
413
+ w_local[1:-1][is_hill] = peak_metric[is_hill]
414
+
415
+ else:
416
+ # --- B. Fallback to Energy Convexity ---
417
+ convexity = E_center - E_neighbors
418
+ peak_score = np.maximum(convexity, 0.0)
419
+ w_local[1:-1] = peak_score
420
+
421
+ # Normalize local score
422
+ p_max = np.max(w_local)
423
+ if p_max > 1e-6:
424
+ w_local /= p_max
425
+
426
+ # Pad endpoints
427
+ w_local[0] = w_local[1]
428
+ w_local[-1] = w_local[-2]
429
+
430
+ # --- 3. Combine Weights ---
431
+ # Mix 50% Global Height + 50% Local Shape + Smoothing
432
+ weights = 0.5 * w_global + 0.5 * w_local + smoothing
433
+
434
+ # --- Calculate weighted cumulative distance ---
435
+ segment_dists = np.diff(path_length_list)
436
+ segment_weights = (weights[:-1] + weights[1:]) / 2.0
437
+ weighted_segments = segment_dists * segment_weights
438
+
439
+ cum_weighted_dist = np.concatenate(([0.0], np.cumsum(weighted_segments)))
440
+ total_weighted_length = cum_weighted_dist[-1]
441
+
442
+ # --- Determine new target distances ---
443
+ target_weighted_grid = np.linspace(0, total_weighted_length, n_points)
444
+ target_physical_dists = np.interp(target_weighted_grid, cum_weighted_dist, path_length_list)
445
+
446
+ # --- Coordinate redistribution using linear interpolation ---
447
+ new_geometry_list = []
448
+
449
+ for dist in target_physical_dists:
450
+ if dist <= 0:
451
+ new_geometry_list.append(geometry_list[0])
452
+ continue
453
+ if dist >= total_length:
454
+ new_geometry_list.append(geometry_list[-1])
455
+ continue
456
+
457
+ found = False
458
+ for j in range(len(path_length_list) - 1):
459
+ if path_length_list[j] <= dist <= path_length_list[j+1]:
460
+ seg_len = path_length_list[j+1] - path_length_list[j]
461
+ delta_t = 0 if seg_len < 1e-10 else (dist - path_length_list[j]) / seg_len
462
+
463
+ vec = geometry_list[j+1] - geometry_list[j]
464
+ new_geometry = geometry_list[j] + vec * delta_t
465
+ new_geometry_list.append(new_geometry)
466
+ found = True
467
+ break
468
+
469
+ if not found:
470
+ new_geometry_list.append(geometry_list[-1])
471
+
472
+ return new_geometry_list
473
+