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,252 @@
1
+ import numpy as np
2
+ from scipy.signal import savgol_filter
3
+ from scipy.interpolate import interp1d
4
+
5
+ from multioptpy.Utils.calc_tools import calc_path_length_list
6
+
7
+ def savitzky_golay_interpolation(structures, n_points=20, window_length=5, polyorder=2):
8
+ """
9
+ Interpolate between arbitrary number of structures using Savitzky-Golay filtering.
10
+
11
+ Args:
12
+ structures: list of np.ndarray, each of shape (n_atoms, 3)
13
+ n_points: int, number of points to interpolate
14
+ window_length: int, the length of the filter window (must be odd and greater than polyorder)
15
+ polyorder: int, the order of the polynomial used to fit the samples
16
+
17
+ Returns:
18
+ path: np.ndarray of shape (n_points, n_atoms, 3)
19
+ """
20
+ print("Using Savitzky-Golay filter interpolation.")
21
+ N = len(structures)
22
+ if N < window_length:
23
+ print("Not enough points for Savitzky-Golay filtering, falling back to linear interpolation.")
24
+ # Fallback to linear interpolation if not enough points
25
+ t_original = np.linspace(0, 1, N)
26
+ t_interp = np.linspace(0, 1, n_points)
27
+ structures = np.array(structures)
28
+ path = []
29
+ for atom_idx in range(structures.shape[1]):
30
+ for coord in range(3):
31
+ interp_func = interp1d(t_original, structures[:, atom_idx, coord], kind='linear')
32
+ path.append(interp_func(t_interp))
33
+ path = np.array(path).reshape(n_points, structures.shape[1], 3)
34
+ return path
35
+
36
+ structures = np.array(structures)
37
+ # Apply Savitzky-Golay filter along the structure sequence
38
+ smoothed_structures = np.zeros_like(structures)
39
+ for atom_idx in range(structures.shape[1]):
40
+ for coord in range(3):
41
+ smoothed_structures[:, atom_idx, coord] = savgol_filter(structures[:, atom_idx, coord], window_length, polyorder)
42
+
43
+ # Interpolate to n_points
44
+ t_original = np.linspace(0, 1, N)
45
+ t_interp = np.linspace(0, 1, n_points)
46
+ path = []
47
+ for atom_idx in range(structures.shape[1]):
48
+ for coord in range(3):
49
+ interp_func = interp1d(t_original, smoothed_structures[:, atom_idx, coord], kind='linear')
50
+ path.append(interp_func(t_interp))
51
+ path = np.array(path).reshape(n_points, structures.shape[1], 3)
52
+ return path
53
+
54
+
55
+ def distribute_geometry_by_length_savgol(geometry_list, angstrom_spacing, window_length=5, polyorder=2):
56
+ """
57
+ Distribute geometries by specified distance spacing using Savitzky-Golay filtering.
58
+
59
+ Args:
60
+ geometry_list: list of np.ndarray, each of shape (n_atoms, 3)
61
+ angstrom_spacing: float, desired spacing
62
+ window_length: int, the length of the filter window (must be odd and greater than polyorder)
63
+ polyorder: int, the order of the polynomial used to fit the samples
64
+
65
+ Returns:
66
+ new_geometry_list: list of np.ndarray, each of shape (n_atoms, 3)
67
+ """
68
+ print("Distributing geometries using Savitzky-Golay filtering.")
69
+ path_length_list = calc_path_length_list(geometry_list)
70
+ interpolate_dist_list = np.arange(0, path_length_list[-1], angstrom_spacing)
71
+ interpolate_dist_list = np.append(interpolate_dist_list, path_length_list[-1])
72
+ t_values = interpolate_dist_list / path_length_list[-1]
73
+
74
+ N = len(geometry_list)
75
+ if N < window_length:
76
+ print("Not enough points for Savitzky-Golay filtering, falling back to linear interpolation.")
77
+ # Fallback to linear interpolation if not enough points
78
+ t_original = np.linspace(0, 1, N)
79
+ geometry_list = np.array(geometry_list)
80
+ new_geometry_list = []
81
+ for t in t_values:
82
+ interp_point = []
83
+ for atom_idx in range(geometry_list.shape[1]):
84
+ for coord in range(3):
85
+ interp_func = interp1d(t_original, geometry_list[:, atom_idx, coord], kind='linear')
86
+ interp_point.append(interp_func(t))
87
+ new_geometry_list.append(np.array(interp_point).reshape(geometry_list.shape[1], 3))
88
+ return np.array(new_geometry_list)
89
+
90
+ geometry_list = np.array(geometry_list)
91
+ # Apply Savitzky-Golay filter along the structure sequence
92
+ smoothed_geometries = np.zeros_like(geometry_list)
93
+ for atom_idx in range(geometry_list.shape[1]):
94
+ for coord in range(3):
95
+ smoothed_geometries[:, atom_idx, coord] = savgol_filter(geometry_list[:, atom_idx, coord], window_length, polyorder)
96
+
97
+ # Interpolate to desired spacing
98
+ t_original = np.linspace(0, 1, N)
99
+ new_geometry_list = []
100
+ for t in t_values:
101
+ interp_point = []
102
+ for atom_idx in range(geometry_list.shape[1]):
103
+ for coord in range(3):
104
+ interp_func = interp1d(t_original, smoothed_geometries[:, atom_idx, coord], kind='linear')
105
+ interp_point.append(interp_func(t))
106
+ new_geometry_list.append(np.array(interp_point).reshape(geometry_list.shape[1], 3))
107
+ return np.array(new_geometry_list)
108
+
109
+
110
+ def savitzky_golay_interpolation_with_derivatives(structures, n_points=20, window_length=5, polyorder=2, deriv_order=1):
111
+ """
112
+ Interpolate between arbitrary number of structures using Savitzky-Golay filtering and compute derivatives.
113
+
114
+ Args:
115
+ structures: list of np.ndarray, each of shape (n_atoms, 3)
116
+ n_points: int, number of points to interpolate
117
+ window_length: int, the length of the filter window (must be odd and greater than polyorder)
118
+ polyorder: int, the order of the polynomial used to fit the samples
119
+ deriv_order: int, the order of the derivative to compute (0 for smoothed, 1 for first derivative, etc.)
120
+
121
+ Returns:
122
+ path: np.ndarray of shape (n_points, n_atoms, 3) if deriv_order == 0, or derivative array if deriv_order > 0
123
+ derivatives: dict containing derivative arrays for each derivative order up to deriv_order
124
+ """
125
+ print(f"Using Savitzky-Golay filter interpolation with derivatives up to order {deriv_order}.")
126
+ N = len(structures)
127
+ if N < window_length:
128
+ # Fallback to linear interpolation if not enough points
129
+ t_original = np.linspace(0, 1, N)
130
+ t_interp = np.linspace(0, 1, n_points)
131
+ structures = np.array(structures)
132
+ path = []
133
+ derivatives = {}
134
+ for d in range(deriv_order + 1):
135
+ deriv_path = []
136
+ for atom_idx in range(structures.shape[1]):
137
+ for coord in range(3):
138
+ interp_func = interp1d(t_original, structures[:, atom_idx, coord], kind='linear')
139
+ if d == 0:
140
+ deriv_path.append(interp_func(t_interp))
141
+ else:
142
+ # Simple finite difference for derivatives in fallback
143
+ deriv_values = np.gradient(interp_func(t_interp), t_interp[1] - t_interp[0], edge_order=2)
144
+ for _ in range(d - 1):
145
+ deriv_values = np.gradient(deriv_values, t_interp[1] - t_interp[0], edge_order=2)
146
+ deriv_path.append(deriv_values)
147
+ derivatives[f'deriv_{d}'] = np.array(deriv_path).reshape(n_points, structures.shape[1], 3)
148
+ if d == 0:
149
+ path = derivatives[f'deriv_{d}']
150
+ return path, derivatives
151
+
152
+ structures = np.array(structures)
153
+ derivatives = {}
154
+ for d in range(deriv_order + 1):
155
+ deriv_path = []
156
+ for atom_idx in range(structures.shape[1]):
157
+ for coord in range(3):
158
+ smoothed = savgol_filter(structures[:, atom_idx, coord], window_length, polyorder, deriv=d)
159
+ deriv_path.append(smoothed)
160
+ derivatives[f'deriv_{d}'] = np.array(deriv_path).reshape(N, structures.shape[1], 3)
161
+
162
+ # Interpolate to n_points for each derivative
163
+ t_original = np.linspace(0, 1, N)
164
+ t_interp = np.linspace(0, 1, n_points)
165
+ for d in range(deriv_order + 1):
166
+ deriv_interp = []
167
+ for atom_idx in range(structures.shape[1]):
168
+ for coord in range(3):
169
+ interp_func = interp1d(t_original, derivatives[f'deriv_{d}'][:, atom_idx, coord], kind='linear')
170
+ deriv_interp.append(interp_func(t_interp))
171
+ derivatives[f'deriv_{d}'] = np.array(deriv_interp).reshape(n_points, structures.shape[1], 3)
172
+
173
+ path = derivatives['deriv_0']
174
+ return path, derivatives
175
+
176
+
177
+ def distribute_geometry_by_length_savgol_with_derivatives(geometry_list, angstrom_spacing, window_length=5, polyorder=2, deriv_order=1):
178
+ """
179
+ Distribute geometries by specified distance spacing using Savitzky-Golay filtering and compute derivatives.
180
+
181
+ Args:
182
+ geometry_list: list of np.ndarray, each of shape (n_atoms, 3)
183
+ angstrom_spacing: float, desired spacing
184
+ window_length: int, the length of the filter window (must be odd and greater than polyorder)
185
+ polyorder: int, the order of the polynomial used to fit the samples
186
+ deriv_order: int, the order of the derivative to compute (0 for smoothed, 1 for first derivative, etc.)
187
+
188
+ Returns:
189
+ new_geometry_list: list of np.ndarray, each of shape (n_atoms, 3)
190
+ derivatives: dict containing derivative arrays for each derivative order up to deriv_order
191
+ """
192
+ print(f"Distributing geometries using Savitzky-Golay filtering with derivatives up to order {deriv_order}.")
193
+ path_length_list = calc_path_length_list(geometry_list)
194
+ interpolate_dist_list = np.arange(0, path_length_list[-1], angstrom_spacing)
195
+ interpolate_dist_list = np.append(interpolate_dist_list, path_length_list[-1])
196
+ t_values = interpolate_dist_list / path_length_list[-1]
197
+
198
+ N = len(geometry_list)
199
+ if N < window_length:
200
+ # Fallback to linear interpolation if not enough points
201
+ t_original = np.linspace(0, 1, N)
202
+ geometry_list = np.array(geometry_list)
203
+ new_geometry_list = []
204
+ derivatives = {}
205
+ for d in range(deriv_order + 1):
206
+ deriv_list = []
207
+ for t in t_values:
208
+ interp_point = []
209
+ for atom_idx in range(geometry_list.shape[1]):
210
+ for coord in range(3):
211
+ interp_func = interp1d(t_original, geometry_list[:, atom_idx, coord], kind='linear')
212
+ if d == 0:
213
+ interp_point.append(interp_func(t))
214
+ else:
215
+ # Simple finite difference for derivatives in fallback
216
+ values = interp_func(t_original)
217
+ deriv_values = np.gradient(values, t_original[1] - t_original[0], edge_order=2)
218
+ for _ in range(d - 1):
219
+ deriv_values = np.gradient(deriv_values, t_original[1] - t_original[0], edge_order=2)
220
+ interp_func_deriv = interp1d(t_original, deriv_values, kind='linear')
221
+ interp_point.append(interp_func_deriv(t))
222
+ deriv_list.append(np.array(interp_point).reshape(geometry_list.shape[1], 3))
223
+ derivatives[f'deriv_{d}'] = np.array(deriv_list)
224
+ if d == 0:
225
+ new_geometry_list = derivatives[f'deriv_{d}']
226
+ return new_geometry_list, derivatives
227
+
228
+ geometry_list = np.array(geometry_list)
229
+ derivatives = {}
230
+ for d in range(deriv_order + 1):
231
+ deriv_path = []
232
+ for atom_idx in range(geometry_list.shape[1]):
233
+ for coord in range(3):
234
+ smoothed = savgol_filter(geometry_list[:, atom_idx, coord], window_length, polyorder, deriv=d)
235
+ deriv_path.append(smoothed)
236
+ derivatives[f'deriv_{d}'] = np.array(deriv_path).reshape(N, geometry_list.shape[1], 3)
237
+
238
+ # Interpolate to desired spacing for each derivative
239
+ t_original = np.linspace(0, 1, N)
240
+ for d in range(deriv_order + 1):
241
+ deriv_interp = []
242
+ for t in t_values:
243
+ interp_point = []
244
+ for atom_idx in range(geometry_list.shape[1]):
245
+ for coord in range(3):
246
+ interp_func = interp1d(t_original, derivatives[f'deriv_{d}'][:, atom_idx, coord], kind='linear')
247
+ interp_point.append(interp_func(t))
248
+ deriv_interp.append(np.array(interp_point).reshape(geometry_list.shape[1], 3))
249
+ derivatives[f'deriv_{d}'] = np.array(deriv_interp)
250
+
251
+ new_geometry_list = derivatives['deriv_0']
252
+ return new_geometry_list, derivatives
@@ -0,0 +1,353 @@
1
+ import numpy as np
2
+ from scipy.interpolate import CubicSpline, make_interp_spline, PchipInterpolator
3
+
4
+ from multioptpy.Utils.calc_tools import calc_path_length_list
5
+ from multioptpy.Interpolation.linear_interpolation import distribute_geometry_by_length, distribute_geometry
6
+
7
+ def spline_interpolation(
8
+ structures,
9
+ n_points=20,
10
+ method='hermite', # Options: 'linear', 'quadratic', 'cubic', 'b-spline', 'hermite'
11
+ bc_type='natural',
12
+ spline_degree=5,
13
+ window=None
14
+ ):
15
+ """
16
+ Interpolates between atomic structures using various spline interpolation methods.
17
+ Supports global and local (windowed) interpolation. For local interpolation,
18
+ only the nearest 'window' structures before and after each node are used.
19
+
20
+ Args:
21
+ structures: list of np.ndarray, each of shape (n_atoms, 3)
22
+ n_points: int, number of interpolated structures to generate
23
+ method: str, interpolation method to use
24
+ Options:
25
+ 'linear' : Piecewise linear interpolation
26
+ 'quadratic': Quadratic spline (degree 2, via make_interp_spline)
27
+ 'cubic' : Cubic spline (via CubicSpline)
28
+ 'b-spline' : B-spline of arbitrary degree (use spline_degree)
29
+ 'hermite' : Hermite spline (PCHIP monotonic cubic)
30
+ bc_type: str or tuple, boundary condition type for cubic spline
31
+ spline_degree: int or None, degree for B-spline interpolation
32
+ window: int or None, number of structures before and after each node to use for local interpolation.
33
+ If None, uses global interpolation.
34
+
35
+ Returns:
36
+ path: np.ndarray of shape (n_points, n_atoms, 3)
37
+ """
38
+ structures = np.array(structures) # (n_structures, n_atoms, 3)
39
+ n_structures = structures.shape[0]
40
+ n_atoms = structures.shape[1]
41
+
42
+ if window is None:
43
+ # Global interpolation
44
+ x = np.linspace(0, 1, n_structures)
45
+ t_values = np.linspace(0, 1, n_points)
46
+ path = np.zeros((n_points, n_atoms, 3))
47
+
48
+ for atom_idx in range(n_atoms):
49
+ for coord_idx in range(3):
50
+ y = structures[:, atom_idx, coord_idx]
51
+ if method == 'linear':
52
+ path[:, atom_idx, coord_idx] = np.interp(t_values, x, y)
53
+ elif method == 'quadratic':
54
+ spline = make_interp_spline(x, y, k=2)
55
+ path[:, atom_idx, coord_idx] = spline(t_values)
56
+ elif method == 'cubic':
57
+ spline = CubicSpline(x, y, bc_type=bc_type)
58
+ path[:, atom_idx, coord_idx] = spline(t_values)
59
+ elif method == 'b-spline':
60
+ deg = spline_degree if spline_degree is not None else 3
61
+ spline = make_interp_spline(x, y, k=deg)
62
+ path[:, atom_idx, coord_idx] = spline(t_values)
63
+ elif method == 'hermite':
64
+ interpolator = PchipInterpolator(x, y)
65
+ path[:, atom_idx, coord_idx] = interpolator(t_values)
66
+ else:
67
+ raise ValueError(
68
+ f"Unknown method '{method}'. Supported methods are: 'linear', 'quadratic', 'cubic', 'b-spline', 'hermite'."
69
+ )
70
+ print(f"Using global '{method}' spline interpolation"
71
+ f"{' degree ' + str(spline_degree) if method == 'b-spline' else ''}"
72
+ f"{' and bc_type ' + str(bc_type) if method == 'cubic' else ''}.")
73
+ return path
74
+
75
+ # Local (windowed) interpolation
76
+ segments = []
77
+ for idx in range(n_structures - 1):
78
+ start = max(0, idx - window)
79
+ end = min(n_structures, idx + window + 2) # +2 to include idx+1
80
+ local_structures = structures[start:end]
81
+ local_n_structures = local_structures.shape[0]
82
+ local_x = np.linspace(0, 1, local_n_structures)
83
+ if idx - window >= 0:
84
+ t0 = local_x[window]
85
+ t1 = local_x[window + 1]
86
+ else:
87
+ t0 = local_x[idx]
88
+ t1 = local_x[idx + 1]
89
+ t_values = np.linspace(t0, t1, n_points)
90
+
91
+ local_path = np.zeros((n_points, n_atoms, 3))
92
+ for atom_idx in range(n_atoms):
93
+ for coord_idx in range(3):
94
+ y = local_structures[:, atom_idx, coord_idx]
95
+ if method == 'linear':
96
+ local_path[:, atom_idx, coord_idx] = np.interp(t_values, local_x, y)
97
+ elif method == 'quadratic':
98
+ spline = make_interp_spline(local_x, y, k=2)
99
+ local_path[:, atom_idx, coord_idx] = spline(t_values)
100
+ elif method == 'cubic':
101
+ spline = CubicSpline(local_x, y, bc_type=bc_type)
102
+ local_path[:, atom_idx, coord_idx] = spline(t_values)
103
+ elif method == 'b-spline':
104
+ deg = spline_degree if spline_degree is not None else 3
105
+ spline = make_interp_spline(local_x, y, k=deg)
106
+ local_path[:, atom_idx, coord_idx] = spline(t_values)
107
+ elif method == 'hermite':
108
+ interpolator = PchipInterpolator(local_x, y)
109
+ local_path[:, atom_idx, coord_idx] = interpolator(t_values)
110
+ else:
111
+ raise ValueError(
112
+ f"Unknown method '{method}'. Supported methods are: 'linear', 'quadratic', 'cubic', 'b-spline', 'hermite'."
113
+ )
114
+ segments.append(local_path)
115
+
116
+ # Concatenate all segments, removing duplicate points at boundaries
117
+ result_path = [segments[0][0]]
118
+ for seg in segments:
119
+ result_path.extend(seg[1:])
120
+ result_path = np.array(result_path)
121
+
122
+ print(f"Using local '{method}' spline interpolation with window={window},"
123
+ f"{' degree ' + str(spline_degree) if method == 'b-spline' else ''}"
124
+ f"{' and bc_type ' + str(bc_type) if method == 'cubic' else ''}.")
125
+
126
+ # Resample to exactly n_points along the full path
127
+ result_path = resample_path(result_path, n_points)
128
+ return result_path
129
+
130
+ def resample_path(path, n_points):
131
+ """
132
+ Resamples a path to have exactly n_points structures.
133
+ path: np.ndarray of shape (N, n_atoms, 3)
134
+ n_points: int, desired number of output structures
135
+ Returns: np.ndarray of shape (n_points, n_atoms, 3)
136
+ """
137
+ N = path.shape[0]
138
+ indices = np.linspace(0, N-1, n_points)
139
+ resampled = np.array([path[int(round(idx))] for idx in indices])
140
+ return resampled
141
+
142
+ # Example usage:
143
+ # S0, S1, S2, S3, S4, S5 = np.array(...), np.array(...), np.array(...), np.array(...), np.array(...), np.array(...)
144
+ # path_global_cubic = spline_interpolation([S0, S1, S2, S3, S4, S5], n_points=20, method='cubic')
145
+ # path_local_bspline = spline_interpolation([S0, S1, S2, S3, S4, S5], n_points=10, method='b-spline', spline_degree=5, window=3)
146
+ # path_local_hermite = spline_interpolation([S0, S1, S2, S3, S4, S5], n_points=10, method='hermite', window=3)
147
+
148
+
149
+
150
+
151
+ def distribute_geometry_by_length_spline(geometry_list, angstrom_spacing, spline_degree=3):
152
+ """
153
+ Distribute geometries by specified distance spacing using B-spline interpolation
154
+
155
+ Parameters:
156
+ -----------
157
+ geometry_list : list
158
+ List of geometry arrays/objects
159
+ angstrom_spacing : float
160
+ Desired spacing in Angstroms between distributed geometries
161
+ spline_degree : int, optional
162
+ Degree of the spline interpolation (default=3 for cubic splines)
163
+
164
+ Returns:
165
+ --------
166
+ list
167
+ New list of geometries distributed at regular intervals using spline interpolation
168
+ """
169
+ # Handle edge cases
170
+ if len(geometry_list) <= 1:
171
+ return geometry_list.copy()
172
+
173
+ # Calculate path lengths
174
+ path_length_list = calc_path_length_list(geometry_list)
175
+ total_length = path_length_list[-1]
176
+
177
+ # Handle case with extremely short paths
178
+ if total_length < angstrom_spacing:
179
+ return [geometry_list[0], geometry_list[-1]]
180
+
181
+ # Ensure spline degree is not greater than number of points minus 1
182
+ k = min(spline_degree, len(geometry_list) - 1)
183
+
184
+ # Check for duplicate path lengths and handle them
185
+ # First create a list of unique indices to keep
186
+ unique_indices = []
187
+ unique_path_lengths = []
188
+ for i, length in enumerate(path_length_list):
189
+ if i == 0 or abs(length - path_length_list[i-1]) > 1e-10: # Use small epsilon for floating-point comparison
190
+ unique_indices.append(i)
191
+ unique_path_lengths.append(length)
192
+
193
+ # If we have duplicates, filter the geometry list and path lengths
194
+ if len(unique_indices) < len(geometry_list):
195
+ print(f"Warning: Removed {len(geometry_list) - len(unique_indices)} duplicate geometries from the path")
196
+ unique_geometries = [geometry_list[i] for i in unique_indices]
197
+ path_length_list = unique_path_lengths
198
+ geometry_list = unique_geometries
199
+
200
+
201
+ # If after removing duplicates we have too few points for the requested spline degree,
202
+ # reduce the degree accordingly
203
+ k = min(k, len(geometry_list) - 1)
204
+
205
+ # If we have too few points for splines, fall back to linear interpolation
206
+ if len(geometry_list) <= 2 or k < 1:
207
+ print("Warning: Insufficient points for spline interpolation. Falling back to linear interpolation.")
208
+ return distribute_geometry_by_length(geometry_list, angstrom_spacing)
209
+
210
+ # Convert geometries to numpy arrays
211
+ geom_arrays = [np.asarray(geom) for geom in geometry_list]
212
+ original_shape = geom_arrays[0].shape
213
+ flattened_geoms = [g.flatten() for g in geom_arrays]
214
+
215
+ # Determine the number of coordinates to interpolate
216
+ n_coords = len(flattened_geoms[0])
217
+
218
+ # Create new geometry list with the first point
219
+ new_geometry_list = []
220
+
221
+ try:
222
+ # Create splines for each coordinate
223
+ splines = []
224
+ for i in range(n_coords):
225
+ # Extract the i-th coordinate from each geometry
226
+ coord_values = [g[i] for g in flattened_geoms]
227
+ # Create a spline for this coordinate
228
+ spline = make_interp_spline(path_length_list, coord_values, k=k)
229
+ splines.append(spline)
230
+
231
+ # Determine sample points along the path including endpoints
232
+ num_points = max(2, int(np.ceil(total_length / angstrom_spacing)) + 1)
233
+ sample_distances = np.linspace(0, total_length, num_points)
234
+
235
+ # Generate new geometries
236
+ for dist in sample_distances:
237
+ # Evaluate all splines at this distance
238
+ interpolated_coords = [spline(dist) for spline in splines]
239
+ # Reshape back to original geometry shape
240
+ new_geom = np.array(interpolated_coords).reshape(original_shape)
241
+ new_geometry_list.append(new_geom)
242
+
243
+ except Exception as e:
244
+ print(f"Warning: Spline interpolation failed with error: {e}. Falling back to linear interpolation.")
245
+ return distribute_geometry_by_length(geometry_list, angstrom_spacing)
246
+
247
+ # Replace first and last points with original endpoints to guarantee exact endpoints
248
+ new_geometry_list[0] = geom_arrays[0]
249
+ new_geometry_list[-1] = geom_arrays[-1]
250
+ new_geometry_list.append(geometry_list[-1])
251
+
252
+ return new_geometry_list
253
+
254
+
255
+
256
+ def distribute_geometry_spline(geometry_list, spline_degree=3):
257
+ """
258
+ Distribute geometries evenly along the path using spline interpolation
259
+
260
+ Parameters:
261
+ -----------
262
+ geometry_list : list
263
+ List of geometry arrays/objects
264
+ spline_degree : int, optional
265
+ Degree of the spline interpolation (default=3 for cubic splines)
266
+
267
+ Returns:
268
+ --------
269
+ list
270
+ New list of geometries distributed at regular intervals using spline interpolation
271
+ """
272
+ nnode = len(geometry_list)
273
+
274
+ # Handle edge cases
275
+ if nnode <= 2:
276
+ return geometry_list.copy()
277
+
278
+ path_length_list = calc_path_length_list(geometry_list)
279
+ total_length = path_length_list[-1]
280
+ node_dist = total_length / (nnode-1)
281
+
282
+ # Ensure spline degree is not greater than number of points minus 1
283
+ k = min(spline_degree, nnode - 1)
284
+
285
+ # Check for duplicate path lengths and handle them
286
+ unique_indices = []
287
+ unique_path_lengths = []
288
+ for i, length in enumerate(path_length_list):
289
+ if i == 0 or abs(length - path_length_list[i-1]) > 1e-10:
290
+ unique_indices.append(i)
291
+ unique_path_lengths.append(length)
292
+
293
+ # If we have duplicates, filter the geometry list and path lengths
294
+ if len(unique_indices) < nnode:
295
+ filtered_geometries = [geometry_list[i] for i in unique_indices]
296
+ unique_path_lengths_array = np.array(unique_path_lengths)
297
+
298
+ # If after filtering we have too few points for spline, fall back to linear
299
+ if len(filtered_geometries) <= k:
300
+ return distribute_geometry(geometry_list)
301
+
302
+ # Convert geometries to arrays for interpolation
303
+ geom_arrays = [np.asarray(geom) for geom in filtered_geometries]
304
+
305
+ else:
306
+ # No duplicates found
307
+ geom_arrays = [np.asarray(geom) for geom in geometry_list]
308
+ unique_path_lengths_array = np.array(path_length_list)
309
+
310
+ # Determine shape of geometries
311
+ original_shape = geom_arrays[0].shape
312
+ flattened_geoms = [g.flatten() for g in geom_arrays]
313
+ n_coords = len(flattened_geoms[0])
314
+
315
+ # Create new geometry list with the first point
316
+ new_geometry_list = [geometry_list[0]]
317
+
318
+ try:
319
+ # Create splines for each coordinate
320
+ splines = []
321
+ for i in range(n_coords):
322
+ # Extract the i-th coordinate from each geometry
323
+ coord_values = [g[i] for g in flattened_geoms]
324
+ # Create a spline for this coordinate
325
+ spline = make_interp_spline(unique_path_lengths_array, coord_values, k=k)
326
+ splines.append(spline)
327
+
328
+ # Generate new geometries at evenly spaced positions
329
+ for i in range(1, nnode-1):
330
+ dist = i * node_dist
331
+
332
+ # Evaluate all splines at this distance
333
+ interpolated_coords = [spline(dist) for spline in splines]
334
+ # Reshape back to original geometry shape
335
+ new_geom = np.array(interpolated_coords).reshape(original_shape)
336
+ new_geometry_list.append(new_geom)
337
+
338
+ except Exception as e:
339
+ # Fall back to linear interpolation if spline fails
340
+ print(f"Warning: Spline interpolation failed with error: {e}. Falling back to linear interpolation.")
341
+ return distribute_geometry(geometry_list)
342
+
343
+ # Always include the last geometry
344
+ new_geometry_list.append(geometry_list[-1])
345
+
346
+ return new_geometry_list
347
+
348
+
349
+
350
+
351
+
352
+
353
+
File without changes