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,633 @@
1
+ import numpy as np
2
+ from scipy.linalg import cholesky, cho_solve, LinAlgError
3
+ from scipy.optimize import minimize
4
+ from scipy.spatial.distance import pdist, squareform
5
+ import time
6
+
7
+ class GPmin:
8
+ def __init__(self, **config):
9
+ """
10
+ Reference: https://doi.org/10.1063/1.5009347
11
+
12
+ Gaussian Process Minimizer (GPmin) with inverse distance coordinates.
13
+ Uses 1/R internal coordinates as input to predict energy and Cartesian forces.
14
+
15
+ Uses RBF kernel and performs surrogate PES optimization with improved step size control.
16
+ """
17
+ # GP parameters
18
+ self.n_points = 0
19
+ self.kernel_func = 'RBF'
20
+ self.noise_energy = 1e-8
21
+ self.length_scale = 0.5
22
+ self.sigma_f = 1.0
23
+
24
+ # Trust radius parameters - larger values for bigger steps
25
+ self.trust_radius = 1.0 # Increased initial trust radius
26
+ self.trust_radius_max = 5.0 # Increased maximum trust radius
27
+ self.trust_radius_min = 2.0 # Minimum trust radius
28
+ self.trust_increase = 5.0 # Larger trust radius increase factor
29
+ self.trust_decrease = 0.2 # Trust radius decrease factor
30
+
31
+ # Step size control
32
+ self.min_step_size = 0.1 # Minimum allowed step size for non-converged systems
33
+ self.step_scale = 1.0 # Global scale factor for step sizes
34
+ self.force_scale_factor = 0.3 # Scaling factor for force-based steps
35
+ self.adaptive_step = True # Enable adaptive step sizing
36
+
37
+ # Internal coordinates parameters
38
+ self.min_dist = 1e-7 # Minimum distance to avoid singularity in 1/R
39
+ self.dist_scale = 1.0 # Scaling factor for distances
40
+
41
+ # Surrogate PES optimization parameters
42
+ self.surrogate_maxiter = 20 # Maximum iterations for surrogate optimization
43
+ self.surrogate_ftol = 1e-1 # Function tolerance for surrogate optimization
44
+ self.surrogate_gtol = 1e-1 # Gradient tolerance for surrogate optimization
45
+
46
+ # Data selection parameters
47
+ self.max_gp_pts = 100
48
+ self.selection_method = 'recent' # 'recent', 'best', 'diverse'
49
+
50
+ # Optimization parameters
51
+ self.alpha = 0.3 # Increased step size factor (was 0.1)
52
+
53
+ # Flags
54
+ self.display_flag = True
55
+ self.Initialization = True
56
+
57
+ # Override defaults with provided config
58
+ for key, value in config.items():
59
+ if hasattr(self, key):
60
+ setattr(self, key, value)
61
+ self.hessian = None
62
+ self.bias_hessian = None
63
+
64
+ def run(self, geom_num_list, B_g, pre_B_g=[], pre_geom=[], B_e=0.0, pre_B_e=0.0,
65
+ pre_move_vector=[], initial_geom_num_list=[], g=[], pre_g=[]):
66
+ """
67
+ Perform one optimization step using GP regression with inverse distance coordinates.
68
+ """
69
+ # Start timer for performance measurement
70
+ start_time = time.time()
71
+
72
+ # Convert inputs to numpy arrays
73
+ geom_array = np.asarray(geom_num_list)
74
+ geom_shape = geom_array.shape
75
+ geom_flat = geom_array.flatten()
76
+ forces_flat = -np.asarray(B_g).flatten() # Forces are negative of gradients
77
+
78
+ if self.Initialization:
79
+ if self.display_flag:
80
+ print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Initializing GPmin with RBF kernel")
81
+ print(f"Trust radius: {self.trust_radius:.3f}, Max trust radius: {self.trust_radius_max:.3f}")
82
+
83
+ # Initialize data structures
84
+ self.dim = len(geom_flat)
85
+ self.n_atoms = len(geom_flat) // 3
86
+ self.geom_shape = geom_shape
87
+
88
+ # Calculate number of distances (n_atoms choose 2)
89
+ self.n_dist = self.n_atoms * (self.n_atoms - 1) // 2
90
+
91
+ # Convert to internal coordinates (inverse distances)
92
+ coords_3d = geom_array.reshape(-1, 3)
93
+ inv_dist = self._cart_to_inverse_dist(coords_3d)
94
+
95
+ # Store historical data
96
+ self.X_cart_all = np.array([geom_flat]) # Cartesian positions
97
+ self.X_all = np.array([inv_dist]) # Inverse distances
98
+ self.Y_all = np.array([float(B_e)]) # Energies
99
+ self.F_all = np.array([forces_flat]) # Forces (Cartesian)
100
+
101
+ # Selected training data
102
+ self.X = np.array([inv_dist])
103
+ self.Y = np.array([float(B_e)])
104
+ self.X_cart = np.array([geom_flat])
105
+ self.F = np.array([forces_flat])
106
+
107
+ # Store Jacobians for coordinate conversion
108
+ self.jacobians = [self._calc_jacobian(coords_3d)]
109
+
110
+ # Initialize GP model
111
+ self._init_gp()
112
+ self.Initialization = False
113
+ self.n_points = 1
114
+
115
+ # First step: take larger force-based step
116
+ force_norm = np.linalg.norm(forces_flat)
117
+ if force_norm > 1e-10:
118
+ # Use alpha * force direction * force_scale_factor
119
+ move_vector_flat = self.alpha * (forces_flat / force_norm) * self.force_scale_factor
120
+
121
+ # Scale the step to be at least min_step_size
122
+ step_size = np.linalg.norm(move_vector_flat)
123
+ if step_size < self.min_step_size:
124
+ move_vector_flat *= self.min_step_size / step_size
125
+
126
+ # Cap at trust radius
127
+ step_size = np.linalg.norm(move_vector_flat)
128
+ if step_size > self.trust_radius:
129
+ move_vector_flat *= self.trust_radius / step_size
130
+ else:
131
+ # If forces are nearly zero, use small random step
132
+ move_vector_flat = np.random.normal(0, 0.1, size=self.dim)
133
+ move_vector_flat *= self.min_step_size / np.linalg.norm(move_vector_flat)
134
+ else:
135
+ # Update data with new point
136
+ coords_3d = geom_array.reshape(-1, 3)
137
+ inv_dist = self._cart_to_inverse_dist(coords_3d)
138
+ jacobian = self._calc_jacobian(coords_3d)
139
+
140
+ self._update_data(geom_flat, inv_dist, jacobian, float(B_e), forces_flat)
141
+
142
+ # Optimize on the surrogate PES with improved step size control
143
+ move_vector_flat = self._optimize_on_surrogate_pes(geom_flat, forces_flat, float(B_e), inv_dist, jacobian)
144
+
145
+ # Apply adaptive step sizing if enabled
146
+ if self.adaptive_step:
147
+ move_vector_flat = self._adapt_step_size(move_vector_flat, forces_flat)
148
+
149
+ # Apply global step scale factor
150
+ move_vector_flat *= self.step_scale
151
+
152
+ # Final check on step size (trust radius constraint)
153
+ step_size = np.linalg.norm(move_vector_flat)
154
+ if step_size > self.trust_radius:
155
+ move_vector_flat *= self.trust_radius / step_size
156
+
157
+ # Reshape move vector to match input geometry shape
158
+ move_vector = move_vector_flat.reshape(geom_shape)
159
+
160
+ # Calculate elapsed time
161
+ elapsed_time = time.time() - start_time
162
+
163
+ if self.display_flag:
164
+ print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] GPmin with RBF kernel")
165
+ print(f"Points: {self.n_points}, Active points: {len(self.X)}")
166
+ print(f"Trust radius: {self.trust_radius:.6f}, Step magnitude: {np.linalg.norm(move_vector_flat):.6f}")
167
+
168
+ if self.n_points > 1:
169
+ # Evaluate model quality
170
+ pred_energy, pred_forces = self._predict(inv_dist, jacobian)
171
+ energy_error = abs(pred_energy - float(B_e))
172
+ force_error = np.linalg.norm(pred_forces - forces_flat)
173
+
174
+ print(f"GP energy prediction error: {energy_error:.6f}")
175
+ print(f"GP force prediction error: {force_error:.6f}")
176
+ print(f"Computation time: {elapsed_time:.3f} seconds")
177
+
178
+ return -1 * move_vector
179
+
180
+ def _adapt_step_size(self, step, forces):
181
+ """
182
+ Adaptively adjust step size based on forces and minimum step size.
183
+
184
+ Args:
185
+ step: Current step vector
186
+ forces: Current forces
187
+
188
+ Returns:
189
+ Adjusted step vector
190
+ """
191
+ step_size = np.linalg.norm(step)
192
+ force_norm = np.linalg.norm(forces)
193
+
194
+ # If step is too small but forces are significant, increase step size
195
+ if step_size < self.min_step_size and force_norm > 1e-3:
196
+ # Scale up to minimum step size while preserving direction
197
+ new_step = step * (self.min_step_size / step_size)
198
+
199
+ if self.display_flag:
200
+ print(f"Step size increased: {step_size:.6f} → {np.linalg.norm(new_step):.6f}")
201
+
202
+ return new_step
203
+
204
+ # If forces are large and step is relatively small compared to trust radius,
205
+ # consider taking a larger step in the force direction
206
+ if force_norm > 0.1 and step_size < 0.3 * self.trust_radius:
207
+ # Mix the current step with force direction
208
+ force_dir = forces / force_norm
209
+ mixed_step = 0.7 * step + 0.3 * force_dir * self.trust_radius
210
+ mixed_step_size = np.linalg.norm(mixed_step)
211
+
212
+ # Make sure we don't exceed trust radius
213
+ if mixed_step_size > self.trust_radius:
214
+ mixed_step *= self.trust_radius / mixed_step_size
215
+
216
+ if self.display_flag and np.linalg.norm(mixed_step) > step_size * 1.2:
217
+ print(f"Step enhanced with force direction: {step_size:.6f} → {np.linalg.norm(mixed_step):.6f}")
218
+
219
+ return mixed_step
220
+
221
+ return step
222
+
223
+ def _cart_to_inverse_dist(self, coords):
224
+ """Convert Cartesian coordinates to inverse distances."""
225
+ # Calculate pairwise distances
226
+ dist_matrix = squareform(pdist(coords))
227
+
228
+ # Extract upper triangular part (excluding diagonal)
229
+ upper_indices = np.triu_indices(len(coords), k=1)
230
+ distances = dist_matrix[upper_indices]
231
+
232
+ # Apply minimum distance threshold to avoid singularities
233
+ distances = np.maximum(distances, self.min_dist)
234
+
235
+ # Calculate inverse distances (1/R)
236
+ inv_dist = 1.0 / (distances * self.dist_scale)
237
+
238
+ return inv_dist
239
+
240
+ def _calc_jacobian(self, coords):
241
+ """
242
+ Calculate the Jacobian matrix for mapping between internal and Cartesian coordinates.
243
+ Returns a matrix of shape (n_dist, n_atoms*3).
244
+ """
245
+ n_atoms = len(coords)
246
+ n_cart = n_atoms * 3
247
+ n_dist = self.n_dist
248
+
249
+ jacobian = np.zeros((n_dist, n_cart))
250
+
251
+ # For each atom pair
252
+ idx = 0
253
+ for i in range(n_atoms):
254
+ for j in range(i+1, n_atoms):
255
+ # Calculate distance vector r_ij and its norm
256
+ r_ij = coords[j] - coords[i]
257
+ r_norm = np.linalg.norm(r_ij)
258
+
259
+ # Avoid division by zero
260
+ if r_norm < self.min_dist:
261
+ r_norm = self.min_dist
262
+
263
+ # Unit vector in direction of r_ij
264
+ r_unit = r_ij / r_norm
265
+
266
+ # For inverse distance (1/R), the derivative is -1/R^2 * unit_vector
267
+ factor = -1.0 / (r_norm**2 * self.dist_scale)
268
+
269
+ # Fill in the Jacobian
270
+ jacobian[idx, i*3:i*3+3] = -r_unit * factor # For atom i
271
+ jacobian[idx, j*3:j*3+3] = r_unit * factor # For atom j
272
+
273
+ idx += 1
274
+
275
+ return jacobian
276
+
277
+ def _init_gp(self):
278
+ """Initialize the Gaussian Process model."""
279
+ # Set length scale if not provided as array
280
+ if isinstance(self.length_scale, (int, float)):
281
+ self.length_scale = np.ones(self.n_dist) * self.length_scale
282
+
283
+ self._update_kernel()
284
+
285
+ def _update_data(self, cart_pos, inv_dist, jacobian, energy, forces):
286
+ """Add new data point to GP model and select training data."""
287
+ energy = float(energy)
288
+
289
+ # Add to history
290
+ self.X_cart_all = np.vstack([self.X_cart_all, [cart_pos]])
291
+ self.X_all = np.vstack([self.X_all, [inv_dist]])
292
+ self.Y_all = np.append(self.Y_all, energy)
293
+ self.F_all = np.vstack([self.F_all, [forces]])
294
+ self.jacobians.append(jacobian)
295
+ self.n_points += 1
296
+
297
+ # Select training data
298
+ self._select_training_data()
299
+
300
+ # Update kernel matrix
301
+ self._update_kernel()
302
+
303
+ # Update trust radius based on prediction accuracy
304
+ if self.n_points > 1:
305
+ prev_energy = float(self.Y_all[-2])
306
+ current_energy = float(self.Y_all[-1])
307
+
308
+ # Calculate relative energy improvement
309
+ rel_improvement = (prev_energy - current_energy) / (abs(prev_energy) + 1e-10)
310
+
311
+ # Update trust radius based on energy improvement
312
+ if current_energy < prev_energy:
313
+ # Good step - increase trust radius more aggressively
314
+ if rel_improvement > 0.05: # Significant improvement
315
+ self.trust_radius = min(self.trust_radius * self.trust_increase, self.trust_radius_max)
316
+ else: # Modest improvement
317
+ self.trust_radius = min(self.trust_radius * 1.2, self.trust_radius_max)
318
+ else:
319
+ # Energy increased - decrease trust radius
320
+ self.trust_radius = max(self.trust_radius * self.trust_decrease, self.trust_radius_min)
321
+
322
+ def _select_training_data(self):
323
+ """Select data points for GP training."""
324
+ n_total = len(self.X_all)
325
+ max_pts = min(self.max_gp_pts, n_total)
326
+
327
+ if self.selection_method == 'recent':
328
+ # Keep most recent points
329
+ indices = np.arange(n_total - max_pts, n_total)
330
+ elif self.selection_method == 'best':
331
+ # Keep lowest energy points
332
+ indices = np.argsort(self.Y_all)[:max_pts]
333
+ elif self.selection_method == 'diverse':
334
+ # Select diverse points + most recent
335
+ indices = self._select_diverse_points(max_pts)
336
+ else:
337
+ # Default to most recent
338
+ indices = np.arange(n_total - max_pts, n_total)
339
+
340
+ # Update active training data
341
+ self.X = self.X_all[indices]
342
+ self.X_cart = self.X_cart_all[indices]
343
+ self.Y = self.Y_all[indices]
344
+ self.F = self.F_all[indices]
345
+ self.active_jacobians = [self.jacobians[i] for i in indices]
346
+
347
+ def _select_diverse_points(self, max_pts):
348
+ """Select diverse set of points."""
349
+ n_total = len(self.X_all)
350
+
351
+ # Always include most recent point
352
+ selected_indices = [n_total - 1]
353
+
354
+ if n_total <= max_pts:
355
+ return np.arange(n_total)
356
+
357
+ # Calculate distances in inverse distance space
358
+ distances = squareform(pdist(self.X_all))
359
+
360
+ # Greedy selection
361
+ remaining = list(set(range(n_total)) - set(selected_indices))
362
+ while len(selected_indices) < max_pts and remaining:
363
+ # Find point with maximum minimum distance to selected points
364
+ min_distances = []
365
+ for idx in remaining:
366
+ min_dist = min(distances[idx, sel_idx] for sel_idx in selected_indices)
367
+ min_distances.append((min_dist, idx))
368
+
369
+ _, next_idx = max(min_distances)
370
+ selected_indices.append(next_idx)
371
+ remaining.remove(next_idx)
372
+
373
+ return np.array(sorted(selected_indices))
374
+
375
+ def _update_kernel(self):
376
+ """Update the kernel matrices for GP regression."""
377
+ n = len(self.X)
378
+
379
+ if n == 0:
380
+ return
381
+
382
+ # Create kernel matrix for energy prediction
383
+ K_ee = np.zeros((n, n))
384
+
385
+ # Calculate kernel between all pairs of points
386
+ for i in range(n):
387
+ for j in range(i+1):
388
+ K_ee[i, j] = self._rbf_kernel(self.X[i], self.X[j])
389
+ if i != j:
390
+ K_ee[j, i] = K_ee[i, j]
391
+
392
+ # Add noise to diagonal
393
+ np.fill_diagonal(K_ee, np.diag(K_ee) + self.noise_energy)
394
+
395
+ self.K_ee = K_ee
396
+
397
+ # Compute Cholesky decomposition with fallbacks for numerical stability
398
+ try:
399
+ self.L_ee = cholesky(self.K_ee, lower=True)
400
+ except LinAlgError:
401
+ np.fill_diagonal(self.K_ee, np.diag(self.K_ee) + self.noise_energy * 10.0)
402
+ try:
403
+ self.L_ee = cholesky(self.K_ee, lower=True)
404
+ except LinAlgError:
405
+ np.fill_diagonal(self.K_ee, np.diag(self.K_ee) + self.noise_energy * 100.0)
406
+ self.L_ee = cholesky(self.K_ee, lower=True)
407
+
408
+ def _rbf_kernel(self, x1, x2):
409
+ """
410
+ RBF (Squared Exponential) kernel function for inverse distance coordinates.
411
+ k(x, x') = σ² * exp(-0.5 * r²)
412
+ where r² is the squared distance between x and x'.
413
+ """
414
+ # Calculate squared distance with scaling
415
+ sq_dist = np.sum(((x1 - x2) / self.length_scale) ** 2)
416
+
417
+ # RBF kernel formula
418
+ return self.sigma_f * np.exp(-0.5 * sq_dist)
419
+
420
+ def _rbf_kernel_gradient(self, x1, x2):
421
+ """Gradient of the RBF kernel with respect to x2."""
422
+ diff = (x1 - x2) / (self.length_scale ** 2)
423
+ k = self._rbf_kernel(x1, x2)
424
+ return k * diff
425
+
426
+ def _predict(self, inv_dist, jacobian):
427
+ """
428
+ Predict energy and forces using the GP model.
429
+
430
+ Args:
431
+ inv_dist: Inverse distances (1/R) for the test point
432
+ jacobian: Jacobian matrix for coordinate conversion
433
+
434
+ Returns:
435
+ (pred_energy, pred_forces): Predicted energy and Cartesian forces
436
+ """
437
+ n = len(self.X)
438
+
439
+ if n == 0:
440
+ return 0.0, np.zeros(self.dim)
441
+
442
+ # Create kernel vector between test point and training points
443
+ k_e = np.zeros(n)
444
+ for i in range(n):
445
+ k_e[i] = self._rbf_kernel(inv_dist, self.X[i])
446
+
447
+ # Predict energy
448
+ alpha_e = cho_solve((self.L_ee, True), self.Y)
449
+ pred_energy = float(np.dot(k_e, alpha_e))
450
+
451
+ # Calculate partial derivatives dE/d(1/R)
452
+ grad_internal = np.zeros(self.n_dist)
453
+ for j in range(self.n_dist):
454
+ for i in range(n):
455
+ # Compute derivative of kernel wrt each inverse distance
456
+ x1 = inv_dist.copy()
457
+ x2 = self.X[i].copy()
458
+
459
+ # Compute analytical gradient for RBF kernel
460
+ grad_k = self._rbf_kernel_gradient(x1, x2)
461
+ dk_dr = -grad_k[j] # Negative because we're taking derivative wrt x1
462
+
463
+ grad_internal[j] += alpha_e[i] * dk_dr
464
+
465
+ # Convert from internal coordinate gradients to Cartesian forces
466
+ # F = -dE/dR = -J^T * dE/d(1/R)
467
+ pred_forces = -np.dot(jacobian.T, grad_internal)
468
+
469
+ return pred_energy, pred_forces
470
+
471
+ def _optimize_on_surrogate_pes(self, position, forces, energy, inv_dist, jacobian):
472
+ """
473
+ Perform optimization on the surrogate PES constructed by GPR.
474
+ This is a more thorough optimization than a single step.
475
+
476
+ Args:
477
+ position: Current Cartesian coordinates
478
+ forces: Current forces
479
+ energy: Current energy
480
+ inv_dist: Current inverse distances
481
+ jacobian: Current Jacobian matrix
482
+
483
+ Returns:
484
+ best_step: Optimized step vector
485
+ """
486
+ if self.n_points <= 1:
487
+ # If only one data point, use gradient-based step
488
+ norm_f = np.linalg.norm(forces)
489
+ if norm_f > 1e-10:
490
+ # Take a larger step using force_scale_factor
491
+ step = self.alpha * (forces / norm_f) * self.force_scale_factor
492
+ step_size = np.linalg.norm(step)
493
+
494
+ # Ensure minimum step size
495
+ if step_size < self.min_step_size:
496
+ step *= self.min_step_size / step_size
497
+
498
+ # Cap at trust radius
499
+ if step_size > self.trust_radius:
500
+ step *= self.trust_radius / step_size
501
+
502
+ return step
503
+ else:
504
+ # Small random step
505
+ step = np.random.normal(0, 0.1, size=self.dim)
506
+ step *= self.min_step_size / np.linalg.norm(step)
507
+ return step
508
+
509
+ if self.display_flag:
510
+ print(f"Starting optimization on surrogate PES (trust radius: {self.trust_radius:.4f})")
511
+
512
+ # Objective function for minimization on surrogate PES
513
+ def surrogate_objective(step):
514
+ # Calculate inverse distances for new position
515
+ new_coords = (position + step).reshape(-1, 3)
516
+ new_inv_dist = self._cart_to_inverse_dist(new_coords)
517
+ new_jacobian = self._calc_jacobian(new_coords)
518
+
519
+ # Predict energy at new position
520
+ pred_energy, _ = self._predict(new_inv_dist, new_jacobian)
521
+ return float(pred_energy)
522
+
523
+ # Gradient function (negative of predicted forces)
524
+ def surrogate_gradient(step):
525
+ # Calculate inverse distances and Jacobian for new position
526
+ new_coords = (position + step).reshape(-1, 3)
527
+ new_inv_dist = self._cart_to_inverse_dist(new_coords)
528
+ new_jacobian = self._calc_jacobian(new_coords)
529
+
530
+ # Predict forces (negative gradient)
531
+ _, pred_forces = self._predict(new_inv_dist, new_jacobian)
532
+ return -pred_forces # Negative because we're minimizing energy
533
+
534
+ # Initial guess: take larger step in force direction
535
+ norm_f = np.linalg.norm(forces)
536
+ if norm_f > 1e-10:
537
+ initial_step = self.trust_radius * 0.8 * forces / norm_f # 80% of trust radius
538
+ else:
539
+ # Random step with significant magnitude
540
+ initial_step = np.random.normal(0, 0.1, size=self.dim)
541
+ initial_step *= self.trust_radius * 0.5 / max(np.linalg.norm(initial_step), 1e-10)
542
+
543
+ # Trust region as spherical constraint
544
+ constraint = {'type': 'ineq',
545
+ 'fun': lambda x: self.trust_radius**2 - np.sum(x**2),
546
+ 'jac': lambda x: -2*x}
547
+
548
+ # Perform thorough optimization on surrogate PES
549
+ try:
550
+ result = minimize(
551
+ surrogate_objective,
552
+ initial_step,
553
+ method='SLSQP',
554
+ jac=surrogate_gradient,
555
+ constraints=[constraint],
556
+ options={
557
+ 'maxiter': self.surrogate_maxiter,
558
+ 'ftol': self.surrogate_ftol,
559
+ 'disp': False
560
+ }
561
+ )
562
+
563
+ if self.display_flag:
564
+ if result.success:
565
+ print(f"Surrogate optimization successful: {result.message}")
566
+ print(f"Final surrogate energy: {result.fun:.8f}")
567
+ print(f"Iterations: {result.nit}")
568
+ else:
569
+ print(f"Surrogate optimization warning: {result.message}")
570
+
571
+ move_vector = result.x
572
+
573
+ except Exception as e:
574
+ if self.display_flag:
575
+ print(f"Surrogate optimization failed: {e}")
576
+ # Use force-based step as fallback
577
+ if norm_f > 1e-10:
578
+ move_vector = self.trust_radius * 0.8 * forces / norm_f
579
+ else:
580
+ # Random step
581
+ move_vector = np.random.normal(0, 0.1, size=self.dim)
582
+ move_vector *= self.min_step_size / np.linalg.norm(move_vector)
583
+
584
+ # Calculate predicted improvement
585
+ new_coords = (position + move_vector).reshape(-1, 3)
586
+ new_inv_dist = self._cart_to_inverse_dist(new_coords)
587
+ new_jacobian = self._calc_jacobian(new_coords)
588
+ pred_new_energy, _ = self._predict(new_inv_dist, new_jacobian)
589
+
590
+ # Check if the optimization actually improved energy
591
+ if pred_new_energy >= energy:
592
+ # If no improvement, take force-based step instead
593
+ if self.display_flag:
594
+ print("Optimization didn't reduce energy, using force-based step")
595
+
596
+ if norm_f > 1e-10:
597
+ move_vector = self.trust_radius * 0.8 * forces / norm_f
598
+ else:
599
+ # Random step as last resort
600
+ move_vector = np.random.normal(0, 0.1, size=self.dim)
601
+ move_vector *= self.min_step_size / np.linalg.norm(move_vector)
602
+
603
+ # Ensure we don't exceed trust radius
604
+ step_size = np.linalg.norm(move_vector)
605
+ if step_size > self.trust_radius:
606
+ move_vector *= self.trust_radius / step_size
607
+
608
+ # Ensure minimum step size when forces are significant
609
+ if step_size < self.min_step_size and norm_f > 1e-3:
610
+ move_vector *= self.min_step_size / step_size
611
+
612
+ if self.display_flag:
613
+ pred_improvement = energy - pred_new_energy
614
+ final_step_size = np.linalg.norm(move_vector)
615
+ print(f"Predicted energy improvement: {pred_improvement:.6f}")
616
+ print(f"Step magnitude: {final_step_size:.6f}")
617
+
618
+ return move_vector
619
+
620
+ def set_hessian(self, hessian):
621
+ """Store the Hessian matrix (required interface)"""
622
+ self.hessian = hessian
623
+
624
+ def set_bias_hessian(self, bias_hessian):
625
+ """Store the bias Hessian matrix (required interface)"""
626
+ self.bias_hessian = bias_hessian
627
+
628
+
629
+ def get_hessian(self):
630
+ return self.hessian
631
+
632
+ def get_bias_hessian(self):
633
+ return self.bias_hessian