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,516 @@
1
+ import numpy as np
2
+ from scipy.optimize import minimize
3
+
4
+ from multioptpy.Parameters.parameter import (
5
+ covalent_radii_lib,
6
+ UFF_VDW_distance_lib,
7
+ number_element,
8
+ UnitValueLib,)
9
+
10
+ class IDPP:
11
+ def __init__(self):
12
+ #ref.: arXiv:1406.1512v1
13
+ self.iteration = 2000
14
+ self.lr = 0.01
15
+ self.threshold = 1e-4
16
+ return
17
+
18
+ def calc_obj_func(self, idpp_dist_matrix, dist_matrix):
19
+ idpp_upper_triangle_indices = np.triu_indices(idpp_dist_matrix.shape[0], k=1)
20
+ idpp_upper_triangle_distances = idpp_dist_matrix[idpp_upper_triangle_indices]
21
+ dist_upper_triangle_indices = np.triu_indices(dist_matrix.shape[0], k=1)
22
+ dist_upper_triangle_distances = dist_matrix[dist_upper_triangle_indices]
23
+ weight_func = (dist_upper_triangle_distances + 1e-15) ** (-4)
24
+ obj_func = np.sum(weight_func * (idpp_upper_triangle_distances - dist_upper_triangle_distances) ** 2.0)
25
+ return obj_func
26
+
27
+ def calc_obj_func_1st_deriv(self, pos, idpp_dist_matrix, dist_matrix):
28
+ diff = pos[:, np.newaxis, :] - pos[np.newaxis, :, :] # Shape: (N, N, 3)
29
+ distances = np.linalg.norm(diff, axis=-1) # Shape: (N, N)
30
+ valid_mask = distances > 0
31
+ unit_diff = np.zeros_like(diff)
32
+ unit_diff[valid_mask] = diff[valid_mask] / (distances[valid_mask][:, np.newaxis] + 1e-15)
33
+ w = (distances + 1e-15)**(-4)
34
+ dw_dr = -4 * (distances + 1e-15)**(-5)
35
+
36
+ diff_matrix = idpp_dist_matrix - dist_matrix
37
+ d_obj_func_d_qij = (
38
+ (dw_dr * diff_matrix**2 - 2.0 * w * diff_matrix)[:, :, np.newaxis]
39
+ * unit_diff
40
+ ) # Shape: (N, N, 3)
41
+
42
+ i_indices, j_indices = np.triu_indices(len(pos), k=1)
43
+
44
+ first_deriv = np.zeros_like(pos)
45
+ np.add.at(first_deriv, i_indices, d_obj_func_d_qij[i_indices, j_indices])
46
+ np.subtract.at(first_deriv, j_indices, d_obj_func_d_qij[i_indices, j_indices])
47
+ return first_deriv
48
+
49
+ def calc_idpp_dist_matrix(self, pos_list, n_node, number_of_node):
50
+ init_pos = pos_list[0]
51
+ term_pos = pos_list[-1]
52
+ init_pos_diff = init_pos[:, np.newaxis, :] - init_pos[np.newaxis, :, :]
53
+ init_pos_dist_matrix = np.sqrt(np.sum(init_pos_diff**2, axis=-1))
54
+ term_pos_diff = term_pos[:, np.newaxis, :] - term_pos[np.newaxis, :, :]
55
+ term_pos_dist_matrix = np.sqrt(np.sum(term_pos_diff**2, axis=-1))
56
+ idpp_dist_matrix = init_pos_dist_matrix + number_of_node * (term_pos_dist_matrix - init_pos_dist_matrix) / (n_node - 1)
57
+
58
+ return idpp_dist_matrix
59
+
60
+ def calc_dist_matrix(self, pos):
61
+ pos_diff = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
62
+ dist_matrix = np.sqrt(np.sum(pos_diff**2, axis=-1))
63
+ return dist_matrix
64
+
65
+ def get_func_and_deriv(self, pos_list, n_node, number_of_node):
66
+ dist_matrix = self.calc_dist_matrix(pos_list[number_of_node])
67
+ idpp_dist_matrix = self.calc_idpp_dist_matrix(pos_list, n_node, number_of_node)
68
+ obj_func = self.calc_obj_func(idpp_dist_matrix, dist_matrix)
69
+ first_deriv = self.calc_obj_func_1st_deriv(pos_list[number_of_node], idpp_dist_matrix, dist_matrix)
70
+
71
+ return obj_func, first_deriv
72
+
73
+
74
+ def opt_path(self, geometry_list, element_list, memory_size=30):
75
+ """
76
+ Optimize the path using L-BFGS algorithm with the original step size limiting.
77
+
78
+ Parameters:
79
+ -----------
80
+ geometry_list : list of numpy arrays
81
+ List of geometries to optimize
82
+ element_list : list
83
+ List of elements (preserved for compatibility)
84
+ memory_size : int
85
+ Number of correction pairs to store for L-BFGS
86
+
87
+ Returns:
88
+ --------
89
+ list of numpy arrays
90
+ Optimized geometries
91
+ """
92
+ print("IDPP Optimization with L-BFGS")
93
+
94
+ # Initialize L-BFGS memory for each image
95
+ s_list = [[] for _ in range(len(geometry_list))]
96
+ y_list = [[] for _ in range(len(geometry_list))]
97
+ rho_list = [[] for _ in range(len(geometry_list))]
98
+
99
+ def lbfgs_direction(gradient, j):
100
+ """Compute the L-BFGS search direction using two-loop recursion"""
101
+ if len(s_list[j]) == 0:
102
+ return -gradient
103
+
104
+ q = gradient.copy()
105
+ alpha_list = []
106
+
107
+ # First loop: compute alpha values and update q
108
+ for i in range(len(s_list[j])-1, -1, -1):
109
+ alpha = rho_list[j][i] * np.sum(s_list[j][i] * q)
110
+ alpha_list.insert(0, alpha)
111
+ q = q - alpha * y_list[j][i]
112
+
113
+ # Scale with gamma
114
+ i = len(s_list[j]) - 1
115
+ denominator = np.sum(y_list[j][i] * y_list[j][i]) # Avoid division by zero
116
+ if np.abs(denominator) > 1e-10:
117
+ gamma = np.sum(s_list[j][i] * y_list[j][i]) / denominator
118
+ else:
119
+ gamma = 0
120
+ r = gamma * q
121
+
122
+ # Second loop: compute search direction
123
+ for i in range(len(s_list[j])):
124
+ beta = rho_list[j][i] * np.sum(y_list[j][i] * r)
125
+ r = r + s_list[j][i] * (alpha_list[i] - beta)
126
+
127
+ return -r
128
+
129
+ for i in range(self.iteration):
130
+ obj_func_list = []
131
+
132
+ for j in range(len(geometry_list)):
133
+ if j == 0 or j == len(geometry_list) - 1:
134
+ continue
135
+
136
+ # Save current position for computing displacement later
137
+ current_pos = geometry_list[j].copy()
138
+
139
+ # Get objective function and gradient
140
+ obj_func, gradient = self.get_func_and_deriv(geometry_list, len(geometry_list), j)
141
+ obj_func_list.append(obj_func)
142
+ gradient *= -1
143
+ # Compute search direction using L-BFGS
144
+ search_dir = lbfgs_direction(gradient, j)
145
+
146
+ # Apply the original step size limiting algorithm
147
+ step_norm = min(self.lr, np.linalg.norm(search_dir))
148
+ if np.linalg.norm(search_dir) > 1e-10: # Avoid division by zero
149
+ norm_step = search_dir / np.linalg.norm(search_dir)
150
+ geometry_list[j] -= step_norm * norm_step
151
+
152
+ # Update L-BFGS memory after taking the step
153
+ new_obj_func, new_gradient = self.get_func_and_deriv(geometry_list, len(geometry_list), j)
154
+
155
+ # Compute s and y vectors
156
+ s = geometry_list[j] - current_pos
157
+ y = new_gradient - gradient
158
+
159
+ # Only update memory if curvature condition is satisfied
160
+ sy_product = np.sum(s * y)
161
+ if sy_product > 1e-10:
162
+ # Manage memory size
163
+ if len(s_list[j]) >= memory_size:
164
+ s_list[j].pop(0)
165
+ y_list[j].pop(0)
166
+ rho_list[j].pop(0)
167
+
168
+ s_list[j].append(s)
169
+ y_list[j].append(y)
170
+ rho_list[j].append(1.0 / sy_product)
171
+
172
+ if i % 200 == 0:
173
+ print("ITR: ", i)
174
+ print("Objective function (Max): ", max(obj_func_list))
175
+
176
+ if max(obj_func_list) < self.threshold:
177
+ print("ITR: ", i)
178
+ print("IDPP Converged!!!")
179
+ break
180
+
181
+ print("IDPP Optimization Done.")
182
+ return geometry_list
183
+
184
+
185
+
186
+ class CFB_ENM:
187
+ """
188
+ Implements a standalone Correlated Flat-Bottom Elastic Network Model (CFB-ENM)
189
+ for optimizing reaction paths. The potential is based on the logic from dmf.py.
190
+
191
+ This class identifies quartets of atoms involved in bond-making and -breaking
192
+ events between a reactant and product structure. It then applies a specialized
193
+ potential function to these quartets to guide the path optimization.
194
+
195
+ The path is optimized using an L-BFGS algorithm implemented from scratch.
196
+ ref. : S.-i. Koda and S. Saito, Flat-bottom Elastic Network Model for Generating Improved Plausible Reaction Paths, JCTC, 20, 7176−7187 (2024). doi: 10.1021/acs.jctc.4c00792
197
+ S.-i. Koda and S. Saito, Correlated Flat-bottom Elastic Network Model for Improved Bond Rearrangement in Reaction Paths, JCTC, 21, 3513−3522 (2025). doi: 10.1021/acs.jctc.4c01549
198
+
199
+ """
200
+
201
+ def __init__(self, iteration=2000, lr=0.01, threshold=1e-4, bond_scale=1.25,
202
+ corr0_scale=1.10, corr1_scale=1.50, corr2_scale=1.60,
203
+ eps=0.05, pivotal=True, single=True, remove_fourmembered=True):
204
+ """
205
+ Initializes the CFB_ENM optimizer.
206
+
207
+ Parameters:
208
+ -----------
209
+ iteration : int
210
+ Maximum number of optimization iterations.
211
+ lr : float
212
+ Learning rate or maximum step size for the L-BFGS update.
213
+ threshold : float
214
+ Convergence threshold for the objective function.
215
+ bond_scale : float
216
+ Factor to determine bonding from covalent radii.
217
+ corr0_scale, corr1_scale, corr2_scale : float
218
+ Scaling factors for correlation distance thresholds.
219
+ eps : float
220
+ Smoothing parameter for the potential function.
221
+ pivotal, single, remove_fourmembered : bool
222
+ Flags to control quartet identification logic.
223
+ """
224
+ self.iteration = int(iteration) # FIX: Ensure iteration is an integer
225
+ self.lr = lr
226
+ self.threshold = threshold
227
+
228
+ # Parameters for CFB_ENM potential from dmf.py
229
+ self.bond_scale = bond_scale
230
+ self.corr0_scale = corr0_scale
231
+ self.corr1_scale = corr1_scale
232
+ self.corr2_scale = corr2_scale
233
+ self.eps = eps
234
+ self.pivotal = pivotal
235
+ self.single = single
236
+ self.remove_fourmembered = remove_fourmembered
237
+
238
+ # These will be populated by _initialize_potential
239
+ self.quartets = []
240
+ self.d_corr0 = None
241
+ self.d_corr1 = None
242
+ self.d_corr2 = None
243
+ self.bohr2ang = UnitValueLib().bohr2angstroms
244
+ return
245
+
246
+ def _get_connectivity_matrix(self, pos, element_list):
247
+ """
248
+ Determines the adjacency matrix for a given geometry.
249
+ """
250
+ radii = np.array([covalent_radii_lib(el) * self.bohr2ang for el in element_list])
251
+ r_cov = radii[:, np.newaxis] + radii[np.newaxis, :]
252
+
253
+ dist_matrix = self.calc_dist_matrix(pos)
254
+
255
+ J = (dist_matrix / r_cov) < self.bond_scale
256
+ np.fill_diagonal(J, False)
257
+ return J, dist_matrix
258
+
259
+ def _get_quartets(self, J_only_r, J_only_p, J_both,
260
+ pivotal=True, single=True, remove_fourmembered=True):
261
+ """
262
+ Identifies quartets of atoms involved in correlated motion.
263
+ This is a direct adaptation of the logic from dmf.py.
264
+ """
265
+ J2 = np.dot(J_both, J_both)
266
+ quartets = []
267
+
268
+ if pivotal:
269
+ if single:
270
+ pivots = np.where((np.sum(J_only_r, axis=1) == 1)
271
+ & (np.sum(J_only_p, axis=1) == 1))[0]
272
+ else:
273
+ pivots = np.where(np.any(J_only_r, axis=1)
274
+ & np.any(J_only_p, axis=1))[0]
275
+ for i in pivots:
276
+ only_r = np.where(J_only_r[i])[0]
277
+ only_p = np.where(J_only_p[i])[0]
278
+ for j in only_r:
279
+ for k in only_p:
280
+ if not (remove_fourmembered and J2[j, k]):
281
+ quartets.append([i, j, i, k])
282
+ else:
283
+ # Non-pivotal logic (adapted from dmf.py)
284
+ pairs_only_r = list(zip(*np.where(np.triu(J_only_r, k=1))))
285
+ pairs_only_p = list(zip(*np.where(np.triu(J_only_p, k=1))))
286
+
287
+ for pr in pairs_only_r:
288
+ for pp in pairs_only_p:
289
+ q = list(pr) + list(pp)
290
+ is_fourmembered = False
291
+ if remove_fourmembered:
292
+ unique_atoms = set(q)
293
+ if len(unique_atoms) == 4:
294
+ is_fourmembered = (J_both[q[0], q[2]] and J_both[q[1], q[3]]) or \
295
+ (J_both[q[0], q[3]] and J_both[q[1], q[2]])
296
+ elif len(unique_atoms) == 3:
297
+ # Find the two atoms that appear once
298
+ counts = {atom: q.count(atom) for atom in unique_atoms}
299
+ uniq_idxs = [atom for atom, count in counts.items() if count == 1]
300
+ if len(uniq_idxs) == 2:
301
+ is_fourmembered = J2[uniq_idxs[0], uniq_idxs[1]]
302
+
303
+ if not is_fourmembered:
304
+ quartets.append(q)
305
+ return quartets
306
+
307
+ def _initialize_potential(self, reactant_pos, product_pos, element_list):
308
+ """
309
+ Initializes the parameters for the CFB-ENM potential function based on
310
+ reactant and product structures.
311
+ """
312
+ natoms = len(element_list)
313
+ images = [reactant_pos, product_pos]
314
+
315
+ Js = []
316
+ d_bonds_list = []
317
+ for pos in images:
318
+ J, d = self._get_connectivity_matrix(pos, element_list)
319
+ Js.append(J)
320
+ d_bonds_list.append(np.where(J, d, 0.0))
321
+
322
+ d_bond = np.max(np.array(d_bonds_list), axis=0)
323
+
324
+ J_only_r = Js[0] & (~Js[1])
325
+ J_only_p = Js[1] & (~Js[0])
326
+ J_both = Js[0] & Js[1]
327
+
328
+ self.quartets = self._get_quartets(J_only_r, J_only_p, J_both,
329
+ self.pivotal, self.single, self.remove_fourmembered)
330
+
331
+ self.d_corr0 = self.corr0_scale * d_bond
332
+ self.d_corr1 = self.corr1_scale * d_bond
333
+ self.d_corr2 = self.corr2_scale * d_bond
334
+
335
+ # Ensure diagonal is zero
336
+ I = np.identity(natoms, dtype='bool')
337
+ self.d_corr0[I] = 0.0
338
+ self.d_corr1[I] = 0.0
339
+ self.d_corr2[I] = 0.0
340
+
341
+ print(f"CFB-ENM: Initialized potential with {len(self.quartets)} quartets.")
342
+
343
+ def calc_dist_matrix(self, pos):
344
+ """
345
+ Calculates the pairwise distance matrix for a given geometry.
346
+ """
347
+ pos_diff = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
348
+ dist_matrix = np.sqrt(np.sum(pos_diff**2, axis=-1))
349
+ return dist_matrix
350
+
351
+ def get_func_and_deriv(self, pos):
352
+ """
353
+ Calculates the CFB-ENM objective function and its analytical gradient
354
+ for a single image, based on the quartet potential from dmf.py.
355
+ """
356
+ natoms = pos.shape[0]
357
+ r = pos
358
+ dr = r[:, np.newaxis, :] - r
359
+ d = np.sqrt(np.sum(dr**2, axis=-1))
360
+
361
+ energy = 0.0
362
+ forces = np.zeros_like(pos)
363
+
364
+ d_d0 = d - self.d_corr0
365
+ d1_d0 = self.d_corr1 - self.d_corr0
366
+ d2_d0 = self.d_corr2 - self.d_corr0
367
+
368
+ for t in self.quartets:
369
+ # t = [atom1, atom2, atom3, atom4]
370
+ # Bond pair 1: (t[0], t[1]), Bond pair 2: (t[2], t[3])
371
+
372
+ # Check if atoms are in the repulsive region
373
+ if (d_d0[t[0], t[1]] > 0.0 and d_d0[t[2], t[3]] > 0.0):
374
+
375
+ pp = (d_d0[t[0], t[1]] * d_d0[t[2], t[3]]
376
+ - d1_d0[t[0], t[1]] * d1_d0[t[2], t[3]])
377
+
378
+ # Check if potential is active
379
+ if pp > 0.0:
380
+ dnm = (d2_d0[t[0], t[1]] * d2_d0[t[2], t[3]]
381
+ - d1_d0[t[0], t[1]] * d1_d0[t[2], t[3]])
382
+
383
+ # Avoid division by zero
384
+ if abs(dnm) < 1e-10: continue
385
+
386
+ pp_norm = pp / dnm
387
+ sqrt_pp2_eps2 = np.sqrt(pp_norm**2 + self.eps**2)
388
+
389
+ energy += sqrt_pp2_eps2 - self.eps
390
+
391
+ # Common factor for gradient
392
+ alpha = pp_norm / sqrt_pp2_eps2
393
+
394
+ # Gradient vectors
395
+ v1 = d_d0[t[2], t[3]] / d[t[0], t[1]] * (r[t[0]] - r[t[1]])
396
+ v2 = d_d0[t[0], t[1]] / d[t[2], t[3]] * (r[t[2]] - r[t[3]])
397
+
398
+ v1_norm = v1 / dnm
399
+ v2_norm = v2 / dnm
400
+
401
+ # Accumulate forces
402
+ forces[t[0]] -= alpha * v1_norm
403
+ forces[t[1]] += alpha * v1_norm
404
+ forces[t[2]] -= alpha * v2_norm
405
+ forces[t[3]] += alpha * v2_norm
406
+
407
+ # The optimizer expects the gradient of the objective function.
408
+ # Force is the negative of the gradient.
409
+ gradient = -forces
410
+
411
+ return energy, gradient
412
+
413
+ def opt_path(self, geometry_list, element_list, memory_size=30):
414
+ """
415
+ Optimize the path using L-BFGS algorithm.
416
+
417
+ Parameters:
418
+ -----------
419
+ geometry_list : list of np.ndarray
420
+ List of geometries (images) to optimize. The first and last are
421
+ fixed as reactant and product.
422
+ element_list : list of str
423
+ List of element symbols for the atoms.
424
+ memory_size : int
425
+ Number of correction pairs to store for L-BFGS.
426
+
427
+ Returns:
428
+ --------
429
+ list of np.ndarray
430
+ The list of optimized geometries.
431
+ """
432
+ print("CFB-ENM Optimization with L-BFGS")
433
+
434
+ # Initialize the potential based on the start and end points of the path
435
+ self._initialize_potential(geometry_list[0], geometry_list[-1], element_list)
436
+
437
+ # Initialize L-BFGS memory for each image
438
+ s_list = [[] for _ in range(len(geometry_list))]
439
+ y_list = [[] for _ in range(len(geometry_list))]
440
+ rho_list = [[] for _ in range(len(geometry_list))]
441
+
442
+ def lbfgs_direction(gradient, j):
443
+ """Compute the L-BFGS search direction using two-loop recursion"""
444
+ if len(s_list[j]) == 0:
445
+ return -gradient
446
+
447
+ q = gradient.copy()
448
+ alpha_list = []
449
+
450
+ for i in range(len(s_list[j])-1, -1, -1):
451
+ alpha = rho_list[j][i] * np.sum(s_list[j][i] * q)
452
+ alpha_list.insert(0, alpha)
453
+ q -= alpha * y_list[j][i]
454
+
455
+ i = len(s_list[j]) - 1
456
+ denominator = np.sum(y_list[j][i] * y_list[j][i])
457
+ if np.abs(denominator) > 1e-10:
458
+ gamma = np.sum(s_list[j][i] * y_list[j][i]) / denominator
459
+ else:
460
+ gamma = 1.0 # Fallback to steepest descent scaling
461
+ r = gamma * q
462
+
463
+ for i in range(len(s_list[j])):
464
+ beta = rho_list[j][i] * np.sum(y_list[j][i] * r)
465
+ r += s_list[j][i] * (alpha_list[i] - beta)
466
+
467
+ return -r
468
+
469
+ # FIX: Ensure self.iteration is an integer before using in range()
470
+ for i in range(int(self.iteration)):
471
+ obj_func_list = []
472
+
473
+ # Iterate over intermediate images (endpoints are fixed)
474
+ for j in range(1, len(geometry_list) - 1):
475
+ current_pos = geometry_list[j].copy()
476
+
477
+ obj_func, gradient = self.get_func_and_deriv(current_pos)
478
+ obj_func_list.append(obj_func)
479
+
480
+ search_dir = lbfgs_direction(gradient, j)
481
+
482
+ # Simple step size control
483
+ step_norm = self.lr
484
+ if np.linalg.norm(search_dir) > 1e-10:
485
+ norm_step = search_dir / np.linalg.norm(search_dir)
486
+ geometry_list[j] += step_norm * norm_step
487
+
488
+ # Update L-BFGS memory
489
+ new_pos = geometry_list[j]
490
+ _, new_gradient = self.get_func_and_deriv(new_pos)
491
+
492
+ s = new_pos - current_pos
493
+ y = new_gradient - gradient
494
+
495
+ sy_product = np.sum(s * y)
496
+ if sy_product > 1e-10: # Curvature condition
497
+ if len(s_list[j]) >= memory_size:
498
+ s_list[j].pop(0)
499
+ y_list[j].pop(0)
500
+ rho_list[j].pop(0)
501
+
502
+ s_list[j].append(s)
503
+ y_list[j].append(y)
504
+ rho_list[j].append(1.0 / sy_product)
505
+
506
+ if i % 200 == 0:
507
+ max_obj = max(obj_func_list) if obj_func_list else 0.0
508
+ print(f"ITR: {i}, Objective function (Max): {max_obj:.6e}")
509
+
510
+ if not obj_func_list or max(obj_func_list) < self.threshold:
511
+ print(f"ITR: {i}")
512
+ print("CFB-ENM Converged!!!")
513
+ break
514
+
515
+ print("CFB-ENM Optimization Done.")
516
+ return geometry_list
@@ -0,0 +1,146 @@
1
+ from multioptpy.Parameters.parameter import UnitValueLib
2
+ from multioptpy.Utils.calc_tools import torch_calc_angle_from_vec
3
+
4
+ import torch
5
+
6
+
7
+ class StructKeepAnglePotential:
8
+ def __init__(self, **kwarg):
9
+ self.config = kwarg
10
+ UVL = UnitValueLib()
11
+ self.hartree2kcalmol = UVL.hartree2kcalmol
12
+ self.bohr2angstroms = UVL.bohr2angstroms
13
+ self.hartree2kjmol = UVL.hartree2kjmol
14
+ return
15
+
16
+ def calc_energy(self, geom_num_list, bias_pot_params=[]):
17
+ """
18
+ # required variables: self.config["keep_angle_atom_pairs"],
19
+ self.config["keep_angle_spring_const"]
20
+ self.config["keep_angle_angle"]
21
+ bias_pot_params[0] : keep_angle_spring_const
22
+ bias_pot_params[1] : keep_angle_angle
23
+ """
24
+
25
+ vector1 = geom_num_list[self.config["keep_angle_atom_pairs"][0]-1] - geom_num_list[self.config["keep_angle_atom_pairs"][1]-1]
26
+ vector2 = geom_num_list[self.config["keep_angle_atom_pairs"][2]-1] - geom_num_list[self.config["keep_angle_atom_pairs"][1]-1]
27
+ theta = torch_calc_angle_from_vec(vector1, vector2)
28
+ if len(bias_pot_params) == 0:
29
+ energy = 0.5 * self.config["keep_angle_spring_const"] * (theta - torch.deg2rad(torch.tensor(self.config["keep_angle_angle"]))) ** 2
30
+ else:
31
+ energy = 0.5 * bias_pot_params[0] * (theta - torch.deg2rad(bias_pot_params[1])) ** 2
32
+ return energy #hartree
33
+
34
+
35
+
36
+ class StructKeepAnglePotentialv2:
37
+ def __init__(self, **kwarg):
38
+ self.config = kwarg
39
+ UVL = UnitValueLib()
40
+ self.hartree2kcalmol = UVL.hartree2kcalmol
41
+ self.bohr2angstroms = UVL.bohr2angstroms
42
+ self.hartree2kjmol = UVL.hartree2kjmol
43
+ return
44
+ def calc_energy(self, geom_num_list, bias_pot_params=[]):
45
+ """
46
+ # required variables: self.config["keep_angle_v2_spring_const"],
47
+ self.config["keep_angle_v2_angle"],
48
+ self.config["keep_angle_v2_fragm1"],
49
+ self.config["keep_angle_v2_fragm2"],
50
+ self.config["keep_angle_v2_fragm3"],
51
+ bias_pot_params[0] : keep_angle_v2_spring_const
52
+ bias_pot_params[1] : keep_angle_v2_angle
53
+
54
+ """
55
+ fragm_1_center = torch.mean(geom_num_list[torch.tensor(self.config["keep_angle_v2_fragm1"]) - 1], dim=0)
56
+ fragm_2_center = torch.mean(geom_num_list[torch.tensor(self.config["keep_angle_v2_fragm2"]) - 1], dim=0)
57
+ fragm_3_center = torch.mean(geom_num_list[torch.tensor(self.config["keep_angle_v2_fragm3"]) - 1], dim=0)
58
+
59
+ vector1 = fragm_1_center - fragm_2_center
60
+ vector2 = fragm_3_center - fragm_2_center
61
+ theta = torch_calc_angle_from_vec(vector1, vector2)
62
+ if len(bias_pot_params) == 0:
63
+ energy = 0.5 * self.config["keep_angle_v2_spring_const"] * (theta - torch.deg2rad(torch.tensor(self.config["keep_angle_v2_angle"]))) ** 2
64
+ else:
65
+ energy = 0.5 * bias_pot_params[0] * (theta - torch.deg2rad(bias_pot_params[1])) ** 2
66
+ return energy #hartree
67
+
68
+
69
+ class StructKeepAnglePotentialAtomDistDependent:
70
+ def __init__(self, **kwarg):
71
+ self.config = kwarg
72
+ UVL = UnitValueLib()
73
+ self.hartree2kcalmol = UVL.hartree2kcalmol
74
+ self.bohr2angstroms = UVL.bohr2angstroms
75
+ self.hartree2kjmol = UVL.hartree2kjmol
76
+ return
77
+
78
+ def calc_energy(self, geom_num_list, bias_pot_params=[]):
79
+ """
80
+ # required variables: self.config["aDD_keep_angle_spring_const"]
81
+ self.config["aDD_keep_angle_min_angle"]
82
+ self.config["aDD_keep_angle_max_angle"]
83
+ self.config["aDD_keep_angle_base_dist"]
84
+ self.config["aDD_keep_angle_reference_atom"]
85
+ self.config["aDD_keep_angle_center_atom"]
86
+ self.config["aDD_keep_angle_atoms"]
87
+
88
+ """
89
+ energy = 0.0
90
+ self.config["keep_angle_spring_const"] = self.config["aDD_keep_angle_spring_const"]
91
+ max_angle = torch.tensor(self.config["aDD_keep_angle_max_angle"])
92
+ min_angle = torch.tensor(self.config["aDD_keep_angle_min_angle"])
93
+ ref_dist = torch.linalg.norm(geom_num_list[self.config["aDD_keep_angle_center_atom"]-1] - geom_num_list[self.config["aDD_keep_angle_reference_atom"]-1]) / self.bohr2angstroms
94
+ base_dist = self.config["aDD_keep_angle_base_dist"] / self.bohr2angstroms
95
+ eq_angle = min_angle + ((max_angle - min_angle)/(1 + torch.exp(-(ref_dist - base_dist))))
96
+
97
+ bias_pot_params = [self.config["aDD_keep_angle_spring_const"], eq_angle]
98
+ KAP = StructKeepAnglePotential(keep_angle_angle=eq_angle, keep_angle_spring_const=self.config["aDD_keep_angle_spring_const"], keep_angle_atom_pairs=[self.config["aDD_keep_angle_atoms"][0], self.config["aDD_keep_angle_center_atom"], self.config["aDD_keep_angle_atoms"][1]])
99
+
100
+ energy += KAP.calc_energy(geom_num_list, bias_pot_params)
101
+
102
+ KAP = StructKeepAnglePotential(keep_angle_angle=eq_angle, keep_angle_spring_const=self.config["aDD_keep_angle_spring_const"], keep_angle_atom_pairs=[self.config["aDD_keep_angle_atoms"][2], self.config["aDD_keep_angle_center_atom"], self.config["aDD_keep_angle_atoms"][1]])
103
+ energy += KAP.calc_energy(geom_num_list, bias_pot_params)
104
+
105
+ KAP = StructKeepAnglePotential(keep_angle_angle=eq_angle, keep_angle_spring_const=self.config["aDD_keep_angle_spring_const"], keep_angle_atom_pairs=[self.config["aDD_keep_angle_atoms"][0], self.config["aDD_keep_angle_center_atom"], self.config["aDD_keep_angle_atoms"][2]])
106
+ energy += KAP.calc_energy(geom_num_list, bias_pot_params)
107
+
108
+ return energy
109
+
110
+
111
+ class StructKeepAnglePotentialLonePairAngle:
112
+ def __init__(self, **kwarg):
113
+ self.config = kwarg
114
+ UVL = UnitValueLib()
115
+ self.hartree2kcalmol = UVL.hartree2kcalmol
116
+ self.bohr2angstroms = UVL.bohr2angstroms
117
+ self.hartree2kjmol = UVL.hartree2kjmol
118
+ return
119
+
120
+ def calc_energy(self, geom_num_list, bias_pot_params=[]):
121
+ """
122
+ # required variables: self.config["lone_pair_keep_angle_spring_const"]
123
+ self.config["lone_pair_keep_angle_angle"]
124
+ self.config["lone_pair_keep_angle_atom_pair_1"]
125
+ self.config["lone_pair_keep_angle_atom_pair_2"]
126
+ """
127
+ lone_pair_1_vec_1 = (geom_num_list[self.config["lone_pair_keep_angle_atom_pair_1"][1]-1] - geom_num_list[self.config["lone_pair_keep_angle_atom_pair_1"][0]-1]) / self.bohr2angstroms
128
+
129
+ lone_pair_1_vec_2 = (geom_num_list[self.config["lone_pair_keep_angle_atom_pair_1"][2]-1] - geom_num_list[self.config["lone_pair_keep_angle_atom_pair_1"][0]-1]) / self.bohr2angstroms
130
+ lone_pair_1_vec_3 = (geom_num_list[self.config["lone_pair_keep_angle_atom_pair_1"][3]-1] - geom_num_list[self.config["lone_pair_keep_angle_atom_pair_1"][0]-1]) / self.bohr2angstroms
131
+ lone_pair_1_vector = (lone_pair_1_vec_1/torch.linalg.norm(lone_pair_1_vec_1)) + (lone_pair_1_vec_2/torch.linalg.norm(lone_pair_1_vec_2)) + (lone_pair_1_vec_3/torch.linalg.norm(lone_pair_1_vec_3))
132
+
133
+ lone_pair_1_vector_norm = lone_pair_1_vector / torch.linalg.norm(lone_pair_1_vector)
134
+
135
+ lone_pair_2_vec_1 = (geom_num_list[self.config["lone_pair_keep_angle_atom_pair_2"][1]-1] - geom_num_list[self.config["lone_pair_keep_angle_atom_pair_2"][0]-1]) / self.bohr2angstroms
136
+ lone_pair_2_vec_2 = (geom_num_list[self.config["lone_pair_keep_angle_atom_pair_2"][2]-1] - geom_num_list[self.config["lone_pair_keep_angle_atom_pair_2"][0]-1]) / self.bohr2angstroms
137
+ lone_pair_2_vec_3 = (geom_num_list[self.config["lone_pair_keep_angle_atom_pair_2"][3]-1] - geom_num_list[self.config["lone_pair_keep_angle_atom_pair_2"][0]-1]) / self.bohr2angstroms
138
+
139
+ lone_pair_2_vector = (lone_pair_2_vec_1/torch.linalg.norm(lone_pair_2_vec_1)) + (lone_pair_2_vec_2/torch.linalg.norm(lone_pair_2_vec_2)) + (lone_pair_2_vec_3/torch.linalg.norm(lone_pair_2_vec_3))
140
+
141
+ lone_pair_2_vector_norm = lone_pair_2_vector / torch.linalg.norm(lone_pair_2_vector)
142
+
143
+ theta = torch_calc_angle_from_vec(lone_pair_1_vector_norm, lone_pair_2_vector_norm)
144
+ energy = 0.5 * self.config["lone_pair_keep_angle_spring_const"] * (theta - torch.deg2rad(torch.tensor(self.config["lone_pair_keep_angle_angle"]))) ** 2
145
+
146
+ return energy