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,785 @@
1
+ import numpy as np
2
+ from scipy.optimize import least_squares
3
+ from multioptpy.Parameters.parameter import covalent_radii_lib
4
+
5
+ #ref.: https://github.com/virtualzx-nad/geodesic-interpolate
6
+
7
+ def distribute_geometry_geodesic(geometry_list, n_points=None, spacing=None, spline_degree=3,
8
+ max_iterations=50, tolerance=1e-4, element_list=None, verbose=True):
9
+ """
10
+ Performs geodesic interpolation between molecular geometries using exponential and logarithmic maps.
11
+
12
+ This function distributes geometries along a geodesic path in the internal coordinate space,
13
+ resulting in more realistic interpolations compared to linear methods in Cartesian space.
14
+
15
+ Parameters:
16
+ -----------
17
+ geometry_list : list
18
+ List of geometry arrays/objects with shape (n_atoms, 3)
19
+ n_points : int, optional
20
+ Number of points to generate along the path (including endpoints)
21
+ spacing : float, optional
22
+ If specified, points will be distributed with this spacing (in Angstroms)
23
+ If both n_points and spacing are specified, n_points takes precedence
24
+ spline_degree : int, optional
25
+ Degree of the spline for initial path estimation (default=3)
26
+ max_iterations : int, optional
27
+ Maximum number of geodesic optimization iterations
28
+ tolerance : float, optional
29
+ Convergence tolerance for geodesic optimization
30
+ element_list : list, optional
31
+ List of element symbols for each atom, used for better distance weighting
32
+ verbose : bool, optional
33
+ If True, print optimization progress (default=True)
34
+
35
+ Returns:
36
+ --------
37
+ list
38
+ List of geometries distributed along the geodesic path
39
+ """
40
+ if verbose:
41
+ print("="*60)
42
+ print("Starting Geodesic Interpolation")
43
+ print("="*60)
44
+
45
+ # Handle input validation
46
+ if len(geometry_list) < 2:
47
+ if verbose:
48
+ print("Warning: Less than 2 geometries provided. Returning original list.")
49
+ return geometry_list.copy()
50
+
51
+ # Convert all geometries to numpy arrays
52
+ geoms = [np.array(geom, dtype=np.float64) for geom in geometry_list]
53
+ n_atoms = geoms[0].shape[0]
54
+
55
+ if verbose:
56
+ print(f"Input: {len(geometry_list)} geometries with {n_atoms} atoms each")
57
+
58
+ # Use default atoms if none provided
59
+ atoms = element_list if element_list is not None else ['C'] * n_atoms
60
+
61
+ # Align geometries to minimize RMSD
62
+ if verbose:
63
+ print("Aligning geometries to minimize RMSD...")
64
+ max_rmsd, aligned_geoms = align_path(geoms, verbose=verbose)
65
+
66
+ # Determine the number of points to generate
67
+ if n_points is None and spacing is None:
68
+ n_points = len(geometry_list)
69
+ elif n_points is None:
70
+ # Estimate path length for spacing-based distribution
71
+ path_length = estimate_path_length(aligned_geoms)
72
+ n_points = max(3, int(np.ceil(path_length / spacing)) + 1)
73
+ if verbose:
74
+ print(f"Estimated path length: {path_length:.3f} Å")
75
+ print(f"Using spacing {spacing:.3f} Å -> {n_points} points")
76
+
77
+ if verbose:
78
+ print(f"Target number of geometries: {n_points}")
79
+
80
+ # Step 1: Generate initial path with correct number of points using redistribution
81
+ if verbose:
82
+ print("\nStep 1: Redistributing geometries...")
83
+ redistributed_path = redistribute(atoms, aligned_geoms, n_points, tol=tolerance, verbose=verbose)
84
+
85
+ # Step 2: Apply geodesic smoothing to optimize the path
86
+ if verbose:
87
+ print(f"\nStep 2: Geodesic optimization (max_iter={max_iterations}, tol={tolerance})")
88
+ geodesic = Geodesic(atoms, redistributed_path, scaler=1.7,
89
+ threshold=3, min_neighbors=4,
90
+ verbose=verbose, friction=1e-3)
91
+
92
+ # Perform geodesic optimization
93
+ optimized_path = geodesic.smooth(tol=tolerance, max_iter=max_iterations, verbose=verbose)
94
+
95
+ if verbose:
96
+ print("="*60)
97
+ print("Geodesic Interpolation Completed Successfully!")
98
+ print("="*60)
99
+
100
+ return [geom.copy() for geom in optimized_path]
101
+
102
+
103
+ def align_path(path, verbose=False):
104
+ """
105
+ Rotate and translate images to minimize RMSD movements along the path.
106
+ Also moves the geometric center of all images to the origin.
107
+ Optimized with vectorized operations where possible.
108
+ """
109
+ path = np.array(path, dtype=np.float64)
110
+ # Center the first geometry
111
+ path[0] -= np.mean(path[0], axis=0)
112
+
113
+ max_rmsd = 0
114
+ # Sequential alignment (cannot be fully vectorized due to dependency)
115
+ for i in range(len(path) - 1):
116
+ rmsd, path[i + 1] = align_geom(path[i], path[i + 1])
117
+ max_rmsd = max(max_rmsd, rmsd)
118
+ if verbose:
119
+ print(f" Aligned geometry {i+1}: RMSD = {rmsd:.4f} Å")
120
+
121
+ if verbose:
122
+ print(f"Alignment completed. Maximum RMSD: {max_rmsd:.4f} Å")
123
+
124
+ return max_rmsd, path
125
+
126
+
127
+ def align_geom(refgeom, geom):
128
+ """
129
+ Find translation/rotation that moves a given geometry to maximally overlap
130
+ with a reference geometry using the Kabsch algorithm.
131
+ """
132
+ center = np.mean(refgeom, axis=0) # Find the geometric center
133
+ ref2 = refgeom - center
134
+ geom2 = geom - np.mean(geom, axis=0)
135
+ cov = np.dot(geom2.T, ref2)
136
+ v, sv, w = np.linalg.svd(cov)
137
+ if np.linalg.det(v) * np.linalg.det(w) < 0:
138
+ sv[-1] = -sv[-1]
139
+ v[:, -1] = -v[:, -1]
140
+ u = np.dot(v, w)
141
+ new_geom = np.dot(geom2, u) + center
142
+ rmsd = np.sqrt(np.mean((new_geom - refgeom) ** 2))
143
+ return rmsd, new_geom
144
+
145
+
146
+ def estimate_path_length(geometries):
147
+ """
148
+ Estimates the total path length in Cartesian space.
149
+ """
150
+ geometries = np.array(geometries)
151
+ # Vectorized calculation of consecutive differences
152
+ diffs = geometries[1:] - geometries[:-1]
153
+ # Calculate RMSD for each consecutive pair
154
+ rmsds = np.sqrt(np.mean(np.sum(diffs**2, axis=2), axis=1))
155
+ return np.sum(rmsds)
156
+
157
+
158
+
159
+ def get_bond_list(geom, atoms=None, threshold=4, min_neighbors=4, snapshots=30, bond_threshold=1.8,
160
+ enforce=()):
161
+ """
162
+ Get the list of all the important atom pairs.
163
+ Samples a number of snapshots from a list of geometries to generate all
164
+ distances that are below a given threshold in any of them.
165
+ """
166
+ from scipy.spatial import KDTree
167
+
168
+ # Type casting and value checks on input parameters
169
+ geom = np.asarray(geom)
170
+ if len(geom.shape) < 3:
171
+ # If there is only one geometry or it is flattened, promote to 3d
172
+ geom = geom.reshape(1, -1, 3)
173
+ min_neighbors = min(min_neighbors, geom.shape[1] - 1)
174
+
175
+ # Determine which images to be used to determine distances
176
+ snapshots = min(len(geom), snapshots)
177
+ images = [0, len(geom) - 1]
178
+ if snapshots > 2:
179
+ images.extend(np.random.choice(range(1, snapshots - 1), snapshots - 2, replace=False))
180
+
181
+ # Get neighbor list for included geometry and merge them (fully vectorized)
182
+ rijset = set(enforce)
183
+
184
+ for image in images:
185
+ tree = KDTree(geom[image])
186
+ pairs = tree.query_pairs(threshold)
187
+ rijset.update(pairs)
188
+ bonded = tree.query_pairs(bond_threshold)
189
+
190
+ if bonded:
191
+ bonded_array = np.array(list(bonded))
192
+ n_atoms = geom.shape[1]
193
+
194
+ # Choose optimal algorithm based on system size and bonding density
195
+ n_bonds = len(bonded_array)
196
+ density = n_bonds / (n_atoms * (n_atoms - 1) / 2)
197
+
198
+ if n_atoms > 500 or density < 0.1:
199
+ # Sparse approach for large or sparse systems
200
+ from scipy.sparse import csr_matrix
201
+
202
+ # Build sparse adjacency matrix with self-connections
203
+ all_i = np.concatenate([bonded_array[:, 0], bonded_array[:, 1], np.arange(n_atoms)])
204
+ all_j = np.concatenate([bonded_array[:, 1], bonded_array[:, 0], np.arange(n_atoms)])
205
+ adj_sparse = csr_matrix((np.ones(len(all_i)), (all_i, all_j)),
206
+ shape=(n_atoms, n_atoms), dtype=bool)
207
+
208
+ # Compute extended connections via sparse matrix multiplication
209
+ extended_sparse = np.dot(adj_sparse, adj_sparse)
210
+ extended_coo = extended_sparse.tocoo()
211
+
212
+ # Filter to upper triangular (i < j) and add to rijset
213
+ mask = extended_coo.row < extended_coo.col
214
+ new_pairs = set(zip(extended_coo.row[mask], extended_coo.col[mask]))
215
+ rijset.update(new_pairs)
216
+ else:
217
+ # Dense approach for smaller dense systems
218
+ adj_matrix = np.eye(n_atoms, dtype=bool) # Start with identity (self-connections)
219
+ adj_matrix[bonded_array[:, 0], bonded_array[:, 1]] = True
220
+ adj_matrix[bonded_array[:, 1], bonded_array[:, 0]] = True
221
+
222
+ # Extended connections via matrix multiplication
223
+ extended = np.dot(adj_matrix, adj_matrix)
224
+
225
+ # Extract upper triangular indices efficiently
226
+ triu_indices = np.triu_indices(n_atoms, k=1)
227
+ valid_mask = extended[triu_indices]
228
+ i_valid = triu_indices[0][valid_mask]
229
+ j_valid = triu_indices[1][valid_mask]
230
+
231
+ # Add to rijset
232
+ new_pairs = set(zip(i_valid, j_valid))
233
+ rijset.update(new_pairs)
234
+ rijlist = sorted(rijset)
235
+
236
+ # Check neighbor count to make sure `min_neighbors` is satisfied using vectorized operations
237
+ rijlist = sorted(rijset)
238
+ if not rijlist:
239
+ re = np.array([])
240
+ return rijlist, re
241
+
242
+ # Vectorized neighbor counting
243
+ rij_array = np.array(rijlist)
244
+ count = np.zeros(geom.shape[1], dtype=int)
245
+ unique_i, counts_i = np.unique(rij_array[:, 0], return_counts=True)
246
+ unique_j, counts_j = np.unique(rij_array[:, 1], return_counts=True)
247
+
248
+ count[unique_i] += counts_i
249
+ count[unique_j] += counts_j
250
+
251
+ # Find atoms with insufficient neighbors
252
+ insufficient_atoms = np.where(count < min_neighbors)[0]
253
+
254
+ for idx in insufficient_atoms:
255
+ tree = KDTree(geom[-1])
256
+ _, neighbors = tree.query(geom[-1, idx], k=min_neighbors + 1)
257
+ for i in neighbors:
258
+ if i == idx:
259
+ continue
260
+ pair = tuple(sorted([i, idx]))
261
+ if pair in rijset:
262
+ continue
263
+ else:
264
+ rijset.add(pair)
265
+ rijlist.append(pair)
266
+ count[i] += 1
267
+ count[idx] += 1
268
+
269
+ if atoms is None:
270
+ re = np.full(len(rijlist), 2.0)
271
+ else:
272
+ radius = np.array([covalent_radii_lib(atom.capitalize()) for atom in atoms])
273
+ re = np.array([radius[i] + radius[j] for i, j in rijlist])
274
+
275
+ return rijlist, re
276
+
277
+
278
+ def morse_scaler(re=1.5, alpha=1.7, beta=0.01):
279
+ """
280
+ Returns a scaling function that determines the metric of the internal
281
+ coordinates using morse potential. Optimized for vectorized operations.
282
+ """
283
+ # Ensure re is a numpy array for vectorized operations
284
+ re = np.asarray(re)
285
+
286
+ def scaler(x):
287
+ x = np.asarray(x)
288
+ ratio = x / re
289
+
290
+ # Vectorized exponential and ratio calculations
291
+ val1 = np.exp(alpha * (1 - ratio))
292
+ val2 = beta / ratio
293
+
294
+ # Vectorized derivative calculation
295
+ dval = -alpha / re * val1 - val2 / x
296
+
297
+ return val1 + val2, dval
298
+ return scaler
299
+
300
+
301
+ def compute_wij(geom, rij_list, func):
302
+ """
303
+ Calculate a list of scaled distances and their derivatives using vectorized operations
304
+ """
305
+ geom = np.asarray(geom).reshape(-1, 3)
306
+ nrij = len(rij_list)
307
+
308
+ if nrij == 0:
309
+ return np.array([]), np.zeros((0, geom.size))
310
+
311
+ rij, bmat = compute_rij(geom, rij_list)
312
+ wij, dwdr = func(rij)
313
+
314
+ # Vectorized multiplication of gradients
315
+ bmat_reshaped = bmat.reshape(nrij, -1)
316
+ bmat_scaled = bmat_reshaped * dwdr[:, np.newaxis]
317
+
318
+ return wij, bmat_scaled
319
+
320
+
321
+ def compute_rij(geom, rij_list):
322
+ """
323
+ Calculate a list of distances and their derivatives using vectorized operations
324
+ """
325
+ geom = np.asarray(geom).reshape(-1, 3)
326
+ nrij = len(rij_list)
327
+
328
+ if nrij == 0:
329
+ return np.array([]), np.zeros((0, len(geom), 3))
330
+
331
+ # Convert rij_list to numpy arrays for vectorized operations
332
+ rij_array = np.array(rij_list)
333
+ i_indices = rij_array[:, 0]
334
+ j_indices = rij_array[:, 1]
335
+
336
+ # Vectorized calculation of displacement vectors
337
+ dvec = geom[i_indices] - geom[j_indices]
338
+
339
+ # Vectorized distance calculation
340
+ rij = np.linalg.norm(dvec, axis=1)
341
+
342
+ # Avoid division by zero
343
+ safe_rij = np.where(rij > 1e-12, rij, 1e-12)
344
+ grad = dvec / safe_rij[:, np.newaxis]
345
+
346
+ # Initialize B matrix
347
+ bmat = np.zeros((nrij, len(geom), 3))
348
+
349
+ # Vectorized gradient assignment
350
+ bmat[np.arange(nrij), i_indices] = grad
351
+ bmat[np.arange(nrij), j_indices] = -grad
352
+
353
+ return rij, bmat
354
+
355
+
356
+ def mid_point(atoms, geom1, geom2, tol=1e-2, nudge=0.01, threshold=4, verbose=False):
357
+ """
358
+ Find the Cartesian geometry that has internal coordinate values closest to the average of
359
+ two geometries.
360
+ """
361
+ # Process the initial geometries, construct coordinate system and obtain average internals
362
+ geom1, geom2 = np.array(geom1), np.array(geom2)
363
+ add_pair = set()
364
+ geom_list = [geom1, geom2]
365
+
366
+ iteration = 0
367
+ while True:
368
+ iteration += 1
369
+ rijlist, re = get_bond_list(geom_list, threshold=threshold + 1, enforce=add_pair)
370
+ scaler = morse_scaler(alpha=0.7, re=re)
371
+ w1, _ = compute_wij(geom1, rijlist, scaler)
372
+ w2, _ = compute_wij(geom2, rijlist, scaler)
373
+ w = (w1 + w2) / 2
374
+ d_min, x_min = np.inf, None
375
+ friction = 0.1 / np.sqrt(geom1.shape[0])
376
+
377
+ if verbose:
378
+ print(f" Mid-point iteration {iteration}: {len(rijlist)} coordinate pairs")
379
+
380
+ # The inner loop performs minimization using either end-point as the starting guess
381
+ for coef_idx, coef in enumerate([0.02, 0.98]):
382
+ x0 = (geom1 * coef + (1 - coef) * geom2).ravel()
383
+ x0 += nudge * np.random.random_sample(x0.shape)
384
+ if verbose:
385
+ print(f" Starting optimization from coef={coef:.2f}")
386
+
387
+ result = least_squares(
388
+ lambda x: np.concatenate([compute_wij(x, rijlist, scaler)[0] - w, (x-x0)*friction]),
389
+ x0,
390
+ lambda x: np.vstack([compute_wij(x, rijlist, scaler)[1], np.identity(x.size) * friction]),
391
+ ftol=tol,
392
+ gtol=tol
393
+ )
394
+
395
+ x_mid = result['x'].reshape(-1, 3)
396
+
397
+ # Take the interpolated geometry, construct new pair list and check for new contacts
398
+ new_list = geom_list + [x_mid]
399
+ new_rij, _ = get_bond_list(new_list, threshold=threshold, min_neighbors=0)
400
+ extras = set(new_rij) - set(rijlist)
401
+
402
+ if extras:
403
+ if verbose:
404
+ print(f" New contacts detected. Adding {len(extras)} pairs.")
405
+ # Update pair list then go back to the minimization loop if new contacts are found
406
+ geom_list = new_list
407
+ add_pair |= extras
408
+ break
409
+
410
+ # Perform local geodesic optimization for the new image
411
+ smoother = Geodesic(atoms, [geom1, x_mid, geom2], 0.7, threshold=threshold, verbose=False, friction=1)
412
+ smoother.compute_disps()
413
+
414
+ # Vectorized width calculation
415
+ geom_array = np.array([geom1, geom2])
416
+ diffs = geom_array - smoother.path[1]
417
+ widths = np.sqrt(np.mean(np.sum(diffs**2, axis=2), axis=1))
418
+ width = np.max(widths)
419
+
420
+ dist, x_mid = width + smoother.length, smoother.path[1]
421
+ if verbose:
422
+ print(f" Path length: {dist:.6f} after {result['nfev']} evaluations")
423
+
424
+ if dist < d_min:
425
+ d_min, x_min = dist, x_mid
426
+
427
+ else: # Both starting guesses finished without new atom pairs. Minimization successful
428
+ if verbose:
429
+ print(f" Mid-point converged with path length: {d_min:.6f}")
430
+ break
431
+
432
+ return x_min
433
+
434
+
435
+ def redistribute(atoms, geoms, nimages, tol=1e-2, verbose=False):
436
+ """
437
+ Add or remove images so that the path length matches the desired number.
438
+ """
439
+ _, geoms = align_path(geoms)
440
+ geoms = list(geoms)
441
+
442
+ if verbose:
443
+ print(f" Initial path has {len(geoms)} geometries, target: {nimages}")
444
+
445
+ # If there are too few images, add bisection points
446
+ while len(geoms) < nimages:
447
+ # Vectorized distance calculation
448
+ geoms_array = np.array(geoms)
449
+ diffs = geoms_array[1:] - geoms_array[:-1]
450
+ dists = np.sqrt(np.mean(np.sum(diffs**2, axis=2), axis=1))
451
+ max_i = np.argmax(dists)
452
+
453
+ if verbose:
454
+ print(f" Inserting geometry between {max_i} and {max_i + 1} (RMSD={dists[max_i]:.3f})")
455
+
456
+ insertion = mid_point(atoms, geoms[max_i], geoms[max_i + 1], tol, verbose=verbose)
457
+ _, insertion = align_geom(geoms[max_i], insertion)
458
+ geoms.insert(max_i + 1, insertion)
459
+ geoms = list(align_path(geoms)[1])
460
+
461
+ if verbose:
462
+ print(f" New path length: {len(geoms)}")
463
+
464
+ # If there are too many images, remove points
465
+ while len(geoms) > nimages:
466
+ # Vectorized distance calculation for removal
467
+ geoms_array = np.array(geoms)
468
+ diffs = geoms_array[2:] - geoms_array[:-2]
469
+ dists = np.sqrt(np.mean(np.sum(diffs**2, axis=2), axis=1))
470
+ min_i = np.argmin(dists)
471
+
472
+ if verbose:
473
+ print(f" Removing geometry {min_i + 1} (merged RMSD={dists[min_i]:.3f})")
474
+
475
+ del geoms[min_i + 1]
476
+ geoms = list(align_path(geoms)[1])
477
+
478
+ if verbose:
479
+ print(f" New path length: {len(geoms)}")
480
+
481
+ return geoms
482
+
483
+
484
+ class Geodesic(object):
485
+ """
486
+ Optimizer to obtain geodesic in redundant internal coordinates.
487
+ Core part is the calculation of the path length in the internal metric.
488
+ """
489
+ def __init__(self, atoms, path, scaler=1.7, threshold=3, min_neighbors=4, verbose=True,
490
+ friction=1e-3):
491
+ """Initialize the interpolater"""
492
+ rmsd0, self.path = align_path(path)
493
+ if verbose:
494
+ print(f" Maximum RMSD change in initial path: {rmsd0:.4f} Å")
495
+
496
+ if self.path.ndim != 3:
497
+ raise ValueError('The path to be interpolated must have 3 dimensions')
498
+ self.nimages, self.natoms, _ = self.path.shape
499
+
500
+ # Construct coordinates
501
+ self.rij_list, self.re = get_bond_list(path, atoms, threshold=threshold, min_neighbors=min_neighbors)
502
+ if isinstance(scaler, float):
503
+ self.scaler = morse_scaler(re=self.re, alpha=1.7)
504
+ else:
505
+ self.scaler = scaler
506
+ self.nrij = len(self.rij_list)
507
+ self.friction = friction
508
+ self.verbose = verbose
509
+
510
+ # Initialize internal storages
511
+ if verbose:
512
+ print(f" Geodesic setup: {self.nimages} images, {self.natoms} atoms, {self.nrij} coordinate pairs")
513
+
514
+ self.neval = 0
515
+ self.w = [None] * len(path)
516
+ self.dwdR = [None] * len(path)
517
+ self.X_mid = [None] * (len(path) - 1)
518
+ self.w_mid = [None] * (len(path) - 1)
519
+ self.dwdR_mid = [None] * (len(path) - 1)
520
+ self.disps = self.grad = self.segment = None
521
+ self.conv_path = []
522
+
523
+ # Track optimization progress
524
+ self.optimization_history = {
525
+ 'iterations': [],
526
+ 'path_lengths': [],
527
+ 'optimalities': [],
528
+ 'rmsds': []
529
+ }
530
+
531
+ def get_optimization_summary(self):
532
+ """
533
+ Return a summary of the optimization process
534
+ """
535
+ if not self.optimization_history['iterations']:
536
+ return "No optimization performed yet."
537
+
538
+ summary = f"""
539
+ Geodesic Optimization Summary:
540
+ ==============================
541
+ Total iterations: {len(self.optimization_history['iterations'])}
542
+ Initial path length: {self.optimization_history['path_lengths'][0]:.6f}
543
+ Final path length: {self.optimization_history['path_lengths'][-1]:.6f}
544
+ Path length reduction: {self.optimization_history['path_lengths'][0] - self.optimization_history['path_lengths'][-1]:.6f}
545
+ Final optimality: {self.optimization_history['optimalities'][-1]:.3e}
546
+ Final RMSD: {self.optimization_history['rmsds'][-1]:.4f} Å
547
+ """
548
+ return summary
549
+
550
+ def print_optimization_progress(self):
551
+ """
552
+ Print detailed optimization progress
553
+ """
554
+ if not self.optimization_history['iterations']:
555
+ print("No optimization data available.")
556
+ return
557
+
558
+ print("\nDetailed Optimization Progress:")
559
+ print("-" * 50)
560
+ print("Iter | Path Length | Optimality | RMSD (Å)")
561
+ print("-" * 50)
562
+
563
+ for i, (iteration, length, opt, rmsd) in enumerate(zip(
564
+ self.optimization_history['iterations'],
565
+ self.optimization_history['path_lengths'],
566
+ self.optimization_history['optimalities'],
567
+ self.optimization_history['rmsds']
568
+ )):
569
+ if i % 5 == 0 or i == len(self.optimization_history['iterations']) - 1:
570
+ print(f"{iteration:4d} | {length:13.6f} | {opt:9.3e} | {rmsd:8.4f}")
571
+ print("-" * 50)
572
+
573
+ def update_intc(self):
574
+ """
575
+ Adjust unknown locations of mid points and compute missing values of
576
+ internal coordinates and their derivatives.
577
+ """
578
+ for i, (X, w, dwdR) in enumerate(zip(self.path, self.w, self.dwdR)):
579
+ if w is None:
580
+ self.w[i], self.dwdR[i] = compute_wij(X, self.rij_list, self.scaler)
581
+ for i, (X0, X1, w) in enumerate(zip(self.path, self.path[1:], self.w_mid)):
582
+ if w is None:
583
+ self.X_mid[i] = Xm = (X0 + X1) / 2
584
+ self.w_mid[i], self.dwdR_mid[i] = compute_wij(Xm, self.rij_list, self.scaler)
585
+
586
+ def update_geometry(self, X, start, end):
587
+ """
588
+ Update the geometry of a segment of the path, then set the corresponding internal
589
+ coordinate, derivatives and midpoint locations to unknown
590
+ """
591
+ X = X.reshape(self.path[start:end].shape)
592
+ if np.array_equal(X, self.path[start:end]):
593
+ return False
594
+ self.path[start:end] = X
595
+ for i in range(start, end):
596
+ self.w_mid[i] = self.w[i] = None
597
+ self.w_mid[start - 1] = None
598
+ return True
599
+
600
+ def compute_disps(self, start=1, end=-1, dx=None, friction=1e-3):
601
+ """
602
+ Compute displacement vectors and total length between two images using vectorized operations.
603
+ Only recalculate internal coordinates if they are unknown.
604
+ """
605
+ if end < 0:
606
+ end += self.nimages
607
+ self.update_intc()
608
+
609
+ # Vectorized calculation of displacement vectors in each segment
610
+ w_left = np.array(self.w[start - 1:end])
611
+ w_mid_left = np.array(self.w_mid[start - 1:end])
612
+ w_right = np.array(self.w[start:end + 1])
613
+ w_mid_right = np.array(self.w_mid[start - 1:end])
614
+
615
+ vecs_l = w_mid_left - w_left
616
+ vecs_r = w_right - w_mid_right
617
+
618
+ # Vectorized norm calculation
619
+ norms_l = np.linalg.norm(vecs_l, axis=1)
620
+ norms_r = np.linalg.norm(vecs_r, axis=1)
621
+ self.length = np.sum(norms_l) + np.sum(norms_r)
622
+
623
+ if dx is None:
624
+ trans = np.zeros(self.path[start:end].size)
625
+ else:
626
+ trans = friction * dx # Translation from initial geometry with friction term
627
+
628
+ self.disps = np.concatenate([vecs_l.ravel(), vecs_r.ravel(), trans])
629
+ self.disps0 = self.disps[:len(vecs_l.ravel()) + len(vecs_r.ravel())]
630
+
631
+ def compute_disp_grad(self, start, end, friction=1e-3):
632
+ """
633
+ Compute derivatives of the displacement vectors with respect to the Cartesian coordinates
634
+ using vectorized operations
635
+ """
636
+ # Calculate derivatives of displacement vectors with respect to image Cartesians
637
+ l = end - start + 1
638
+ total_size = l * 2 * self.nrij + 3 * (end - start) * self.natoms
639
+ dof_size = (end - start) * 3 * self.natoms
640
+
641
+ self.grad = np.zeros((total_size, dof_size))
642
+ self.grad0 = self.grad[:l * 2 * self.nrij]
643
+
644
+ grad_shape = (l, self.nrij, end - start, 3 * self.natoms)
645
+ grad_l = self.grad[:l * self.nrij].reshape(grad_shape)
646
+ grad_r = self.grad[l * self.nrij:l * self.nrij * 2].reshape(grad_shape)
647
+
648
+ # Vectorized gradient calculation
649
+ for i, image in enumerate(range(start, end)):
650
+ dmid1 = self.dwdR_mid[image - 1] / 2
651
+ dmid2 = self.dwdR_mid[image] / 2
652
+ dwdR_image = self.dwdR[image]
653
+
654
+ if i + 1 < l:
655
+ grad_l[i + 1, :, i, :] = dmid2 - dwdR_image
656
+ grad_l[i, :, i, :] = dmid1
657
+
658
+ if i + 1 < l:
659
+ grad_r[i + 1, :, i, :] = -dmid2
660
+ grad_r[i, :, i, :] = dwdR_image - dmid1
661
+
662
+ # Vectorized friction term assignment
663
+ friction_indices = np.arange((end - start) * 3 * self.natoms)
664
+ self.grad[l * self.nrij * 2 + friction_indices, friction_indices] = friction
665
+
666
+ def compute_target_func(self, X=None, start=1, end=-1, x0=None, friction=1e-3):
667
+ """
668
+ Compute the vectorized target function, which is then used for least squares minimization.
669
+ """
670
+ if end < 0:
671
+ end += self.nimages
672
+ if X is not None and not self.update_geometry(X, start, end) and self.segment == (start, end):
673
+ return
674
+
675
+ self.segment = start, end
676
+ dx = np.zeros(self.path[start:end].size) if x0 is None else self.path[start:end].ravel() - x0.ravel()
677
+ self.compute_disps(start, end, dx=dx, friction=friction)
678
+ self.compute_disp_grad(start, end, friction=friction)
679
+ self.optimality = np.linalg.norm(np.einsum('i,i...', self.disps, self.grad), ord=np.inf)
680
+
681
+ # Calculate current RMSD for tracking
682
+ current_rmsd = 0.0
683
+ for i in range(len(self.path) - 1):
684
+ rmsd = np.sqrt(np.mean((self.path[i+1] - self.path[i]) ** 2))
685
+ current_rmsd = max(current_rmsd, rmsd)
686
+
687
+ # Store optimization history
688
+ self.optimization_history['iterations'].append(self.neval)
689
+ self.optimization_history['path_lengths'].append(self.length)
690
+ self.optimization_history['optimalities'].append(self.optimality)
691
+ self.optimization_history['rmsds'].append(current_rmsd)
692
+
693
+ if self.verbose:
694
+ # Check if we should print this iteration
695
+ should_print = True
696
+ if hasattr(self, '_progress_freq') and self._progress_freq > 1:
697
+ should_print = (self.neval % self._progress_freq == 0) or (self.neval == 0)
698
+
699
+ if should_print:
700
+ print(f" Iteration {self.neval:3d}: Length={self.length:10.6f}, |dL|={self.optimality:8.3e}, RMSD={current_rmsd:.4f}")
701
+
702
+ self.conv_path.append(self.path[1].copy())
703
+ self.neval += 1
704
+
705
+ def target_func(self, X, **kwargs):
706
+ """
707
+ Wrapper around `compute_target_func` to prevent repeated evaluation at the same geometry
708
+ """
709
+ self.compute_target_func(X, **kwargs)
710
+ return self.disps
711
+
712
+ def target_deriv(self, X, **kwargs):
713
+ """
714
+ Wrapper around `compute_target_func` to prevent repeated evaluation at the same geometry
715
+ """
716
+ self.compute_target_func(X, **kwargs)
717
+ return self.grad
718
+
719
+ def smooth(self, tol=1e-3, max_iter=50, start=1, end=-1, verbose=None, friction=None,
720
+ xref=None, progress_freq=1):
721
+ """
722
+ Minimize the path length as an overall function of the coordinates of all the images.
723
+
724
+ Parameters:
725
+ -----------
726
+ progress_freq : int, optional
727
+ Print progress every N iterations (default=1, set to 0 to disable progress)
728
+ """
729
+ if verbose is None:
730
+ verbose = self.verbose
731
+
732
+ # Temporarily adjust verbosity for iteration printing
733
+ original_verbose = self.verbose
734
+ if progress_freq == 0:
735
+ self.verbose = False
736
+ elif progress_freq > 1:
737
+ self._progress_freq = progress_freq
738
+ self._iteration_count = 0
739
+
740
+ X0 = np.array(self.path[start:end]).ravel()
741
+ if xref is None:
742
+ xref = X0
743
+ self.disps = self.grad = self.segment = None
744
+
745
+ if verbose:
746
+ print(f" Starting geodesic optimization with {len(X0)} degrees of freedom")
747
+ if progress_freq > 1:
748
+ print(f" Progress will be shown every {progress_freq} iterations")
749
+
750
+ if friction is None:
751
+ friction = self.friction
752
+
753
+ # Configure the keyword arguments that will be sent to the target function
754
+ kwargs = dict(start=start, end=end, x0=xref, friction=friction)
755
+ self.compute_target_func(**kwargs) # Compute length and optimality
756
+
757
+ if self.optimality > tol:
758
+ if verbose:
759
+ print(" Starting least-squares optimization...")
760
+
761
+ result = least_squares(self.target_func, X0, self.target_deriv, ftol=tol, gtol=tol,
762
+ max_nfev=max_iter, kwargs=kwargs, loss='soft_l1')
763
+ self.update_geometry(result['x'], start, end)
764
+
765
+ if verbose:
766
+ print(f" Optimization converged after {result['nfev']} iterations")
767
+ print(f" Success: {result['success']}, Message: {result['message']}")
768
+ else:
769
+ if verbose:
770
+ print(" Path already optimal, skipping optimization")
771
+
772
+ # Restore original verbosity
773
+ self.verbose = original_verbose
774
+
775
+ rmsd, self.path = align_path(self.path)
776
+
777
+ if verbose:
778
+ print(f" Final path length: {self.length:.6f}")
779
+ print(f" Maximum RMSD in path: {rmsd:.4f} Å")
780
+
781
+ # Print optimization summary
782
+ if len(self.optimization_history['iterations']) > 1:
783
+ print(self.get_optimization_summary())
784
+
785
+ return self.path