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,662 @@
1
+ import numpy as np
2
+ import os
3
+ import copy
4
+ import csv
5
+
6
+ from multioptpy.Utils.calc_tools import Calculationtools
7
+ from multioptpy.Optimizer.hessian_update import ModelHessianUpdate
8
+ from multioptpy.Potential.potential import BiasPotentialCalculation
9
+ from multioptpy.Parameters.parameter import UnitValueLib, atomic_mass
10
+ from multioptpy.IRC.converge_criteria import convergence_check
11
+ from multioptpy.Visualization.visualization import Graph
12
+
13
+
14
+ class ModeKill:
15
+ """ModeKill class for removing specific imaginary frequencies
16
+
17
+ This class implements the ModeKill algorithm which selectively removes
18
+ specific unwanted imaginary frequency modes by stepping downhill along
19
+ those eigenvectors.
20
+
21
+ References
22
+ ----------
23
+ Based on the concept from pysisyphus package
24
+ """
25
+
26
+ def __init__(self, element_list, electric_charge_and_multiplicity, FC_count, file_directory,
27
+ final_directory, force_data, kill_inds=None, nu_thresh=-5.0, max_step=1000,
28
+ step_size=0.1, init_coord=None, init_hess=None, calc_engine=None,
29
+ xtb_method=None, do_hess=True, hessian_update=True):
30
+ """Initialize ModeKill calculator
31
+
32
+ Parameters
33
+ ----------
34
+ element_list : list
35
+ List of atomic elements
36
+ electric_charge_and_multiplicity : tuple
37
+ Charge and multiplicity for the system
38
+ FC_count : int
39
+ Frequency of full hessian recalculation
40
+ file_directory : str
41
+ Working directory
42
+ final_directory : str
43
+ Directory for final output
44
+ force_data : dict
45
+ Force field data for bias potential
46
+ kill_inds : list or numpy.ndarray, optional
47
+ Indices of modes to be removed (typically imaginary modes)
48
+ nu_thresh : float, optional
49
+ Threshold for considering a mode as imaginary (in cm^-1), default=-5.0
50
+ max_step : int, optional
51
+ Maximum number of steps
52
+ step_size : float, optional
53
+ Step size for the optimization
54
+ init_coord : numpy.ndarray, optional
55
+ Initial coordinates
56
+ init_hess : numpy.ndarray, optional
57
+ Initial hessian
58
+ calc_engine : object, optional
59
+ Calculator engine
60
+ xtb_method : str, optional
61
+ XTB method specification
62
+ do_hess : bool, optional
63
+ Whether to calculate final hessian, default=True
64
+ hessian_update : bool, optional
65
+ Whether to update hessian during optimization, default=True
66
+ """
67
+ self.max_step = max_step
68
+ self.step_size = step_size
69
+ self.ModelHessianUpdate = ModelHessianUpdate()
70
+ self.CE = calc_engine
71
+ self.FC_count = FC_count
72
+
73
+ # Kill mode parameters
74
+ self.kill_inds = kill_inds # Will be set in run() if None
75
+ self.nu_thresh = float(nu_thresh)
76
+ self.do_hess = do_hess
77
+ self.hessian_update = hessian_update
78
+ self.ovlp_thresh = 0.3
79
+
80
+ # Initialize tracking variables
81
+ self.prev_full_eigenvectors = None
82
+ self.converged = False
83
+ self.mw_down_step = None
84
+
85
+ # initial condition
86
+ self.coords = init_coord
87
+ self.init_hess = init_hess
88
+ self.mw_hessian = init_hess # Mass-weighted hessian
89
+ self.xtb_method = xtb_method
90
+
91
+ # convergence criteria - using tight criteria
92
+ self.MAX_FORCE_THRESHOLD = 0.0004
93
+ self.RMS_FORCE_THRESHOLD = 0.0001
94
+
95
+ self.element_list = element_list
96
+ self.electric_charge_and_multiplicity = electric_charge_and_multiplicity
97
+ self.directory = file_directory
98
+ self.final_directory = final_directory
99
+ self.force_data = force_data
100
+
101
+ # Data storage for tracking the modes and their frequencies
102
+ self.indices = np.arange(len(self.element_list) * 3)
103
+ self.neg_nus = []
104
+ self.kill_modes = None
105
+
106
+ # IRC data storage for calculations
107
+ self.irc_bias_energy_list = []
108
+ self.irc_energy_list = []
109
+ self.irc_mw_coords = []
110
+ self.irc_mw_gradients = []
111
+ self.irc_mw_bias_gradients = []
112
+ self.path_bending_angle_list = []
113
+
114
+ # Create data files
115
+ self.create_csv_file()
116
+ self.create_xyz_file()
117
+
118
+ def create_csv_file(self):
119
+ """Create CSV file for energy and gradient data"""
120
+ self.csv_filename = os.path.join(self.directory, "modekill_energies_gradients.csv")
121
+ with open(self.csv_filename, 'w', newline='') as csvfile:
122
+ writer = csv.writer(csvfile)
123
+ writer.writerow(['Step', 'Energy (Hartree)', 'Bias Energy (Hartree)',
124
+ 'RMS Gradient', 'RMS Bias Gradient', 'Imaginary Frequencies'])
125
+
126
+ def create_xyz_file(self):
127
+ """Create XYZ file for structure data"""
128
+ self.xyz_filename = os.path.join(self.directory, "modekill_structures.xyz")
129
+ # Create empty file (will be appended to later)
130
+ open(self.xyz_filename, 'w').close()
131
+
132
+ def save_to_csv(self, step, energy, bias_energy, gradient, bias_gradient, imaginary_freqs=None):
133
+ """Save energy and gradient data to CSV file
134
+
135
+ Parameters
136
+ ----------
137
+ step : int
138
+ Current step number
139
+ energy : float
140
+ Energy in Hartree
141
+ bias_energy : float
142
+ Bias energy in Hartree
143
+ gradient : numpy.ndarray
144
+ Gradient array
145
+ bias_gradient : numpy.ndarray
146
+ Bias gradient array
147
+ imaginary_freqs : list, optional
148
+ List of imaginary frequencies
149
+ """
150
+ rms_grad = np.sqrt((gradient**2).mean())
151
+ rms_bias_grad = np.sqrt((bias_gradient**2).mean())
152
+
153
+ if imaginary_freqs is None:
154
+ imaginary_freqs = []
155
+
156
+ with open(self.csv_filename, 'a', newline='') as csvfile:
157
+ writer = csv.writer(csvfile)
158
+ writer.writerow([step, energy, bias_energy, rms_grad, rms_bias_grad, str(imaginary_freqs)])
159
+
160
+ def save_xyz_structure(self, step, coords):
161
+ """Save molecular structure to XYZ file
162
+
163
+ Parameters
164
+ ----------
165
+ step : int
166
+ Current step number
167
+ coords : numpy.ndarray
168
+ Atomic coordinates in Bohr
169
+ """
170
+ # Convert coordinates to Angstroms
171
+ coords_angstrom = coords * UnitValueLib().bohr2angstroms
172
+
173
+ with open(self.xyz_filename, 'a') as f:
174
+ # Number of atoms and comment line
175
+ f.write(f"{len(coords)}\n")
176
+ f.write(f"ModeKill Step {step}\n")
177
+
178
+ # Atomic coordinates
179
+ for i, coord in enumerate(coords_angstrom):
180
+ f.write(f"{self.element_list[i]:<3} {coord[0]:15.10f} {coord[1]:15.10f} {coord[2]:15.10f}\n")
181
+
182
+ def get_mass_array(self):
183
+ """Create arrays of atomic masses for mass-weighting operations"""
184
+ elem_mass_list = np.array([atomic_mass(elem) for elem in self.element_list], dtype="float64")
185
+ sqrt_mass_list = np.sqrt(elem_mass_list)
186
+
187
+ # Create arrays for 3D operations (x,y,z for each atom)
188
+ three_elem_mass_list = np.repeat(elem_mass_list, 3)
189
+ three_sqrt_mass_list = np.repeat(sqrt_mass_list, 3)
190
+
191
+ return elem_mass_list, sqrt_mass_list, three_elem_mass_list, three_sqrt_mass_list
192
+
193
+ def mass_weight_hessian(self, hessian, three_sqrt_mass_list):
194
+ """Apply mass-weighting to the hessian matrix
195
+
196
+ Parameters
197
+ ----------
198
+ hessian : numpy.ndarray
199
+ Hessian matrix in non-mass-weighted coordinates
200
+ three_sqrt_mass_list : numpy.ndarray
201
+ Array of sqrt(mass) values repeated for x,y,z per atom
202
+
203
+ Returns
204
+ -------
205
+ numpy.ndarray
206
+ Mass-weighted hessian
207
+ """
208
+ mass_mat = np.diag(1.0 / three_sqrt_mass_list)
209
+ return np.dot(mass_mat, np.dot(hessian, mass_mat))
210
+
211
+ def mass_weight_coordinates(self, coordinates, sqrt_mass_list):
212
+ """Convert coordinates to mass-weighted coordinates
213
+
214
+ Parameters
215
+ ----------
216
+ coordinates : numpy.ndarray
217
+ Coordinates in non-mass-weighted form
218
+ sqrt_mass_list : numpy.ndarray
219
+ Array of sqrt(mass) values for each atom
220
+
221
+ Returns
222
+ -------
223
+ numpy.ndarray
224
+ Mass-weighted coordinates
225
+ """
226
+ mw_coords = copy.deepcopy(coordinates)
227
+ for i in range(len(coordinates)):
228
+ mw_coords[i] = coordinates[i] * sqrt_mass_list[i]
229
+ return mw_coords
230
+
231
+ def mass_weight_gradient(self, gradient, sqrt_mass_list):
232
+ """Convert gradient to mass-weighted form
233
+
234
+ Parameters
235
+ ----------
236
+ gradient : numpy.ndarray
237
+ Gradient in non-mass-weighted form
238
+ sqrt_mass_list : numpy.ndarray
239
+ Array of sqrt(mass) values for each atom
240
+
241
+ Returns
242
+ -------
243
+ numpy.ndarray
244
+ Mass-weighted gradient
245
+ """
246
+ mw_gradient = copy.deepcopy(gradient)
247
+ for i in range(len(gradient)):
248
+ mw_gradient[i] = gradient[i] / sqrt_mass_list[i]
249
+ return mw_gradient
250
+
251
+ def unmass_weight_step(self, step, sqrt_mass_list):
252
+ """Convert a step vector from mass-weighted to non-mass-weighted coordinates
253
+
254
+ Parameters
255
+ ----------
256
+ step : numpy.ndarray
257
+ Step in mass-weighted form
258
+ sqrt_mass_list : numpy.ndarray
259
+ Array of sqrt(mass) values for each atom
260
+
261
+ Returns
262
+ -------
263
+ numpy.ndarray
264
+ Step in non-mass-weighted coordinates
265
+ """
266
+ unmw_step = copy.deepcopy(step)
267
+ for i in range(len(step)):
268
+ unmw_step[i] = step[i] / sqrt_mass_list[i]
269
+ return unmw_step
270
+
271
+ def eigval_to_wavenumber(self, eigenvalue):
272
+ """Convert eigenvalue to wavenumber (cm^-1)
273
+
274
+ Parameters
275
+ ----------
276
+ eigenvalue : float or numpy.ndarray
277
+ Eigenvalue(s) from hessian
278
+
279
+ Returns
280
+ -------
281
+ float or numpy.ndarray
282
+ Corresponding wavenumber(s) in cm^-1
283
+ """
284
+ # Constants
285
+ au2rcm = 5140.48678
286
+
287
+ # Convert eigenvalue to wavenumber
288
+ sign = np.sign(eigenvalue)
289
+ return sign * np.sqrt(np.abs(eigenvalue)) * au2rcm
290
+
291
+ def update_mw_down_step(self, sqrt_mass_list, mw_gradient):
292
+ """Update the downhill step direction based on current hessian
293
+
294
+ Parameters
295
+ ----------
296
+ sqrt_mass_list : numpy.ndarray
297
+ Array of sqrt(mass) values for each atom
298
+ mw_gradient : numpy.ndarray
299
+ Mass-weighted gradient
300
+ """
301
+ # Diagonalize the mass-weighted hessian
302
+ w, v = np.linalg.eigh(self.mw_hessian)
303
+
304
+ # Convert eigenvalues to wavenumbers
305
+ nus = self.eigval_to_wavenumber(w)
306
+
307
+ # Check if we have any negative eigenvalues below threshold
308
+ neg_inds = nus < self.nu_thresh
309
+ neg_nus = nus[neg_inds]
310
+
311
+ # Check if we have modes to track
312
+ if len(self.kill_inds) == 0:
313
+ self.converged = True
314
+ return
315
+
316
+ # First time initialization
317
+ if self.prev_full_eigenvectors is None:
318
+ # Verify the kill indices point to imaginary modes
319
+ try:
320
+ assert all(nus[self.kill_inds] < self.nu_thresh), \
321
+ "ModeKill is intended for removal of imaginary frequencies " \
322
+ f"below {self.nu_thresh} cm^-1! The specified indices " \
323
+ f"{self.kill_inds} contain modes with positive frequencies " \
324
+ f"({nus[self.kill_inds]} cm^-1). Please choose different kill_inds!"
325
+ except IndexError:
326
+ print("Warning: Kill indices out of range. Using available negative modes.")
327
+ self.kill_inds = np.where(nus < self.nu_thresh)[0]
328
+ if len(self.kill_inds) == 0:
329
+ print("No negative modes found. ModeKill will exit.")
330
+ self.converged = True
331
+ return
332
+
333
+ # Store the full eigenvector set for later comparison
334
+ self.prev_full_eigenvectors = v
335
+ self.kill_modes = v[:, self.kill_inds]
336
+ else:
337
+ # Calculate overlaps between previous eigenvectors and current eigenvectors
338
+ new_kill_inds = []
339
+
340
+ for idx in self.kill_inds:
341
+ # Get the original kill mode from previous eigenvectors
342
+ orig_mode = self.prev_full_eigenvectors[:, idx]
343
+
344
+ # Find current mode with maximum overlap with original mode
345
+ overlaps_with_orig = np.abs(np.dot(orig_mode, v))
346
+
347
+ # Only consider negative eigenvalues (imaginary frequencies)
348
+ neg_mask = w < 0
349
+ overlaps_with_orig[~neg_mask] = 0
350
+
351
+ # Find index with highest overlap
352
+ max_overlap_idx = np.argmax(overlaps_with_orig)
353
+
354
+ # Only keep the mode if it still has a significant overlap and is imaginary
355
+ if overlaps_with_orig[max_overlap_idx] > self.ovlp_thresh and w[max_overlap_idx] < 0:
356
+ new_kill_inds.append(max_overlap_idx)
357
+ print(f"Mode {idx} tracked to current mode {max_overlap_idx} with overlap {overlaps_with_orig[max_overlap_idx]:.4f}")
358
+
359
+ # If no modes to track anymore, we're done
360
+ if len(new_kill_inds) == 0:
361
+ print("No modes left to track. ModeKill will exit.")
362
+ self.converged = True
363
+ return
364
+
365
+ # Update kill indices for the current iteration
366
+ self.kill_inds = np.array(new_kill_inds, dtype=int)
367
+ self.prev_full_eigenvectors = v
368
+ self.kill_modes = v[:, self.kill_inds]
369
+
370
+ # Determine correct sign for eigenvectors based on gradient overlap
371
+ mw_grad_flatten = mw_gradient.flatten()
372
+ mw_grad_normed = mw_grad_flatten / np.linalg.norm(mw_grad_flatten)
373
+ overlaps = np.dot(self.kill_modes.T, mw_grad_normed)
374
+ print("Overlaps between gradient and eigenvectors:")
375
+ print(overlaps)
376
+
377
+ # Flip eigenvector signs if needed (we want negative overlaps for downhill direction)
378
+ flip = overlaps > 0
379
+ print("Eigenvector signs to be flipped:")
380
+ print(str(flip))
381
+ self.kill_modes[:, flip] *= -1
382
+
383
+ # Create the step as the sum of the downhill steps along the modes to remove
384
+ self.mw_down_step = (self.step_size * self.kill_modes).sum(axis=1)
385
+
386
+ # Log information about the current modes being killed
387
+ print("\nCurrent modes to kill:")
388
+ for i, mode_idx in enumerate(self.kill_inds):
389
+ print(f"Mode {mode_idx}: {nus[mode_idx]:.2f} cm^-1")
390
+
391
+ def get_additional_print(self):
392
+ """Return additional information for printing during optimization
393
+
394
+ Returns
395
+ -------
396
+ str
397
+ String with additional information
398
+ """
399
+ if len(self.neg_nus) > 0:
400
+ neg_nus = np.array2string(self.neg_nus[-1], precision=2)
401
+ return f"\timag. ῦ: {neg_nus} cm^-1"
402
+ return ""
403
+
404
+ def run(self):
405
+ """Run the ModeKill calculation"""
406
+ print("ModeKill: Selective Imaginary Mode Removal")
407
+ geom_num_list = self.coords
408
+ CalcBiaspot = BiasPotentialCalculation(self.directory)
409
+
410
+ # Initialize the Hessian if needed to identify modes to kill
411
+ if self.mw_hessian is None or self.kill_inds is None:
412
+ # Calculate initial Hessian
413
+ self.CE.hessian_flag = True
414
+ e, g, geom_num_list, finish_frag = self.CE.single_point(
415
+ self.final_directory,
416
+ self.element_list,
417
+ 0,
418
+ self.electric_charge_and_multiplicity,
419
+ self.xtb_method,
420
+ UnitValueLib().bohr2angstroms * geom_num_list
421
+ )
422
+ self.mw_hessian = self.CE.Model_hess
423
+ self.CE.hessian_flag = False
424
+
425
+ # Project out translation and rotation
426
+ self.mw_hessian = Calculationtools().project_out_hess_tr_and_rot(
427
+ self.mw_hessian, self.element_list, geom_num_list
428
+ )
429
+
430
+ # Find negative eigenvalues if kill_inds not specified
431
+ if self.kill_inds is None:
432
+ w, v = np.linalg.eigh(self.mw_hessian)
433
+ nus = self.eigval_to_wavenumber(w)
434
+ neg_inds = np.where(nus < self.nu_thresh)[0]
435
+ if len(neg_inds) > 0:
436
+ print(f"Found {len(neg_inds)} imaginary modes below {self.nu_thresh} cm^-1")
437
+ # By default, kill all imaginary modes except the first one (IRC mode)
438
+ if len(neg_inds) > 1:
439
+ self.kill_inds = neg_inds[1:]
440
+ else:
441
+ self.kill_inds = np.array([], dtype=int)
442
+ print("No secondary imaginary modes to remove.")
443
+ return
444
+ else:
445
+ self.kill_inds = np.array([], dtype=int)
446
+ print("No imaginary modes found. Nothing to remove.")
447
+ return
448
+
449
+ print(f"Will attempt to remove modes: {self.kill_inds}")
450
+
451
+ # First step is just preparation
452
+ cur_cycle = 0
453
+
454
+ # Get mass arrays for consistent mass-weighting
455
+ elem_mass_list, sqrt_mass_list, three_elem_mass_list, three_sqrt_mass_list = self.get_mass_array()
456
+
457
+ while not self.converged and cur_cycle < self.max_step:
458
+ cur_cycle += 1
459
+ print(f"# STEP: {cur_cycle}")
460
+
461
+ # Check for early termination file
462
+ exit_file_detect = os.path.exists(self.directory + "end.txt")
463
+ if exit_file_detect:
464
+ break
465
+
466
+ # Calculate energy, gradient and update geometry
467
+ e, g, geom_num_list, finish_frag = self.CE.single_point(
468
+ self.final_directory,
469
+ self.element_list,
470
+ cur_cycle,
471
+ self.electric_charge_and_multiplicity,
472
+ self.xtb_method,
473
+ UnitValueLib().bohr2angstroms * geom_num_list
474
+ )
475
+
476
+ # Calculate bias potential
477
+ _, B_e, B_g, BPA_hessian = CalcBiaspot.main(
478
+ e, g, geom_num_list, self.element_list,
479
+ self.force_data, g, cur_cycle-1, geom_num_list
480
+ )
481
+
482
+ if finish_frag:
483
+ break
484
+
485
+ # Recalculate Hessian if needed
486
+ if cur_cycle % self.FC_count == 0 or not self.hessian_update:
487
+ self.CE.hessian_flag = True
488
+ e, g, geom_num_list, finish_frag = self.CE.single_point(
489
+ self.final_directory,
490
+ self.element_list,
491
+ cur_cycle,
492
+ self.electric_charge_and_multiplicity,
493
+ self.xtb_method,
494
+ UnitValueLib().bohr2angstroms * geom_num_list
495
+ )
496
+ self.mw_hessian = self.CE.Model_hess
497
+ self.CE.hessian_flag = False
498
+
499
+ self.mw_hessian = Calculationtools().project_out_hess_tr_and_rot(
500
+ self.mw_hessian, self.element_list, geom_num_list
501
+ )
502
+ print("Recalculated exact hessian.")
503
+ elif self.hessian_update and cur_cycle > 1:
504
+ # Hessian update with mass-weighted values
505
+ if len(self.irc_mw_coords) >= 2 and len(self.irc_mw_gradients) >= 2:
506
+ dx = self.irc_mw_coords[-1] - self.irc_mw_coords[-2]
507
+ dg = self.irc_mw_gradients[-1] - self.irc_mw_gradients[-2]
508
+
509
+ dx_flat = dx.reshape(-1, 1)
510
+ dg_flat = dg.reshape(-1, 1)
511
+
512
+ # Only update if the step and gradient difference are meaningful
513
+ inner_prod = np.dot(dx_flat.T, dg_flat)[0, 0]
514
+ if inner_prod > 1e-10:
515
+ delta_hess = self.ModelHessianUpdate.BFGS_hessian_update(
516
+ self.mw_hessian, dx_flat, dg_flat
517
+ )
518
+ self.mw_hessian += delta_hess
519
+
520
+ norm_dx = np.linalg.norm(dx_flat)
521
+ norm_dg = np.linalg.norm(dg_flat)
522
+ print(f"Did BFGS hessian update: norm(dx)={norm_dx:.4e}, "
523
+ f"norm(dg)={norm_dg:.4e}.")
524
+
525
+ # Mass-weight the bias potential hessian
526
+ mw_BPA_hessian = self.mass_weight_hessian(BPA_hessian, three_sqrt_mass_list)
527
+
528
+ # Mass-weight the coordinates
529
+ mw_geom_num_list = self.mass_weight_coordinates(geom_num_list, sqrt_mass_list)
530
+
531
+ # Mass-weight the gradients
532
+ mw_g = self.mass_weight_gradient(g, sqrt_mass_list)
533
+ mw_B_g = self.mass_weight_gradient(B_g, sqrt_mass_list)
534
+
535
+ # Store data for next iteration
536
+ if len(self.irc_mw_coords) >= 2:
537
+ self.irc_mw_coords.pop(0)
538
+ self.irc_mw_gradients.pop(0)
539
+ self.irc_mw_bias_gradients.pop(0)
540
+
541
+ self.irc_mw_coords.append(mw_geom_num_list.flatten())
542
+ self.irc_mw_gradients.append(mw_g.flatten())
543
+ self.irc_mw_bias_gradients.append(mw_B_g.flatten())
544
+
545
+ # Save structure to XYZ file
546
+ self.save_xyz_structure(cur_cycle, geom_num_list)
547
+
548
+ # Update the downhill step
549
+ self.update_mw_down_step(sqrt_mass_list, mw_B_g.flatten())
550
+
551
+ # If converged flag was set in update_mw_down_step, break out
552
+ if self.converged:
553
+ print("All targeted modes have been removed.")
554
+ break
555
+
556
+ # Diagonalize the current hessian to check modes
557
+ w, v = np.linalg.eigh(self.mw_hessian)
558
+
559
+ # Calculate wavenumbers
560
+ nus = self.eigval_to_wavenumber(w)
561
+ neg_inds = nus <= self.nu_thresh
562
+ neg_nus = nus[neg_inds]
563
+ self.neg_nus.append(neg_nus)
564
+
565
+ # Save energy and gradient data to CSV
566
+ self.save_to_csv(cur_cycle, e, B_e, g, B_g, neg_nus.tolist())
567
+
568
+ # Take a step
569
+ if not self.converged and self.mw_down_step is not None:
570
+ # Step along the downhill direction
571
+ mw_step = self.mw_down_step
572
+ unmw_step = self.unmass_weight_step(
573
+ mw_step.reshape(len(geom_num_list), 3),
574
+ sqrt_mass_list
575
+ )
576
+ geom_num_list = geom_num_list + unmw_step
577
+
578
+ # Remove center of mass motion
579
+ geom_num_list -= Calculationtools().calc_center_of_mass(geom_num_list, self.element_list)
580
+
581
+ # Calculate path bending angle if we have enough points
582
+ if len(self.irc_mw_coords) >= 3:
583
+ bend_angle = Calculationtools().calc_multi_dim_vec_angle(
584
+ self.irc_mw_coords[0] - self.irc_mw_coords[1],
585
+ self.irc_mw_coords[2] - self.irc_mw_coords[1]
586
+ )
587
+ self.path_bending_angle_list.append(np.degrees(bend_angle))
588
+ print("Path bending angle: ", np.degrees(bend_angle))
589
+
590
+ # Check for convergence of gradients
591
+ if convergence_check(B_g, self.MAX_FORCE_THRESHOLD, self.RMS_FORCE_THRESHOLD) and len(neg_nus) == 0:
592
+ print("Converged: All imaginary modes removed and gradient criteria satisfied.")
593
+ self.converged = True
594
+ break
595
+
596
+ # Print current geometry and info
597
+ print()
598
+ for i in range(len(geom_num_list)):
599
+ x = geom_num_list[i][0] * UnitValueLib().bohr2angstroms
600
+ y = geom_num_list[i][1] * UnitValueLib().bohr2angstroms
601
+ z = geom_num_list[i][2] * UnitValueLib().bohr2angstroms
602
+ print(f"{self.element_list[i]:<3} {x:17.12f} {y:17.12f} {z:17.12f}")
603
+
604
+ # Display information
605
+ print()
606
+ print("Energy : ", e)
607
+ print("Bias Energy : ", B_e)
608
+ print("RMS B. grad : ", np.sqrt((B_g**2).mean()))
609
+ print(f"Imag. freqs : {neg_nus} cm^-1")
610
+ print()
611
+
612
+ # Final hessian calculation if requested
613
+ if self.do_hess and not finish_frag:
614
+ print("Calculating final hessian...")
615
+ self.CE.hessian_flag = True
616
+ e_final, g_final, geom_num_list_final, _ = self.CE.single_point(
617
+ self.final_directory,
618
+ self.element_list,
619
+ cur_cycle + 1,
620
+ self.electric_charge_and_multiplicity,
621
+ self.xtb_method,
622
+ UnitValueLib().bohr2angstroms * geom_num_list
623
+ )
624
+
625
+ # Print final frequencies
626
+ final_hess = self.CE.Model_hess
627
+ final_hess = Calculationtools().project_out_hess_tr_and_rot(
628
+ final_hess, self.element_list, geom_num_list
629
+ )
630
+
631
+ # Reset QM interface settings
632
+ self.CE.hessian_flag = False
633
+ self.CE.FC_COUNT = self.FC_count
634
+
635
+ # Calculate and print final frequencies
636
+ w_final, _ = np.linalg.eigh(final_hess)
637
+ nus_final = self.eigval_to_wavenumber(w_final)
638
+ neg_inds_final = nus_final <= self.nu_thresh
639
+ neg_nus_final = nus_final[neg_inds_final]
640
+
641
+ print(f"Final wavenumbers of imaginary modes (<= {self.nu_thresh} cm^-1):")
642
+ print(f"{neg_nus_final} cm^-1")
643
+
644
+ # Save final structure with frequencies
645
+ self.save_to_csv(cur_cycle + 1, e_final, 0.0, g_final, g_final, neg_nus_final.tolist())
646
+ self.save_xyz_structure(cur_cycle + 1, geom_num_list)
647
+
648
+ # Save bending angle plot if we have enough data
649
+ if len(self.path_bending_angle_list) > 0:
650
+ G = Graph(self.directory)
651
+ G.single_plot(
652
+ np.array(range(len(self.path_bending_angle_list))),
653
+ np.array(self.path_bending_angle_list),
654
+ self.directory,
655
+ atom_num=0,
656
+ axis_name_1="# STEP",
657
+ axis_name_2="bending angle [degrees]",
658
+ name="ModeKill_bending"
659
+ )
660
+
661
+ print("ModeKill calculation finished.")
662
+ return