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,895 @@
1
+ import numpy as np
2
+ import datetime
3
+ import os
4
+ import glob
5
+
6
+ from multioptpy.Potential.potential import BiasPotentialCalculation
7
+ from multioptpy.Visualization.visualization import Graph
8
+ from multioptpy.fileio import make_workspace, xyz2list
9
+ from multioptpy.Parameters.parameter import UnitValueLib, element_number
10
+
11
+
12
+ class DimerMethod:
13
+ def __init__(self, config):
14
+ """
15
+ Implementation of the Dimer method for finding saddle points.
16
+
17
+ The Dimer method is a minimum mode following technique that uses
18
+ two points (a dimer) to find the lowest curvature mode without
19
+ explicitly calculating the Hessian matrix.
20
+
21
+ References:
22
+ - J. Chem. Phys. 111, 7010 (1999) - Original Dimer Method
23
+ - J. Chem. Phys. 121, 9776 (2004) - Improvements
24
+ - J. Chem. Phys. 123, 224101 (2005) - Additional improvements
25
+ - J. Chem. Phys. 128, 014106 (2008) - Force extrapolation scheme
26
+ """
27
+ self.config = config
28
+ self.energy_list = []
29
+ self.gradient_list = []
30
+ self.curvature_list = []
31
+ self.init_displacement = 0.03 / self.get_unit_conversion() # Bohr
32
+ self.date = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
33
+ self.converge_criteria = 0.001 # Force convergence criteria in a.u.
34
+ self.element_number_list = None
35
+ self.optimized_structures = {} # Dictionary to store optimized structures
36
+
37
+ # Dimer method specific parameters
38
+ self.dimer_parameters = {
39
+ 'dimer_separation': 0.0001, # Distance between dimer images (Bohr)
40
+ 'trial_angle': np.pi / 32.0, # Trial rotation angle (radians)
41
+ 'f_rot_min': 0.1, # Min rotational force for rotation
42
+ 'f_rot_max': 1.0, # Max rotational force for only one rotation
43
+ 'max_num_rot': 5, # Maximum number of rotations per step
44
+ 'extrapolate_forces': True, # Use force extrapolation scheme
45
+ 'max_iterations': 1000, # Maximum iterations for dimer optimization
46
+ 'max_step': 0.1, # Maximum translation step size
47
+ 'trial_step': 0.001, # Step size for curvature estimation
48
+ 'cg_translation': True, # Use conjugate gradient for translation
49
+ 'cg_rotation': False, # Use conjugate gradient for rotation
50
+ 'quickmin_rotation': True, # Use quickmin for rotation
51
+ 'max_rot_iterations': 64, # Max iterations for rotation
52
+ 'max_force_rotation': 1e-3, # Max force for rotation convergence
53
+ 'potim': 0.1, # Time step for quickmin
54
+ }
55
+
56
+ # For CG optimization (translation)
57
+ self.cg_init_translation = True
58
+ self.old_direction_translation = None
59
+ self.cg_direction_translation = None
60
+
61
+ # For CG optimization (rotation)
62
+ self.cg_init_rotation = True
63
+ self.old_rot_force = None
64
+ self.old_rot_gradient = None
65
+ self.current_rot_gradient_unit = None
66
+ self.rot_velocity = None # For QuickMin rotation
67
+
68
+ def get_unit_conversion(self):
69
+ """Return bohr to angstrom conversion factor"""
70
+ return UnitValueLib().bohr2angstroms # Approximate value for bohr2angstroms
71
+
72
+ def adjust_center2origin(self, coord):
73
+ """Adjust coordinates to have center at origin"""
74
+ center = np.mean(coord, axis=0)
75
+ return coord - center
76
+
77
+ def normalize(self, vector):
78
+ """Create a unit vector along *vector*"""
79
+ vector_flat = vector.flatten()
80
+ norm = np.linalg.norm(vector_flat)
81
+ if norm < 1e-10:
82
+ return vector # Return original vector if it's too small
83
+ return (vector_flat / norm).reshape(vector.shape)
84
+
85
+ def parallel_vector(self, vector, base):
86
+ """Extract the components of *vector* that are parallel to *base*"""
87
+ vector_flat = vector.flatten()
88
+ base_flat = base.flatten()
89
+ base_norm = np.linalg.norm(base_flat)
90
+ if base_norm < 1e-10:
91
+ return np.zeros_like(vector)
92
+ base_unit = base_flat / base_norm
93
+ return (np.dot(vector_flat, base_unit) * base_unit).reshape(vector.shape)
94
+
95
+ def perpendicular_vector(self, vector, base):
96
+ """Remove the components of *vector* that are parallel to *base*"""
97
+ return vector - self.parallel_vector(vector, base)
98
+
99
+ def rotate_vector_around_axis(self, vec_to_rotate, axis, angle):
100
+ """Rotates a vector around a given axis by a specified angle (Rodrigues' rotation formula)"""
101
+ axis = self.normalize(axis)
102
+ k = axis.flatten()
103
+ v = vec_to_rotate.flatten()
104
+
105
+ v_rot = v * np.cos(angle) + np.cross(k, v) * np.sin(angle) + k * np.dot(k, v) * (1 - np.cos(angle))
106
+ return v_rot.reshape(vec_to_rotate.shape)
107
+
108
+ def print_status(self, iteration, energy, curvature, max_force, rot_force=None, rotation_angle=None):
109
+ """Print status information during optimization"""
110
+ status = f"Iteration {iteration}: Energy = {energy:.6f}, Curvature = {curvature:.6f}, Max Force = {max_force:.6f}"
111
+ if rot_force is not None:
112
+ status += f", Rotational Force = {rot_force:.6f}"
113
+ if rotation_angle is not None:
114
+ status += f", Rotation Angle = {rotation_angle:.6f} rad"
115
+ print(status)
116
+
117
+ def get_cg_direction_translation(self, direction):
118
+ """Apply Conjugate Gradient algorithm to step direction for translation"""
119
+ direction_shape = direction.shape
120
+ direction_flat = direction.flatten()
121
+
122
+ if self.cg_init_translation:
123
+ self.cg_init_translation = False
124
+ self.old_direction_translation = direction_flat.copy()
125
+ self.cg_direction_translation = direction_flat.copy()
126
+
127
+ old_norm = np.dot(self.old_direction_translation, self.old_direction_translation)
128
+
129
+ # Polak-Ribiere formula for conjugate gradient
130
+ if old_norm > 1e-10:
131
+ betaPR = np.dot(direction_flat,
132
+ (direction_flat - self.old_direction_translation)) / old_norm
133
+ else:
134
+ betaPR = 0.0
135
+
136
+ if betaPR < 0.0:
137
+ betaPR = 0.0
138
+
139
+ self.cg_direction_translation = direction_flat + self.cg_direction_translation * betaPR
140
+ self.old_direction_translation = direction_flat.copy()
141
+
142
+ return self.cg_direction_translation.reshape(direction_shape)
143
+
144
+ def calculate_gradient(self, QMC, x):
145
+ """Calculate gradient at point x"""
146
+ element_number_list = self.get_element_number_list()
147
+ _, grad_x, _, iscalculationfailed = QMC.single_point(
148
+ None, element_number_list, "", self.electric_charge_and_multiplicity,
149
+ self.method, x
150
+ )
151
+
152
+ if iscalculationfailed:
153
+ return False
154
+
155
+ # Apply bias if needed
156
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
157
+ _, _, bias_gradient, _ = BPC.main(
158
+ 0, grad_x, x, element_number_list, self.config.force_data
159
+ )
160
+
161
+ return bias_gradient
162
+
163
+ def dimer_rotate(self, SP_obj, center_coords, dimer_axis, element_list, charge_multiplicity, method):
164
+ """
165
+ Perform dimer rotation to find the lowest curvature direction.
166
+
167
+ Parameters:
168
+ -----------
169
+ SP_obj : Calculation object
170
+ Object for performing single point calculations
171
+ center_coords : ndarray
172
+ Coordinates of the center point of the dimer
173
+ dimer_axis : ndarray
174
+ Current orientation of the dimer axis
175
+ element_list : list
176
+ List of element symbols
177
+ charge_multiplicity : list
178
+ [charge, multiplicity]
179
+ method : str
180
+ Calculation method
181
+
182
+ Returns:
183
+ --------
184
+ ndarray, float, bool
185
+ New dimer axis, curvature along this axis, and success flag
186
+ """
187
+ # Parameters for rotation
188
+ dR = self.dimer_parameters['dimer_separation']
189
+ trial_angle = self.dimer_parameters['trial_angle']
190
+ max_rot_iterations = self.dimer_parameters['max_rot_iterations']
191
+ max_force_rotation = self.dimer_parameters['max_force_rotation']
192
+ cg_rotation = self.dimer_parameters['cg_rotation']
193
+ quickmin_rotation = self.dimer_parameters['quickmin_rotation']
194
+ potim = self.dimer_parameters['potim']
195
+
196
+ # Ensure dimer_axis has correct shape and is normalized
197
+ dimer_axis = self.normalize(np.array(dimer_axis).reshape(center_coords.shape))
198
+
199
+ # Initial forces and energies at center_coords
200
+ energy_center, forces_center, _, failed = SP_obj.single_point(
201
+ None, element_list, "", charge_multiplicity, method, center_coords
202
+ )
203
+ if failed:
204
+ return None, None, True
205
+
206
+ # Calculate forces at the dimer endpoints
207
+ pos1 = center_coords + dimer_axis * dR
208
+ pos2 = center_coords - dimer_axis * dR
209
+
210
+ energy1, forces1, _, failed = SP_obj.single_point(
211
+ None, element_list, "", charge_multiplicity, method, pos1
212
+ )
213
+ if failed:
214
+ return None, None, True
215
+
216
+ energy2, forces2, _, failed = SP_obj.single_point(
217
+ None, element_list, "", charge_multiplicity, method, pos2
218
+ )
219
+ if failed:
220
+ return None, None, True
221
+
222
+ # Apply bias potential
223
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
224
+ _, bias_energy_center, bias_forces_center, _ = BPC.main(
225
+ energy_center, forces_center, center_coords, element_list, self.config.force_data
226
+ )
227
+ _, bias_energy1, bias_forces1, _ = BPC.main(
228
+ energy1, forces1, pos1, element_list, self.config.force_data
229
+ )
230
+ _, bias_energy2, bias_forces2, _ = BPC.main(
231
+ energy2, forces2, pos2, element_list, self.config.force_data
232
+ )
233
+
234
+ # Update with bias forces
235
+ forces_center = bias_forces_center
236
+ forces1 = bias_forces1
237
+ forces2 = bias_forces2
238
+
239
+ # Calculate rotational forces and curvature
240
+ fd1 = np.sum(forces1 * dimer_axis)
241
+ fd2 = np.sum(forces2 * dimer_axis)
242
+ fn1 = forces1 - dimer_axis * fd1
243
+ fn2 = forces2 - dimer_axis * fd2
244
+
245
+ rotational_force_gradient = (fn1 - fn2) / dR
246
+ rotational_force_magnitude = np.linalg.norm(rotational_force_gradient.flatten())
247
+
248
+ curvature = np.dot((forces2 - forces1).flatten(), dimer_axis.flatten()) / (2.0 * dR)
249
+
250
+ # Initialize for rotation
251
+ current_dimer_axis = dimer_axis.copy()
252
+ rot_iteration = 0
253
+
254
+ if quickmin_rotation:
255
+ # QuickMin for dimer rotation
256
+ if self.rot_velocity is None:
257
+ self.rot_velocity = np.zeros_like(center_coords)
258
+
259
+ while rot_iteration < max_rot_iterations:
260
+ # Calculate velocity update
261
+ dv_rot = rotational_force_gradient * (dR * potim)
262
+ vdv = np.sum(self.rot_velocity * dv_rot)
263
+
264
+ # Update velocity
265
+ if vdv > 0.0 and np.sum(dv_rot**2) > 1e-10:
266
+ self.rot_velocity = dv_rot * (1.0 + vdv / np.sum(dv_rot**2))
267
+ else:
268
+ self.rot_velocity = dv_rot
269
+
270
+ # Update dimer axis
271
+ current_r1 = center_coords + current_dimer_axis * dR
272
+ new_r1 = current_r1 + self.rot_velocity * potim
273
+
274
+ new_dimer_axis_unnormalized = new_r1 - center_coords
275
+ current_dimer_axis = self.normalize(new_dimer_axis_unnormalized)
276
+
277
+ # Recalculate forces for new dimer axis
278
+ pos1_new = center_coords + current_dimer_axis * dR
279
+ energy1_new, forces1_new, _, failed = SP_obj.single_point(
280
+ None, element_list, "", charge_multiplicity, method, pos1_new
281
+ )
282
+ if failed:
283
+ return None, None, True
284
+
285
+ # Apply bias potential
286
+ _, _, bias_forces1_new, _ = BPC.main(
287
+ energy1_new, forces1_new, pos1_new, element_list, self.config.force_data
288
+ )
289
+ forces1_new = bias_forces1_new
290
+
291
+ forces2_new = 2 * forces_center - forces1_new
292
+
293
+ fd1_new = np.sum(forces1_new * current_dimer_axis)
294
+ fd2_new = np.sum(forces2_new * current_dimer_axis)
295
+ fn1_new = forces1_new - current_dimer_axis * fd1_new
296
+ fn2_new = forces2_new - current_dimer_axis * fd2_new
297
+ rotational_force_gradient = (fn1_new - fn2_new) / dR
298
+ rotational_force_magnitude = np.linalg.norm(rotational_force_gradient.flatten())
299
+
300
+ # Update curvature
301
+ curvature = np.dot((forces2_new - forces1_new).flatten(), current_dimer_axis.flatten()) / (2.0 * dR)
302
+
303
+ self.print_status(rot_iteration, bias_energy_center, curvature,
304
+ np.max(np.abs(forces_center)), rotational_force_magnitude)
305
+
306
+ if rotational_force_magnitude < max_force_rotation or rot_iteration >= max_rot_iterations - 1:
307
+ break
308
+
309
+ rot_iteration += 1
310
+
311
+ elif cg_rotation:
312
+ # Conjugate Gradient for rotation
313
+ if self.cg_init_rotation:
314
+ self.old_rot_force = rotational_force_gradient.copy()
315
+ self.old_rot_gradient = rotational_force_gradient.copy()
316
+ self.current_rot_gradient_unit = self.normalize(rotational_force_gradient)
317
+ self.cg_init_rotation = False
318
+
319
+ while rot_iteration < max_rot_iterations:
320
+ # Polak-Ribiere formula
321
+ gam_n = 0.0
322
+ if np.linalg.norm(self.old_rot_force.flatten()) > 1e-10:
323
+ gam_n = np.dot(rotational_force_gradient.flatten(),
324
+ (rotational_force_gradient - self.old_rot_force).flatten()) / np.dot(self.old_rot_force.flatten(), self.old_rot_force.flatten())
325
+ if gam_n < 0:
326
+ gam_n = 0.0
327
+
328
+ # Update gradient
329
+ current_rot_gradient = rotational_force_gradient + self.old_rot_gradient * gam_n
330
+ self.current_rot_gradient_unit = self.normalize(current_rot_gradient)
331
+
332
+ # Calculate force components
333
+ fnp1 = self.current_rot_gradient_unit * np.sum(rotational_force_gradient * self.current_rot_gradient_unit)
334
+ fnrp1 = np.sum(fnp1 * self.current_rot_gradient_unit)
335
+
336
+ # Trial rotation
337
+ n_tmp = current_dimer_axis.copy()
338
+ gnu_tmp = self.current_rot_gradient_unit.copy()
339
+ rotated_n_trial = self.rotate_vector_around_axis(n_tmp, gnu_tmp, trial_angle)
340
+
341
+ # Calculate forces at trial position
342
+ pos1_trial = center_coords + rotated_n_trial * dR
343
+ energy1_trial, forces1_trial, _, failed = SP_obj.single_point(
344
+ None, element_list, "", charge_multiplicity, method, pos1_trial
345
+ )
346
+ if failed:
347
+ return None, None, True
348
+
349
+ # Apply bias potential
350
+ _, _, bias_forces1_trial, _ = BPC.main(
351
+ energy1_trial, forces1_trial, pos1_trial, element_list, self.config.force_data
352
+ )
353
+ forces1_trial = bias_forces1_trial
354
+
355
+ forces2_trial = 2 * forces_center - forces1_trial
356
+
357
+ # Calculate rotational force for trial
358
+ fd1_trial = np.sum(forces1_trial * rotated_n_trial)
359
+ fd2_trial = np.sum(forces2_trial * rotated_n_trial)
360
+ fn1_trial = forces1_trial - rotated_n_trial * fd1_trial
361
+ fn2_trial = forces2_trial - rotated_n_trial * fd2_trial
362
+ rotational_force_gradient_trial = (fn1_trial - fn2_trial) / dR
363
+
364
+ # Calculate optimal rotation angle
365
+ fnp2 = gnu_tmp * np.sum(rotational_force_gradient_trial * gnu_tmp)
366
+ fnrp2 = np.sum(fnp2 * gnu_tmp)
367
+
368
+ cth = (fnrp1 - fnrp2) / trial_angle
369
+ fnrp = (fnrp1 + fnrp2) / 2.0
370
+
371
+ rotation_angle = 0.0
372
+ if abs(cth) > 1e-10:
373
+ rotation_angle = np.arctan((fnrp / cth) * 2.0) / 2.0 + trial_angle / 2.0
374
+ if cth < 0:
375
+ rotation_angle += np.pi / 2.0
376
+ else:
377
+ rotation_angle = trial_angle / 2.0
378
+
379
+ # Apply optimal rotation
380
+ current_dimer_axis = self.rotate_vector_around_axis(n_tmp, gnu_tmp, rotation_angle)
381
+ current_dimer_axis = self.normalize(current_dimer_axis)
382
+
383
+ # Update gradient history
384
+ self.old_rot_force = rotational_force_gradient.copy()
385
+ self.old_rot_gradient = current_rot_gradient.copy()
386
+
387
+ # Recalculate forces for new axis
388
+ pos1_new = center_coords + current_dimer_axis * dR
389
+ energy1_new, forces1_new, _, failed = SP_obj.single_point(
390
+ None, element_list, "", charge_multiplicity, method, pos1_new
391
+ )
392
+ if failed:
393
+ return None, None, True
394
+
395
+ # Apply bias potential
396
+ _, _, bias_forces1_new, _ = BPC.main(
397
+ energy1_new, forces1_new, pos1_new, element_list, self.config.force_data
398
+ )
399
+ forces1_new = bias_forces1_new
400
+
401
+ forces2_new = 2 * forces_center - forces1_new
402
+
403
+ # Update rotational forces and curvature
404
+ fd1_new = np.sum(forces1_new * current_dimer_axis)
405
+ fd2_new = np.sum(forces2_new * current_dimer_axis)
406
+ fn1_new = forces1_new - current_dimer_axis * fd1_new
407
+ fn2_new = forces2_new - current_dimer_axis * fd2_new
408
+ rotational_force_gradient = (fn1_new - fn2_new) / dR
409
+ rotational_force_magnitude = np.linalg.norm(rotational_force_gradient.flatten())
410
+
411
+ curvature = np.dot((forces2_new - forces1_new).flatten(), current_dimer_axis.flatten()) / (2.0 * dR)
412
+
413
+ self.print_status(rot_iteration, bias_energy_center, curvature,
414
+ np.max(np.abs(forces_center)), rotational_force_magnitude, rotation_angle)
415
+
416
+ if rotational_force_magnitude < max_force_rotation or rot_iteration >= max_rot_iterations - 1:
417
+ break
418
+
419
+ rot_iteration += 1
420
+
421
+ else:
422
+ # Original dimer rotation algorithm
423
+ rotation_count = 0
424
+ forces1A = forces1.copy()
425
+
426
+ while rotation_count < max_rot_iterations:
427
+ rot_force = self.perpendicular_vector((forces1 - forces2), current_dimer_axis)
428
+ rot_force_magnitude = np.linalg.norm(rot_force.flatten())
429
+
430
+ if rot_force_magnitude <= self.dimer_parameters["f_rot_min"]:
431
+ break
432
+ if rot_force_magnitude <= self.dimer_parameters["f_rot_max"] and rotation_count > 0:
433
+ break
434
+
435
+ n_A = current_dimer_axis.copy()
436
+ rot_unit_A = self.normalize(rot_force)
437
+
438
+ c0 = curvature
439
+ c0d = np.dot((forces2 - forces1).flatten(), rot_unit_A.flatten()) / dR
440
+
441
+ # Trial rotation
442
+ n_B = self.rotate_vector_around_axis(n_A, rot_unit_A, trial_angle)
443
+ n_B = self.normalize(n_B)
444
+
445
+ pos1B = center_coords + n_B * dR
446
+ energy1B, forces1B, _, failed = SP_obj.single_point(
447
+ None, element_list, "", charge_multiplicity, method, pos1B
448
+ )
449
+ if failed:
450
+ return None, None, True
451
+
452
+ # Apply bias potential
453
+ _, _, bias_forces1B, _ = BPC.main(
454
+ energy1B, forces1B, pos1B, element_list, self.config.force_data
455
+ )
456
+ forces1B = bias_forces1B
457
+
458
+ forces2B = 2 * forces_center - forces1B
459
+
460
+ c1d = np.dot((forces2B - forces1B).flatten(), rot_unit_A.flatten()) / dR
461
+
462
+ # Calculate optimal rotation angle
463
+ a1 = c0d * np.cos(2 * trial_angle) - c1d / (2 * np.sin(2 * trial_angle))
464
+ b1 = 0.5 * c0d
465
+ a0 = 2 * (c0 - a1)
466
+
467
+ rotation_angle = 0.0
468
+ if abs(a1) > 1e-10:
469
+ rotation_angle = np.arctan(b1 / a1) / 2.0
470
+
471
+ cmin = a0 / 2.0 + a1 * np.cos(2 * rotation_angle) + b1 * np.sin(2 * rotation_angle)
472
+ if c0 < cmin:
473
+ rotation_angle += np.pi / 2.0
474
+
475
+ # Apply optimal rotation
476
+ current_dimer_axis = self.rotate_vector_around_axis(n_A, rot_unit_A, rotation_angle)
477
+ current_dimer_axis = self.normalize(current_dimer_axis)
478
+
479
+ curvature = cmin
480
+
481
+ # Calculate forces at new orientation
482
+ pos1 = center_coords + current_dimer_axis * dR
483
+ energy1, forces1, _, failed = SP_obj.single_point(
484
+ None, element_list, "", charge_multiplicity, method, pos1
485
+ )
486
+ if failed:
487
+ return None, None, True
488
+
489
+ # Apply bias potential
490
+ _, _, bias_forces1, _ = BPC.main(
491
+ energy1, forces1, pos1, element_list, self.config.force_data
492
+ )
493
+ forces1 = bias_forces1
494
+
495
+ forces2 = 2 * forces_center - forces1
496
+
497
+ self.print_status(rotation_count, bias_energy_center, curvature,
498
+ np.max(np.abs(forces_center)), rot_force_magnitude, rotation_angle)
499
+
500
+ rotation_count += 1
501
+
502
+ return current_dimer_axis, curvature, False
503
+
504
+ def dimer_translate(self, SP_obj, coords, dimer_axis, curvature, element_list, charge_multiplicity, method):
505
+ """
506
+ Translate the dimer to find a saddle point.
507
+
508
+ Parameters:
509
+ -----------
510
+ SP_obj : Calculation object
511
+ Object for performing single point calculations
512
+ coords : ndarray
513
+ Current coordinates
514
+ dimer_axis : ndarray
515
+ Current dimer axis (normalized)
516
+ curvature : float
517
+ Current curvature along the dimer axis
518
+ element_list : list
519
+ List of element symbols
520
+ charge_multiplicity : list
521
+ [charge, multiplicity]
522
+ method : str
523
+ Calculation method
524
+
525
+ Returns:
526
+ --------
527
+ ndarray, float, bool
528
+ New coordinates, energy, and success flag
529
+ """
530
+ # Parameters for translation
531
+ max_step = self.dimer_parameters["max_step"]
532
+ cg_translation = self.dimer_parameters["cg_translation"]
533
+
534
+ # Normalize dimer_axis
535
+ dimer_axis = self.normalize(np.array(dimer_axis).reshape(coords.shape))
536
+
537
+ # Get forces at current position
538
+ energy, forces, _, failed = SP_obj.single_point(
539
+ None, element_list, "", charge_multiplicity, method, coords
540
+ )
541
+ if failed:
542
+ return None, None, True
543
+
544
+ # Apply bias potential
545
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
546
+ _, bias_energy, bias_forces, _ = BPC.main(
547
+ energy, forces, coords, element_list, self.config.force_data
548
+ )
549
+ forces = bias_forces
550
+
551
+ # Modify forces according to dimer method
552
+ f_parallel = self.parallel_vector(forces, dimer_axis)
553
+
554
+ if curvature < 0:
555
+ # Invert component parallel to dimer axis
556
+ modified_forces = forces - 2 * f_parallel
557
+ else:
558
+ # Only consider inverted parallel component
559
+ modified_forces = -f_parallel
560
+
561
+ # Apply conjugate gradient if enabled
562
+ if cg_translation:
563
+ direction = self.get_cg_direction_translation(modified_forces)
564
+ else:
565
+ direction = modified_forces
566
+
567
+ # Normalize direction and apply step size
568
+ direction = self.normalize(direction)
569
+ step_size = max_step
570
+
571
+ # Calculate new coordinates
572
+ new_coords = coords + direction * step_size
573
+
574
+ # Calculate energy at new position
575
+ new_energy, _, _, failed = SP_obj.single_point(
576
+ None, element_list, "", charge_multiplicity, method, new_coords
577
+ )
578
+ if failed:
579
+ return None, None, True
580
+
581
+ # Apply bias potential
582
+ _, bias_new_energy, _, _ = BPC.main(
583
+ new_energy, np.zeros_like(coords), new_coords, element_list, self.config.force_data
584
+ )
585
+ new_energy = bias_new_energy
586
+
587
+ return new_coords, new_energy, False
588
+
589
+ def save_structure(self, coords, element_list, iteration, energy, curvature, label):
590
+ """Save structure to XYZ file"""
591
+ # Create filename
592
+ filename = f"{label}_iter_{iteration}.xyz"
593
+ filepath = os.path.join(self.directory, "dimer_structures", filename)
594
+
595
+ # Convert coordinates to Angstroms
596
+ coords_ang = coords * self.get_unit_conversion()
597
+
598
+ # Write XYZ file
599
+ with open(filepath, 'w') as f:
600
+ f.write(f"{len(element_list)}\n")
601
+ f.write(f"Dimer {label} - Iteration {iteration} - Energy {energy:.6f} - Curvature {curvature:.6f}\n")
602
+ for i, (element, coord) in enumerate(zip(element_list, coords_ang)):
603
+ f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
604
+
605
+ def save_eigenmode(self, eigenmode, element_list, iteration, curvature):
606
+ """Save eigenmode to XYZ file"""
607
+ # Create filename
608
+ filename = f"eigenmode_iter_{iteration}.xyz"
609
+ filepath = os.path.join(self.directory, "dimer_structures", filename)
610
+
611
+ # Scale eigenmode for visualization
612
+ scale_factor = 0.3 / np.max(np.abs(eigenmode)) if np.max(np.abs(eigenmode)) > 1e-10 else 0.3
613
+ scaled_mode = eigenmode * scale_factor
614
+
615
+ # Write XYZ file
616
+ with open(filepath, 'w') as f:
617
+ f.write(f"{len(element_list)}\n")
618
+ f.write(f"Dimer eigenmode - Iteration {iteration} - Curvature {curvature:.6f}\n")
619
+ for i, (element, mode) in enumerate(zip(element_list, scaled_mode)):
620
+ f.write(f"{element} {mode[0]:.12f} {mode[1]:.12f} {mode[2]:.12f}\n")
621
+
622
+ def create_trajectory_file(self, element_list):
623
+ """Create a trajectory file from all saved structures"""
624
+ # Get all structure files sorted by iteration
625
+ structure_files = glob.glob(os.path.join(self.directory, "dimer_structures", "optimization_iter_*.xyz"))
626
+ structure_files.sort(key=lambda x: int(x.split("_")[-1].split(".")[0]))
627
+
628
+ # If no files found, return
629
+ if not structure_files:
630
+ return
631
+
632
+ # Create trajectory file
633
+ trajectory_file = os.path.join(self.directory, "dimer_trajectory.xyz")
634
+
635
+ with open(trajectory_file, 'w') as outfile:
636
+ for file in structure_files:
637
+ with open(file, 'r') as infile:
638
+ outfile.write(infile.read())
639
+
640
+ print(f"Created trajectory file: {trajectory_file}")
641
+
642
+ def create_distance_plots(self):
643
+ """Create CSV files with distance and energy data for plotting"""
644
+ iterations = range(len(self.energy_list))
645
+
646
+ # Path for the data file
647
+ data_file = os.path.join(self.directory, "dimer_results.csv")
648
+
649
+ # Write the data
650
+ with open(data_file, 'w') as f:
651
+ f.write("iteration,energy,curvature\n")
652
+ for i in iterations:
653
+ f.write(f"{i},{self.energy_list[i]:.6f},{self.curvature_list[i]:.6f}\n")
654
+
655
+ print(f"Created data file: {data_file}")
656
+
657
+ # Getters and setters - keep same style as ADDFlikeMethod
658
+ def set_molecule(self, element_list, coords):
659
+ self.element_list = element_list
660
+ self.coords = coords
661
+
662
+ def set_gradient(self, gradient):
663
+ self.gradient = gradient
664
+
665
+ def set_hessian(self, hessian):
666
+ self.hessian = hessian
667
+
668
+ def set_energy(self, energy):
669
+ self.energy = energy
670
+
671
+ def set_coords(self, coords):
672
+ self.coords = coords
673
+
674
+ def set_element_list(self, element_list):
675
+ self.element_list = element_list
676
+ self.element_number_list = [element_number(i) for i in self.element_list]
677
+
678
+ def set_coord(self, coord):
679
+ self.coords = coord
680
+
681
+ def get_coord(self):
682
+ return self.coords
683
+
684
+ def get_element_list(self):
685
+ return self.element_list
686
+
687
+ def get_element_number_list(self):
688
+ if self.element_number_list is None:
689
+ if self.element_list is None:
690
+ raise ValueError('Element list is not set.')
691
+ self.element_number_list = [element_number(i) for i in self.element_list]
692
+ return self.element_number_list
693
+
694
+ def set_mole_info(self, base_file_name, electric_charge_and_multiplicity):
695
+ """Load molecular information from XYZ file"""
696
+ coord, element_list, electric_charge_and_multiplicity = xyz2list(
697
+ base_file_name + ".xyz", electric_charge_and_multiplicity)
698
+
699
+ if self.config.usextb != "None":
700
+ self.method = self.config.usextb
701
+ elif self.config.usedxtb != "None":
702
+ self.method = self.config.usedxtb
703
+ else:
704
+ self.method = "None"
705
+
706
+ self.coords = np.array(coord, dtype="float64")
707
+ self.element_list = element_list
708
+ self.electric_charge_and_multiplicity = electric_charge_and_multiplicity
709
+
710
+ def run(self, file_directory, SP, electric_charge_and_multiplicity, FIO_img):
711
+ """
712
+ Main method to run Dimer optimization.
713
+
714
+ Parameters:
715
+ -----------
716
+ file_directory : str
717
+ Path to input file
718
+ SP : SinglePointCalculation object
719
+ Object for performing single point calculations
720
+ electric_charge_and_multiplicity : list
721
+ [charge, multiplicity]
722
+ FIO_img : FileIO object
723
+ Object for file I/O operations
724
+
725
+ Returns:
726
+ --------
727
+ bool
728
+ True if optimization succeeded, False otherwise
729
+ """
730
+ print("### Start Dimer Method ###")
731
+
732
+ # Preparation
733
+ base_file_name = os.path.splitext(FIO_img.START_FILE)[0]
734
+ self.set_mole_info(base_file_name, electric_charge_and_multiplicity)
735
+
736
+ self.directory = make_workspace(file_directory)
737
+
738
+ # Create directory for dimer structures
739
+ os.makedirs(os.path.join(self.directory, "dimer_structures"), exist_ok=True)
740
+
741
+ # Initial coordinates
742
+ initial_coords = self.get_coord()
743
+ element_list = self.get_element_list()
744
+
745
+ # Initial energy and forces
746
+ energy, forces, _, failed = SP.single_point(
747
+ None, element_list, "", electric_charge_and_multiplicity, self.method, initial_coords
748
+ )
749
+ if failed:
750
+ print("Initial calculation failed.")
751
+ return False
752
+
753
+ # Apply bias potential
754
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
755
+ _, bias_energy, bias_forces, _ = BPC.main(
756
+ energy, forces, initial_coords, element_list, self.config.force_data
757
+ )
758
+
759
+ print(f"Initial energy: {bias_energy:.6f}, Max force: {np.max(np.abs(bias_forces)):.6f}")
760
+
761
+ # Save initial structure
762
+ self.save_structure(initial_coords, element_list, 0, bias_energy, 0.0, "initial")
763
+
764
+ # Initialize lists for tracking
765
+ self.energy_list = [bias_energy]
766
+ self.gradient_list = [np.linalg.norm(bias_forces)]
767
+ self.curvature_list = [0.0]
768
+
769
+ # Initialize dimer axis (random for now)
770
+ coords_flat = initial_coords.flatten()
771
+ dimer_axis = np.random.rand(*initial_coords.shape) - 0.5
772
+ dimer_axis = self.normalize(dimer_axis)
773
+
774
+ # Main iteration loop
775
+ max_iterations = self.dimer_parameters["max_iterations"]
776
+ converged = False
777
+ iteration = 0
778
+ current_coords = initial_coords.copy()
779
+
780
+ while iteration < max_iterations and not converged:
781
+ print(f"\n### Iteration {iteration+1} ###")
782
+
783
+ # Step 1: Rotate the dimer to find lowest curvature mode
784
+ print("Rotating dimer to find lowest curvature mode...")
785
+ dimer_axis, curvature, failed = self.dimer_rotate(
786
+ SP, current_coords, dimer_axis, element_list,
787
+ electric_charge_and_multiplicity, self.method
788
+ )
789
+
790
+ if failed:
791
+ print(f"Dimer rotation failed at iteration {iteration+1}")
792
+ break
793
+
794
+ print(f"After rotation: Curvature = {curvature:.6f}")
795
+
796
+ # Save the eigenmode
797
+ self.save_eigenmode(dimer_axis, element_list, iteration, curvature)
798
+
799
+ # Step 2: Translate the dimer
800
+ print("Translating dimer...")
801
+ new_coords, new_energy, failed = self.dimer_translate(
802
+ SP, current_coords, dimer_axis, curvature, element_list,
803
+ electric_charge_and_multiplicity, self.method
804
+ )
805
+
806
+ if failed:
807
+ print(f"Dimer translation failed at iteration {iteration+1}")
808
+ break
809
+
810
+ # Calculate forces at new position
811
+ _, new_forces, _, failed = SP.single_point(
812
+ None, element_list, "", electric_charge_and_multiplicity, self.method, new_coords
813
+ )
814
+ if failed:
815
+ print(f"Force calculation failed at iteration {iteration+1}")
816
+ break
817
+
818
+ # Apply bias potential
819
+ _, _, bias_new_forces, _ = BPC.main(
820
+ 0, new_forces, new_coords, element_list, self.config.force_data
821
+ )
822
+ new_forces = bias_new_forces
823
+
824
+ # Calculate maximum force component
825
+ max_force = np.max(np.abs(new_forces))
826
+
827
+ # Store results for this iteration
828
+ self.energy_list.append(new_energy)
829
+ self.curvature_list.append(curvature)
830
+ self.gradient_list.append(max_force)
831
+
832
+ # Print status
833
+ energy_change = new_energy - self.energy_list[-2] if iteration > 0 else 0.0
834
+ print(f"After translation: Energy = {new_energy:.6f} (Δ = {energy_change:.6f})")
835
+ print(f" Max Force = {max_force:.6f}")
836
+ print(f" Curvature = {curvature:.6f}")
837
+
838
+ # Save structure for this iteration
839
+ self.save_structure(new_coords, element_list, iteration+1, new_energy, curvature, "optimization")
840
+
841
+ # Check convergence
842
+ if max_force < self.converge_criteria and curvature < 0:
843
+ converged = True
844
+ print("\n### Dimer method converged to a saddle point! ###")
845
+ self.save_structure(new_coords, element_list, iteration+1, new_energy, curvature, "final_saddle_point")
846
+
847
+ # Store this structure
848
+ structure_info = {
849
+ 'iteration': iteration+1,
850
+ 'energy': new_energy,
851
+ 'curvature': curvature,
852
+ 'max_force': max_force,
853
+ 'coords': new_coords.copy(),
854
+ 'comment': f"Dimer Iteration {iteration+1} Energy {new_energy:.6f} Curvature {curvature:.6f}"
855
+ }
856
+ self.optimized_structures[iteration+1] = structure_info
857
+
858
+ # Update for next iteration
859
+ current_coords = new_coords
860
+
861
+ # Reset CG for next step (for translation)
862
+ self.cg_init_translation = True
863
+
864
+ iteration += 1
865
+
866
+ # Create trajectory file
867
+ self.create_trajectory_file(element_list)
868
+
869
+ # Create data plots
870
+ self.create_distance_plots()
871
+
872
+ # Plot optimization progress using Graph class from ieip.py
873
+ G = Graph(self.config.iEIP_FOLDER_DIRECTORY)
874
+ iterations_plot = list(range(len(self.energy_list)))
875
+
876
+ G.single_plot(iterations_plot, self.energy_list, file_directory, "dimer_energy",
877
+ axis_name_2="Energy [Hartree]", name="dimer_energy")
878
+ G.single_plot(iterations_plot, self.curvature_list, file_directory, "dimer_curvature",
879
+ axis_name_2="Curvature", name="dimer_curvature")
880
+ G.single_plot(iterations_plot, self.gradient_list, file_directory, "dimer_gradient",
881
+ axis_name_2="Max Force [a.u.]", name="dimer_gradient")
882
+
883
+ if converged:
884
+ print(f"Dimer method converged after {iteration} iterations.")
885
+ print(f"Final energy: {new_energy:.6f}")
886
+ print(f"Final curvature: {curvature:.6f}")
887
+ print(f"Final max force: {max_force:.6f}")
888
+ return True
889
+ else:
890
+ print(f"Dimer method did not converge after {iteration} iterations.")
891
+ if iteration > 0:
892
+ print(f"Final energy: {new_energy:.6f}")
893
+ print(f"Final curvature: {curvature:.6f}")
894
+ print(f"Final max force: {max_force:.6f}")
895
+ return False