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,454 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+ import os
4
+
5
+ from multioptpy.Potential.potential import BiasPotentialCalculation
6
+ from multioptpy.Utils.calc_tools import Calculationtools
7
+ from multioptpy.Visualization.visualization import Graph
8
+
9
+ class NewtonTrajectory:
10
+ """
11
+ Implementation of the Growing Newton Trajectory (GNT) method for finding transition states.
12
+
13
+ Reference:
14
+ [1] Quapp, "Finding the Transition State without Initial Guess: The Growing
15
+ String Method for Newton Trajectory to Isomerization and Enantiomerization",
16
+ J. Comput. Chem. 2005, 26, 1383-1399, DOI: 10.1063/1.1885467
17
+ """
18
+ def __init__(self, config):
19
+ self.config = config
20
+ self.step_len = config.gnt_step_len
21
+ self.rms_thresh = config.gnt_rms_thresh
22
+ self.gnt_vec = config.gnt_vec
23
+ self.micro_iter_limit = config.gnt_microiter
24
+ self.out_dir = Path(config.iEIP_FOLDER_DIRECTORY) if hasattr(Path, '__call__') else config.iEIP_FOLDER_DIRECTORY
25
+
26
+ # Initialize storage for trajectory data
27
+ self.images = []
28
+ self.all_energies = []
29
+ self.all_real_forces = []
30
+ self.sp_images = []
31
+ self.ts_images = []
32
+ self.min_images = []
33
+ self.ts_imag_freqs = []
34
+
35
+ # Flags for tracking stationary points
36
+ self.passed_min = False
37
+ self.passed_ts = False
38
+ self.did_reparametrization = False
39
+
40
+
41
+ def rms(self, vector):
42
+ """Calculate root mean square of a vector"""
43
+ return np.sqrt(np.mean(np.square(vector)))
44
+
45
+ def get_r(self, current_geom, final_geom=None):
46
+ """Determine search direction vector"""
47
+ if final_geom is not None:
48
+ current_geom, _ = Calculationtools().kabsch_algorithm(current_geom, final_geom)
49
+ r = final_geom - current_geom
50
+ elif self.gnt_vec is not None:
51
+ # Parse atom indices from gnt_vec string
52
+ atom_indices = list(map(int, self.gnt_vec.split(",")))
53
+ if len(atom_indices) % 2 != 0:
54
+ raise ValueError("Invalid gnt_vec format. Need even number of atom indices.")
55
+
56
+ r = np.zeros_like(current_geom)
57
+ for i in range(len(atom_indices) // 2):
58
+ atom_i = atom_indices[2*i] - 1 # Convert to 0-indexed
59
+ atom_j = atom_indices[2*i+1] - 1
60
+ # Create a displacement vector between these atoms
61
+ r[atom_i] = current_geom[atom_j] - current_geom[atom_i]
62
+ r[atom_j] = current_geom[atom_i] - current_geom[atom_j]
63
+ else:
64
+ raise ValueError("Need to specify either final_geom or gnt_vec")
65
+
66
+ # Normalize the direction vector
67
+ r = r / np.linalg.norm(r)
68
+ return r
69
+
70
+ def calc_projector(self, r):
71
+ """Calculate projector that keeps perpendicular component"""
72
+ flat_r = r.reshape(-1)
73
+ return np.eye(flat_r.size) - np.outer(flat_r, flat_r)
74
+
75
+ def grow_image(self, SP, FIO, geom, element_list, charge_multiplicity, r, iter_num, file_directory):
76
+ """Grow a new image along the Newton trajectory"""
77
+ # Store current image
78
+ self.images.append(geom.copy())
79
+
80
+ # Calculate new displacement along r
81
+ step = self.step_len * r
82
+ new_geom = geom + step
83
+
84
+ # Prepare and run calculation at the new geometry
85
+ new_geom_tolist = (new_geom * self.config.bohr2angstroms).tolist()
86
+ for i, elem in enumerate(element_list):
87
+ new_geom_tolist[i].insert(0, elem)
88
+
89
+ new_geom_tolist.insert(0, charge_multiplicity)
90
+
91
+ file_directory = FIO.make_psi4_input_file([new_geom_tolist], iter_num)
92
+ energy, forces, geom_coords, error_flag = SP.single_point(
93
+ file_directory, element_list, iter_num, charge_multiplicity, self.config.force_data["xtb"]
94
+ )
95
+
96
+ if error_flag:
97
+ print("Error in QM calculation during trajectory growth.")
98
+ with open(os.path.join(self.out_dir, "end.txt"), "w") as f:
99
+ f.write("Error in QM calculation during trajectory growth.")
100
+ return None, None, None, True, None
101
+
102
+ # Store results
103
+ self.all_energies.append(energy)
104
+ self.all_real_forces.append(forces)
105
+
106
+ return energy, forces, geom_coords, False, file_directory
107
+
108
+ def initialize(self, SP, FIO, initial_geom, element_list, charge_multiplicity, file_directory, final_geom=None, iter_num=0):
109
+ """Initialize the Newton trajectory
110
+
111
+ Parameters:
112
+ -----------
113
+ SP : SinglePoint
114
+ Object to perform single point calculations
115
+ FIO : FileIO
116
+ Object for file I/O operations
117
+ initial_geom : ndarray
118
+ Initial geometry coordinates
119
+ element_list : list
120
+ List of element symbols
121
+ charge_multiplicity : list
122
+ [charge, multiplicity]
123
+ file_directory : str
124
+ Path to current input file
125
+ final_geom : ndarray, optional
126
+ Final geometry coordinates
127
+ iter_num : int, optional
128
+ Current iteration number
129
+ """
130
+ # Use the provided file_directory instead of trying to get it from FIO
131
+ energy, forces, geom_coords, error_flag = SP.single_point(
132
+ file_directory, element_list, iter_num, charge_multiplicity, self.config.force_data["xtb"]
133
+ )
134
+
135
+ if error_flag:
136
+ print("Error in QM calculation during initialization.")
137
+ return None, None, True
138
+
139
+ # Store initial data
140
+ self.images.append(geom_coords.copy())
141
+ self.all_energies.append(energy)
142
+ self.all_real_forces.append(forces)
143
+
144
+ # Calculate search direction
145
+ self.r = self.get_r(geom_coords, final_geom)
146
+ self.r_org = self.r.copy()
147
+
148
+ # Calculate projector
149
+ self.P = self.calc_projector(self.r)
150
+
151
+ # Grow first image
152
+ energy, forces, geom_coords, error_flag, new_file_directory = self.grow_image(
153
+ SP, FIO, geom_coords, element_list, charge_multiplicity, self.r, iter_num, file_directory
154
+ )
155
+
156
+ return geom_coords, new_file_directory, error_flag
157
+
158
+ def optimize_frontier_image(self, SP, FIO, geom, element_list, charge_multiplicity, iter_num, file_directory):
159
+ """Optimize the frontier image using projected forces"""
160
+ # Initialize BFGS variables
161
+ num_atoms = len(element_list)
162
+ num_coords = num_atoms * 3
163
+ H_inv = np.eye(num_coords) # Initial inverse Hessian approximation
164
+ prev_geom = None
165
+ prev_proj_grad = None
166
+
167
+ # Get current energy and forces - use provided file_directory
168
+ energy, forces, geom_coords, error_flag = SP.single_point(
169
+ file_directory, element_list, iter_num, charge_multiplicity, self.config.force_data["xtb"]
170
+ )
171
+
172
+ if error_flag:
173
+ print("Error in QM calculation during frontier optimization.")
174
+ return None, None, None, True, None
175
+
176
+ # Main optimization loop
177
+ for micro_iter in range(self.micro_iter_limit):
178
+ # Project forces onto perpendicular space
179
+ flat_forces = forces.reshape(-1)
180
+ proj_forces = np.dot(self.P, flat_forces).reshape(geom_coords.shape)
181
+
182
+ # Calculate RMS of projected forces
183
+ proj_rms = self.rms(proj_forces)
184
+
185
+ if micro_iter % 5 == 0:
186
+ print(f"Micro-iteration {micro_iter}: Projected force RMS = {proj_rms:.6f}, Energy = {energy:.8f}")
187
+
188
+ # Check convergence
189
+ if proj_rms <= self.rms_thresh:
190
+ print(f"Frontier image converged after {micro_iter} micro-iterations")
191
+ break
192
+
193
+ # BFGS update
194
+ flat_geom = geom_coords.flatten()
195
+ flat_proj_forces = proj_forces.flatten()
196
+
197
+ if prev_geom is not None:
198
+ s = flat_geom - prev_geom # Position difference
199
+ y = prev_proj_grad - flat_proj_forces # Force difference (note: forces = -gradient)
200
+
201
+ # Check curvature condition
202
+ sy = np.dot(s, y)
203
+ if sy > 1e-10:
204
+ # BFGS update formula
205
+ rho = 1.0 / sy
206
+ V = np.eye(len(s)) - rho * np.outer(s, y)
207
+ H_inv = np.dot(V.T, np.dot(H_inv, V)) + rho * np.outer(s, s)
208
+
209
+ # Store current values for next iteration
210
+ prev_geom = flat_geom.copy()
211
+ prev_proj_grad = flat_proj_forces.copy()
212
+
213
+ # Calculate search direction
214
+ search_dir = -np.dot(H_inv, flat_proj_forces).reshape(geom_coords.shape)
215
+
216
+ # Determine step size (simple trust radius approach)
217
+ trust_radius = 0.02 # Bohr
218
+ step_norm = np.linalg.norm(search_dir)
219
+ if step_norm > trust_radius:
220
+ search_dir = search_dir * (trust_radius / step_norm)
221
+
222
+ # Update geometry
223
+ geom_coords = geom_coords + search_dir
224
+
225
+ # Prepare and run calculation at the new geometry
226
+ new_geom_tolist = (geom_coords * self.config.bohr2angstroms).tolist()
227
+ for i, elem in enumerate(element_list):
228
+ new_geom_tolist[i].insert(0, elem)
229
+
230
+ new_geom_tolist.insert(0, charge_multiplicity)
231
+
232
+ file_directory = FIO.make_psi4_input_file([new_geom_tolist], iter_num)
233
+ energy, forces, geom_coords, error_flag = SP.single_point(
234
+ file_directory, element_list, iter_num, charge_multiplicity, self.config.force_data["xtb"]
235
+ )
236
+
237
+ if error_flag:
238
+ print("Error in QM calculation during frontier optimization.")
239
+ return None, None, None, True, None
240
+
241
+ # Return optimized geometry
242
+ return energy, forces, geom_coords, False, file_directory
243
+
244
+ def reparametrize(self, SP, FIO, geom, element_list, charge_multiplicity, iter_num, file_directory):
245
+ """Check if NT can be grown and update trajectory"""
246
+ # Get latest energy and forces
247
+ energy = self.all_energies[-1]
248
+ real_forces = self.all_real_forces[-1]
249
+
250
+ # Get projected forces
251
+ flat_forces = real_forces.reshape(-1)
252
+ proj_forces = np.dot(self.P, flat_forces).reshape(real_forces.shape)
253
+
254
+ # Check if we can grow the NT (convergence of frontier image)
255
+ proj_rms = self.rms(proj_forces)
256
+ can_grow = proj_rms <= self.rms_thresh
257
+
258
+ if can_grow:
259
+
260
+ # Check for stationary points
261
+ ae = self.all_energies
262
+ if len(ae) >= 3:
263
+ self.passed_min = ae[-3] > ae[-2] < ae[-1]
264
+ self.passed_ts = ae[-3] < ae[-2] > ae[-1]
265
+
266
+ if self.passed_min or self.passed_ts:
267
+ sp_image = self.images[-2].copy()
268
+ sp_kind = "Minimum" if self.passed_min else "TS"
269
+ self.sp_images.append(sp_image)
270
+ print(f"Passed stationary point! It seems to be a {sp_kind}.")
271
+
272
+ if self.passed_ts:
273
+ self.ts_images.append(sp_image)
274
+ # Calculate Hessian at TS if needed
275
+ # This would require additional implementation
276
+ elif self.passed_min:
277
+ self.min_images.append(sp_image)
278
+
279
+ # Update search direction if needed
280
+ r_new = self.get_r(geom)
281
+ r_dot = np.dot(r_new.reshape(-1), self.r.reshape(-1))
282
+ r_org_dot = np.dot(r_new.reshape(-1), self.r_org.reshape(-1))
283
+ print(f"r.dot(r')={r_dot:.6f} r_org.dot(r')={r_org_dot:.6f}")
284
+
285
+ # Update r if direction has changed significantly
286
+ if r_org_dot <= 0.5 and self.passed_min: # Using 0.5 as threshold
287
+ self.r = r_new
288
+ self.P = self.calc_projector(self.r)
289
+ print("Updated r")
290
+
291
+ # Grow new image
292
+ energy, forces, geom_coords, error_flag, new_file_directory = self.grow_image(
293
+ SP, FIO, geom, element_list, charge_multiplicity, self.r, iter_num, file_directory
294
+ )
295
+
296
+ if error_flag:
297
+ return None, True, None
298
+
299
+ self.did_reparametrization = True
300
+ return geom_coords, False, new_file_directory
301
+ else:
302
+ # Optimize frontier image since it's not converged yet
303
+ energy, forces, geom_coords, error_flag, new_file_directory = self.optimize_frontier_image(
304
+ SP, FIO, geom, element_list, charge_multiplicity, iter_num, file_directory
305
+ )
306
+
307
+ if error_flag:
308
+ return None, True, None
309
+
310
+ # Update stored energy and forces
311
+ self.all_energies[-1] = energy
312
+ self.all_real_forces[-1] = forces
313
+
314
+ self.did_reparametrization = False
315
+ return geom_coords, False, new_file_directory
316
+
317
+ def check_convergence(self):
318
+ """Check if the Newton Trajectory calculation has converged"""
319
+ if len(self.ts_images) == 0:
320
+ return False
321
+
322
+ # Consider converged if we've found a TS
323
+ return True
324
+
325
+ def get_additional_print(self):
326
+ """Get additional information for printing"""
327
+ if self.did_reparametrization:
328
+ img_num = len(self.images)
329
+ str_ = f"Grew Newton trajectory to {img_num} images."
330
+ if self.passed_min:
331
+ str_ += f" Passed minimum geometry at image {img_num-1}."
332
+ elif self.passed_ts:
333
+ str_ += f" Passed transition state geometry at image {img_num-1}."
334
+ else:
335
+ str_ = None
336
+
337
+ # Reset flags
338
+ self.did_reparametrization = False
339
+ self.passed_min = False
340
+ self.passed_ts = False
341
+
342
+ return str_
343
+
344
+ def main(self, file_directory_1, file_directory_2, SP1, SP2, element_list, init_electric_charge_and_multiplicity, final_electric_charge_and_multiplicity, FIO1, FIO2):
345
+ """Main method to run Newton Trajectory calculation"""
346
+ G = Graph(self.config.iEIP_FOLDER_DIRECTORY)
347
+ BIAS_GRAD_LIST_A = []
348
+ BIAS_ENERGY_LIST_A = []
349
+ GRAD_LIST_A = []
350
+ ENERGY_LIST_A = []
351
+
352
+ # Get initial geometry from first file
353
+ energy_1, gradient_1, geom_num_list_1, error_flag_1 = SP1.single_point(
354
+ file_directory_1, element_list, 0, init_electric_charge_and_multiplicity, self.config.force_data["xtb"]
355
+ )
356
+
357
+ if error_flag_1:
358
+ print("Error in initial QM calculation.")
359
+ with open(os.path.join(self.config.iEIP_FOLDER_DIRECTORY, "end.txt"), "w") as f:
360
+ f.write("Error in initial QM calculation.")
361
+ return
362
+
363
+ # If using final geometry for direction, get it
364
+ final_geom = None
365
+ if self.gnt_vec is None:
366
+ energy_2, gradient_2, geom_num_list_2, error_flag_2 = SP2.single_point(
367
+ file_directory_2, element_list, 0, final_electric_charge_and_multiplicity, self.config.force_data["xtb"]
368
+ )
369
+ if error_flag_2:
370
+ print("Error in second QM calculation.")
371
+ with open(os.path.join(self.config.iEIP_FOLDER_DIRECTORY, "end.txt"), "w") as f:
372
+ f.write("Error in second QM calculation.")
373
+ return
374
+ final_geom = geom_num_list_2
375
+
376
+ # Initialize Newton trajectory
377
+ geom, file_directory, error_flag = self.initialize(
378
+ SP1, FIO1, geom_num_list_1, element_list, init_electric_charge_and_multiplicity, file_directory_1, final_geom
379
+ )
380
+
381
+ if error_flag:
382
+ return
383
+
384
+ # Main iteration loop
385
+ for iter in range(1, self.config.microiterlimit):
386
+ print(f"==========================================================")
387
+ print(f"Newton Trajectory Iteration ({iter}/{self.config.microiterlimit})")
388
+
389
+ # Check for early termination
390
+ if os.path.isfile(os.path.join(self.config.iEIP_FOLDER_DIRECTORY, "end.txt")):
391
+ break
392
+
393
+ # Grow trajectory or optimize frontier image
394
+ geom, error_flag, file_directory = self.reparametrize(
395
+ SP1, FIO1, geom, element_list, init_electric_charge_and_multiplicity, iter, file_directory
396
+ )
397
+
398
+ if error_flag:
399
+ break
400
+
401
+ # Get current energy and forces
402
+ energy = self.all_energies[-1]
403
+ forces = self.all_real_forces[-1]
404
+
405
+ # Calculate bias potential if needed
406
+ BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
407
+ _, bias_energy, bias_gradient, _ = BPC.main(
408
+ energy, forces, geom, element_list, self.config.force_data
409
+ )
410
+
411
+ # Record data for plotting
412
+ ENERGY_LIST_A.append(energy * self.config.hartree2kcalmol)
413
+ GRAD_LIST_A.append(np.sqrt(np.sum(forces**2)))
414
+ BIAS_ENERGY_LIST_A.append(bias_energy * self.config.hartree2kcalmol)
415
+ BIAS_GRAD_LIST_A.append(np.sqrt(np.sum(bias_gradient**2)))
416
+
417
+ # Print current status
418
+ add_info = self.get_additional_print() or ""
419
+ print(f"Energy : {energy}")
420
+ print(f"Bias Energy : {bias_energy}")
421
+ print(f"Gradient Norm : {np.linalg.norm(forces)}")
422
+ print(f"Bias Gradient Norm : {np.linalg.norm(bias_gradient)}")
423
+ print(add_info)
424
+ print(f"==========================================================")
425
+
426
+ # Check for convergence
427
+ if self.check_convergence():
428
+ print("Newton Trajectory converged to transition state!")
429
+ break
430
+
431
+ else:
432
+ print("Reached maximum number of iterations. Newton trajectory calculation completed.")
433
+
434
+ # Create energy and gradient profile plots
435
+ NUM_LIST = list(range(len(ENERGY_LIST_A)))
436
+
437
+ G.single_plot(NUM_LIST, ENERGY_LIST_A, file_directory_1, "energy",
438
+ axis_name_2="energy [kcal/mol]", name="nt_energy")
439
+ G.single_plot(NUM_LIST, GRAD_LIST_A, file_directory_1, "gradient",
440
+ axis_name_2="grad (RMS) [a.u.]", name="nt_gradient")
441
+ G.single_plot(NUM_LIST, BIAS_ENERGY_LIST_A, file_directory_1, "bias_energy",
442
+ axis_name_2="energy [kcal/mol]", name="nt_bias_energy")
443
+ G.single_plot(NUM_LIST, BIAS_GRAD_LIST_A, file_directory_1, "bias_gradient",
444
+ axis_name_2="grad (RMS) [a.u.]", name="nt_bias_gradient")
445
+
446
+ # Create trajectory file
447
+ FIO1.make_traj_file_for_DM(img_1="A", img_2="B")
448
+
449
+ # Identify critical points
450
+ FIO1.argrelextrema_txt_save(ENERGY_LIST_A, "approx_TS", "max")
451
+ FIO1.argrelextrema_txt_save(ENERGY_LIST_A, "approx_EQ", "min")
452
+ FIO1.argrelextrema_txt_save(GRAD_LIST_A, "local_min_grad", "min")
453
+
454
+ return