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,1150 @@
1
+ import copy
2
+ import numpy as np
3
+ import datetime
4
+ import os
5
+
6
+ from multioptpy.Potential.potential import BiasPotentialCalculation
7
+ from multioptpy.Utils.calc_tools import Calculationtools
8
+ from multioptpy.fileio import make_workspace, xyz2list
9
+ from multioptpy.Parameters.parameter import UnitValueLib, element_number
10
+
11
+
12
+ class ADDFlikeMethod:
13
+ def __init__(self, config):
14
+ """
15
+ Implementation of ADD (Anharmonic Downward Distortion) method based on SHS4py approach
16
+ # ref. : Journal of chemical theory and computation 16.6 (2020): 3869-3878. (https://pubs.acs.org/doi/10.1021/acs.jctc.0c00010)
17
+ # ref. : Chem. Phys. Lett. 2004, 384 (4–6), 277–282.
18
+ # ref. : Chem. Phys. Lett. 2005, 404 (1–3), 95–99.
19
+ """
20
+ self.config = config
21
+ self.addf_config = {
22
+ 'step_number': int(config.addf_step_num),
23
+ 'number_of_add': int(config.nadd),
24
+ 'IOEsphereA_initial': 0.01, # Initial hypersphere radius
25
+ 'IOEsphereA_dist': float(config.addf_step_size), # Increment for hypersphere radius
26
+ 'IOEthreshold': 0.01, # Threshold for IOE
27
+ 'minimize_threshold': 1.0e-5,# Threshold for minimization
28
+ }
29
+ self.energy_list_1 = []
30
+ self.energy_list_2 = []
31
+ self.gradient_list_1 = []
32
+ self.gradient_list_2 = []
33
+ self.init_displacement = 0.03 / self.get_unit_conversion() # Bohr
34
+ self.date = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
35
+ self.converge_criteria = 0.12
36
+ self.element_number_list = None
37
+ self.ADDths = [] # List to store ADD theta classes
38
+ self.optimized_structures = {} # Dictionary to store optimized structures by ADD ID
39
+ self.max_iterations = 10
40
+
41
+ def get_unit_conversion(self):
42
+ """Return bohr to angstrom conversion factor"""
43
+ return UnitValueLib().bohr2angstroms # Approximate value for bohr2angstroms
44
+
45
+ def adjust_center2origin(self, coord):
46
+ """Adjust coordinates to have center at origin"""
47
+ center = np.mean(coord, axis=0)
48
+ return coord - center
49
+
50
+ def SuperSphere_cartesian(self, A, thetalist, SQ, dim):
51
+ """
52
+ Vector of super sphere by cartesian (basis transformation from polar to cartesian)
53
+ {sqrt(2*A), theta_1,..., theta_n-1} -> {q_1,..., q_n} -> {x_1, x_2,..., x_n}
54
+ """
55
+ n_components = min(dim, SQ.shape[1] if SQ.ndim > 1 else dim)
56
+
57
+ qlist = np.zeros(n_components)
58
+
59
+ # Fill q-list using hyperspherical coordinates
60
+ a_k = np.sqrt(2.0 * A)
61
+ for i in range(min(len(thetalist), n_components-1)):
62
+ qlist[i] = a_k * np.cos(thetalist[i])
63
+ a_k *= np.sin(thetalist[i])
64
+
65
+ # Handle the last component
66
+ if n_components > 0:
67
+ qlist[n_components-1] = a_k
68
+
69
+ # Transform to original space
70
+ SSvec = np.dot(SQ, qlist)
71
+
72
+ return SSvec # This is a vector in the reduced space
73
+
74
+ def calctheta(self, vec, eigVlist, eigNlist):
75
+ """
76
+ Calculate the polar coordinates (theta) from a vector
77
+ """
78
+ # Get actual dimensions
79
+ n_features = eigVlist[0].shape[0] # Length of each eigenvector
80
+ n_components = min(len(eigVlist), len(eigNlist)) # Number of eigenvectors
81
+
82
+ # Check vector dimensions
83
+ if len(vec) != n_features:
84
+ # If dimensions don't match, truncate or pad the vector
85
+ if len(vec) > n_features:
86
+ vec = vec[:n_features] # Truncate
87
+ else:
88
+ padded_vec = np.zeros(n_features)
89
+ padded_vec[:len(vec)] = vec
90
+ vec = padded_vec
91
+
92
+ # Create SQ_inv matrix with correct dimensions
93
+ SQ_inv = np.zeros((n_components, n_features))
94
+
95
+ for i in range(n_components):
96
+ SQ_inv[i] = eigVlist[i] / np.sqrt(abs(eigNlist[i]))
97
+
98
+ # Perform the dot product
99
+ qvec = np.dot(SQ_inv, vec)
100
+
101
+ r = np.linalg.norm(qvec)
102
+ if r < 1e-10:
103
+ return np.zeros(n_components - 1)
104
+
105
+ thetalist = []
106
+ for i in range(len(qvec) - 1):
107
+ # Handle possible numerical issues with normalization
108
+ norm_q = np.linalg.norm(qvec[i:])
109
+ if norm_q < 1e-10:
110
+ theta = 0.0
111
+ else:
112
+ cos_theta = qvec[i] / norm_q
113
+ cos_theta = max(-1.0, min(1.0, cos_theta)) # Ensure within bounds
114
+ theta = np.arccos(cos_theta)
115
+ if i == len(qvec) - 2 and qvec[-1] < 0:
116
+ theta = 2*np.pi - theta
117
+ thetalist.append(theta)
118
+
119
+ return np.array(thetalist)
120
+
121
+ def SQaxes(self, eigNlist, eigVlist, dim):
122
+ """Calculate the SQ matrix for transformation"""
123
+ # Get actual available dimensions
124
+ n_features = eigVlist[0].shape[0] # Length of each eigenvector
125
+ n_components = min(len(eigVlist), len(eigNlist), dim) # Number of eigenvectors to use
126
+
127
+ # Initialize with correct dimensions
128
+ SQ = np.zeros((n_features, n_components))
129
+
130
+ # Only iterate up to the available components
131
+ for i in range(n_components):
132
+ SQ[:, i] = eigVlist[i] * np.sqrt(abs(eigNlist[i]))
133
+
134
+ return SQ
135
+
136
+ def SQaxes_inv(self, eigNlist, eigVlist, dim):
137
+ """Calculate the inverse SQ matrix for transformation"""
138
+ # Get actual available dimensions
139
+ n_features = eigVlist[0].shape[0] # Length of each eigenvector
140
+ n_components = min(len(eigVlist), len(eigNlist), dim) # Number of eigenvectors to use
141
+
142
+ # Initialize with correct dimensions
143
+ SQ_inv = np.zeros((n_components, n_features))
144
+
145
+ # Only iterate up to the available components
146
+ for i in range(n_components):
147
+ SQ_inv[i] = eigVlist[i] / np.sqrt(abs(eigNlist[i]))
148
+
149
+ return SQ_inv
150
+
151
+ def angle(self, v1, v2):
152
+ """Calculate angle between two vectors"""
153
+ # Check for zero vectors or invalid inputs
154
+ if np.linalg.norm(v1) < 1e-10 or np.linalg.norm(v2) < 1e-10:
155
+ return 0.0
156
+
157
+ v1_u = v1 / np.linalg.norm(v1)
158
+ v2_u = v2 / np.linalg.norm(v2)
159
+
160
+ # Handle potential numerical issues
161
+ dot_product = np.clip(np.dot(v1_u, v2_u), -1.0, 1.0)
162
+
163
+ return np.arccos(dot_product)
164
+
165
+ def angle_SHS(self, v1, v2, SQ_inv):
166
+ """Calculate angle between two vectors in SHS space"""
167
+ # Ensure vectors are flattened
168
+ v1_flat = v1.flatten() if hasattr(v1, 'flatten') else v1
169
+ v2_flat = v2.flatten() if hasattr(v2, 'flatten') else v2
170
+
171
+ # Handle potential dimension mismatches
172
+ min_dim = min(len(v1_flat), len(v2_flat), SQ_inv.shape[1])
173
+ v1_flat = v1_flat[:min_dim]
174
+ v2_flat = v2_flat[:min_dim]
175
+
176
+ q_v1 = np.dot(SQ_inv[:, :min_dim], v1_flat)
177
+ q_v2 = np.dot(SQ_inv[:, :min_dim], v2_flat)
178
+ return self.angle(q_v1, q_v2)
179
+
180
+ def calc_onHS(self, deltaTH, func, eqpoint, thetalist, IOEsphereA, A_eq):
181
+ """
182
+ Calculate function value on hypersphere
183
+ """
184
+ thetalist_new = thetalist + deltaTH
185
+ nADD = self.SuperSphere_cartesian(IOEsphereA, thetalist_new, self.SQ, self.dim)
186
+ x = eqpoint + nADD
187
+ x = self.periodicpoint(x)
188
+
189
+ result = func(x) - IOEsphereA - A_eq
190
+ result += self.IOE_total(nADD)
191
+ return result
192
+
193
+ def IOE_total(self, nADD):
194
+ """Sum up all IOE illumination"""
195
+ result = 0.0
196
+ for ADDth in self.ADDths:
197
+ if self.current_id == ADDth.IDnum:
198
+ continue
199
+ if ADDth.ADDoptQ:
200
+ continue
201
+ if ADDth.ADD_IOE <= -1000000 or ADDth.ADD_IOE > 10000000:
202
+ continue
203
+ if ADDth.ADD <= self.current_ADD:
204
+ result -= self.IOE(nADD, ADDth)
205
+ return result
206
+
207
+ def IOE(self, nADD, neiborADDth):
208
+ """Calculate IOE between current point and neighbor"""
209
+ deltaTH = self.angle_SHS(nADD, neiborADDth.nADD, self.SQ_inv)
210
+ if deltaTH <= np.pi * 0.5:
211
+ cosdamp = np.cos(deltaTH)
212
+ return neiborADDth.ADD_IOE * cosdamp * cosdamp * cosdamp
213
+ else:
214
+ return 0.0
215
+
216
+ def grad_hypersphere(self, f, grad, eqpoint, IOEsphereA, thetalist):
217
+ """Calculate gradient on hypersphere"""
218
+ # Generate nADD in the reduced space (this will be a vector in the Hessian eigenspace)
219
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
220
+
221
+ # We need to convert the reduced space vector back to the full coordinate space
222
+ # First create a zero vector in the full space
223
+ n_atoms = eqpoint.shape[0]
224
+ n_coords = n_atoms * 3
225
+ nADD_full = np.zeros(n_coords)
226
+
227
+ # Map the reduced vector to the full space (this is approximate)
228
+ for i in range(min(len(nADD_reduced), n_coords)):
229
+ nADD_full[i] = nADD_reduced[i]
230
+
231
+ # Reshape to molecular geometry format
232
+ nADD = nADD_full.reshape(n_atoms, 3)
233
+
234
+ # Calculate the normalized direction vector for projecting out later
235
+ EnADD = nADD_full / np.linalg.norm(nADD_full)
236
+
237
+ # Apply the displacement to the initial geometry
238
+ target_point = eqpoint + nADD
239
+ target_point = self.periodicpoint(target_point)
240
+
241
+ # Calculate gradient at this point
242
+ grad_x = grad(target_point)
243
+ if isinstance(grad_x, bool) and grad_x is False:
244
+ return False, False
245
+
246
+ # Flatten the gradient for vector operations
247
+ grad_x_flat = grad_x.flatten()
248
+
249
+ # Project gradient onto tangent space of hypersphere
250
+ # We remove the component along the displacement vector
251
+ returngrad_flat = grad_x_flat - np.dot(grad_x_flat, EnADD) * EnADD
252
+
253
+ # Apply IOE contributions (if implemented)
254
+ for ADDth in self.ADDths:
255
+ # Skip if this is the current ADD or if current_id is None
256
+ if self.current_id is not None and self.current_id == ADDth.IDnum:
257
+ continue
258
+
259
+ # Skip if this ADD is being optimized
260
+ if ADDth.ADDoptQ:
261
+ continue
262
+
263
+ # Only apply IOE for ADDs with lower energy (if current_ADD is set)
264
+ if self.current_ADD is not None and hasattr(ADDth, 'ADD') and ADDth.ADD is not None:
265
+ if ADDth.ADD <= self.current_ADD:
266
+ ioe_grad = self.IOE_grad(nADD_full, ADDth)
267
+ if ioe_grad is not None:
268
+ returngrad_flat -= ioe_grad
269
+
270
+ # Reshape gradient back to molecular geometry format for easier handling
271
+ returngrad = returngrad_flat.reshape(n_atoms, 3)
272
+
273
+ return target_point, returngrad
274
+
275
+ def IOE_grad(self, nADD, neiborADDth):
276
+ """Calculate gradient of IOE"""
277
+ # Make sure we're working with flattened arrays
278
+ nADD_flat = nADD.flatten() if hasattr(nADD, 'flatten') else nADD
279
+ nADD_neibor = neiborADDth.nADD_full if hasattr(neiborADDth, 'nADD_full') else neiborADDth.nADD.flatten()
280
+
281
+ # Get minimum dimension we can work with
282
+ min_dim = min(len(nADD_flat), len(nADD_neibor), self.SQ_inv.shape[1])
283
+ nADD_flat = nADD_flat[:min_dim]
284
+ nADD_neibor = nADD_neibor[:min_dim]
285
+
286
+ # Transform to eigenspace
287
+ q_x = np.dot(self.SQ_inv[:, :min_dim], nADD_flat)
288
+ q_y = np.dot(self.SQ_inv[:, :min_dim], nADD_neibor)
289
+
290
+ # Check for valid vectors before calculating angle
291
+ if np.isnan(q_x).any() or np.isnan(q_y).any() or np.linalg.norm(q_x) < 1e-10 or np.linalg.norm(q_y) < 1e-10:
292
+ return None
293
+
294
+ # Calculate angle in eigenspace
295
+ deltaTH = self.angle(q_x, q_y)
296
+
297
+ # Check if deltaTH is valid (not None and not NaN)
298
+ if deltaTH is None or np.isnan(deltaTH):
299
+ return None
300
+
301
+ # Initialize gradient vector
302
+ returngrad = np.zeros(len(nADD_flat))
303
+ eps = 1.0e-3
304
+
305
+ # Calculate IOE gradient using finite differences
306
+ if deltaTH <= np.pi * 0.5:
307
+ cosdamp = np.cos(deltaTH)
308
+ for i in range(len(nADD_flat)):
309
+ nADD_eps = copy.copy(nADD_flat)
310
+ nADD_eps[i] += eps
311
+
312
+ # Transform to eigenspace
313
+ qx_i = np.dot(self.SQ_inv[:, :min_dim], nADD_eps[:min_dim])
314
+ deltaTH_eps = self.angle(qx_i, q_y)
315
+
316
+ # Check if the new angle is valid
317
+ if deltaTH_eps is None or np.isnan(deltaTH_eps):
318
+ continue
319
+
320
+ cosdamp_eps = np.cos(deltaTH_eps)
321
+ IOE_center = neiborADDth.ADD_IOE * cosdamp * cosdamp * cosdamp
322
+ IOE_eps = neiborADDth.ADD_IOE * cosdamp_eps * cosdamp_eps * cosdamp_eps
323
+
324
+ returngrad[i] = (IOE_eps - IOE_center) / eps
325
+
326
+ # Pad the gradient to the full space if needed
327
+ full_grad = np.zeros(self.n_coords if hasattr(self, 'n_coords') else len(nADD))
328
+ full_grad[:len(returngrad)] = returngrad
329
+ return full_grad
330
+
331
+ return None
332
+
333
+ def periodicpoint(self, point):
334
+ """Apply periodic boundary conditions if needed"""
335
+ # Implement according to your specific requirements
336
+ return point
337
+
338
+ def minimizeTh_SD_SS(self, ADDth, initialpoint, f, grad, eqpoint, IOEsphereA):
339
+ """
340
+ Steepest descent optimization on hypersphere with step size control
341
+ Following the implementation in SHS4py.ADD.py with added robustness
342
+ """
343
+ whileN = 0
344
+ thetalist = ADDth.thetalist + initialpoint
345
+ stepsize = 0.001
346
+ n_atoms = eqpoint.shape[0]
347
+ n_coords = n_atoms * 3
348
+
349
+ # Generate initial nADD
350
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
351
+
352
+ # Convert reduced space vector to full coordinate space with proper dimensions
353
+ nADD_full = np.zeros(n_coords)
354
+ for i in range(min(len(nADD_reduced), n_coords)):
355
+ nADD_full[i] = nADD_reduced[i]
356
+
357
+ # Reshape to match eqpoint dimensions
358
+ nADD = nADD_full.reshape(n_atoms, 3)
359
+
360
+ # Keep track of best solution
361
+ best_thetalist = thetalist.copy()
362
+ best_energy = float('inf')
363
+
364
+ # Initial point
365
+ tergetpoint = eqpoint + nADD
366
+ tergetpoint = self.periodicpoint(tergetpoint)
367
+
368
+ # Try to calculate initial energy
369
+ try:
370
+ initial_energy = f(tergetpoint)
371
+ if isinstance(initial_energy, (int, float)) and not np.isnan(initial_energy):
372
+ best_energy = initial_energy
373
+ except Exception:
374
+ pass # Continue even if initial energy calculation fails
375
+
376
+ # Main optimization loop
377
+ while whileN < self.max_iterations:
378
+ try:
379
+ # Get gradient at current point
380
+ grad_x = grad(tergetpoint)
381
+
382
+ # If gradient calculation fails, continue with smaller step or different approach
383
+ if grad_x is False:
384
+ # Try a random perturbation and continue
385
+ print(f"Gradient calculation failed at iteration {whileN}, trying random perturbation")
386
+ random_perturbation = np.random.rand(n_atoms, 3) * 0.01 - 0.005 # Small random perturbation
387
+ tergetpoint = tergetpoint + random_perturbation
388
+ tergetpoint = self.periodicpoint(tergetpoint)
389
+
390
+ # Calculate new nADD
391
+ nADD = tergetpoint - eqpoint
392
+ thetalist = self.calctheta(nADD.flatten(), self.eigVlist, self.eigNlist)
393
+
394
+ # Ensure we're on the hypersphere with correct radius
395
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
396
+ nADD_full = np.zeros(n_coords)
397
+ for i in range(min(len(nADD_reduced), n_coords)):
398
+ nADD_full[i] = nADD_reduced[i]
399
+ nADD = nADD_full.reshape(n_atoms, 3)
400
+
401
+ tergetpoint = eqpoint + nADD
402
+ tergetpoint = self.periodicpoint(tergetpoint)
403
+
404
+ whileN += 1
405
+ continue
406
+
407
+ # Apply IOE contributions
408
+ grad_flat = grad_x.flatten()
409
+ for neiborADDth in self.ADDths:
410
+ if ADDth.IDnum == neiborADDth.IDnum:
411
+ continue
412
+ if neiborADDth.ADDoptQ:
413
+ continue
414
+ if neiborADDth.ADD <= ADDth.ADD:
415
+ ioe_grad = self.IOE_grad(nADD.flatten(), neiborADDth)
416
+ if ioe_grad is not None:
417
+ grad_flat = grad_flat - ioe_grad
418
+
419
+ # Reshape back to molecular geometry format
420
+ grad_x = grad_flat.reshape(n_atoms, 3)
421
+
422
+ # Project gradient onto tangent space
423
+ nADD_norm = np.linalg.norm(nADD.flatten())
424
+ if nADD_norm < 1e-10:
425
+ # If nADD is too small, generate a new one
426
+ print(f"nADD norm too small at iteration {whileN}, regenerating")
427
+ thetalist = self.calctheta(np.random.rand(n_coords) - 0.5, self.eigVlist, self.eigNlist)
428
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
429
+ nADD_full = np.zeros(n_coords)
430
+ for i in range(min(len(nADD_reduced), n_coords)):
431
+ nADD_full[i] = nADD_reduced[i]
432
+ nADD = nADD_full.reshape(n_atoms, 3)
433
+ tergetpoint = eqpoint + nADD
434
+ tergetpoint = self.periodicpoint(tergetpoint)
435
+ whileN += 1
436
+ continue
437
+
438
+ nADD_unit = nADD.flatten() / nADD_norm
439
+
440
+ # Project gradient component along nADD
441
+ grad_along_nADD = np.dot(grad_x.flatten(), nADD_unit)
442
+
443
+ # Subtract this component to get the tangent space gradient
444
+ SSgrad_flat = grad_x.flatten() - grad_along_nADD * nADD_unit
445
+ SSgrad = SSgrad_flat.reshape(n_atoms, 3)
446
+
447
+ # Check convergence
448
+ if np.linalg.norm(SSgrad) < 1.0e-1:
449
+ # Update best solution if better
450
+ try:
451
+ current_energy = f(tergetpoint)
452
+ if isinstance(current_energy, (int, float)) and not np.isnan(current_energy):
453
+ if current_energy < best_energy:
454
+ best_energy = current_energy
455
+ best_thetalist = thetalist.copy()
456
+ except Exception:
457
+ pass # Just keep the current best if energy calculation fails
458
+
459
+ return thetalist # Converged successfully
460
+
461
+ # Store current point
462
+ _point_initial = copy.copy(tergetpoint)
463
+
464
+ # Line search
465
+ whileN2 = 0
466
+ stepsizedamp = stepsize
467
+ found_valid_step = False
468
+
469
+ # Try multiple step sizes
470
+ for whileN2 in range(1, 5): # Try up to 10 steps with varying sizes
471
+ try:
472
+ # Take step with dynamic step size
473
+ step_scale = whileN2 if whileN2 <= 5 else (whileN2 - 5) * 0.1
474
+ tergetpoint = _point_initial - step_scale * SSgrad / np.linalg.norm(SSgrad) * stepsizedamp
475
+
476
+ # Calculate new nADD
477
+ nADD2 = tergetpoint - eqpoint
478
+
479
+ # Convert to theta parameters
480
+ thetalist_new = self.calctheta(nADD2.flatten(), self.eigVlist, self.eigNlist)
481
+
482
+ # Ensure we're on the hypersphere with correct radius
483
+ nADD2_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist_new, self.SQ, self.dim)
484
+
485
+ # Convert reduced space vector to full coordinate space
486
+ nADD2_full = np.zeros(n_coords)
487
+ for i in range(min(len(nADD2_reduced), n_coords)):
488
+ nADD2_full[i] = nADD2_reduced[i]
489
+
490
+ # Reshape to match eqpoint dimensions
491
+ nADD2 = nADD2_full.reshape(n_atoms, 3)
492
+
493
+ # Calculate new point on hypersphere
494
+ new_point = eqpoint + nADD2
495
+ new_point = self.periodicpoint(new_point)
496
+
497
+ # Calculate step size
498
+ delta = np.linalg.norm(nADD.flatten() - nADD2.flatten())
499
+
500
+ # Calculate energy at new point to check improvement
501
+ try:
502
+ new_energy = f(new_point)
503
+ if isinstance(new_energy, (int, float)) and not np.isnan(new_energy):
504
+ # Accept step if it improves energy or makes reasonable movement
505
+ if new_energy < best_energy or delta > 0.005:
506
+ found_valid_step = True
507
+ if new_energy < best_energy:
508
+ best_energy = new_energy
509
+ best_thetalist = thetalist_new.copy()
510
+ tergetpoint = new_point
511
+ thetalist = thetalist_new
512
+ nADD = nADD2
513
+ break
514
+ except Exception:
515
+ # If energy calculation fails, accept step if it's a reasonable move
516
+ if delta > 0.005 and delta < 0.1:
517
+ found_valid_step = True
518
+ tergetpoint = new_point
519
+ thetalist = thetalist_new
520
+ nADD = nADD2
521
+ break
522
+ except Exception as e:
523
+ print(f"Step calculation error: {e}, trying different step")
524
+ continue
525
+
526
+ # If no valid step found, try a random perturbation
527
+ if not found_valid_step:
528
+ print(f"No valid step found at iteration {whileN}, trying random perturbation")
529
+ # Generate random perturbation but keep on hypersphere
530
+ random_theta = self.calctheta(np.random.rand(n_coords) - 0.5, self.eigVlist, self.eigNlist)
531
+ random_nADD = self.SuperSphere_cartesian(IOEsphereA, random_theta, self.SQ, self.dim)
532
+
533
+ # Interpolate between current point and random point
534
+ alpha = 0.1 # Small mixing factor
535
+ mixed_theta = thetalist * (1-alpha) + random_theta * alpha
536
+
537
+ # Generate new point
538
+ mixed_nADD = self.SuperSphere_cartesian(IOEsphereA, mixed_theta, self.SQ, self.dim)
539
+ mixed_nADD_full = np.zeros(n_coords)
540
+ for i in range(min(len(mixed_nADD), n_coords)):
541
+ mixed_nADD_full[i] = mixed_nADD[i]
542
+ mixed_nADD = mixed_nADD_full.reshape(n_atoms, 3)
543
+
544
+ # Update point
545
+ tergetpoint = eqpoint + mixed_nADD
546
+ tergetpoint = self.periodicpoint(tergetpoint)
547
+ nADD = mixed_nADD
548
+ thetalist = mixed_theta
549
+
550
+ # Increment counter
551
+ whileN += 1
552
+
553
+ # Print progress periodically
554
+ if whileN % 10 == 0:
555
+ print(f"Optimization step {whileN}: gradient norm = {np.linalg.norm(SSgrad):.6f}")
556
+
557
+ except Exception as e:
558
+ print(f"Error in optimization step {whileN}: {e}, continuing with best solution")
559
+ whileN += 1
560
+
561
+ # Try to recover with random perturbation
562
+ if whileN % 3 == 0: # Every third error, try a more drastic change
563
+ try:
564
+ # Generate a completely new point on hypersphere
565
+ random_theta = self.calctheta(np.random.rand(n_coords) - 0.5, self.eigVlist, self.eigNlist)
566
+ random_nADD = self.SuperSphere_cartesian(IOEsphereA, random_theta, self.SQ, self.dim)
567
+
568
+ # Create full vector
569
+ random_nADD_full = np.zeros(n_coords)
570
+ for i in range(min(len(random_nADD), n_coords)):
571
+ random_nADD_full[i] = random_nADD[i]
572
+ random_nADD = random_nADD_full.reshape(n_atoms, 3)
573
+
574
+ # Try the new point
575
+ new_point = eqpoint + random_nADD
576
+ new_point = self.periodicpoint(new_point)
577
+
578
+ # Check if it's better
579
+ try:
580
+ random_energy = f(new_point)
581
+ if isinstance(random_energy, (int, float)) and not np.isnan(random_energy):
582
+ if random_energy < best_energy:
583
+ best_energy = random_energy
584
+ best_thetalist = random_theta.copy()
585
+ tergetpoint = new_point
586
+ nADD = random_nADD
587
+ thetalist = random_theta
588
+ except Exception:
589
+ pass # Ignore failed energy calculations
590
+ except Exception:
591
+ pass # Ignore errors in recovery attempt
592
+
593
+ print(f"Optimization completed with {whileN} iterations")
594
+ # Return the best solution found
595
+ return best_thetalist if best_energy < float('inf') else thetalist
596
+
597
+ def detect_add(self, QMC):
598
+ """Detect ADD directions from Hessian"""
599
+ coord_1 = self.get_coord()
600
+ coord_1 = self.adjust_center2origin(coord_1)
601
+ n_atoms = coord_1.shape[0]
602
+ n_coords = n_atoms * 3
603
+
604
+ element_number_list_1 = self.get_element_number_list()
605
+ print("### Checking whether initial structure is EQ. ###")
606
+
607
+ QMC.hessian_flag = True
608
+ self.init_energy, self.init_gradient, _, iscalculationfailed = QMC.single_point(
609
+ None, element_number_list_1, "", self.electric_charge_and_multiplicity,
610
+ self.method, coord_1
611
+ )
612
+
613
+ if iscalculationfailed:
614
+ print("Initial calculation failed.")
615
+ return False
616
+
617
+ # Apply bias potential if needed
618
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
619
+ _, bias_energy, bias_gradient, bias_hess = BPC.main(
620
+ self.init_energy, self.init_gradient, coord_1, element_number_list_1,
621
+ self.config.force_data
622
+ )
623
+ self.init_energy = bias_energy
624
+ self.init_gradient = bias_gradient
625
+
626
+ QMC.hessian_flag = False
627
+ self.init_geometry = coord_1 # Shape: (n_atoms, 3)
628
+ self.set_coord(coord_1)
629
+
630
+ if np.linalg.norm(self.init_gradient) > 1e-3:
631
+ print("Norm of gradient is too large. Structure is not at equilibrium.")
632
+ return False
633
+
634
+ print("Initial structure is EQ.")
635
+ print("### Start calculating Hessian matrix to detect ADD. ###")
636
+
637
+ hessian = QMC.Model_hess + bias_hess
638
+
639
+ # Project out translation and rotation
640
+ projection_hessian = Calculationtools().project_out_hess_tr_and_rot_for_coord(hessian, self.element_list, coord_1)
641
+
642
+ eigenvalues, eigenvectors = np.linalg.eigh(projection_hessian)
643
+ eigenvalues = eigenvalues.astype(np.float64)
644
+
645
+ # Filter out near-zero eigenvalues
646
+ nonzero_indices = np.where(np.abs(eigenvalues) > 1e-10)[0]
647
+ nonzero_eigenvectors = eigenvectors[:, nonzero_indices].astype(np.float64)
648
+ nonzero_eigenvalues = eigenvalues[nonzero_indices].astype(np.float64)
649
+
650
+ sorted_idx = np.argsort(nonzero_eigenvalues)
651
+
652
+ self.init_eigenvalues = nonzero_eigenvalues
653
+ self.init_eigenvectors = nonzero_eigenvectors
654
+ self.dim = len(nonzero_eigenvalues)
655
+ self.n_atoms = n_atoms
656
+ self.n_coords = n_coords
657
+
658
+ # Store flattened versions for matrix operations
659
+ self.init_geometry_flat = self.init_geometry.flatten()
660
+
661
+ # Prepare SQ matrices
662
+ self.SQ = self.SQaxes(nonzero_eigenvalues, nonzero_eigenvectors, self.dim)
663
+ self.SQ_inv = self.SQaxes_inv(nonzero_eigenvalues, nonzero_eigenvectors, self.dim)
664
+
665
+ self.eigNlist = nonzero_eigenvalues
666
+ self.eigVlist = nonzero_eigenvectors
667
+
668
+ # Calculate mode eigenvectors to try - focus on lower eigenvectors first
669
+ # (corresponding to softer modes that are more likely to lead to transition states)
670
+ search_idx = len(sorted_idx) # Start with more eigenvectors than needed
671
+ self.sorted_eigenvalues_idx = sorted_idx[0:search_idx]
672
+
673
+ # Initialize ADDths with initial directions
674
+ self.ADDths = []
675
+ IDnum = 0
676
+
677
+ print("### Checking ADD energy. ###")
678
+ with open(self.directory + "/add_energy_list.csv", "w") as f:
679
+ f.write("index_of_principal_axis,eigenvalue,direction,add_energy,abs_add_energy\n")
680
+
681
+ IOEsphereA = self.addf_config['IOEsphereA_initial']
682
+
683
+ # Create candidate ADDs for eigenvectors
684
+ candidate_ADDths = []
685
+
686
+ for idx in self.sorted_eigenvalues_idx:
687
+ for pm in [-1.0, 1.0]:
688
+ eigV = self.init_eigenvectors[:, idx]
689
+
690
+ # Create a new ADD point
691
+ ADDth = type('ADDthetaClass', (), {})
692
+ ADDth.IDnum = IDnum
693
+ ADDth.dim = self.dim
694
+ ADDth.SQ = self.SQ
695
+ ADDth.SQ_inv = self.SQ_inv
696
+ ADDth.thetalist = self.calctheta(pm * eigV, nonzero_eigenvectors, nonzero_eigenvalues)
697
+
698
+ # Generate nADD (this will be a flattened vector in the Hessian eigenspace)
699
+ ADDth.nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, ADDth.thetalist, self.SQ, self.dim)
700
+
701
+ # We need to convert the reduced space vector back to the full coordinate space
702
+ # First create a zero vector in the full space
703
+ ADDth.nADD_full = np.zeros(n_coords)
704
+
705
+ # Map the reduced vector to the full space (this is approximate)
706
+ for i in range(min(len(ADDth.nADD_reduced), n_coords)):
707
+ ADDth.nADD_full[i] = ADDth.nADD_reduced[i]
708
+
709
+ # Reshape to molecular geometry format
710
+ ADDth.nADD = ADDth.nADD_full.reshape(n_atoms, 3)
711
+
712
+ # Apply the displacement to the initial geometry
713
+ ADDth.x = self.init_geometry + ADDth.nADD
714
+ ADDth.x = self.periodicpoint(ADDth.x)
715
+
716
+ # Calculate energy
717
+ energy, grad_x, _, iscalculationfailed = QMC.single_point(
718
+ None, element_number_list_1, "", self.electric_charge_and_multiplicity,
719
+ self.method, ADDth.x
720
+ )
721
+
722
+ if iscalculationfailed:
723
+ continue
724
+
725
+ # Apply bias if needed
726
+ _, bias_energy, bias_gradient, _ = BPC.main(
727
+ energy, grad_x, ADDth.x, element_number_list_1,
728
+ self.config.force_data
729
+ )
730
+
731
+ ADDth.A = bias_energy
732
+ ADDth.ADD = ADDth.A - IOEsphereA - self.init_energy
733
+ ADDth.ADD_IOE = ADDth.ADD # Initial value, will be updated later
734
+ ADDth.grad = bias_gradient
735
+ ADDth.grad_vec = np.dot(bias_gradient.flatten(), ADDth.nADD.flatten())
736
+ ADDth.grad_vec /= np.linalg.norm(ADDth.nADD.flatten())
737
+ ADDth.findTSQ = False
738
+ ADDth.ADDoptQ = False
739
+ ADDth.ADDremoveQ = False
740
+
741
+ # Add to candidate list
742
+ candidate_ADDths.append(ADDth)
743
+ IDnum += 1
744
+
745
+ with open(self.directory + "/add_energy_list.csv", "a") as f:
746
+ f.write(f"{idx},{self.init_eigenvalues[idx]},{pm},{ADDth.ADD},{abs(ADDth.ADD)}\n")
747
+
748
+ # Sort candidate ADDths by negative ADD value in descending order (-ADD value)
749
+ # This prioritizes more negative (favorable) paths first
750
+ candidate_ADDths.sort(key=lambda x: -x.ADD, reverse=True)
751
+
752
+ # Select only the top n ADDs according to config.nadd
753
+ num_add = min(self.addf_config['number_of_add'], len(candidate_ADDths))
754
+ self.ADDths = candidate_ADDths[:num_add]
755
+
756
+ # Reassign IDs to be sequential
757
+ for i, ADDth in enumerate(self.ADDths):
758
+ ADDth.IDnum = i
759
+
760
+ print(f"### Selected top {len(self.ADDths)} ADD paths (sign-inverted ADD values, most negative first) ###")
761
+ for ADDth in self.ADDths:
762
+ print(f"ADD {ADDth.IDnum}: {ADDth.ADD:.8f} (-ADD = {-ADDth.ADD:.8f})")
763
+
764
+ # Initialize the optimized structures dictionary
765
+ self.optimized_structures = {}
766
+
767
+ # Create directory for optimized structures
768
+ os.makedirs(os.path.join(self.directory, "optimized_structures"), exist_ok=True)
769
+
770
+ print("### ADD detection complete. ###")
771
+ return True
772
+
773
+ def save_optimized_structure(self, ADDth, sphere_num, IOEsphereA):
774
+ """Save optimized structure for a specific ADD and sphere"""
775
+ # Create directory path for ADD-specific structures
776
+ add_dir = os.path.join(self.directory, "optimized_structures", f"ADD_{ADDth.IDnum}")
777
+ os.makedirs(add_dir, exist_ok=True)
778
+
779
+ # Create filename with radius information
780
+ radius = np.sqrt(IOEsphereA)
781
+ filename = f"optimized_r_{radius:.4f}.xyz"
782
+ filepath = os.path.join(add_dir, filename)
783
+
784
+ # Write XYZ file
785
+ with open(filepath, 'w') as f:
786
+ f.write(f"{len(self.element_list)}\n")
787
+ f.write(f"ADD_{ADDth.IDnum} Sphere {sphere_num} Radius {radius:.4f} Energy {ADDth.ADD:.6f}\n")
788
+ for i, (element, coord) in enumerate(zip(self.element_list, ADDth.x)):
789
+ f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
790
+
791
+ # Store structure information in dictionary
792
+ if ADDth.IDnum not in self.optimized_structures:
793
+ self.optimized_structures[ADDth.IDnum] = []
794
+
795
+ self.optimized_structures[ADDth.IDnum].append({
796
+ 'sphere': sphere_num,
797
+ 'radius': radius,
798
+ 'energy': ADDth.ADD,
799
+ 'file': filepath,
800
+ 'coords': ADDth.x.copy(),
801
+ 'comment': f"ADD_{ADDth.IDnum} Sphere {sphere_num} Radius {radius:.4f} Energy {ADDth.ADD:.6f}"
802
+ })
803
+
804
+ def create_separate_xyz_files(self):
805
+ """Create separate XYZ files for each ADD path"""
806
+ created_files = []
807
+
808
+ for add_id, structures in self.optimized_structures.items():
809
+ # Sort structures by sphere number
810
+ structures.sort(key=lambda x: x['sphere'])
811
+
812
+ # Path for the ADD-specific file
813
+ add_trajectory_file = os.path.join(self.directory, f"ADD_{add_id}_trajectory.xyz")
814
+
815
+ # Write the trajectory file
816
+ with open(add_trajectory_file, 'w') as f:
817
+ for structure in structures:
818
+ f.write(f"{len(self.element_list)}\n")
819
+ f.write(f"{structure['comment']}\n")
820
+ for i, (element, coord) in enumerate(zip(self.element_list, structure['coords'])):
821
+ f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
822
+
823
+ created_files.append(add_trajectory_file)
824
+
825
+ if created_files:
826
+ paths_str = "\n".join(created_files)
827
+ print(f"Created {len(created_files)} ADD trajectory files:\n{paths_str}")
828
+
829
+ def Opt_hyper_sphere(self, ADDths, QMC, eqpoint, IOEsphereA, IOEsphereA_r, A_eq, sphereN):
830
+ """Optimize points on the hypersphere"""
831
+ print(f"Starting optimization on sphere {sphereN} with radius {np.sqrt(IOEsphereA):.4f}")
832
+
833
+ # Reset optimization flags
834
+ for ADDth in ADDths:
835
+ if not ADDth.findTSQ and not ADDth.ADDremoveQ:
836
+ ADDth.ADDoptQ = True
837
+ else:
838
+ ADDth.ADDoptQ = False
839
+
840
+ optturnN = 0
841
+ newADDths = []
842
+ n_atoms = eqpoint.shape[0]
843
+
844
+ # Create a directory for intermediate optimization steps
845
+ sphere_dir = os.path.join(self.directory, "optimized_structures", f"sphere_{sphereN}")
846
+ os.makedirs(sphere_dir, exist_ok=True)
847
+
848
+ # Optimization loop
849
+ while any(ADDth.ADDoptQ for ADDth in ADDths):
850
+ optturnN += 1
851
+ if optturnN >= 100:
852
+ print(f"Optimization exceeded 100 iterations, breaking.")
853
+ break
854
+
855
+ print(f"Optimization iteration {optturnN}")
856
+
857
+ # Process each ADD point in order of negative ADD value (most negative first)
858
+ for ADDth in sorted(ADDths, key=lambda x: -x.ADD, reverse=True):
859
+ if not ADDth.ADDoptQ or ADDth.ADDremoveQ:
860
+ continue
861
+
862
+ # Optimize this ADD point
863
+ self.current_id = ADDth.IDnum
864
+ self.current_ADD = ADDth.ADD
865
+
866
+ # Starting from zero displacement
867
+ x_initial = np.zeros(len(ADDth.thetalist))
868
+
869
+ # Minimize on hypersphere using our modified steepest descent
870
+ thetalist = self.minimizeTh_SD_SS(
871
+ ADDth, x_initial,
872
+ lambda x: QMC.single_point(None, self.get_element_number_list(), "",
873
+ self.electric_charge_and_multiplicity, self.method, x)[0],
874
+ lambda x: self.calculate_gradient(QMC, x),
875
+ eqpoint, IOEsphereA
876
+ )
877
+
878
+ if thetalist is False:
879
+ ADDth.ADDremoveQ = True
880
+ print(f"ADD {ADDth.IDnum} optimization failed, marking for removal")
881
+ continue
882
+
883
+ # Update ADD point with optimized position
884
+ ADDth.thetalist = thetalist
885
+
886
+ # Generate nADD in the reduced space
887
+ ADDth.nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, ADDth.thetalist, self.SQ, self.dim)
888
+
889
+ # Map to full coordinate space
890
+ ADDth.nADD_full = np.zeros(n_atoms * 3)
891
+ for i in range(min(len(ADDth.nADD_reduced), n_atoms * 3)):
892
+ ADDth.nADD_full[i] = ADDth.nADD_reduced[i]
893
+
894
+ # Reshape to molecular geometry
895
+ ADDth.nADD = ADDth.nADD_full.reshape(n_atoms, 3)
896
+
897
+ # Calculate new coordinates
898
+ ADDth.x = eqpoint + ADDth.nADD
899
+ ADDth.x = self.periodicpoint(ADDth.x)
900
+
901
+ # Calculate new energy
902
+ energy, grad, _, iscalculationfailed = QMC.single_point(
903
+ None, self.get_element_number_list(), "",
904
+ self.electric_charge_and_multiplicity, self.method, ADDth.x
905
+ )
906
+
907
+ if iscalculationfailed:
908
+ ADDth.ADDremoveQ = True
909
+ continue
910
+
911
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
912
+ _, bias_energy, bias_grad, _ = BPC.main(
913
+ energy, grad, ADDth.x, self.get_element_number_list(),
914
+ self.config.force_data
915
+ )
916
+
917
+ ADDth.A = bias_energy
918
+ ADDth.ADD = ADDth.A - IOEsphereA - A_eq
919
+
920
+ # Calculate ADD_IOE with IOE contributions
921
+ self.current_id = ADDth.IDnum
922
+ self.current_ADD = ADDth.ADD
923
+ ADDth.ADD_IOE = ADDth.ADD + self.IOE_total(ADDth.nADD_full)
924
+
925
+ # Mark as optimized
926
+ ADDth.ADDoptQ = False
927
+
928
+ print(f"ADD {ADDth.IDnum} optimized: ADD={ADDth.ADD:.4f} (-ADD = {-ADDth.ADD:.4f}), ADD_IOE={ADDth.ADD_IOE:.4f}")
929
+ print(f"Grad {np.linalg.norm(grad):.6f}")
930
+ print(f"Energy {energy:.6f}")
931
+ print()
932
+
933
+ # Save XYZ file after each ADD optimization step
934
+ # Use both iteration number and ADD ID in filename to ensure uniqueness
935
+ filename = f"iteration_{optturnN}_ADD_{ADDth.IDnum}.xyz"
936
+ filepath = os.path.join(sphere_dir, filename)
937
+
938
+ # Write XYZ file for this optimization step
939
+ with open(filepath, 'w') as f:
940
+ f.write(f"{len(self.element_list)}\n")
941
+ f.write(f"Sphere {sphereN} Iteration {optturnN} ADD_{ADDth.IDnum} Radius {np.sqrt(IOEsphereA):.4f} Energy {ADDth.ADD:.6f}\n")
942
+ for i, (element, coord) in enumerate(zip(self.element_list, ADDth.x)):
943
+ f.write(f"{element} {coord[0]:.6f} {coord[1]:.6f} {coord[2]:.6f}\n")
944
+
945
+ print(f"Saved intermediate structure to {filepath}")
946
+
947
+ # Check for similar points already found
948
+ for existing_add in newADDths:
949
+ if self.angle_SHS(ADDth.nADD_full, existing_add.nADD_full, self.SQ_inv) < 0.01:
950
+ ADDth.ADDremoveQ = True
951
+ print(f"ADD {ADDth.IDnum} too similar to ADD {existing_add.IDnum}, marking for removal")
952
+ break
953
+
954
+ if not ADDth.ADDremoveQ and ADDth.ADD_IOE < 0: # Only keep negative ADD_IOE points
955
+ newADDths.append(ADDth)
956
+
957
+ # Save optimized structure for this sphere (in the regular directory structure)
958
+ self.save_optimized_structure(ADDth, sphereN, IOEsphereA)
959
+
960
+ # Filter ADDths list
961
+ ADDths = [ADDth for ADDth in ADDths if not ADDth.ADDremoveQ]
962
+
963
+ # Sort by negative ADD value (most negative first) for next iteration
964
+ ADDths.sort(key=lambda x: -x.ADD, reverse=True)
965
+
966
+ # Sort the final ADDths list by negative ADD value (most negative first)
967
+ newADDths.sort(key=lambda x: -x.ADD, reverse=True)
968
+
969
+ return newADDths if newADDths else ADDths
970
+
971
+ def add_following(self, QMC):
972
+ """Follow ADD paths to find transition states"""
973
+ print("### Start ADD Following. ###")
974
+
975
+ IOEsphereA = self.addf_config['IOEsphereA_initial']
976
+ IOEsphereA_r = self.addf_config['IOEsphereA_dist']
977
+ A_eq = self.init_energy
978
+
979
+ TSinitialpoints = []
980
+ sphereN = 0
981
+
982
+ # Main ADD following loop
983
+ while sphereN < self.addf_config["step_number"]: # Limit to prevent infinite loops
984
+ sphereN += 1
985
+ print(f"\n### Sphere {sphereN} with radius {np.sqrt(IOEsphereA):.4f} ###\n")
986
+
987
+ # Sort ADDths by absolute ADD value (largest magnitude first)
988
+ self.ADDths.sort(key=lambda x: abs(x.ADD), reverse=True)
989
+
990
+ # Optimize on current hypersphere
991
+ self.ADDths = self.Opt_hyper_sphere(
992
+ self.ADDths, QMC, self.init_geometry, IOEsphereA, IOEsphereA_r, A_eq, sphereN
993
+ )
994
+
995
+ # Check for TS points and update ADD status
996
+ for ADDth in self.ADDths:
997
+ if ADDth.ADDremoveQ:
998
+ continue
999
+
1000
+ # Calculate gradient at current point
1001
+ grad_x = self.calculate_gradient(QMC, ADDth.x)
1002
+ if grad_x is False:
1003
+ ADDth.ADDremoveQ = True
1004
+ continue
1005
+
1006
+ ADDth.grad = grad_x
1007
+
1008
+ # Calculate normalized displacement vector
1009
+ normalized_nADD = ADDth.nADD.flatten() / np.linalg.norm(ADDth.nADD.flatten())
1010
+
1011
+ # Calculate projection of gradient onto displacement vector
1012
+ ADDth.grad_vec = np.dot(ADDth.grad.flatten(), normalized_nADD)
1013
+
1014
+ # Check if we've found a TS (gradient points downward)
1015
+ if sphereN > 5 and ADDth.grad_vec < 0.0:
1016
+ print(f"New TS point found at ADD {ADDth.IDnum}")
1017
+ ADDth.findTSQ = True
1018
+ TSinitialpoints.append(ADDth.x)
1019
+
1020
+ # If all ADDs are done, exit
1021
+ if all(ADDth.findTSQ or ADDth.ADDremoveQ for ADDth in self.ADDths):
1022
+ print("All ADD paths complete.")
1023
+ break
1024
+
1025
+ # Increase sphere size for next iteration
1026
+ IOEsphereA = (np.sqrt(IOEsphereA) + IOEsphereA_r) ** 2
1027
+ print(f"Expanding sphere to radius {np.sqrt(IOEsphereA):.4f}")
1028
+
1029
+ # Save displacement vectors for debugging
1030
+ with open(os.path.join(self.directory, f"displacement_vectors_sphere_{sphereN}.csv"), "w") as f:
1031
+ f.write("ADD_ID,x,y,z,ADD,ADD_IOE\n")
1032
+ for ADDth in self.ADDths:
1033
+ if ADDth.ADDremoveQ:
1034
+ continue
1035
+ for i in range(len(ADDth.nADD)):
1036
+ f.write(f"{ADDth.IDnum},{ADDth.nADD[i][0]},{ADDth.nADD[i][1]},{ADDth.nADD[i][2]},{ADDth.ADD},{ADDth.ADD_IOE}\n")
1037
+
1038
+ # Create separate trajectory files for each ADD path
1039
+ self.create_separate_xyz_files()
1040
+
1041
+ # Write TS points
1042
+ if TSinitialpoints:
1043
+ print(f"Found {len(TSinitialpoints)} potential transition states.")
1044
+ self.write_ts_points(TSinitialpoints)
1045
+
1046
+ return len(TSinitialpoints) > 0
1047
+
1048
+ def calculate_gradient(self, QMC, x):
1049
+ """Calculate gradient at point x"""
1050
+ element_number_list = self.get_element_number_list()
1051
+ _, grad_x, _, iscalculationfailed = QMC.single_point(
1052
+ None, element_number_list, "", self.electric_charge_and_multiplicity,
1053
+ self.method, x
1054
+ )
1055
+
1056
+ if iscalculationfailed:
1057
+ return False
1058
+
1059
+ # Apply bias if needed
1060
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
1061
+ _, _, bias_gradient, _ = BPC.main(
1062
+ 0, grad_x, x, element_number_list, self.config.force_data
1063
+ )
1064
+
1065
+ return bias_gradient
1066
+
1067
+ def write_ts_points(self, ts_points):
1068
+ """Write TS points to file"""
1069
+ with open(f"{self.directory}/TSpoints.xyz", "w") as f:
1070
+ for i, point in enumerate(ts_points):
1071
+ f.write(f"{len(self.element_list)}\n")
1072
+ f.write(f"TS candidate {i+1}\n")
1073
+ for j, (element, coord) in enumerate(zip(self.element_list, point)):
1074
+ f.write(f"{element} {coord[0]:.6f} {coord[1]:.6f} {coord[2]:.6f}\n")
1075
+
1076
+ # Getters and setters - keep as is
1077
+ def set_molecule(self, element_list, coords):
1078
+ self.element_list = element_list
1079
+ self.coords = coords
1080
+
1081
+ def set_gradient(self, gradient):
1082
+ self.gradient = gradient
1083
+
1084
+ def set_hessian(self, hessian):
1085
+ self.hessian = hessian
1086
+
1087
+ def set_energy(self, energy):
1088
+ self.energy = energy
1089
+
1090
+ def set_coords(self, coords):
1091
+ self.coords = coords
1092
+
1093
+ def set_element_list(self, element_list):
1094
+ self.element_list = element_list
1095
+ self.element_number_list = [element_number(i) for i in self.element_list]
1096
+
1097
+ def set_coord(self, coord):
1098
+ self.coords = coord
1099
+
1100
+ def get_coord(self):
1101
+ return self.coords
1102
+
1103
+ def get_element_list(self):
1104
+ return self.element_list
1105
+
1106
+ def get_element_number_list(self):
1107
+ if self.element_number_list is None:
1108
+ if self.element_list is None:
1109
+ raise ValueError('Element list is not set.')
1110
+ self.element_number_list = [element_number(i) for i in self.element_list]
1111
+ return self.element_number_list
1112
+
1113
+ def set_mole_info(self, base_file_name, electric_charge_and_multiplicity):
1114
+ coord, element_list, electric_charge_and_multiplicity = xyz2list(
1115
+ base_file_name + ".xyz", electric_charge_and_multiplicity)
1116
+
1117
+ if self.config.usextb != "None":
1118
+ self.method = self.config.usextb
1119
+ elif self.config.usedxtb != "None":
1120
+ self.method = self.config.usedxtb
1121
+ else:
1122
+ self.method = "None"
1123
+
1124
+ self.coords = np.array(coord, dtype="float64")
1125
+ self.element_list = element_list
1126
+ self.electric_charge_and_multiplicity = electric_charge_and_multiplicity
1127
+
1128
+ def run(self, file_directory, SP, electric_charge_and_multiplicity, FIO_img):
1129
+ print("### Start Anharmonic Downward Distortion (ADD) method ###")
1130
+
1131
+ # Preparation
1132
+ base_file_name = os.path.splitext(FIO_img.START_FILE)[0]
1133
+ self.set_mole_info(base_file_name, electric_charge_and_multiplicity)
1134
+
1135
+ self.directory = make_workspace(file_directory)
1136
+
1137
+ # Create main directory for optimized structures
1138
+ os.makedirs(os.path.join(self.directory, "optimized_structures"), exist_ok=True)
1139
+
1140
+ # Detect initial ADD directions
1141
+ success = self.detect_add(SP)
1142
+ if not success:
1143
+ return False
1144
+
1145
+ # Follow ADD paths to find transition states
1146
+ isConverged = self.add_following(SP)
1147
+
1148
+ print("### ADD Following is done. ###")
1149
+
1150
+ return isConverged