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,1095 @@
1
+ import copy
2
+ import datetime
3
+ import os
4
+ import numpy as np
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 twoPSHSlikeMethod:
13
+ def __init__(self, config):
14
+ """
15
+ Implementation of 2PSHS 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
+ # ref. : Chemical Physics Letters 404 (2005) 95–99.
20
+ """
21
+ self.config = config
22
+ self.addf_config = {
23
+ 'step_number': int(config.addf_step_num),
24
+ 'number_of_add': int(config.nadd),
25
+ 'IOEsphereA_initial': 0.01, # Initial hypersphere radius (will be overridden)
26
+ 'IOEsphereA_dist': float(config.addf_step_size), # Decrement for hypersphere radius
27
+ 'IOEthreshold': 0.01, # Threshold for IOE
28
+ 'minimize_threshold': 1.0e-5,# Threshold for minimization
29
+ }
30
+ self.energy_list_1 = []
31
+ self.energy_list_2 = []
32
+ self.gradient_list_1 = []
33
+ self.gradient_list_2 = []
34
+ self.init_displacement = 0.03 / self.get_unit_conversion() # Bohr
35
+ self.date = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
36
+ self.converge_criteria = 0.12 # Convergence criteria in Angstroms
37
+ self.element_number_list_1 = None
38
+ self.element_number_list_2 = None
39
+ self.ADDths = [] # List to store ADD theta classes
40
+ self.optimized_structures = {} # Dictionary to store optimized structures by ADD ID
41
+ self.max_iterations = 1 # Limit to 1 major iteration per hypersphere radius
42
+ self.max_inner_iterations = 20 # Maximum inner iterations for minimization
43
+ self.sp1_structure = None # Will store SP_1's structure
44
+ self.sp1_energy = None # Will store SP_1's energy
45
+ self.sp2_structure = None # Will store SP_2's structure
46
+ self.sp2_energy = None # Will store SP_2's energy
47
+ self.initial_distance = None # Will store distance between SP_1 and SP_2
48
+ self.stalled_count = 0 # Counter for detecting stalled optimization
49
+
50
+ def get_unit_conversion(self):
51
+ """Return bohr to angstrom conversion factor"""
52
+ return UnitValueLib().bohr2angstroms # Approximate value for bohr2angstroms
53
+
54
+ def adjust_center2origin(self, coord):
55
+ """Adjust coordinates to have center at origin"""
56
+ center = np.mean(coord, axis=0)
57
+ return coord - center
58
+
59
+ def SuperSphere_cartesian(self, A, thetalist, SQ, dim):
60
+ """
61
+ Vector of super sphere by cartesian (basis transformation from polar to cartesian)
62
+ {sqrt(2*A), theta_1,..., theta_n-1} -> {q_1,..., q_n} -> {x_1, x_2,..., x_n}
63
+ """
64
+ n_components = min(dim, SQ.shape[1] if SQ.ndim > 1 else dim)
65
+
66
+ qlist = np.zeros(n_components)
67
+
68
+ # Fill q-list using hyperspherical coordinates
69
+ a_k = np.sqrt(2.0 * A)
70
+ for i in range(min(len(thetalist), n_components-1)):
71
+ qlist[i] = a_k * np.cos(thetalist[i])
72
+ a_k *= np.sin(thetalist[i])
73
+
74
+ # Handle the last component
75
+ if n_components > 0:
76
+ qlist[n_components-1] = a_k
77
+
78
+ # Transform to original space
79
+ SSvec = np.dot(SQ, qlist)
80
+
81
+ return SSvec # This is a vector in the reduced space
82
+
83
+ def calctheta(self, vec, eigVlist, eigNlist):
84
+ """
85
+ Calculate the polar coordinates (theta) from a vector
86
+ """
87
+ # Get actual dimensions
88
+ n_features = eigVlist[0].shape[0] # Length of each eigenvector
89
+ n_components = min(len(eigVlist), len(eigNlist)) # Number of eigenvectors
90
+
91
+ # Check vector dimensions
92
+ if len(vec) != n_features:
93
+ # If dimensions don't match, truncate or pad the vector
94
+ if len(vec) > n_features:
95
+ vec = vec[:n_features] # Truncate
96
+ else:
97
+ padded_vec = np.zeros(n_features)
98
+ padded_vec[:len(vec)] = vec
99
+ vec = padded_vec
100
+
101
+ # Create SQ_inv matrix with correct dimensions
102
+ SQ_inv = np.zeros((n_components, n_features))
103
+
104
+ for i in range(n_components):
105
+ SQ_inv[i] = eigVlist[i] / np.sqrt(abs(eigNlist[i]))
106
+
107
+ # Perform the dot product
108
+ qvec = np.dot(SQ_inv, vec)
109
+
110
+ r = np.linalg.norm(qvec)
111
+ if r < 1e-10:
112
+ return np.zeros(n_components - 1)
113
+
114
+ thetalist = []
115
+ for i in range(len(qvec) - 1):
116
+ # Handle possible numerical issues with normalization
117
+ norm_q = np.linalg.norm(qvec[i:])
118
+ if norm_q < 1e-10:
119
+ theta = 0.0
120
+ else:
121
+ cos_theta = qvec[i] / norm_q
122
+ cos_theta = max(-1.0, min(1.0, cos_theta)) # Ensure within bounds
123
+ theta = np.arccos(cos_theta)
124
+ if i == len(qvec) - 2 and qvec[-1] < 0:
125
+ theta = 2*np.pi - theta
126
+ thetalist.append(theta)
127
+
128
+ return np.array(thetalist)
129
+
130
+ def SQaxes(self, eigNlist, eigVlist, dim):
131
+ """Calculate the SQ matrix for transformation"""
132
+ # Get actual available dimensions
133
+ n_features = eigVlist[0].shape[0] # Length of each eigenvector
134
+ n_components = min(len(eigVlist), len(eigNlist), dim) # Number of eigenvectors to use
135
+
136
+ # Initialize with correct dimensions
137
+ SQ = np.zeros((n_features, n_components))
138
+
139
+ # Only iterate up to the available components
140
+ for i in range(n_components):
141
+ SQ[:, i] = eigVlist[i] * np.sqrt(abs(eigNlist[i]))
142
+
143
+ return SQ
144
+
145
+ def SQaxes_inv(self, eigNlist, eigVlist, dim):
146
+ """Calculate the inverse SQ matrix for transformation"""
147
+ # Get actual available dimensions
148
+ n_features = eigVlist[0].shape[0] # Length of each eigenvector
149
+ n_components = min(len(eigVlist), len(eigNlist), dim) # Number of eigenvectors to use
150
+
151
+ # Initialize with correct dimensions
152
+ SQ_inv = np.zeros((n_components, n_features))
153
+
154
+ # Only iterate up to the available components
155
+ for i in range(n_components):
156
+ SQ_inv[i] = eigVlist[i] / np.sqrt(abs(eigNlist[i]))
157
+
158
+ return SQ_inv
159
+
160
+ def angle(self, v1, v2):
161
+ """Calculate angle between two vectors"""
162
+ # Check for zero vectors or invalid inputs
163
+ if np.linalg.norm(v1) < 1e-10 or np.linalg.norm(v2) < 1e-10:
164
+ return 0.0
165
+
166
+ v1_u = v1 / np.linalg.norm(v1)
167
+ v2_u = v2 / np.linalg.norm(v2)
168
+
169
+ # Handle potential numerical issues
170
+ dot_product = np.clip(np.dot(v1_u, v2_u), -1.0, 1.0)
171
+
172
+ return np.arccos(dot_product)
173
+
174
+ def angle_SHS(self, v1, v2, SQ_inv):
175
+ """Calculate angle between two vectors in SHS space"""
176
+ # Ensure vectors are flattened
177
+ v1_flat = v1.flatten() if hasattr(v1, 'flatten') else v1
178
+ v2_flat = v2.flatten() if hasattr(v2, 'flatten') else v2
179
+
180
+ # Handle potential dimension mismatches
181
+ min_dim = min(len(v1_flat), len(v2_flat), SQ_inv.shape[1])
182
+ v1_flat = v1_flat[:min_dim]
183
+ v2_flat = v2_flat[:min_dim]
184
+
185
+ q_v1 = np.dot(SQ_inv[:, :min_dim], v1_flat)
186
+ q_v2 = np.dot(SQ_inv[:, :min_dim], v2_flat)
187
+ return self.angle(q_v1, q_v2)
188
+
189
+ def calc_cartesian_distance(self, structure1, structure2):
190
+ """Calculate the RMSD between two molecular structures in Cartesian coordinates"""
191
+ if structure1.shape != structure2.shape:
192
+ raise ValueError("Structures have different shapes")
193
+
194
+ # Calculate squared differences
195
+ squared_diff = np.sum((structure1 - structure2)**2)
196
+
197
+ # Return RMSD
198
+ return np.sqrt(squared_diff / structure1.shape[0])
199
+
200
+ def grad_hypersphere(self, f, grad, eqpoint, IOEsphereA, thetalist):
201
+ """Calculate gradient on hypersphere - MODIFIED for 2PSHS to minimize ADD"""
202
+ # Generate nADD in the reduced space (this will be a vector in the Hessian eigenspace)
203
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
204
+
205
+ # We need to convert the reduced space vector back to the full coordinate space
206
+ # First create a zero vector in the full space
207
+ n_atoms = eqpoint.shape[0]
208
+ n_coords = n_atoms * 3
209
+ nADD_full = np.zeros(n_coords)
210
+
211
+ # Map the reduced vector to the full space (this is approximate)
212
+ for i in range(min(len(nADD_reduced), n_coords)):
213
+ nADD_full[i] = nADD_reduced[i]
214
+
215
+ # Reshape to molecular geometry format
216
+ nADD = nADD_full.reshape(n_atoms, 3)
217
+
218
+ # Calculate the normalized direction vector for projecting out later
219
+ EnADD = nADD_full / np.linalg.norm(nADD_full)
220
+
221
+ # Apply the displacement to the initial geometry
222
+ target_point = eqpoint + nADD
223
+ target_point = self.periodicpoint(target_point)
224
+
225
+ # Calculate gradient at this point
226
+ grad_x = grad(target_point)
227
+ if isinstance(grad_x, bool) and grad_x is False:
228
+ return False, False
229
+
230
+ # Flatten the gradient for vector operations
231
+ grad_x_flat = grad_x.flatten()
232
+
233
+ # Project gradient onto tangent space of hypersphere
234
+ # We remove the component along the displacement vector
235
+ returngrad_flat = grad_x_flat - np.dot(grad_x_flat, EnADD) * EnADD
236
+
237
+ # 2PSHS modification: Calculate distance to SP_1 structure
238
+ distance_to_sp1 = self.calc_cartesian_distance(target_point, self.sp1_structure)
239
+
240
+ # Create a gradient component that points toward SP_1's structure
241
+ # This is the key modification for 2PSHS - we want to minimize ADD
242
+ direction_to_sp1 = self.sp1_structure - target_point
243
+ direction_to_sp1_flat = direction_to_sp1.flatten()
244
+
245
+ # Normalize the direction vector
246
+ direction_norm = np.linalg.norm(direction_to_sp1_flat)
247
+ if direction_norm > 1e-10:
248
+ direction_to_sp1_flat /= direction_norm
249
+
250
+ # Project this direction to be tangent to the hypersphere
251
+ direction_component_along_nADD = np.dot(direction_to_sp1_flat, EnADD)
252
+ direction_on_tangent = direction_to_sp1_flat - direction_component_along_nADD * EnADD
253
+ direction_on_tangent_norm = np.linalg.norm(direction_on_tangent)
254
+
255
+ if direction_on_tangent_norm > 1e-10:
256
+ direction_on_tangent /= direction_on_tangent_norm
257
+
258
+ # Add this component to our gradient
259
+ # Weight decreases as we get closer to SP_1
260
+ weight = min(1.0, distance_to_sp1 / self.converge_criteria)
261
+ returngrad_flat += direction_on_tangent * weight * np.linalg.norm(returngrad_flat)
262
+
263
+ # Reshape gradient back to molecular geometry format for easier handling
264
+ returngrad = returngrad_flat.reshape(n_atoms, 3)
265
+
266
+ return target_point, returngrad
267
+
268
+ def periodicpoint(self, point):
269
+ """Apply periodic boundary conditions if needed"""
270
+ # Implement according to your specific requirements
271
+ return point
272
+
273
+ def minimizeTh_SD_SS(self, ADDth, initialpoint, f, grad, eqpoint, IOEsphereA):
274
+ """
275
+ Steepest descent optimization on hypersphere to minimize ADD
276
+ For 2PSHS, we want to find minimum ADD on each hypersphere before reducing radius
277
+ """
278
+ whileN = 0
279
+ thetalist = ADDth.thetalist + initialpoint
280
+ stepsize = 0.1
281
+ n_atoms = eqpoint.shape[0]
282
+ n_coords = n_atoms * 3
283
+
284
+ # Generate initial nADD
285
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
286
+
287
+ # Convert reduced space vector to full coordinate space with proper dimensions
288
+ nADD_full = np.zeros(n_coords)
289
+ for i in range(min(len(nADD_reduced), n_coords)):
290
+ nADD_full[i] = nADD_reduced[i]
291
+
292
+ # Reshape to match eqpoint dimensions
293
+ nADD = nADD_full.reshape(n_atoms, 3)
294
+
295
+ # Keep track of best solution (for minimizing ADD)
296
+ best_thetalist = thetalist.copy()
297
+ best_add_value = float('inf') # We want to minimize ADD, so start with positive infinity
298
+
299
+ # Initial point
300
+ targetpoint = eqpoint + nADD
301
+ targetpoint = self.periodicpoint(targetpoint)
302
+
303
+ # Try to calculate initial energy and ADD
304
+ try:
305
+ initial_energy = f(targetpoint)
306
+ if isinstance(initial_energy, (int, float)) and not np.isnan(initial_energy):
307
+ # Calculate initial ADD
308
+ initial_add = initial_energy - IOEsphereA - self.sp1_energy
309
+ best_add_value = initial_add
310
+ except Exception:
311
+ pass # Continue even if initial energy calculation fails
312
+
313
+ # Variables to detect convergence
314
+ prev_add_values = []
315
+ no_improvement_count = 0
316
+
317
+ # Main optimization loop
318
+ while whileN < self.max_inner_iterations:
319
+ try:
320
+ # Get gradient at current point
321
+ grad_x = grad(targetpoint)
322
+
323
+ # If gradient calculation fails, continue with smaller step or different approach
324
+ if grad_x is False:
325
+ # Try a random perturbation and continue
326
+ print(f"Gradient calculation failed at iteration {whileN}, trying random perturbation")
327
+ random_perturbation = np.random.rand(n_atoms, 3) * 0.01 - 0.005 # Small random perturbation
328
+ targetpoint = targetpoint + random_perturbation
329
+ targetpoint = self.periodicpoint(targetpoint)
330
+
331
+ # Calculate new nADD
332
+ nADD = targetpoint - eqpoint
333
+ thetalist = self.calctheta(nADD.flatten(), self.eigVlist, self.eigNlist)
334
+
335
+ # Ensure we're on the hypersphere with correct radius
336
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
337
+ nADD_full = np.zeros(n_coords)
338
+ for i in range(min(len(nADD_reduced), n_coords)):
339
+ nADD_full[i] = nADD_reduced[i]
340
+ nADD = nADD_full.reshape(n_atoms, 3)
341
+
342
+ targetpoint = eqpoint + nADD
343
+ targetpoint = self.periodicpoint(targetpoint)
344
+
345
+ whileN += 1
346
+ continue
347
+
348
+ # Calculate nADD norm
349
+ nADD_norm = np.linalg.norm(nADD.flatten())
350
+ if nADD_norm < 1e-10:
351
+ # If nADD is too small, generate a new one
352
+ print(f"nADD norm too small at iteration {whileN}, regenerating")
353
+ thetalist = self.calctheta(np.random.rand(n_coords) - 0.5, self.eigVlist, self.eigNlist)
354
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
355
+ nADD_full = np.zeros(n_coords)
356
+ for i in range(min(len(nADD_reduced), n_coords)):
357
+ nADD_full[i] = nADD_reduced[i]
358
+ nADD = nADD_full.reshape(n_atoms, 3)
359
+ targetpoint = eqpoint + nADD
360
+ targetpoint = self.periodicpoint(targetpoint)
361
+ whileN += 1
362
+ continue
363
+
364
+ # Project gradient onto tangent space of hypersphere
365
+ nADD_unit = nADD.flatten() / nADD_norm
366
+ grad_along_nADD = np.dot(grad_x.flatten(), nADD_unit)
367
+ SSgrad_flat = grad_x.flatten() - grad_along_nADD * nADD_unit
368
+
369
+ # For minimizing ADD, we follow the negative gradient
370
+ # This is the key aspect - when minimizing, we move opposite to gradient direction
371
+ SSgrad = -1.0 * SSgrad_flat.reshape(n_atoms, 3)
372
+
373
+ # Calculate energy and current ADD
374
+ current_energy = f(targetpoint)
375
+ current_add = current_energy - IOEsphereA - self.sp1_energy
376
+
377
+ # Store current ADD value for convergence detection
378
+ prev_add_values.append(current_add)
379
+ if len(prev_add_values) > 3:
380
+ prev_add_values.pop(0)
381
+
382
+ # Check if gradient is small enough (local minimum on the hypersphere)
383
+ if np.linalg.norm(SSgrad) < 1.0e-3:
384
+ print(f"Small gradient: {np.linalg.norm(SSgrad):.6f}, potential minimum ADD found: {current_add:.6f}")
385
+ # If gradient is small, we may have found a local minimum
386
+ if current_add < best_add_value:
387
+ best_add_value = current_add
388
+ best_thetalist = thetalist.copy()
389
+ ADDth.converged = True
390
+ print(f"New best ADD value: {best_add_value:.6f}")
391
+ return thetalist
392
+
393
+ # Store current point
394
+ _point_initial = copy.copy(targetpoint)
395
+
396
+ # Line search
397
+ stepsizedamp = stepsize
398
+ found_valid_step = False
399
+
400
+ # Try multiple step sizes
401
+ for whileN2 in range(1, 5): # Try up to 4 steps with varying sizes
402
+ try:
403
+ # Take step with dynamic step size
404
+ step_scale = whileN2 if whileN2 <= 2 else (whileN2 - 2) * 0.1
405
+
406
+ # Follow the negative gradient to minimize ADD
407
+ targetpoint = _point_initial + step_scale * SSgrad / np.linalg.norm(SSgrad) * stepsizedamp
408
+
409
+ # Calculate new nADD
410
+ nADD2 = targetpoint - eqpoint
411
+
412
+ # Convert to theta parameters
413
+ thetalist_new = self.calctheta(nADD2.flatten(), self.eigVlist, self.eigNlist)
414
+
415
+ # Ensure we're on the hypersphere with correct radius
416
+ nADD2_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist_new, self.SQ, self.dim)
417
+
418
+ # Convert reduced space vector to full coordinate space
419
+ nADD2_full = np.zeros(n_coords)
420
+ for i in range(min(len(nADD2_reduced), n_coords)):
421
+ nADD2_full[i] = nADD2_reduced[i]
422
+
423
+ # Reshape to match eqpoint dimensions
424
+ nADD2 = nADD2_full.reshape(n_atoms, 3)
425
+
426
+ # Calculate new point on hypersphere
427
+ new_point = eqpoint + nADD2
428
+ new_point = self.periodicpoint(new_point)
429
+
430
+ # Calculate energy and ADD at new point
431
+ new_energy = f(new_point)
432
+ new_add = new_energy - IOEsphereA - self.sp1_energy
433
+
434
+ # Accept step if it decreases ADD (since we're minimizing)
435
+ if new_add < current_add:
436
+ found_valid_step = True
437
+ targetpoint = new_point
438
+ thetalist = thetalist_new
439
+ nADD = nADD2
440
+
441
+ # Update best solution if better
442
+ if new_add < best_add_value:
443
+ best_add_value = new_add
444
+ best_thetalist = thetalist_new.copy()
445
+
446
+ break
447
+
448
+ except Exception as e:
449
+ print(f"Step calculation error: {e}, trying different step")
450
+ continue
451
+
452
+ # If no valid step found, try random steps to escape local minima
453
+ if not found_valid_step:
454
+ # Check if we've been stuck for too long
455
+ if len(prev_add_values) >= 3 and all(abs(prev_add_values[i] - prev_add_values[i-1]) < 1e-4 for i in range(1, len(prev_add_values))):
456
+ no_improvement_count += 1
457
+ if no_improvement_count > 3:
458
+ print(f"No improvement in ADD for several iterations. Current ADD: {current_add:.6f}")
459
+ # If we're stuck, return the best solution found so far
460
+ if current_add < best_add_value:
461
+ best_add_value = current_add
462
+ best_thetalist = thetalist.copy()
463
+ ADDth.converged = True
464
+ return best_thetalist
465
+
466
+ # Try a random step
467
+ random_theta_offset = np.random.uniform(-0.1, 0.1, len(thetalist))
468
+ thetalist_new = thetalist + random_theta_offset
469
+
470
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist_new, self.SQ, self.dim)
471
+ nADD_full = np.zeros(n_coords)
472
+ for i in range(min(len(nADD_reduced), n_coords)):
473
+ nADD_full[i] = nADD_reduced[i]
474
+ nADD = nADD_full.reshape(n_atoms, 3)
475
+
476
+ targetpoint = eqpoint + nADD
477
+ targetpoint = self.periodicpoint(targetpoint)
478
+ thetalist = thetalist_new
479
+
480
+ # Check ADD at new random point
481
+ try:
482
+ new_energy = f(targetpoint)
483
+ new_add = new_energy - IOEsphereA - self.sp1_energy
484
+
485
+ if new_add < best_add_value:
486
+ best_add_value = new_add
487
+ best_thetalist = thetalist.copy()
488
+ except Exception:
489
+ pass
490
+
491
+ # Print progress periodically
492
+ if whileN % 5 == 0:
493
+ print(f"Iteration {whileN}: Current ADD = {current_add:.6f}, Best ADD = {best_add_value:.6f}")
494
+
495
+ whileN += 1
496
+
497
+ except Exception as e:
498
+ print(f"Error in optimization step {whileN}: {e}")
499
+ whileN += 1
500
+ # Try a random step to recover
501
+ random_theta_offset = np.random.uniform(-0.1, 0.1, len(thetalist))
502
+ thetalist = thetalist + random_theta_offset
503
+
504
+ nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
505
+ nADD_full = np.zeros(n_coords)
506
+ for i in range(min(len(nADD_reduced), n_coords)):
507
+ nADD_full[i] = nADD_reduced[i]
508
+ nADD = nADD_full.reshape(n_atoms, 3)
509
+
510
+ targetpoint = eqpoint + nADD
511
+ targetpoint = self.periodicpoint(targetpoint)
512
+
513
+ print(f"Max iterations ({self.max_inner_iterations}) reached, returning best solution with ADD = {best_add_value:.6f}")
514
+ # Return the best solution found based on minimizing ADD
515
+ return best_thetalist
516
+
517
+ def save_optimized_structure(self, ADDth, iteration_num, IOEsphereA):
518
+ """Save optimized structure for a specific ADD and iteration"""
519
+ # Create directory path for ADD-specific structures
520
+ add_dir = os.path.join(self.directory, "optimized_structures", f"ADD_{ADDth.IDnum}")
521
+ os.makedirs(add_dir, exist_ok=True)
522
+
523
+ # Create filename with radius and iteration information
524
+ radius = np.sqrt(IOEsphereA)
525
+ filename = f"iteration_{iteration_num}_r_{radius:.4f}.xyz"
526
+ filepath = os.path.join(add_dir, filename)
527
+
528
+ # Calculate distance to SP_1
529
+ distance = self.calc_cartesian_distance(ADDth.x, self.sp1_structure)
530
+
531
+ # Write XYZ file
532
+ with open(filepath, 'w') as f:
533
+ f.write(f"{len(self.element_list_1)}\n")
534
+ f.write(f"ADD_{ADDth.IDnum} Iteration {iteration_num} Radius {radius:.4f} Distance_to_SP1 {distance:.6f}\n")
535
+ for i, (element, coord) in enumerate(zip(self.element_list_1, ADDth.x)):
536
+ f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
537
+
538
+ # Store structure information in dictionary
539
+ if ADDth.IDnum not in self.optimized_structures:
540
+ self.optimized_structures[ADDth.IDnum] = []
541
+
542
+ self.optimized_structures[ADDth.IDnum].append({
543
+ 'iteration': iteration_num,
544
+ 'radius': radius,
545
+ 'distance': distance,
546
+ 'file': filepath,
547
+ 'coords': ADDth.x.copy(),
548
+ 'comment': f"ADD_{ADDth.IDnum} Iteration {iteration_num} Radius {radius:.4f} Distance_to_SP1 {distance:.6f}"
549
+ })
550
+
551
+ def create_separate_xyz_files(self):
552
+ """Create separate XYZ files for each ADD path"""
553
+ created_files = []
554
+
555
+ for add_id, structures in self.optimized_structures.items():
556
+ # Sort structures by iteration number
557
+ structures.sort(key=lambda x: x['iteration'])
558
+
559
+ # Path for the ADD-specific file
560
+ add_trajectory_file = os.path.join(self.directory, f"ADD_{add_id}_trajectory.xyz")
561
+
562
+ # Write the trajectory file
563
+ with open(add_trajectory_file, 'w') as f:
564
+ for structure in structures:
565
+ f.write(f"{len(self.element_list_1)}\n")
566
+ f.write(f"{structure['comment']}\n")
567
+ for i, (element, coord) in enumerate(zip(self.element_list_1, structure['coords'])):
568
+ f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
569
+
570
+ created_files.append(add_trajectory_file)
571
+
572
+ if created_files:
573
+ paths_str = "\n".join(created_files)
574
+ print(f"Created {len(created_files)} ADD trajectory files:\n{paths_str}")
575
+
576
+ def create_distance_plots(self):
577
+ """Create CSV files with distance data for plotting"""
578
+ for add_id, structures in self.optimized_structures.items():
579
+ # Sort structures by iteration number
580
+ structures.sort(key=lambda x: x['iteration'])
581
+
582
+ # Path for the distance data file
583
+ distance_file = os.path.join(self.directory, f"ADD_{add_id}_distances.csv")
584
+
585
+ # Write the distance data
586
+ with open(distance_file, 'w') as f:
587
+ f.write("iteration,radius,distance_to_sp1\n")
588
+ for structure in structures:
589
+ f.write(f"{structure['iteration']},{structure['radius']:.4f},{structure['distance']:.6f}\n")
590
+
591
+ print(f"Created distance data file: {distance_file}")
592
+
593
+ def detect_add(self, SP_1, SP_2):
594
+ """
595
+ Calculate coordinate axes from SP_1's Hessian for creating the hypersphere
596
+ Use the direction from SP_1 to SP_2 as the primary direction
597
+ """
598
+ # Get coordinates for SP_1
599
+ coord_1 = self.coords_1
600
+ coord_1 = self.adjust_center2origin(coord_1)
601
+ n_atoms = coord_1.shape[0]
602
+ n_coords = n_atoms * 3
603
+
604
+ # Get coordinates for SP_2
605
+ coord_2 = self.coords_2
606
+ coord_2 = self.adjust_center2origin(coord_2)
607
+
608
+ # Check that both structures have the same number of atoms
609
+ if coord_1.shape != coord_2.shape:
610
+ print("SP_1 and SP_2 structures have different shapes")
611
+ return False
612
+
613
+ element_number_list_1 = self.get_element_number_list_1()
614
+ print("### Calculating SP_1 structure and Hessian to create coordinate system ###")
615
+
616
+ SP_1.hessian_flag = True
617
+ self.init_energy_1, self.init_gradient_1, _, iscalculationfailed = SP_1.single_point(
618
+ None, element_number_list_1, "", self.electric_charge_and_multiplicity_1,
619
+ self.method, coord_1
620
+ )
621
+
622
+ if iscalculationfailed:
623
+ print("Initial calculation with SP_1 failed.")
624
+ return False
625
+
626
+ # Apply bias potential if needed for SP_1
627
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
628
+ _, bias_energy_1, bias_gradient_1, bias_hess_1 = BPC.main(
629
+ self.init_energy_1, self.init_gradient_1, coord_1, element_number_list_1,
630
+ self.config.force_data
631
+ )
632
+ self.init_energy_1 = bias_energy_1
633
+ self.init_gradient_1 = bias_gradient_1
634
+
635
+ SP_1.hessian_flag = False
636
+ self.init_geometry = coord_1 # Shape: (n_atoms, 3)
637
+
638
+ # Store SP_1 structure and energy
639
+ self.sp1_structure = copy.deepcopy(coord_1)
640
+ self.sp1_energy = self.init_energy_1
641
+
642
+ print(f"SP_1 energy: {self.sp1_energy:.6f}")
643
+ print("### Calculating Hessian matrix to set up coordinate system ###")
644
+
645
+ hessian = SP_1.Model_hess + bias_hess_1
646
+
647
+ # Project out translation and rotation
648
+ projection_hessian = Calculationtools().project_out_hess_tr_and_rot_for_coord(hessian, self.element_list_1, coord_1)
649
+
650
+ eigenvalues, eigenvectors = np.linalg.eigh(projection_hessian)
651
+ eigenvalues = eigenvalues.astype(np.float64)
652
+
653
+ # Filter out near-zero eigenvalues
654
+ nonzero_indices = np.where(np.abs(eigenvalues) > 1e-10)[0]
655
+ nonzero_eigenvectors = eigenvectors[:, nonzero_indices].astype(np.float64)
656
+ nonzero_eigenvalues = eigenvalues[nonzero_indices].astype(np.float64)
657
+
658
+ sorted_idx = np.argsort(nonzero_eigenvalues)
659
+
660
+ self.init_eigenvalues = nonzero_eigenvalues
661
+ self.init_eigenvectors = nonzero_eigenvectors
662
+ self.dim = len(nonzero_eigenvalues)
663
+ self.n_atoms = n_atoms
664
+ self.n_coords = n_coords
665
+
666
+ # Store flattened versions for matrix operations
667
+ self.init_geometry_flat = self.init_geometry.flatten()
668
+
669
+ # Prepare SQ matrices
670
+ self.SQ = self.SQaxes(nonzero_eigenvalues, nonzero_eigenvectors, self.dim)
671
+ self.SQ_inv = self.SQaxes_inv(nonzero_eigenvalues, nonzero_eigenvectors, self.dim)
672
+
673
+ self.eigNlist = nonzero_eigenvalues
674
+ self.eigVlist = nonzero_eigenvectors
675
+
676
+ # Calculate mode eigenvectors to try - focus on lower eigenvectors first
677
+ search_idx = len(sorted_idx) # Start with all eigenvectors
678
+ self.sorted_eigenvalues_idx = sorted_idx[0:search_idx]
679
+
680
+ # Initialize ADDths with initial directions
681
+ self.ADDths = []
682
+
683
+ print("### Getting SP_2 structure and energy ###")
684
+ # Calculate SP_2's energy at its initial structure
685
+ element_number_list_2 = self.get_element_number_list_2()
686
+ sp2_energy, sp2_gradient, _, iscalculationfailed = SP_2.single_point(
687
+ None, element_number_list_2, "", self.electric_charge_and_multiplicity_2,
688
+ self.method, coord_2
689
+ )
690
+
691
+ if iscalculationfailed:
692
+ print("Initial calculation with SP_2 failed.")
693
+ return False
694
+
695
+ # Apply bias potential if needed for SP_2
696
+ _, bias_energy_2, bias_gradient_2, _ = BPC.main(
697
+ sp2_energy, sp2_gradient, coord_2, element_number_list_2,
698
+ self.config.force_data
699
+ )
700
+
701
+ self.sp2_structure = copy.deepcopy(coord_2)
702
+ self.sp2_energy = bias_energy_2
703
+
704
+ print(f"SP_2 energy: {self.sp2_energy:.6f}")
705
+
706
+ # Calculate vector from SP_1 to SP_2
707
+ direction_vector = self.sp1_structure - self.sp2_structure
708
+ direction_vector_flat = direction_vector.flatten()
709
+
710
+ # Calculate distance between SP_1 and SP_2
711
+ direction_norm = np.linalg.norm(direction_vector_flat)
712
+ if direction_norm < 1e-10:
713
+ print("SP_1 and SP_2 structures are too similar. Cannot establish a meaningful direction.")
714
+ return False
715
+
716
+ # Set the initial distance
717
+ self.initial_distance = direction_norm
718
+
719
+ # Normalize the direction vector
720
+ direction_vector_flat = direction_vector_flat / direction_norm
721
+
722
+ print("### Setting up initial point on the hypersphere ###")
723
+ print(f"Distance between SP_1 and SP_2: {direction_norm:.6f} Å")
724
+
725
+ with open(self.directory + "/direction_info.csv", "w") as f:
726
+ f.write("direction,distance_between_sp1_sp2\n")
727
+ f.write(f"SP_1_to_SP_2,{direction_norm:.6f}\n")
728
+
729
+ # Use the distance between SP_1 and SP_2 as the initial sphere radius
730
+ IOEsphereA = direction_norm ** 2 # Convert to A value (squared radius)
731
+
732
+ # Create a single ADD point based on the SP_1 to SP_2 direction
733
+ ADDth = type('ADDthetaClass', (), {})
734
+ ADDth.IDnum = 0
735
+ ADDth.dim = self.dim
736
+ ADDth.SQ = self.SQ
737
+ ADDth.SQ_inv = self.SQ_inv
738
+
739
+ # Calculate theta parameters from the direction vector
740
+ ADDth.thetalist = self.calctheta(direction_vector_flat, nonzero_eigenvectors, nonzero_eigenvalues)
741
+
742
+ # Generate nADD (this will be a flattened vector in the Hessian eigenspace)
743
+ ADDth.nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, ADDth.thetalist, self.SQ, self.dim)
744
+
745
+ # Convert the reduced space vector back to the full coordinate space
746
+ ADDth.nADD_full = np.zeros(n_coords)
747
+ for i in range(min(len(ADDth.nADD_reduced), n_coords)):
748
+ ADDth.nADD_full[i] = ADDth.nADD_reduced[i]
749
+
750
+ # Reshape to molecular geometry format
751
+ ADDth.nADD = ADDth.nADD_full.reshape(n_atoms, 3)
752
+
753
+ # Apply the displacement to the initial geometry
754
+ ADDth.x = self.init_geometry + ADDth.nADD
755
+ ADDth.x = self.periodicpoint(ADDth.x)
756
+
757
+ # Initialize flags
758
+ ADDth.converged = False
759
+ ADDth.ADDoptQ = True
760
+ ADDth.ADDremoveQ = False
761
+ ADDth.last_distance = float('inf') # For tracking progress
762
+
763
+ # Store the reference direction
764
+ ADDth.direction_vector = direction_vector
765
+
766
+ # Add to ADDths list
767
+ self.ADDths = [ADDth]
768
+
769
+ print(f"### Primary direction established with initial radius {np.sqrt(IOEsphereA):.6f} ###")
770
+
771
+ # Initialize the optimized structures dictionary
772
+ self.optimized_structures = {}
773
+
774
+ # Create directory for optimized structures
775
+ os.makedirs(os.path.join(self.directory, "optimized_structures"), exist_ok=True)
776
+
777
+ print("### Coordinate system setup complete ###")
778
+ return True
779
+
780
+ def optimize_with_sp2(self, ADDths, SP_2, eqpoint, IOEsphereA, sphereN):
781
+ """
782
+ Optimize points on the hypersphere using SP_2
783
+ Minimize ADD on the hypersphere
784
+ """
785
+ print(f"Starting optimization on sphere {sphereN} with radius {np.sqrt(IOEsphereA):.4f}")
786
+
787
+ # Reset optimization flags
788
+ for ADDth in ADDths:
789
+ if not ADDth.converged and not ADDth.ADDremoveQ:
790
+ ADDth.ADDoptQ = True
791
+ else:
792
+ ADDth.ADDoptQ = False
793
+
794
+ # Create a directory for intermediate optimization steps
795
+ sphere_dir = os.path.join(self.directory, "optimized_structures", f"sphere_{sphereN}")
796
+ os.makedirs(sphere_dir, exist_ok=True)
797
+
798
+ # One iteration per hypersphere
799
+ iteration_num = 1
800
+ print(f"Iteration {iteration_num}")
801
+
802
+ # Process each ADD point
803
+ for ADDth in ADDths:
804
+ if not ADDth.ADDoptQ or ADDth.ADDremoveQ:
805
+ continue
806
+
807
+ # Optimize this ADD point
808
+ self.current_id = ADDth.IDnum
809
+
810
+ # Starting from zero displacement
811
+ x_initial = np.zeros(len(ADDth.thetalist))
812
+
813
+ # Minimize ADD on hypersphere using our modified steepest descent
814
+ thetalist = self.minimizeTh_SD_SS(
815
+ ADDth, x_initial,
816
+ lambda x: SP_2.single_point(None, self.get_element_number_list_2(), "",
817
+ self.electric_charge_and_multiplicity_2, self.method, x)[0],
818
+ lambda x: self.calculate_gradient(SP_2, x, self.element_number_list_2,
819
+ self.electric_charge_and_multiplicity_2),
820
+ eqpoint, IOEsphereA
821
+ )
822
+
823
+ if thetalist is False:
824
+ ADDth.ADDremoveQ = True
825
+ print(f"ADD {ADDth.IDnum} optimization failed, marking for removal")
826
+ continue
827
+
828
+ # Update ADD point with optimized position
829
+ ADDth.thetalist = thetalist
830
+
831
+ # Generate nADD in the reduced space
832
+ n_atoms = eqpoint.shape[0]
833
+ ADDth.nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, ADDth.thetalist, self.SQ, self.dim)
834
+
835
+ # Map to full coordinate space
836
+ ADDth.nADD_full = np.zeros(n_atoms * 3)
837
+ for i in range(min(len(ADDth.nADD_reduced), n_atoms * 3)):
838
+ ADDth.nADD_full[i] = ADDth.nADD_reduced[i]
839
+
840
+ # Reshape to molecular geometry
841
+ ADDth.nADD = ADDth.nADD_full.reshape(n_atoms, 3)
842
+
843
+ # Calculate new coordinates
844
+ ADDth.x = eqpoint + ADDth.nADD
845
+ ADDth.x = self.periodicpoint(ADDth.x)
846
+
847
+ # Calculate new energy with SP_2
848
+ energy, grad, _, iscalculationfailed = SP_2.single_point(
849
+ None, self.get_element_number_list_2(), "",
850
+ self.electric_charge_and_multiplicity_2, self.method, ADDth.x
851
+ )
852
+
853
+ if iscalculationfailed:
854
+ ADDth.ADDremoveQ = True
855
+ continue
856
+
857
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
858
+ _, bias_energy, bias_grad, _ = BPC.main(
859
+ energy, grad, ADDth.x, self.get_element_number_list_2(),
860
+ self.config.force_data
861
+ )
862
+
863
+ ADDth.A = bias_energy
864
+ ADDth.ADD = ADDth.A - IOEsphereA - self.sp1_energy # Use SP_1's energy as reference
865
+
866
+ # Calculate distance to SP_1 structure (for information only)
867
+ distance_to_sp1 = self.calc_cartesian_distance(ADDth.x, self.sp1_structure)
868
+ ADDth.distance_to_sp1 = distance_to_sp1
869
+
870
+ # Print results
871
+ print(f"ADD {ADDth.IDnum}: Energy={energy:.6f}, ADD={ADDth.ADD:.6f}, ||Grad||={np.linalg.norm(grad):.6f}, Distance to SP_1={distance_to_sp1:.6f} Å")
872
+
873
+ # Save structure
874
+ self.save_optimized_structure(ADDth, iteration_num, IOEsphereA)
875
+
876
+ # Filter ADDths list
877
+ ADDths = [ADDth for ADDth in ADDths if not ADDth.ADDremoveQ]
878
+
879
+ # Create a summary of final ADD values
880
+ print("\n### Final ADD values for this hypersphere ###")
881
+ min_add = float('inf')
882
+ min_add_id = -1
883
+
884
+ for ADDth in ADDths:
885
+ if hasattr(ADDth, 'ADD'):
886
+ print(f"ADD {ADDth.IDnum}: {ADDth.ADD:.6f}")
887
+ if ADDth.ADD < min_add:
888
+ min_add = ADDth.ADD
889
+ min_add_id = ADDth.IDnum
890
+
891
+ if min_add_id >= 0:
892
+ print(f"\nMinimum ADD on this hypersphere: {min_add:.6f} (ADD {min_add_id})")
893
+
894
+ return ADDths
895
+
896
+ def calculate_gradient(self, SP, x, element_number_list, electric_charge_and_multiplicity):
897
+
898
+ """Calculate gradient at point x"""
899
+ _, grad_x, _, iscalculationfailed = SP.single_point(
900
+ None, element_number_list, "", electric_charge_and_multiplicity,
901
+ self.method, x
902
+ )
903
+
904
+ if iscalculationfailed:
905
+ return False
906
+
907
+ # Apply bias if needed
908
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
909
+ _, _, bias_gradient, _ = BPC.main(
910
+ 0, grad_x, x, element_number_list,
911
+ self.config.force_data
912
+ )
913
+
914
+ return bias_gradient
915
+ def run(self, file_directory, SP_1, SP_2, electric_charge_and_multiplicity, FIO_img_1, FIO_img_2):
916
+ """
917
+ Main function to run the 2PSHS method.
918
+ SP_1 is used to create the hypersphere.
919
+ SP_2 moves on the hypersphere to minimize ADD.
920
+ The hypersphere radius starts at the distance between SP_1 and SP_2
921
+ and decreases gradually until it reaches zero.
922
+ """
923
+ print("### Start Two-Point Scaled Hypersphere Search (2PSHS) method ###")
924
+
925
+ # Preparation
926
+ base_file_name_1 = os.path.splitext(FIO_img_1.START_FILE)[0]
927
+ base_file_name_2 = os.path.splitext(FIO_img_2.START_FILE)[0]
928
+ self.set_mole_info(base_file_name_1, base_file_name_2, electric_charge_and_multiplicity)
929
+
930
+ self.directory = make_workspace(file_directory)
931
+
932
+ # Create main directory for optimized structures
933
+ os.makedirs(os.path.join(self.directory, "optimized_structures"), exist_ok=True)
934
+
935
+ # Detect initial ADD directions using SP_1 and SP_2
936
+ print("Step 1: Using SP_1 and SP_2 to detect coordinate system and SP_1-SP_2 direction")
937
+ success = self.detect_add(SP_1, SP_2)
938
+ if not success:
939
+ print("Failed to establish SP_1-SP_2 direction.")
940
+ return False
941
+
942
+ # Start with radius equal to the distance between SP_1 and SP_2
943
+ radius = self.initial_distance
944
+ IOEsphereA = radius ** 2 # Convert to A value (squared radius)
945
+
946
+ print("\nStep 2: Optimizing SP_2 to minimize ADD on hypersphere")
947
+
948
+ # Main optimization loop - decrease the radius in each iteration
949
+ sphere_num = 1
950
+ best_add_value = float('inf')
951
+ best_structure = None
952
+ best_radius = None
953
+
954
+ while IOEsphereA > 0 and sphere_num <= self.addf_config['step_number'] and len(self.ADDths) > 0:
955
+ print(f"\nStep {sphere_num+1}: Using hypersphere with radius {np.sqrt(IOEsphereA):.4f}")
956
+
957
+ # Reset optimization flags
958
+ for ADDth in self.ADDths:
959
+ ADDth.converged = False
960
+ ADDth.ADDoptQ = True
961
+
962
+ # Optimize on the current sphere to minimize ADD
963
+ self.ADDths = self.optimize_with_sp2(
964
+ self.ADDths, SP_2, self.init_geometry, IOEsphereA, sphere_num
965
+ )
966
+
967
+ # Check if we found a better ADD value on this hypersphere
968
+ for ADDth in self.ADDths:
969
+ if hasattr(ADDth, 'ADD') and ADDth.ADD < best_add_value:
970
+ best_add_value = ADDth.ADD
971
+ best_structure = ADDth.x.copy()
972
+ best_radius = np.sqrt(IOEsphereA)
973
+ print(f"New best ADD value: {best_add_value:.6f} at radius {best_radius:.4f}")
974
+
975
+ # Decrease the radius for next iteration
976
+ radius -= self.addf_config['IOEsphereA_dist']
977
+ IOEsphereA = radius ** 2 # Convert to A value
978
+
979
+ # Check if the radius has become zero or negative
980
+ if radius <= 0:
981
+ print("Radius has reached zero or negative value. Stopping the search.")
982
+ break
983
+
984
+ sphere_num += 1
985
+
986
+ # Create separate trajectory files for each ADD path
987
+ self.create_separate_xyz_files()
988
+
989
+ # Create distance plots
990
+ self.create_distance_plots()
991
+
992
+ if best_structure is not None:
993
+ print(f"\n### Success! Found minimum ADD value {best_add_value:.6f} at radius {best_radius:.4f} ###")
994
+ # Save the best structure
995
+ best_file = os.path.join(self.directory, "best_add_structure.xyz")
996
+ with open(best_file, 'w') as f:
997
+ f.write(f"{len(self.element_list_1)}\n")
998
+ f.write(f"Best ADD structure with value {best_add_value:.6f} at radius {best_radius:.4f}\n")
999
+ for i, (element, coord) in enumerate(zip(self.element_list_1, best_structure)):
1000
+ f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
1001
+ print(f"Best structure saved to: {best_file}")
1002
+ else:
1003
+ print("\n### Warning: Could not find a good ADD minimum. ###")
1004
+
1005
+ return best_structure is not None
1006
+
1007
+ # Getters and setters
1008
+ def set_molecule_1(self, element_list, coords):
1009
+ self.element_list_1 = element_list
1010
+ self.coords_1 = coords
1011
+
1012
+ def set_molecule_2(self, element_list, coords):
1013
+ self.element_list_2 = element_list
1014
+ self.coords_2 = coords
1015
+
1016
+ def set_gradient_1(self, gradient):
1017
+ self.gradient_1 = gradient
1018
+
1019
+ def set_gradient_2(self, gradient):
1020
+ self.gradient_2 = gradient
1021
+
1022
+ def set_hessian_1(self, hessian):
1023
+ self.hessian_1 = hessian
1024
+
1025
+ def set_hessian_2(self, hessian):
1026
+ self.hessian_2 = hessian
1027
+
1028
+ def set_energy_1(self, energy):
1029
+ self.energy_1 = energy
1030
+
1031
+ def set_energy_2(self, energy):
1032
+ self.energy_2 = energy
1033
+
1034
+ def set_coords_1(self, coords):
1035
+ self.coords_1 = coords
1036
+
1037
+ def set_coords_2(self, coords):
1038
+ self.coords_2 = coords
1039
+
1040
+ def set_element_list_1(self, element_list):
1041
+ self.element_list_1 = element_list
1042
+ self.element_number_list_1 = [element_number(i) for i in self.element_list_1]
1043
+
1044
+ def set_element_list_2(self, element_list):
1045
+ self.element_list_2 = element_list
1046
+ self.element_number_list_2 = [element_number(i) for i in self.element_list_2]
1047
+
1048
+ def get_coords_1(self):
1049
+ return self.coords_1
1050
+
1051
+ def get_coords_2(self):
1052
+ return self.coords_2
1053
+
1054
+ def get_element_list_1(self):
1055
+ return self.element_list_1
1056
+
1057
+ def get_element_list_2(self):
1058
+ return self.element_list_2
1059
+
1060
+ def get_element_number_list_1(self):
1061
+ if self.element_number_list_1 is None:
1062
+ if self.element_list_1 is None:
1063
+ raise ValueError('Element list 1 is not set.')
1064
+ self.element_number_list_1 = [element_number(i) for i in self.element_list_1]
1065
+ return self.element_number_list_1
1066
+
1067
+ def get_element_number_list_2(self):
1068
+ if self.element_number_list_2 is None:
1069
+ if self.element_list_2 is None:
1070
+ raise ValueError('Element list 2 is not set.')
1071
+ self.element_number_list_2 = [element_number(i) for i in self.element_list_2]
1072
+ return self.element_number_list_2
1073
+
1074
+ def set_mole_info(self, base_file_name_1, base_file_name_2, electric_charge_and_multiplicity):
1075
+ """Load molecular information for both SP_1 and SP_2 structures"""
1076
+ coord_1, element_list_1, electric_charge_and_multiplicity = xyz2list(
1077
+ base_file_name_1 + ".xyz", electric_charge_and_multiplicity)
1078
+
1079
+ coord_2, element_list_2, _ = xyz2list(
1080
+ base_file_name_2 + ".xyz", electric_charge_and_multiplicity)
1081
+
1082
+ if self.config.usextb != "None":
1083
+ self.method = self.config.usextb
1084
+ elif self.config.usedxtb != "None":
1085
+ self.method = self.config.usedxtb
1086
+ else:
1087
+ self.method = "None"
1088
+
1089
+ self.coords_1 = np.array(coord_1, dtype="float64")
1090
+ self.element_list_1 = element_list_1
1091
+ self.electric_charge_and_multiplicity_1 = electric_charge_and_multiplicity
1092
+
1093
+ self.coords_2 = np.array(coord_2, dtype="float64")
1094
+ self.element_list_2 = element_list_2
1095
+ self.electric_charge_and_multiplicity_2 = electric_charge_and_multiplicity