mlmm-toolkit 0.2.2.dev0__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 (372) hide show
  1. hessian_ff/__init__.py +50 -0
  2. hessian_ff/analytical_hessian.py +609 -0
  3. hessian_ff/constants.py +46 -0
  4. hessian_ff/forcefield.py +339 -0
  5. hessian_ff/loaders.py +608 -0
  6. hessian_ff/native/Makefile +8 -0
  7. hessian_ff/native/__init__.py +28 -0
  8. hessian_ff/native/analytical_hessian.py +88 -0
  9. hessian_ff/native/analytical_hessian_ext.cpp +258 -0
  10. hessian_ff/native/bonded.py +82 -0
  11. hessian_ff/native/bonded_ext.cpp +640 -0
  12. hessian_ff/native/loader.py +349 -0
  13. hessian_ff/native/nonbonded.py +118 -0
  14. hessian_ff/native/nonbonded_ext.cpp +1150 -0
  15. hessian_ff/prmtop_parmed.py +23 -0
  16. hessian_ff/system.py +107 -0
  17. hessian_ff/terms/__init__.py +14 -0
  18. hessian_ff/terms/angle.py +73 -0
  19. hessian_ff/terms/bond.py +44 -0
  20. hessian_ff/terms/cmap.py +406 -0
  21. hessian_ff/terms/dihedral.py +141 -0
  22. hessian_ff/terms/nonbonded.py +209 -0
  23. hessian_ff/tests/__init__.py +0 -0
  24. hessian_ff/tests/conftest.py +75 -0
  25. hessian_ff/tests/data/small/complex.parm7 +1346 -0
  26. hessian_ff/tests/data/small/complex.pdb +125 -0
  27. hessian_ff/tests/data/small/complex.rst7 +63 -0
  28. hessian_ff/tests/test_coords_input.py +44 -0
  29. hessian_ff/tests/test_energy_force.py +49 -0
  30. hessian_ff/tests/test_hessian.py +137 -0
  31. hessian_ff/tests/test_smoke.py +18 -0
  32. hessian_ff/tests/test_validation.py +40 -0
  33. hessian_ff/workflows.py +889 -0
  34. mlmm/__init__.py +36 -0
  35. mlmm/__main__.py +7 -0
  36. mlmm/_version.py +34 -0
  37. mlmm/add_elem_info.py +374 -0
  38. mlmm/advanced_help.py +91 -0
  39. mlmm/align_freeze_atoms.py +601 -0
  40. mlmm/all.py +3535 -0
  41. mlmm/bond_changes.py +231 -0
  42. mlmm/bool_compat.py +223 -0
  43. mlmm/cli.py +574 -0
  44. mlmm/cli_utils.py +166 -0
  45. mlmm/default_group.py +337 -0
  46. mlmm/defaults.py +467 -0
  47. mlmm/define_layer.py +526 -0
  48. mlmm/dft.py +1041 -0
  49. mlmm/energy_diagram.py +253 -0
  50. mlmm/extract.py +2213 -0
  51. mlmm/fix_altloc.py +464 -0
  52. mlmm/freq.py +1406 -0
  53. mlmm/harmonic_constraints.py +140 -0
  54. mlmm/hessian_cache.py +44 -0
  55. mlmm/hessian_calc.py +174 -0
  56. mlmm/irc.py +638 -0
  57. mlmm/mlmm_calc.py +2262 -0
  58. mlmm/mm_parm.py +945 -0
  59. mlmm/oniom_export.py +1983 -0
  60. mlmm/oniom_import.py +457 -0
  61. mlmm/opt.py +1742 -0
  62. mlmm/path_opt.py +1353 -0
  63. mlmm/path_search.py +2299 -0
  64. mlmm/preflight.py +88 -0
  65. mlmm/py.typed +1 -0
  66. mlmm/pysis_runner.py +45 -0
  67. mlmm/scan.py +1047 -0
  68. mlmm/scan2d.py +1226 -0
  69. mlmm/scan3d.py +1265 -0
  70. mlmm/scan_common.py +184 -0
  71. mlmm/summary_log.py +736 -0
  72. mlmm/trj2fig.py +448 -0
  73. mlmm/tsopt.py +2871 -0
  74. mlmm/utils.py +2309 -0
  75. mlmm/xtb_embedcharge_correction.py +475 -0
  76. mlmm_toolkit-0.2.2.dev0.dist-info/METADATA +1159 -0
  77. mlmm_toolkit-0.2.2.dev0.dist-info/RECORD +372 -0
  78. mlmm_toolkit-0.2.2.dev0.dist-info/WHEEL +5 -0
  79. mlmm_toolkit-0.2.2.dev0.dist-info/entry_points.txt +2 -0
  80. mlmm_toolkit-0.2.2.dev0.dist-info/licenses/LICENSE +674 -0
  81. mlmm_toolkit-0.2.2.dev0.dist-info/top_level.txt +4 -0
  82. pysisyphus/Geometry.py +1667 -0
  83. pysisyphus/LICENSE +674 -0
  84. pysisyphus/TableFormatter.py +63 -0
  85. pysisyphus/TablePrinter.py +74 -0
  86. pysisyphus/__init__.py +12 -0
  87. pysisyphus/calculators/AFIR.py +452 -0
  88. pysisyphus/calculators/AnaPot.py +20 -0
  89. pysisyphus/calculators/AnaPot2.py +48 -0
  90. pysisyphus/calculators/AnaPot3.py +12 -0
  91. pysisyphus/calculators/AnaPot4.py +20 -0
  92. pysisyphus/calculators/AnaPotBase.py +337 -0
  93. pysisyphus/calculators/AnaPotCBM.py +25 -0
  94. pysisyphus/calculators/AtomAtomTransTorque.py +154 -0
  95. pysisyphus/calculators/CFOUR.py +250 -0
  96. pysisyphus/calculators/Calculator.py +844 -0
  97. pysisyphus/calculators/CerjanMiller.py +24 -0
  98. pysisyphus/calculators/Composite.py +123 -0
  99. pysisyphus/calculators/ConicalIntersection.py +171 -0
  100. pysisyphus/calculators/DFTBp.py +430 -0
  101. pysisyphus/calculators/DFTD3.py +66 -0
  102. pysisyphus/calculators/DFTD4.py +84 -0
  103. pysisyphus/calculators/Dalton.py +61 -0
  104. pysisyphus/calculators/Dimer.py +681 -0
  105. pysisyphus/calculators/Dummy.py +20 -0
  106. pysisyphus/calculators/EGO.py +76 -0
  107. pysisyphus/calculators/EnergyMin.py +224 -0
  108. pysisyphus/calculators/ExternalPotential.py +264 -0
  109. pysisyphus/calculators/FakeASE.py +35 -0
  110. pysisyphus/calculators/FourWellAnaPot.py +28 -0
  111. pysisyphus/calculators/FreeEndNEBPot.py +39 -0
  112. pysisyphus/calculators/Gaussian09.py +18 -0
  113. pysisyphus/calculators/Gaussian16.py +726 -0
  114. pysisyphus/calculators/HardSphere.py +159 -0
  115. pysisyphus/calculators/IDPPCalculator.py +49 -0
  116. pysisyphus/calculators/IPIClient.py +133 -0
  117. pysisyphus/calculators/IPIServer.py +234 -0
  118. pysisyphus/calculators/LEPSBase.py +24 -0
  119. pysisyphus/calculators/LEPSExpr.py +139 -0
  120. pysisyphus/calculators/LennardJones.py +80 -0
  121. pysisyphus/calculators/MOPAC.py +219 -0
  122. pysisyphus/calculators/MullerBrownSympyPot.py +51 -0
  123. pysisyphus/calculators/MultiCalc.py +85 -0
  124. pysisyphus/calculators/NFK.py +45 -0
  125. pysisyphus/calculators/OBabel.py +87 -0
  126. pysisyphus/calculators/ONIOMv2.py +1129 -0
  127. pysisyphus/calculators/ORCA.py +893 -0
  128. pysisyphus/calculators/ORCA5.py +6 -0
  129. pysisyphus/calculators/OpenMM.py +88 -0
  130. pysisyphus/calculators/OpenMolcas.py +281 -0
  131. pysisyphus/calculators/OverlapCalculator.py +908 -0
  132. pysisyphus/calculators/Psi4.py +218 -0
  133. pysisyphus/calculators/PyPsi4.py +37 -0
  134. pysisyphus/calculators/PySCF.py +341 -0
  135. pysisyphus/calculators/PyXTB.py +73 -0
  136. pysisyphus/calculators/QCEngine.py +106 -0
  137. pysisyphus/calculators/Rastrigin.py +22 -0
  138. pysisyphus/calculators/Remote.py +76 -0
  139. pysisyphus/calculators/Rosenbrock.py +15 -0
  140. pysisyphus/calculators/SocketCalc.py +97 -0
  141. pysisyphus/calculators/TIP3P.py +111 -0
  142. pysisyphus/calculators/TransTorque.py +161 -0
  143. pysisyphus/calculators/Turbomole.py +965 -0
  144. pysisyphus/calculators/VRIPot.py +37 -0
  145. pysisyphus/calculators/WFOWrapper.py +333 -0
  146. pysisyphus/calculators/WFOWrapper2.py +341 -0
  147. pysisyphus/calculators/XTB.py +418 -0
  148. pysisyphus/calculators/__init__.py +81 -0
  149. pysisyphus/calculators/cosmo_data.py +139 -0
  150. pysisyphus/calculators/parser.py +150 -0
  151. pysisyphus/color.py +19 -0
  152. pysisyphus/config.py +133 -0
  153. pysisyphus/constants.py +65 -0
  154. pysisyphus/cos/AdaptiveNEB.py +230 -0
  155. pysisyphus/cos/ChainOfStates.py +725 -0
  156. pysisyphus/cos/FreeEndNEB.py +25 -0
  157. pysisyphus/cos/FreezingString.py +103 -0
  158. pysisyphus/cos/GrowingChainOfStates.py +71 -0
  159. pysisyphus/cos/GrowingNT.py +309 -0
  160. pysisyphus/cos/GrowingString.py +508 -0
  161. pysisyphus/cos/NEB.py +189 -0
  162. pysisyphus/cos/SimpleZTS.py +64 -0
  163. pysisyphus/cos/__init__.py +22 -0
  164. pysisyphus/cos/stiffness.py +199 -0
  165. pysisyphus/drivers/__init__.py +17 -0
  166. pysisyphus/drivers/afir.py +855 -0
  167. pysisyphus/drivers/barriers.py +271 -0
  168. pysisyphus/drivers/birkholz.py +138 -0
  169. pysisyphus/drivers/cluster.py +318 -0
  170. pysisyphus/drivers/diabatization.py +133 -0
  171. pysisyphus/drivers/merge.py +368 -0
  172. pysisyphus/drivers/merge_mol2.py +322 -0
  173. pysisyphus/drivers/opt.py +375 -0
  174. pysisyphus/drivers/perf.py +91 -0
  175. pysisyphus/drivers/pka.py +52 -0
  176. pysisyphus/drivers/precon_pos_rot.py +669 -0
  177. pysisyphus/drivers/rates.py +480 -0
  178. pysisyphus/drivers/replace.py +219 -0
  179. pysisyphus/drivers/scan.py +212 -0
  180. pysisyphus/drivers/spectrum.py +166 -0
  181. pysisyphus/drivers/thermo.py +31 -0
  182. pysisyphus/dynamics/Gaussian.py +103 -0
  183. pysisyphus/dynamics/__init__.py +20 -0
  184. pysisyphus/dynamics/colvars.py +136 -0
  185. pysisyphus/dynamics/driver.py +297 -0
  186. pysisyphus/dynamics/helpers.py +256 -0
  187. pysisyphus/dynamics/lincs.py +105 -0
  188. pysisyphus/dynamics/mdp.py +364 -0
  189. pysisyphus/dynamics/rattle.py +121 -0
  190. pysisyphus/dynamics/thermostats.py +128 -0
  191. pysisyphus/dynamics/wigner.py +266 -0
  192. pysisyphus/elem_data.py +3473 -0
  193. pysisyphus/exceptions.py +2 -0
  194. pysisyphus/filtertrj.py +69 -0
  195. pysisyphus/helpers.py +623 -0
  196. pysisyphus/helpers_pure.py +649 -0
  197. pysisyphus/init_logging.py +50 -0
  198. pysisyphus/intcoords/Bend.py +69 -0
  199. pysisyphus/intcoords/Bend2.py +25 -0
  200. pysisyphus/intcoords/BondedFragment.py +32 -0
  201. pysisyphus/intcoords/Cartesian.py +41 -0
  202. pysisyphus/intcoords/CartesianCoords.py +140 -0
  203. pysisyphus/intcoords/Coords.py +56 -0
  204. pysisyphus/intcoords/DLC.py +197 -0
  205. pysisyphus/intcoords/DistanceFunction.py +34 -0
  206. pysisyphus/intcoords/DummyImproper.py +70 -0
  207. pysisyphus/intcoords/DummyTorsion.py +72 -0
  208. pysisyphus/intcoords/LinearBend.py +105 -0
  209. pysisyphus/intcoords/LinearDisplacement.py +80 -0
  210. pysisyphus/intcoords/OutOfPlane.py +59 -0
  211. pysisyphus/intcoords/PrimTypes.py +286 -0
  212. pysisyphus/intcoords/Primitive.py +137 -0
  213. pysisyphus/intcoords/RedundantCoords.py +659 -0
  214. pysisyphus/intcoords/RobustTorsion.py +59 -0
  215. pysisyphus/intcoords/Rotation.py +147 -0
  216. pysisyphus/intcoords/Stretch.py +31 -0
  217. pysisyphus/intcoords/Torsion.py +101 -0
  218. pysisyphus/intcoords/Torsion2.py +25 -0
  219. pysisyphus/intcoords/Translation.py +45 -0
  220. pysisyphus/intcoords/__init__.py +61 -0
  221. pysisyphus/intcoords/augment_bonds.py +126 -0
  222. pysisyphus/intcoords/derivatives.py +10512 -0
  223. pysisyphus/intcoords/eval.py +80 -0
  224. pysisyphus/intcoords/exceptions.py +37 -0
  225. pysisyphus/intcoords/findiffs.py +48 -0
  226. pysisyphus/intcoords/generate_derivatives.py +414 -0
  227. pysisyphus/intcoords/helpers.py +235 -0
  228. pysisyphus/intcoords/logging_conf.py +10 -0
  229. pysisyphus/intcoords/mp_derivatives.py +10836 -0
  230. pysisyphus/intcoords/setup.py +962 -0
  231. pysisyphus/intcoords/setup_fast.py +176 -0
  232. pysisyphus/intcoords/update.py +272 -0
  233. pysisyphus/intcoords/valid.py +89 -0
  234. pysisyphus/interpolate/Geodesic.py +93 -0
  235. pysisyphus/interpolate/IDPP.py +55 -0
  236. pysisyphus/interpolate/Interpolator.py +116 -0
  237. pysisyphus/interpolate/LST.py +70 -0
  238. pysisyphus/interpolate/Redund.py +152 -0
  239. pysisyphus/interpolate/__init__.py +9 -0
  240. pysisyphus/interpolate/helpers.py +34 -0
  241. pysisyphus/io/__init__.py +22 -0
  242. pysisyphus/io/aomix.py +178 -0
  243. pysisyphus/io/cjson.py +24 -0
  244. pysisyphus/io/crd.py +101 -0
  245. pysisyphus/io/cube.py +220 -0
  246. pysisyphus/io/fchk.py +184 -0
  247. pysisyphus/io/hdf5.py +49 -0
  248. pysisyphus/io/hessian.py +72 -0
  249. pysisyphus/io/mol2.py +146 -0
  250. pysisyphus/io/molden.py +293 -0
  251. pysisyphus/io/orca.py +189 -0
  252. pysisyphus/io/pdb.py +269 -0
  253. pysisyphus/io/psf.py +79 -0
  254. pysisyphus/io/pubchem.py +31 -0
  255. pysisyphus/io/qcschema.py +34 -0
  256. pysisyphus/io/sdf.py +29 -0
  257. pysisyphus/io/xyz.py +61 -0
  258. pysisyphus/io/zmat.py +175 -0
  259. pysisyphus/irc/DWI.py +108 -0
  260. pysisyphus/irc/DampedVelocityVerlet.py +134 -0
  261. pysisyphus/irc/Euler.py +22 -0
  262. pysisyphus/irc/EulerPC.py +345 -0
  263. pysisyphus/irc/GonzalezSchlegel.py +187 -0
  264. pysisyphus/irc/IMKMod.py +164 -0
  265. pysisyphus/irc/IRC.py +878 -0
  266. pysisyphus/irc/IRCDummy.py +10 -0
  267. pysisyphus/irc/Instanton.py +307 -0
  268. pysisyphus/irc/LQA.py +53 -0
  269. pysisyphus/irc/ModeKill.py +136 -0
  270. pysisyphus/irc/ParamPlot.py +53 -0
  271. pysisyphus/irc/RK4.py +36 -0
  272. pysisyphus/irc/__init__.py +31 -0
  273. pysisyphus/irc/initial_displ.py +219 -0
  274. pysisyphus/linalg.py +411 -0
  275. pysisyphus/line_searches/Backtracking.py +88 -0
  276. pysisyphus/line_searches/HagerZhang.py +184 -0
  277. pysisyphus/line_searches/LineSearch.py +232 -0
  278. pysisyphus/line_searches/StrongWolfe.py +108 -0
  279. pysisyphus/line_searches/__init__.py +9 -0
  280. pysisyphus/line_searches/interpol.py +15 -0
  281. pysisyphus/modefollow/NormalMode.py +40 -0
  282. pysisyphus/modefollow/__init__.py +10 -0
  283. pysisyphus/modefollow/davidson.py +199 -0
  284. pysisyphus/modefollow/lanczos.py +95 -0
  285. pysisyphus/optimizers/BFGS.py +99 -0
  286. pysisyphus/optimizers/BacktrackingOptimizer.py +113 -0
  287. pysisyphus/optimizers/ConjugateGradient.py +98 -0
  288. pysisyphus/optimizers/CubicNewton.py +75 -0
  289. pysisyphus/optimizers/FIRE.py +113 -0
  290. pysisyphus/optimizers/HessianOptimizer.py +1176 -0
  291. pysisyphus/optimizers/LBFGS.py +228 -0
  292. pysisyphus/optimizers/LayerOpt.py +411 -0
  293. pysisyphus/optimizers/MicroOptimizer.py +169 -0
  294. pysisyphus/optimizers/NCOptimizer.py +90 -0
  295. pysisyphus/optimizers/Optimizer.py +1084 -0
  296. pysisyphus/optimizers/PreconLBFGS.py +260 -0
  297. pysisyphus/optimizers/PreconSteepestDescent.py +7 -0
  298. pysisyphus/optimizers/QuickMin.py +74 -0
  299. pysisyphus/optimizers/RFOptimizer.py +181 -0
  300. pysisyphus/optimizers/RSA.py +99 -0
  301. pysisyphus/optimizers/StabilizedQNMethod.py +248 -0
  302. pysisyphus/optimizers/SteepestDescent.py +23 -0
  303. pysisyphus/optimizers/StringOptimizer.py +173 -0
  304. pysisyphus/optimizers/__init__.py +41 -0
  305. pysisyphus/optimizers/closures.py +301 -0
  306. pysisyphus/optimizers/cls_map.py +58 -0
  307. pysisyphus/optimizers/exceptions.py +6 -0
  308. pysisyphus/optimizers/gdiis.py +280 -0
  309. pysisyphus/optimizers/guess_hessians.py +311 -0
  310. pysisyphus/optimizers/hessian_updates.py +355 -0
  311. pysisyphus/optimizers/poly_fit.py +285 -0
  312. pysisyphus/optimizers/precon.py +153 -0
  313. pysisyphus/optimizers/restrict_step.py +24 -0
  314. pysisyphus/pack.py +172 -0
  315. pysisyphus/peakdetect.py +948 -0
  316. pysisyphus/plot.py +1031 -0
  317. pysisyphus/run.py +2106 -0
  318. pysisyphus/socket_helper.py +74 -0
  319. pysisyphus/stocastic/FragmentKick.py +132 -0
  320. pysisyphus/stocastic/Kick.py +81 -0
  321. pysisyphus/stocastic/Pipeline.py +303 -0
  322. pysisyphus/stocastic/__init__.py +21 -0
  323. pysisyphus/stocastic/align.py +127 -0
  324. pysisyphus/testing.py +96 -0
  325. pysisyphus/thermo.py +156 -0
  326. pysisyphus/trj.py +824 -0
  327. pysisyphus/tsoptimizers/RSIRFOptimizer.py +56 -0
  328. pysisyphus/tsoptimizers/RSPRFOptimizer.py +182 -0
  329. pysisyphus/tsoptimizers/TRIM.py +59 -0
  330. pysisyphus/tsoptimizers/TSHessianOptimizer.py +463 -0
  331. pysisyphus/tsoptimizers/__init__.py +23 -0
  332. pysisyphus/wavefunction/Basis.py +239 -0
  333. pysisyphus/wavefunction/DIIS.py +76 -0
  334. pysisyphus/wavefunction/__init__.py +25 -0
  335. pysisyphus/wavefunction/build_ext.py +42 -0
  336. pysisyphus/wavefunction/cart2sph.py +190 -0
  337. pysisyphus/wavefunction/diabatization.py +304 -0
  338. pysisyphus/wavefunction/excited_states.py +435 -0
  339. pysisyphus/wavefunction/gen_ints.py +1811 -0
  340. pysisyphus/wavefunction/helpers.py +104 -0
  341. pysisyphus/wavefunction/ints/__init__.py +0 -0
  342. pysisyphus/wavefunction/ints/boys.py +193 -0
  343. pysisyphus/wavefunction/ints/boys_table_N_64_xasym_27.1_step_0.01.npy +0 -0
  344. pysisyphus/wavefunction/ints/cart_gto3d.py +176 -0
  345. pysisyphus/wavefunction/ints/coulomb3d.py +25928 -0
  346. pysisyphus/wavefunction/ints/diag_quadrupole3d.py +10036 -0
  347. pysisyphus/wavefunction/ints/dipole3d.py +8762 -0
  348. pysisyphus/wavefunction/ints/int2c2e3d.py +7198 -0
  349. pysisyphus/wavefunction/ints/int3c2e3d_sph.py +65040 -0
  350. pysisyphus/wavefunction/ints/kinetic3d.py +8240 -0
  351. pysisyphus/wavefunction/ints/ovlp3d.py +3777 -0
  352. pysisyphus/wavefunction/ints/quadrupole3d.py +15054 -0
  353. pysisyphus/wavefunction/ints/self_ovlp3d.py +198 -0
  354. pysisyphus/wavefunction/localization.py +458 -0
  355. pysisyphus/wavefunction/multipole.py +159 -0
  356. pysisyphus/wavefunction/normalization.py +36 -0
  357. pysisyphus/wavefunction/pop_analysis.py +134 -0
  358. pysisyphus/wavefunction/shells.py +1171 -0
  359. pysisyphus/wavefunction/wavefunction.py +504 -0
  360. pysisyphus/wrapper/__init__.py +11 -0
  361. pysisyphus/wrapper/exceptions.py +2 -0
  362. pysisyphus/wrapper/jmol.py +120 -0
  363. pysisyphus/wrapper/mwfn.py +169 -0
  364. pysisyphus/wrapper/packmol.py +71 -0
  365. pysisyphus/xyzloader.py +168 -0
  366. pysisyphus/yaml_mods.py +45 -0
  367. thermoanalysis/LICENSE +674 -0
  368. thermoanalysis/QCData.py +244 -0
  369. thermoanalysis/__init__.py +0 -0
  370. thermoanalysis/config.py +3 -0
  371. thermoanalysis/constants.py +20 -0
  372. thermoanalysis/thermo.py +1011 -0
pysisyphus/irc/IRC.py ADDED
@@ -0,0 +1,878 @@
1
+ # https://verahill.blogspot.de/2013/06/439-calculate-frequencies-from-hessian.html
2
+ # https://chemistry.stackexchange.com/questions/74639
3
+
4
+ import logging
5
+ from math import ceil, log
6
+ import os
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ import h5py
11
+ import numpy as np
12
+
13
+ from pysisyphus.constants import BOHR2ANG, AU2KJPERMOL
14
+ from pysisyphus.Geometry import Geometry
15
+ from pysisyphus.helpers import check_for_end_sign
16
+ from pysisyphus.helpers_pure import (
17
+ highlight_text,
18
+ eigval_to_wavenumber,
19
+ report_isotopes,
20
+ rms,
21
+ )
22
+ from pysisyphus.irc.Instanton import T_crossover_from_eigval
23
+ from pysisyphus.io import save_third_deriv
24
+ from pysisyphus.optimizers.guess_hessians import get_guess_hessian
25
+ from pysisyphus.TablePrinter import TablePrinter
26
+ from pysisyphus.xyzloader import make_trj_str, make_xyz_str
27
+
28
+ import torch
29
+
30
+
31
+ class IRC:
32
+ valid_displs = ("energy", "length")
33
+
34
+ def __init__(
35
+ self,
36
+ geometry,
37
+ step_length=0.1,
38
+ max_cycles=125,
39
+ downhill=False,
40
+ forward=True,
41
+ backward=True,
42
+ root=0,
43
+ hessian_init=None,
44
+ displ="energy",
45
+ displ_energy=1e-3,
46
+ displ_length=0.1,
47
+ rms_grad_thresh=1e-3,
48
+ hard_rms_grad_thresh=None,
49
+ energy_thresh=1e-6,
50
+ imag_below=0.0,
51
+ force_inflection=True,
52
+ check_bonds=False,
53
+ out_dir=".",
54
+ prefix="",
55
+ dump_fn="irc_data.h5",
56
+ dump_every=5,
57
+ ):
58
+ """Base class for IRC calculations.
59
+
60
+ Parameters
61
+ ----------
62
+ geometry : Geometry
63
+ Transtion state geometry, or initial geometry for downhill run.
64
+ step_length : float, optional
65
+ Step length in unweighted coordinates.
66
+ max_cycles : int, optional
67
+ Positive integer, controlloing the maximum number of IRC steps
68
+ taken in a direction (forward/backward/downhill).
69
+ downhill : bool, default=False
70
+ Downhill run from a non-stationary point with non-vanishing
71
+ gradient. Disables forward and backward runs.
72
+ forward : bool, default=True
73
+ Integrate IRC in positive s direction.
74
+ backward : bool, default=True
75
+ Integrate IRC in negative s direction.
76
+ root : int, default=0
77
+ Use n-th root for initial displacement from TS.
78
+ hessian_init : str, default=None
79
+ Path to Hessian HDF5 file, e.g., from a previous TS calculation.
80
+ displ: str, one of ("energy", "length")
81
+ Controlls initial displacement from the TS. 'energy' assumes a
82
+ quadratic model, from which a step length for a given energy
83
+ lowering (see 'displ_energy') is determined. 'length' corresponds
84
+ to a displacement along the transition vector.
85
+ displ_energy : float, default=1e-3
86
+ Required energy lowering from the TS in au (Hartree). Used with
87
+ 'displ: energy'.
88
+ displ_length : float, default=0.1
89
+ Step length along the transition vector. Used only with
90
+ 'displ: length'.
91
+ rms_grad_thresh : float, default=1e-3,
92
+ Convergence is signalled when to root mean square of the unweighted
93
+ gradient is less than or equal to this value
94
+ energy_thresh : float, default=1e-6,
95
+ Signal convergence when the energy difference between two points
96
+ is equal to or less than 'energy_thresh'.
97
+ imag_below : float, default=0.0
98
+ Require the wavenumber of the imaginary mode to be below the
99
+ given threshold. If given, it should be a negative number.
100
+ force_inflection : bool, optional
101
+ Don't indicate convergence before passing an inflection point.
102
+ check_bonds : bool, optional, default=True
103
+ Report whether bonds are formed/broken along the IRC, w.r.t the TS.
104
+ out_dir : str, optional
105
+ Dump everything into 'out_dir' directory instead of the CWD.
106
+ prefix : str, optional
107
+ Short string that is prepended to all files that are created
108
+ by this class, e.g., trajectories and HDF5 dumps.
109
+ dump_fn : str, optional
110
+ Base name for the HDF5 files.
111
+ dump_every : int, optional
112
+ Dump to HDF5 every n-th cycle.
113
+ """
114
+ assert step_length > 0, "step_length must be positive"
115
+ assert max_cycles > 0, "max_cycles must be positive"
116
+
117
+ self.logger = logging.getLogger("irc")
118
+
119
+ self.geometry = geometry
120
+ self.atoms = self.geometry.atoms
121
+ assert self.geometry.coord_type == "cart"
122
+
123
+ report_isotopes(self.geometry, "the IRC")
124
+
125
+ self.step_length = step_length
126
+ self.max_cycles = max_cycles
127
+ self.downhill = downhill
128
+ # Disable forward/backward when downhill is set
129
+ self.forward = not self.downhill and forward
130
+ self.backward = not self.downhill and backward
131
+ self.root = root
132
+ if hessian_init is None:
133
+ hessian_init = "calc" if not self.downhill else "unit"
134
+ self.hessian_init = hessian_init
135
+ self.displ = displ
136
+ assert (
137
+ self.displ in self.valid_displs
138
+ ), f"'displ: {self.displ}' not in {self.valid_displs}"
139
+ self.displ_energy = float(displ_energy)
140
+ self.displ_length = float(displ_length)
141
+ self.rms_grad_thresh = float(rms_grad_thresh)
142
+ self.hard_rms_grad_thresh = hard_rms_grad_thresh
143
+ self.energy_thresh = float(energy_thresh)
144
+ assert imag_below <= 0.0
145
+ self.imag_below = imag_below
146
+ self.force_inflection = force_inflection
147
+ self.check_bonds = check_bonds
148
+ self.out_dir = out_dir
149
+ self.out_dir = Path(self.out_dir)
150
+ if not self.out_dir.exists():
151
+ os.mkdir(self.out_dir)
152
+ self.prefix = f"{prefix}_" if prefix else prefix
153
+ self.dump_fn = dump_fn
154
+ self.dump_every = int(dump_every)
155
+
156
+ # Determine bonds at TS
157
+ self.ts_bond_sets = self.geometry.bond_sets
158
+ self.ref_bond_sets = {}
159
+
160
+ self._m_sqrt = np.sqrt(self.geometry.masses_rep)
161
+
162
+ # cache frequently‑used active indices
163
+ if getattr(self.geometry, "within_partial_hessian", None) is not None:
164
+ self._act_atoms = self.geometry.hess_active_atom_indices
165
+ self._act_dofs = self.geometry.hess_active_dof_indices
166
+ else:
167
+ self._act_atoms = self.geometry.active_atom_indices
168
+ self._act_dofs = self.geometry.active_dof_indices
169
+
170
+ self.all_energies = list()
171
+ self.all_coords = list()
172
+ self.all_gradients = list()
173
+ self.all_mw_coords = list()
174
+ self.all_mw_gradients = list()
175
+
176
+ # step length dE max(|grad|) rms(grad)
177
+ col_fmts = "int float float float float".split()
178
+ header = ("Step", "IRC length", "dE / au", "max(|grad|)", "rms(grad)")
179
+ self.table = TablePrinter(header, col_fmts)
180
+
181
+ self.cycle_places = ceil(log(self.max_cycles, 10))
182
+
183
+ self.mm_inv2 = self.geometry.mm_sqrt_inv[np.ix_(self._act_dofs, self._act_dofs)]
184
+
185
+ def get_path_for_fn(self, fn):
186
+ return self.out_dir / f"{self.prefix}{fn}"
187
+
188
+ @property
189
+ def coords(self):
190
+ return self.geometry.coords
191
+
192
+ @coords.setter
193
+ def coords(self, coords):
194
+ self.geometry.coords = coords
195
+
196
+ @property
197
+ def mw_coords(self):
198
+ return self.geometry.mw_coords
199
+
200
+ @mw_coords.setter
201
+ def mw_coords(self, mw_coords):
202
+ self.geometry.mw_coords = mw_coords
203
+
204
+ @property
205
+ def energy(self):
206
+ return self.geometry.energy
207
+
208
+ @property
209
+ def gradient(self):
210
+ return self.geometry.gradient
211
+
212
+ @property
213
+ def mw_gradient(self):
214
+ return self.geometry.mw_gradient
215
+
216
+ # @property
217
+ # def mw_hessian(self):
218
+ # # TODO: This can be removed when the mw_hessian property is updated
219
+ # # in Geometry.py.
220
+ # return self.geometry.mw_hessian
221
+
222
+ def log(self, msg):
223
+ # self.logger.debug(f"step {self.cur_cycle:03d}, {msg}")
224
+ self.logger.debug(msg)
225
+
226
+ @property
227
+ def m_sqrt(self):
228
+ return self._m_sqrt
229
+
230
+ def unweight_vec(self, vec):
231
+ return self.m_sqrt * vec
232
+
233
+ def mass_weigh_hessian(self, hessian):
234
+ return self.geometry.mass_weigh_hessian(hessian)
235
+
236
+ # mass‑weight only the sub‑Hessian that belongs to the moving atoms
237
+ def _mw_hessian_active(self, H_act):
238
+ if isinstance(H_act, torch.Tensor):
239
+ if not isinstance(self.mm_inv2, torch.Tensor):
240
+ self.mm_inv2 = torch.as_tensor(self.mm_inv2, dtype=H_act.dtype, device=H_act.device)
241
+ return self.mm_inv2 @ H_act @ self.mm_inv2 # in‑place not possible → tiny matrix
242
+ return self.mm_inv2.dot(H_act).dot(self.mm_inv2)
243
+
244
+ # Eckart projector that *ignores* frozen atoms
245
+ def _project_active(self, mw_H_act, *, return_P=False):
246
+ if self.geometry.is_analytical_2d or mw_H_act.shape[0] <= 6:
247
+ return (mw_H_act, None) if return_P else mw_H_act
248
+
249
+ from pysisyphus.Geometry import get_trans_rot_projector
250
+
251
+ coords_act = self.geometry.coords3d[self._act_atoms].flatten()
252
+ masses_act = self.geometry.masses[self._act_atoms]
253
+ P = get_trans_rot_projector(coords_act, masses=masses_act, full=False)
254
+
255
+ if isinstance(mw_H_act, torch.Tensor):
256
+ P = torch.as_tensor(P, dtype=mw_H_act.dtype, device=mw_H_act.device)
257
+ proj = (P @ mw_H_act @ P.T)
258
+ proj = 0.5 * (proj + proj.T) # restore symmetry
259
+ else:
260
+ proj = P.dot(mw_H_act).dot(P.T)
261
+ proj = 0.5 * (proj + proj.T)
262
+ return (proj, P) if return_P else proj
263
+
264
+ # Expand an active‑vector (or ‑step) to 3N
265
+ def _full(self, vec_act):
266
+ inds = self._act_dofs
267
+ if isinstance(vec_act, torch.Tensor):
268
+ idx = torch.as_tensor(inds, dtype=torch.long, device=vec_act.device)
269
+ full = torch.zeros(self.coords.size, dtype=vec_act.dtype, device=vec_act.device)
270
+ full.index_copy_(0, idx, vec_act)
271
+ return full
272
+ full = np.zeros(self.coords.size, dtype=vec_act.dtype if hasattr(vec_act, "dtype") else float)
273
+ full[inds] = vec_act
274
+ return full
275
+
276
+ def prepare(self, direction):
277
+ self.direction = direction
278
+ self.converged = False
279
+ self.energy_increased = False
280
+ self.energy_converged = False
281
+ self.past_inflection = not self.force_inflection
282
+
283
+ self.irc_energies = list()
284
+ # Not mass-weighted
285
+ self.irc_coords = list()
286
+ self.irc_gradients = list()
287
+ # Mass-weighted
288
+ self.irc_mw_coords = list()
289
+ self.irc_mw_gradients = list()
290
+
291
+ self.ref_bond_sets = self.ts_bond_sets.copy()
292
+
293
+ # Over the course of the IRC the hessian may get updated.
294
+ # Copying the initial hessian here ensures a clean start in combined
295
+ # forward and backward runs. Otherwise we may accidentally use
296
+ # the updated hessian from the end of the first run for the second
297
+ # run.
298
+
299
+ self.mw_hessian = self._mw_hessian_active(self.init_hessian)
300
+
301
+ trj_fn = self.get_path_for_fn(f"{direction}_irc_trj.xyz")
302
+ self.trj_handle = open(trj_fn, "w")
303
+
304
+ # We don't need an initial displacement when going downhill
305
+ if self.downhill:
306
+ return
307
+
308
+ # Do inital displacement from the TS
309
+ if direction == "forward":
310
+ initial_step = self.init_displ_plus
311
+ elif direction == "backward":
312
+ initial_step = self.init_displ_minus
313
+ else:
314
+ raise Exception("Invalid direction='{direction}'!")
315
+ self.coords = self.ts_coords + initial_step
316
+
317
+ if self.displ in ("energy"):
318
+ actual_energy = self.energy
319
+ actual_lowering = self.ts_energy - actual_energy
320
+ diff = self.displ_energy - actual_lowering
321
+
322
+ def en_str(en):
323
+ return f"{en: .4f} au ({en*AU2KJPERMOL: .2f} kJ mol⁻¹)"
324
+
325
+ print(
326
+ f"Requested energy lowering: {en_str(self.displ_energy)}\n"
327
+ f" Actual energy lowering: {en_str(actual_lowering)}\n"
328
+ f" Δ: {en_str(diff)}"
329
+ )
330
+ if actual_lowering < 0.0:
331
+ print("Displaced geometry is higher in energy compared to TS!")
332
+ print("\n")
333
+ sys.stdout.flush()
334
+ initial_step_length = np.linalg.norm(initial_step)
335
+ self.logger.info(
336
+ f"Did inital step of length {initial_step_length:.4f} " "from the TS."
337
+ )
338
+
339
+ def initial_displacement(self):
340
+ """Returns non-mass-weighted steps in +s and -s direction
341
+ for initial displacement from the TS. Earlier version only
342
+ returned one step, that was later multiplied by either 1 or -1,
343
+ depending on the desired IRC direction (forward/backward).
344
+ The current implementation directly returns two steps for forward
345
+ and backward direction. Whereas for plus and minus steps for
346
+ displ 'length' and displ 'energy'
347
+ step_plus = -step_minus
348
+ is valid. The latter step is formed as
349
+ x(ds) = ds * v0 + ds**2 * v1
350
+ so
351
+ x(ds) != -x(ds)
352
+ as
353
+ ds * v0 + ds**2 * v1 != -ds * v0 - ds**2 * v1 .
354
+
355
+ So, all required step are formed directly and later used as appropriate.
356
+
357
+ See
358
+ https://aip.scitation.org/doi/pdf/10.1063/1.454172
359
+ https://pubs.acs.org/doi/10.1021/j100338a027
360
+ https://aip.scitation.org/doi/pdf/10.1063/1.459634
361
+ """
362
+
363
+ mw_hessian = self._mw_hessian_active(self.init_hessian)
364
+ if self.coords.size > 3:
365
+ proj_hessian, P = self._project_active(mw_hessian, return_P=True)
366
+ # Don't project single atom species and analytical potentials
367
+ else:
368
+ proj_hessian = mw_hessian
369
+ if isinstance(proj_hessian, torch.Tensor):
370
+ P = torch.eye(self.coords.size, device=proj_hessian.device, dtype=proj_hessian.dtype)
371
+ else:
372
+ P = np.eye(self.coords.size)
373
+
374
+ del mw_hessian
375
+
376
+ if isinstance(proj_hessian, torch.Tensor):
377
+ eigvals, eigvecs = torch.linalg.eigh(proj_hessian)
378
+ eigvals = eigvals.to(torch.double).cpu().numpy()
379
+ mw_cart_displs = P.T @ eigvecs if P is not None else eigvecs
380
+ if not isinstance(self.mm_inv2, torch.Tensor):
381
+ self.mm_inv2 = torch.as_tensor(self.mm_inv2, dtype=proj_hessian.dtype, device=proj_hessian.device)
382
+ cart_displs = self.mm_inv2 @ mw_cart_displs
383
+ else:
384
+ eigvals, eigvecs = np.linalg.eigh(proj_hessian)
385
+ mw_cart_displs = P.T.dot(eigvecs)
386
+ cart_displs = self.mm_inv2.dot(mw_cart_displs)
387
+
388
+ nus = eigval_to_wavenumber(eigvals)
389
+ nu_root = nus[self.root]
390
+ assert nu_root <= self.imag_below, (
391
+ f"Wavenumber {nu_root:.2f} cm⁻¹ of imaginary mode {self.root} is above "
392
+ f"the threshold of {self.imag_below:.2f} cm⁻¹."
393
+ )
394
+ neg_inds = eigvals < -1e-8
395
+ assert sum(neg_inds) > 0, "The hessian does not have any negative eigenvalues!"
396
+
397
+ min_eigval = eigvals[self.root]
398
+ min_nu = nus[self.root]
399
+ T_c = T_crossover_from_eigval(min_eigval)
400
+ min_msg = (
401
+ f"Transition vector is mode {self.root} with wavenumber {min_nu:.2f} cm⁻¹.\n"
402
+ f"Crossover temperature T_c: {T_c:.2f} K"
403
+ )
404
+ # Doing it this way hurts ... I'll have to improve my logging game...
405
+ self.log(min_msg)
406
+ print(min_msg)
407
+
408
+ # Mass-weighted
409
+ mw_trans_vec = mw_cart_displs[:, self.root]
410
+ if isinstance(mw_trans_vec, torch.Tensor):
411
+ mw_trans_vec = mw_trans_vec.cpu().numpy()
412
+
413
+ # Not mass-weighted
414
+ trans_vec = cart_displs[:, self.root]
415
+ if isinstance(trans_vec, torch.Tensor):
416
+ trans_vec = trans_vec.cpu().numpy()
417
+
418
+ mw_trans_vec = self._full(mw_trans_vec)
419
+ trans_vec = self._full(trans_vec)
420
+
421
+ self.mw_transition_vector = mw_trans_vec
422
+ self.transition_vector = trans_vec / np.linalg.norm(trans_vec)
423
+
424
+ if self.downhill:
425
+ mw_step_plus = mw_step_minus = np.zeros_like(self.transition_vector)
426
+ msg = "Downhill run. No initial displacement from the TS."
427
+ elif self.displ == "length":
428
+ msg = "Using length-based initial displacement from the TS."
429
+ mw_step_plus = self.displ_length * mw_trans_vec
430
+ mw_step_minus = -mw_step_plus
431
+ elif self.displ == "energy":
432
+ # Calculate the length of the initial step away from the TS to initiate
433
+ # the IRC/MEP. We assume a quadratic potential and calculate the
434
+ # displacement for a given energy lowering.
435
+ # dE = (k*dq**2)/2 (dE = energy lowering, k = eigenvalue corresponding
436
+ # to the transition vector/imaginary mode, dq = step length)
437
+ # dq = sqrt(dE*2/k)
438
+ # See 10.1021/ja00295a002 and 10.1063/1.462674
439
+ # 10.1002/jcc.540080808 proposes 3 kcal/mol as initial energy lowering
440
+ msg = (
441
+ f"Energy-based (ΔE={self.displ_energy} au) initial displacement from "
442
+ "the TS using 2rd derivatives."
443
+ )
444
+ step_length = np.sqrt(self.displ_energy * 2 / np.abs(min_eigval))
445
+ # Guard against near-zero eigenvalue producing an excessively
446
+ # large initial displacement.
447
+ max_displ = 0.5 # au in mass-weighted coordinates
448
+ if step_length > max_displ:
449
+ print(
450
+ f"Warning: energy-based initial displacement {step_length:.4f} au "
451
+ f"exceeds {max_displ} au (|eigval|={np.abs(min_eigval):.6e}). "
452
+ f"Clamping to {max_displ} au."
453
+ )
454
+ self.log(
455
+ f"Clamped initial displacement from {step_length:.4f} to "
456
+ f"{max_displ} au."
457
+ )
458
+ step_length = max_displ
459
+ # This calculation is derived from the mass-weighted hessian, so we
460
+ # have to multiply this step length with the mass-weighted
461
+ # mode and un-weigh it.
462
+ mw_step_plus = step_length * mw_trans_vec
463
+ mw_step_minus = -mw_step_plus
464
+ else:
465
+ raise Exception(f"self.displ={self.displ} is invalid!")
466
+
467
+ step_plus = mw_step_plus / self.m_sqrt
468
+ step_minus = mw_step_minus / self.m_sqrt
469
+ self.log(msg)
470
+ print(msg)
471
+ print(
472
+ "Initial step lengths (not mass-weighted):\n"
473
+ f"\t Forward: {np.linalg.norm(step_plus):.4f} au\n"
474
+ f"\tBackward: {np.linalg.norm(step_minus):.4f} au"
475
+ )
476
+ return step_plus, step_minus
477
+
478
+ def get_conv_fact(self, mw_grad, min_fact=2.0):
479
+ # Numerical integration of differential equations requires a step length and/or
480
+ # we have to terminate the integration at some point, e.g. when the desired
481
+ # step length is reached. IRCs are integrated in mass-weighted coordinates,
482
+ # but self.step_length is given in unweighted coordinates. Unweighting a step
483
+ # in mass-weighted coordinates will reduce its norm as we divide by sqrt(m).
484
+ #
485
+ # If we want to do an Euler-integration we have to decide on a step size
486
+ # when a desired integration length is to be reached in a given number of steps.
487
+ # [3] proposes using Δs/250 with a maximum of 500 steps, so something like
488
+ # Δs/(max_steps / 2). It seems we can't use this because (at
489
+ # least for the systems I tested) this will lead to a step length that is too
490
+ # small, so the predictor Euler-integration will fail to converge in the
491
+ # prescribed number of cycles. It fails because simply dividing the desired
492
+ # step length in unweighted coordinates does not take into account the mass
493
+ # dependence. Such a step size is appropriate for integrations in unweighted
494
+ # coordinates, but not when using mass-weighted coordinates.
495
+ #
496
+ # We determine a conversion factor from comparing the magnitudes (norms) of
497
+ # the mass-weighted and un-mass-weighted gradients. This takes into account
498
+ # which atoms are actually moving, so it should be a good guess.
499
+ norm_mw_grad = np.linalg.norm(mw_grad)
500
+ if mw_grad.shape[0] == self._m_sqrt.shape[0]:
501
+ norm_grad = np.linalg.norm(self.unweight_vec(mw_grad))
502
+ else:
503
+ m_sqrt_vec = self._m_sqrt[self._act_dofs]
504
+ norm_grad = np.linalg.norm(mw_grad * m_sqrt_vec)
505
+
506
+ if not np.isfinite(norm_mw_grad) or norm_mw_grad == 0.0:
507
+ conv_fact = min_fact
508
+ self.log("mw_grad norm is zero/NaN; using minimum conversion factor.")
509
+ else:
510
+ conv_fact = norm_grad / norm_mw_grad
511
+ # Cap conversion factor when using an active subspace to avoid huge steps.
512
+ if mw_grad.shape[0] != self._m_sqrt.shape[0]:
513
+ max_fact = 50.0
514
+ if conv_fact > max_fact:
515
+ self.log(f"Clamping conversion factor {conv_fact:.4f} -> {max_fact:.1f}.")
516
+ conv_fact = max_fact
517
+ conv_fact = max(min_fact, conv_fact)
518
+ self.log(f"Un-weighted / mass-weighted conversion factor {conv_fact:.4f}")
519
+ return conv_fact
520
+
521
+ def report_bonds(self, prefix, bonds):
522
+ if len(bonds) == 0:
523
+ return
524
+
525
+ plural = "s" if len(bonds) > 1 else ""
526
+ bond_strs = list()
527
+ for from_, to_ in bonds:
528
+ from_atom = self.atoms[from_]
529
+ to_atom = self.atoms[to_]
530
+ bond_strs.append(f"[{from_atom}{from_}-{to_atom}{to_}]")
531
+ bonds_str = ", ".join(bond_strs)
532
+ self.table.print(f"Bond{plural} {prefix}: {bonds_str}")
533
+
534
+ def irc(self, direction):
535
+ self.log(highlight_text(f"IRC {direction}", level=1))
536
+ self.cur_direction = direction
537
+ self.prepare(direction)
538
+ # Calculate gradient
539
+ self.gradient
540
+ self.irc_energies.append(self.energy)
541
+ # Non mass-weighted
542
+ self.irc_coords.append(self.coords)
543
+ self.irc_gradients.append(self.gradient)
544
+ # Mass-weighted
545
+ self.irc_mw_coords.append(self.mw_coords)
546
+ self.irc_mw_gradients.append(self.mw_gradient)
547
+
548
+ self.table.print_header()
549
+ for self.cur_cycle in range(self.max_cycles):
550
+ self.log(highlight_text(f"IRC step {self.cur_cycle:03d}") + "\n")
551
+
552
+ # Dump current coordinates to trj
553
+ comment = f"{direction} IRC, step {self.cur_cycle}"
554
+ coords_str = make_xyz_str(
555
+ self.atoms, BOHR2ANG * self.coords.reshape((-1, 3)), comment
556
+ )
557
+ self.trj_handle.write(coords_str + "\n")
558
+ self.trj_handle.flush()
559
+
560
+ self.log(f"Current energy: {self.energy:.6f} au")
561
+ #
562
+ # Take IRC step.
563
+ #
564
+ self.step()
565
+
566
+ # Calculate gradient and energy on the new geometry
567
+ # Non mass-weighted
568
+ self.log("Calculating energy and gradient at new geometry.")
569
+ self.irc_coords.append(self.coords)
570
+ self.irc_gradients.append(self.gradient)
571
+ self.irc_energies.append(self.energy)
572
+ # Mass-weighted
573
+ self.irc_mw_coords.append(self.mw_coords)
574
+ self.irc_mw_gradients.append(self.mw_gradient)
575
+
576
+ rms_grad = rms(self.gradient)
577
+
578
+ # Only update once
579
+ if not self.past_inflection:
580
+ self.past_inflection = rms_grad >= self.rms_grad_thresh
581
+ _ = "" if self.past_inflection else "not yet"
582
+ self.log(f"(rms(grad) > threshold) {_} fullfilled!")
583
+
584
+ irc_length = np.linalg.norm(self.irc_mw_coords[0] - self.irc_mw_coords[-1])
585
+ dE = self.irc_energies[-1] - self.irc_energies[-2]
586
+ max_grad = np.abs(self.gradient).max()
587
+
588
+ row_args = (self.cur_cycle, irc_length, dE, max_grad, rms_grad)
589
+ self.table.print_row(row_args)
590
+ try:
591
+ # The derived IRC classes may want to do some printing
592
+ add_info = self.get_additional_print()
593
+ self.table.print(add_info)
594
+ except AttributeError:
595
+ pass
596
+
597
+ if self.check_bonds:
598
+ cur_bond_sets = self.geometry.bond_sets
599
+ formed = cur_bond_sets - self.ref_bond_sets
600
+ broken = self.ref_bond_sets - cur_bond_sets
601
+ self.report_bonds("formed", formed)
602
+ self.report_bonds("broken", broken)
603
+ # Update bond sets to avoid repeated reporting of bond topology changes
604
+ self.ref_bond_sets -= broken
605
+ self.ref_bond_sets |= formed # union
606
+
607
+ last_energy = self.irc_energies[-2]
608
+ this_energy = self.irc_energies[-1]
609
+
610
+ break_msg = ""
611
+ self.energy_increased = this_energy > last_energy
612
+ self.energy_converged = abs(last_energy - this_energy) <= self.energy_thresh
613
+ if self.converged:
614
+ break_msg = "Integrator indicated convergence!"
615
+ elif self.past_inflection and (rms_grad <= self.rms_grad_thresh):
616
+ break_msg = "rms(grad) converged!"
617
+ self.converged = True
618
+ elif (
619
+ self.hard_rms_grad_thresh
620
+ and (not self.past_inflection)
621
+ and (rms_grad <= self.hard_rms_grad_thresh)
622
+ ):
623
+ break_msg = "rms(grad) below hard threshold."
624
+ # TODO: Allow some threshold?
625
+ elif self.energy_increased:
626
+ break_msg = "Energy increased!"
627
+ elif self.energy_converged:
628
+ break_msg = "Energy converged!"
629
+ self.converged = True
630
+
631
+ # dumped = (self.cur_cycle % self.dump_every) == 0
632
+ # if dumped:
633
+ # dump_fn = self.get_path_for_fn(f"{direction}_{self.dump_fn}")
634
+ # self.dump_data(dump_fn)
635
+
636
+ if break_msg:
637
+ self.table.print(break_msg)
638
+ break
639
+
640
+ if check_for_end_sign():
641
+ break
642
+ self.log("")
643
+ sys.stdout.flush()
644
+ else:
645
+ print("IRC steps exceeded. Stopping.")
646
+ print()
647
+
648
+ if direction == "forward":
649
+ self.irc_energies.reverse()
650
+ self.irc_coords.reverse()
651
+ self.irc_gradients.reverse()
652
+ self.irc_mw_coords.reverse()
653
+ self.irc_mw_gradients.reverse()
654
+
655
+ # if not dumped:
656
+ # self.dump_data(dump_fn)
657
+
658
+ self.cur_direction = None
659
+ self.trj_handle.close()
660
+
661
+ def set_data(self, prefix):
662
+ energies_name = f"{prefix}_energies"
663
+ coords_name = f"{prefix}_coords"
664
+ grad_name = f"{prefix}_gradients"
665
+ mw_coords_name = f"{prefix}_mw_coords"
666
+ mw_grad_name = f"{prefix}_mw_gradients"
667
+
668
+ setattr(self, coords_name, self.irc_coords)
669
+ setattr(self, grad_name, self.irc_gradients)
670
+ setattr(self, mw_coords_name, self.irc_mw_coords)
671
+ setattr(self, mw_grad_name, self.irc_mw_gradients)
672
+ setattr(self, energies_name, self.irc_energies)
673
+
674
+ self.all_energies.extend(getattr(self, energies_name))
675
+ self.all_coords.extend(getattr(self, coords_name))
676
+ self.all_gradients.extend(getattr(self, grad_name))
677
+ self.all_mw_coords.extend(getattr(self, mw_coords_name))
678
+ self.all_mw_gradients.extend(getattr(self, mw_grad_name))
679
+
680
+ # Free per-direction lists to reduce memory usage
681
+ del self.irc_coords
682
+ del self.irc_gradients
683
+ del self.irc_mw_coords
684
+ del self.irc_mw_gradients
685
+ del self.irc_energies
686
+
687
+ setattr(self, f"{prefix}_is_converged", self.converged)
688
+ setattr(self, f"{prefix}_energy_increased", self.energy_increased)
689
+ setattr(self, f"{prefix}_energy_converged", self.energy_converged)
690
+ setattr(self, f"{prefix}_cycle", self.cur_cycle)
691
+ self.dump_ends(".", prefix, getattr(self, mw_coords_name))
692
+
693
+ def report_conv_thresholds(self):
694
+ threshs = [
695
+ f"\t rms(|gradient|) <= {self.rms_grad_thresh:.6f} E_h a_0⁻¹",
696
+ f"\t Δenergy <= {self.energy_thresh:.6f} E_h",
697
+ ]
698
+ # Drop hard rms grad item
699
+ if self.hard_rms_grad_thresh is not None:
700
+ threshs.insert(
701
+ 1,
702
+ f"\thard rms(|gradient|) <= {self.hard_rms_grad_thresh:.6f} E_h a_0⁻¹",
703
+ )
704
+ print(
705
+ "Convergence thresholds (non mass-weighted gradient):\n"
706
+ + "\n".join(threshs)
707
+ + "\n"
708
+ )
709
+
710
+ def run(self):
711
+ self.report_conv_thresholds()
712
+ # Calculate data at TS and create backup
713
+ self.ts_coords = self.coords.copy()
714
+ self.ts_mw_coords = self.mw_coords.copy()
715
+ print("Calculating energy and gradient at TS.")
716
+ self.ts_gradient = self.gradient.copy()
717
+ self.ts_mw_gradient = self.mw_gradient.copy()
718
+ self.ts_energy = self.energy
719
+
720
+ ts_grad_norm = np.linalg.norm(self.ts_gradient)
721
+ ts_grad_max = np.abs(self.ts_gradient).max()
722
+ ts_grad_rms = rms(self.ts_gradient)
723
+
724
+ self.log(
725
+ "Transition state (TS):\n"
726
+ f"\t energy={self.ts_energy:.6f} au\n"
727
+ f"\tnorm(grad)={ts_grad_norm:.6f}\n"
728
+ f"\t max(grad)={ts_grad_max:.6f}\n"
729
+ f"\t rms(grad)={ts_grad_rms:.6f}"
730
+ )
731
+
732
+ self.init_hessian = self.geometry.hessian
733
+ has_partial = getattr(self.geometry, "within_partial_hessian", None) is not None
734
+ act_n_dof = (
735
+ int(self.geometry.within_partial_hessian.get("active_n_dof", 0))
736
+ if has_partial else 0
737
+ )
738
+ if has_partial:
739
+ self._act_atoms = self.geometry.hess_active_atom_indices
740
+ self._act_dofs = self.geometry.hess_active_dof_indices
741
+ self.mm_inv2 = self.geometry.mm_sqrt_inv[np.ix_(self._act_dofs, self._act_dofs)]
742
+ self.geometry.clear()
743
+ # convert to active doFs (skip if already partial)
744
+ if has_partial:
745
+ if self.init_hessian.shape != (act_n_dof, act_n_dof):
746
+ self.init_hessian = self.init_hessian[self._act_dofs][:, self._act_dofs]
747
+ else:
748
+ self.init_hessian = self.init_hessian[self._act_dofs][:, self._act_dofs]
749
+
750
+ # For forward/backward runs from a TS we need an intial displacement,
751
+ # calculated from the transition vector (imaginary mode) of the TS
752
+ # hessian. If we need/want a Hessian for a downhill run from a
753
+ # non-stationary point (with non-vanishing gradient) depends on the
754
+ # actual IRC integrator (e.g. EulerPC and LQA need a Hessian).
755
+ if not self.downhill:
756
+ self.init_displ_plus, self.init_displ_minus = self.initial_displacement()
757
+
758
+ print(
759
+ "IRC length in mw. coords, max(|grad|) and rms(grad) in "
760
+ "unweighted coordinates."
761
+ )
762
+
763
+ if self.forward:
764
+ print("\n" + highlight_text("IRC - Forward") + "\n")
765
+ self.irc("forward")
766
+ self.set_data("forward")
767
+ if isinstance(self.mw_hessian, torch.Tensor):
768
+ self.forward_mw_hessian = self.mw_hessian.detach().clone()
769
+ else:
770
+ self.forward_mw_hessian = self.mw_hessian.copy()
771
+
772
+ # Add TS/starting data
773
+ self.all_energies.append(self.ts_energy)
774
+ self.all_coords.append(self.ts_coords)
775
+ self.all_gradients.append(self.ts_gradient)
776
+ self.all_mw_coords.append(self.ts_mw_coords)
777
+ self.all_mw_gradients.append(self.ts_mw_gradient)
778
+ self.ts_index = len(self.all_energies) - 1
779
+
780
+ if self.backward:
781
+ print("\n" + highlight_text("IRC - Backward") + "\n")
782
+ self.irc("backward")
783
+ self.set_data("backward")
784
+
785
+ if self.downhill:
786
+ print("\n" + highlight_text("IRC - Downhill") + "\n")
787
+ self.irc("downhill")
788
+ self.set_data("downhill")
789
+
790
+ self.all_mw_coords = np.array(self.all_mw_coords)
791
+ self.all_energies = np.array(self.all_energies)
792
+ self.postprocess()
793
+ if not self.downhill:
794
+ self.dump_ends(".", "finished", trj=True)
795
+
796
+ # # Dump the whole IRC to HDF5
797
+ # dump_fn = self.get_path_for_fn("finished_" + self.dump_fn)
798
+ # self.dump_data(dump_fn, full=True)
799
+
800
+ # Convert to arrays and free original Python lists
801
+ for name in "all_energies all_coords all_gradients all_mw_coords all_mw_gradients".split():
802
+ setattr(self, name, np.array(getattr(self, name)))
803
+
804
+ # Right now self.all_mw_coords is still in mass-weighted coordinates.
805
+ # Convert them to un-mass-weighted coordinates.
806
+ self.all_mw_coords_umw = self.all_mw_coords / self.m_sqrt
807
+
808
+ def postprocess(self):
809
+ pass
810
+
811
+ def dump_ends(self, path, prefix, coords=None, trj=False):
812
+ if coords is None:
813
+ coords = self.all_mw_coords
814
+ coords = coords.copy()
815
+ coords /= self.m_sqrt
816
+ coords = coords.reshape(-1, len(self.atoms), 3) * BOHR2ANG
817
+ if trj:
818
+ trj_string = make_trj_str(self.atoms, coords, comments=self.all_energies)
819
+ trj_fn = self.get_path_for_fn(f"{prefix}_irc_trj.xyz")
820
+ with open(trj_fn, "w") as handle:
821
+ handle.write(trj_string)
822
+
823
+ first_coords = coords[0]
824
+ first_fn = self.get_path_for_fn(f"{prefix}_first.xyz")
825
+ with open(first_fn, "w") as handle:
826
+ handle.write(make_xyz_str(self.atoms, first_coords))
827
+
828
+ last_coords = coords[-1]
829
+ first_fn = self.get_path_for_fn(f"{prefix}_last.xyz")
830
+ with open(first_fn, "w") as handle:
831
+ handle.write(make_xyz_str(self.atoms, last_coords))
832
+
833
+ def get_irc_data(self):
834
+ data_dict = {
835
+ "energies": np.array(self.irc_energies, dtype=float),
836
+ "coords": np.array(self.irc_coords, dtype=float),
837
+ "gradients": np.array(self.irc_gradients, dtype=float),
838
+ "mw_coords": np.array(self.irc_mw_coords, dtype=float),
839
+ "mw_gradients": np.array(self.irc_mw_gradients, dtype=float),
840
+ }
841
+ return data_dict
842
+
843
+ def get_full_irc_data(self):
844
+ data_dict = {
845
+ "energies": np.array(self.all_energies, dtype=float),
846
+ "coords": np.array(self.all_coords, dtype=float),
847
+ "gradients": np.array(self.all_gradients, dtype=float),
848
+ "mw_coords": np.array(self.all_mw_coords, dtype=float),
849
+ "mw_gradients": np.array(self.all_mw_gradients, dtype=float),
850
+ "ts_index": np.array(self.ts_index, dtype=int),
851
+ }
852
+ return data_dict
853
+
854
+ def dump_data(self, dump_fn=None, full=False):
855
+ get_data = self.get_full_irc_data if full else self.get_irc_data
856
+ data_dict = get_data()
857
+
858
+ data_dict.update(
859
+ {
860
+ "atoms": np.array(self.atoms, dtype="S"),
861
+ "rms_grad_thresh": np.array(self.rms_grad_thresh),
862
+ }
863
+ )
864
+
865
+ if dump_fn is None:
866
+ dump_fn = self.get_path_for_fn(self.dump_fn)
867
+
868
+ with h5py.File(dump_fn, "w") as handle:
869
+ for key, val in data_dict.items():
870
+ handle.create_dataset(name=key, dtype=val.dtype, data=val)
871
+
872
+ def get_endpoint_and_ts_geoms(self):
873
+ assert not self.downhill, "Downhill is not yet handled"
874
+ first = self.all_coords[0]
875
+ last = self.all_coords[-1]
876
+ ts = self.ts_coords.copy()
877
+ geoms = [Geometry(self.atoms, coords) for coords in (first, ts, last)]
878
+ return geoms