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
@@ -0,0 +1,725 @@
1
+ from copy import copy
2
+ import logging
3
+ import sys
4
+
5
+ from distributed import Client
6
+ import numpy as np
7
+ from scipy.interpolate import interp1d, splprep, splev
8
+
9
+ from pysisyphus.helpers import align_coords, get_coords_diffs
10
+ from pysisyphus.helpers_pure import hash_arr
11
+ from pysisyphus.modefollow import geom_lanczos
12
+
13
+
14
+ # [1] http://dx.doi.org/10.1063/1.1323224
15
+
16
+
17
+ class ChainOfStates:
18
+ logger = logging.getLogger("cos")
19
+ valid_coord_types = ("cart", "cartesian", "dlc")
20
+
21
+ def __init__(
22
+ self,
23
+ images,
24
+ fix_first=True,
25
+ fix_last=True,
26
+ align_fixed=True,
27
+ climb=False,
28
+ climb_rms=5e-3,
29
+ climb_lanczos=False,
30
+ climb_lanczos_rms=5e-3,
31
+ climb_fixed=True,
32
+ energy_min_mix=False,
33
+ scheduler=None,
34
+ progress=False,
35
+ ):
36
+ assert len(images) >= 2, "Need at least 2 images!"
37
+ self.images = list(images)
38
+ self.fix_first = fix_first
39
+ self.fix_last = fix_last
40
+ self.align_fixed = align_fixed
41
+ self.climb = climb
42
+ self.climb_rms = climb_rms
43
+ self.climb_lanczos = climb_lanczos
44
+ self.climb_fixed = climb_fixed
45
+ self.energy_min_mix = energy_min_mix
46
+ # Must not be lower than climb_rms
47
+ self.climb_lanczos_rms = min(self.climb_rms, climb_lanczos_rms)
48
+ self.scheduler = scheduler
49
+ self.progress = progress
50
+
51
+ self._coords = None
52
+ self._forces = None
53
+ self._energy = None
54
+ self.counter = 0
55
+ self.coords_length = self.images[0].coords.size
56
+ self.cart_coords_length = self.images[0].cart_coords.size
57
+ self.zero_vec = np.zeros(self.coords_length)
58
+
59
+ self.coords_list = list()
60
+ self.forces_list = list()
61
+ self.all_energies = list()
62
+ self.all_true_forces = list()
63
+ self.lanczos_tangents = dict()
64
+ self.prev_lanczos_hash = None
65
+
66
+ # Start climbing immediateley with climb_rms == -1
67
+ self.started_climbing = self.climb_rms == -1
68
+ if self.started_climbing:
69
+ self.log("Will start climbing immediately.")
70
+ self.started_climbing_lanczos = False
71
+ self.fixed_climb_indices = None
72
+ # Use original forces for these images
73
+ self.org_forces_indices = list()
74
+
75
+ img0 = self.images[0]
76
+ self.image_atoms = copy(img0.atoms)
77
+ self.coord_type = img0.coord_type
78
+ assert (
79
+ self.coord_type in self.valid_coord_types
80
+ ), f"Invalid coord_type! Supported types are: {self.valid_coord_types}"
81
+ assert all(
82
+ [img.coord_type == self.coord_type for img in self.images]
83
+ ), "coord_type of images differ!"
84
+ try:
85
+ self.typed_prims = img0.internal.typed_prims
86
+ except AttributeError:
87
+ self.typed_prims = None
88
+
89
+ @property
90
+ def calculator(self):
91
+ try:
92
+ calc = self.images[0].calculator
93
+ except IndexError:
94
+ calc = None
95
+ return calc
96
+
97
+ def log(self, message):
98
+ self.logger.debug(f"Counter {self.counter+1:03d}, {message}")
99
+
100
+ def get_fixed_indices(self):
101
+ fixed = list()
102
+ if self.fix_first:
103
+ fixed.append(0)
104
+ if self.fix_last:
105
+ fixed.append(len(self.images) - 1)
106
+ return fixed
107
+
108
+ @property
109
+ def moving_indices(self):
110
+ """Returns the indices of the images that aren't fixed and can be
111
+ optimized."""
112
+ fixed = self.get_fixed_indices()
113
+ return [i for i in range(len(self.images)) if i not in fixed]
114
+
115
+ @property
116
+ def last_index(self):
117
+ return len(self.images) - 1
118
+
119
+ @property
120
+ def moving_images(self):
121
+ return [self.images[i] for i in self.moving_indices]
122
+
123
+ @property
124
+ def max_image_num(self):
125
+ return len(self.images)
126
+
127
+ @property
128
+ def image_inds(self):
129
+ return list(range(self.max_image_num))
130
+
131
+ def zero_fixed_vector(self, vector):
132
+ fixed = self.get_fixed_indices()
133
+ for i in fixed:
134
+ vector[i] = self.zero_vec
135
+ return vector
136
+
137
+ def clear(self):
138
+ self._energy = None
139
+ self._forces = None
140
+ self._hessian = None
141
+ try:
142
+ self._tangents = None
143
+ except AttributeError:
144
+ # TODO: move this to another logging level?!
145
+ self.log("There are no tangents to reset.")
146
+
147
+ # @property
148
+ # def freeze_atoms(self):
149
+ # image_freeze_atoms = [image.freeze_atoms for image in self.images]
150
+ # lens = [len(fa) for fa in image_freeze_atoms]
151
+ # len0 = lens[0]
152
+ # assert all([len_ == len0 for len_ in lens])
153
+ # return image_freeze_atoms[0]
154
+
155
+ @property
156
+ def atoms(self):
157
+ atoms_ = self.images[0].atoms
158
+ return len(self.images) * atoms_
159
+
160
+ def set_vector(self, name, vector, clear=False):
161
+ vec_per_image = vector.reshape(-1, self.coords_length)
162
+ assert len(self.images) == len(vec_per_image)
163
+ for i in self.moving_indices:
164
+ setattr(self.images[i], name, vec_per_image[i])
165
+ if clear:
166
+ self.clear()
167
+
168
+ @property
169
+ def coords(self):
170
+ """Return a flat 1d array containing the coordinates of all images."""
171
+ all_coords = [image.coords for image in self.images]
172
+ # Note: why does this getter set self._coords? ... I wrote this line 6 years ago.
173
+ self._coords = np.concatenate(all_coords)
174
+ return self._coords
175
+
176
+ @coords.setter
177
+ def coords(self, coords):
178
+ """Distribute the flat 1d coords array over all images."""
179
+ self.set_vector("coords", coords, clear=True)
180
+
181
+ @property
182
+ def cart_coords(self):
183
+ """Return a flat 1d array containing the cartesian coordinates of all
184
+ images."""
185
+ return np.concatenate([image.cart_coords for image in self.images])
186
+
187
+ @property
188
+ def coords3d(self):
189
+ assert self.images[0].coord_type == "cart"
190
+ return self.coords.reshape(-1, 3)
191
+
192
+ @property
193
+ def image_coords(self):
194
+ return np.array([image.coords for image in self.images])
195
+
196
+ def set_coords_at(self, i, coords):
197
+ """Called from helpers.procrustes with cartesian coordinates.
198
+ Then tries to set cartesian coordinate as self.images[i].coords
199
+ which will raise an error when coord_type != "cart".
200
+ """
201
+ assert self.images[i].coord_type in ("cart", "cartesian"), (
202
+ "ChainOfStates.set_coords_at() has to be reworked to support "
203
+ "internal coordiantes. Try to set 'align: False' in the 'opt' "
204
+ "section of the .yaml input file."
205
+ )
206
+ if i in self.moving_indices:
207
+ self.images[i].coords = coords
208
+ # When dealing with a fixed image don't set coords through the
209
+ # property, which would result in resetting the image's calculated
210
+ # data. Instead, assign coords directly. This only occurs when
211
+ # aligning the fixed images.
212
+ elif self.align_fixed:
213
+ self.images[i]._coords = coords
214
+
215
+ @property
216
+ def energy(self):
217
+ self._energy = np.array([image.energy for image in self.images])
218
+ return self._energy
219
+
220
+ @energy.setter
221
+ def energy(self, energies):
222
+ """This is needed for some optimizers like CG and BFGS."""
223
+ assert len(self.images) == len(energies)
224
+ for i in self.moving_indices:
225
+ self.images[i].energy = energies[i]
226
+
227
+ self._energy = energies
228
+
229
+ def par_image_calc(self, image):
230
+ image.calc_energy_and_forces()
231
+ return image
232
+
233
+ def set_images(self, indices, images):
234
+ for ind, image in zip(indices, images):
235
+ self.images[ind] = image
236
+
237
+ def concurrent_force_calcs(self, images_to_calculate, image_indices):
238
+ client = self.get_dask_client()
239
+ self.log(client)
240
+
241
+ # save original pals to restore them later
242
+ orig_pal = images_to_calculate[0].calculator.pal
243
+
244
+ # divide pal of each image by the number of workers available or available images
245
+ # number of workers available
246
+ n_workers = len(client.scheduler_info()["workers"])
247
+ # number of images to calculate
248
+ n_images = len(images_to_calculate)
249
+ # divide pal by the number of workers (or 1 if more workers)
250
+ new_pal = max(1, orig_pal // n_workers)
251
+
252
+ # split images to calculate into batches of n_workers and set pal of each image
253
+ n_batches = n_images // n_workers
254
+ for i in range(0, n_batches):
255
+ for j in range(i * n_workers, (i + 1) * n_workers):
256
+ images_to_calculate[j].calculator.pal = new_pal
257
+
258
+ # distribute the pals among the remaining images
259
+ n_last_batch = n_images % n_workers
260
+ if n_last_batch > 0:
261
+ # divide pal by the remainder
262
+ new_pal = max(1, orig_pal // n_last_batch)
263
+ for i in range(n_batches * n_workers, n_images):
264
+ images_to_calculate[i].calculator.pal = new_pal
265
+
266
+ # map images to workers
267
+ image_futures = client.map(self.par_image_calc, images_to_calculate)
268
+ # set images to the results of the calculations
269
+ self.set_images(image_indices, client.gather(image_futures))
270
+
271
+ # Restore original pals
272
+ for i in range(0, n_images):
273
+ self.images[i].calculator.pal = orig_pal
274
+
275
+ def calculate_forces(self):
276
+ # Determine the number of images for which we have to do calculations.
277
+ # There may also be calculations for fixed images, as they need an
278
+ # energy value. But every fixed image only needs a calculation once.
279
+ images_to_calculate = self.moving_images
280
+ image_indices = self.moving_indices
281
+ if self.fix_first and (self.images[0]._energy is None):
282
+ images_to_calculate = [self.images[0]] + images_to_calculate
283
+ image_indices = [0] + list(image_indices)
284
+ if self.fix_last and (self.images[-1]._energy is None):
285
+ images_to_calculate = images_to_calculate + [self.images[-1]]
286
+ image_indices = list(image_indices) + [-1]
287
+ assert len(images_to_calculate) <= len(self.images)
288
+
289
+ # Parallel calculation with dask
290
+ if self.scheduler:
291
+ self.concurrent_force_calcs(images_to_calculate, image_indices)
292
+ # Serial calculation
293
+ else:
294
+ for image in images_to_calculate:
295
+ image.calc_energy_and_forces()
296
+ # Poor mans progress bar ;)
297
+ if self.progress:
298
+ print(".", end="")
299
+ sys.stdout.flush()
300
+ if self.progress:
301
+ print("\r", end="")
302
+ self.set_zero_forces_for_fixed_images()
303
+ self.counter += 1
304
+
305
+ if self.energy_min_mix:
306
+ # Will be None for calculators that already mix
307
+ all_energies = np.array([image.all_energies for image in self.images])
308
+ energy_diffs = np.diff(all_energies, axis=1).flatten()
309
+ calc_inds = all_energies.argmin(axis=1)
310
+ mix_at = []
311
+ for i, calc_ind in enumerate(calc_inds[:-1]):
312
+ next_ind = calc_inds[i + 1]
313
+ if (
314
+ (calc_ind != next_ind)
315
+ and (i not in self.org_forces_indices)
316
+ and (i + 1 not in self.org_forces_indices)
317
+ ):
318
+ min_diff_offset = energy_diffs[[i, i + 1]].argmin()
319
+ mix_at.append(i + min_diff_offset)
320
+
321
+ for ind in mix_at:
322
+ self.images[ind].calculator.mix = True
323
+ # Recalculate correct energy and forces
324
+ print(
325
+ f"Switch after calc_ind={calc_ind} at index {ind}. Recalculating."
326
+ )
327
+ self.images[ind].calc_energy_and_forces()
328
+ self.org_forces_indices.append(ind)
329
+ calc_ind = calc_inds[ind]
330
+
331
+ energies = [image.energy for image in self.images]
332
+ forces = np.array([image.forces for image in self.images])
333
+ self.all_energies.append(energies)
334
+ self.all_true_forces.append(forces)
335
+
336
+ return {
337
+ "energies": energies,
338
+ "forces": forces,
339
+ }
340
+
341
+ @property
342
+ def forces(self):
343
+ self.set_zero_forces_for_fixed_images()
344
+ forces = [image.forces for image in self.images]
345
+ self._forces = np.concatenate(forces)
346
+ self.counter += 1
347
+ return self._forces
348
+
349
+ @forces.setter
350
+ def forces(self, forces):
351
+ self.set_vector("forces", forces)
352
+
353
+ @property
354
+ def perpendicular_forces(self):
355
+ indices = range(len(self.images))
356
+ perp_forces = [self.get_perpendicular_forces(i) for i in indices]
357
+ return np.array(perp_forces).flatten()
358
+
359
+ def get_perpendicular_forces(self, i):
360
+ """[1] Eq. 12"""
361
+ # Our goal in optimizing a ChainOfStates is minimizing the
362
+ # perpendicular force. Always return zero perpendicular
363
+ # forces for fixed images, so that they don't interfere
364
+ # with the convergence check.
365
+ if i not in self.moving_indices:
366
+ return self.zero_vec
367
+
368
+ forces = self.images[i].forces
369
+ tangent = self.get_tangent(i)
370
+ perp_forces = forces - forces.dot(tangent) * tangent
371
+ return perp_forces
372
+
373
+ @property
374
+ def gradient(self):
375
+ return -self.forces
376
+
377
+ @gradient.setter
378
+ def gradient(self, gradient):
379
+ self.forces = -gradient
380
+
381
+ @property
382
+ def masses_rep(self):
383
+ return np.array([image.masses_rep for image in self.images]).flatten()
384
+
385
+ @property
386
+ def results(self):
387
+ tmp_results = list()
388
+ for image in self.images:
389
+ res = image.results
390
+ res["coords"] = image.coords
391
+ res["cart_coords"] = image.cart_coords
392
+ tmp_results.append(res)
393
+ return tmp_results
394
+
395
+ def set_zero_forces_for_fixed_images(self):
396
+ """This is always done in cartesian coordinates, independent
397
+ of the actual coord_type of the images as setting forces only
398
+ work with cartesian forces."""
399
+ zero_forces = np.zeros_like(self.images[0].cart_coords)
400
+ if self.fix_first:
401
+ self.images[0].cart_forces = zero_forces
402
+ self.log("Zeroed forces on fixed first image.")
403
+ if self.fix_last:
404
+ self.images[-1].cart_forces = zero_forces
405
+ self.log("Zeroed forces on fixed last image.")
406
+
407
+ def get_tangent(
408
+ self, i, kind="upwinding", lanczos_guess=None, disable_lanczos=False
409
+ ):
410
+ """[1] Equations (8) - (11)"""
411
+
412
+ # Converge to lowest curvature mode at the climbing image.
413
+ # In the current implementation the given kind may be overwritten when
414
+ # Lanczos iterations are enabled and there are climbing images. By
415
+ # setting 'disable_lanczos=True' the provided kind is never overwritten.
416
+ if (
417
+ not disable_lanczos
418
+ and self.started_climbing_lanczos
419
+ # and (i in self.get_climbing_indices())
420
+ and (i == self.get_hei_index())
421
+ ):
422
+ kind = "lanczos"
423
+
424
+ tangent_kinds = ("upwinding", "simple", "bisect", "lanczos")
425
+ assert kind in tangent_kinds, "Invalid kind! Valid kinds are: {tangent_kinds}"
426
+ prev_index = max(i - 1, 0)
427
+ next_index = min(i + 1, len(self.images) - 1)
428
+
429
+ prev_image = self.images[prev_index]
430
+ ith_image = self.images[i]
431
+ next_image = self.images[next_index]
432
+
433
+ # If (i == 0) or (i == len(self.images)-1) then one
434
+ # of this tangents is zero.
435
+ tangent_plus = next_image - ith_image
436
+ tangent_minus = ith_image - prev_image
437
+
438
+ # Handle first and last image
439
+ if i == 0:
440
+ return tangent_plus / np.linalg.norm(tangent_plus)
441
+ elif i == (len(self.images) - 1):
442
+ return tangent_minus / np.linalg.norm(tangent_minus)
443
+
444
+ # [1], Eq. (1)
445
+ if kind == "simple":
446
+ tangent = next_image - prev_image
447
+ # [1], Eq. (2)
448
+ elif kind == "bisect":
449
+ first_term = tangent_minus / np.linalg.norm(tangent_minus)
450
+ sec_term = tangent_plus / np.linalg.norm(tangent_plus)
451
+ tangent = first_term + sec_term
452
+ # Upwinding tangent from [1] Eq. (8) and so on
453
+ elif kind == "upwinding":
454
+ prev_energy = prev_image.energy
455
+ ith_energy = ith_image.energy
456
+ next_energy = next_image.energy
457
+
458
+ next_energy_diff = abs(next_energy - ith_energy)
459
+ prev_energy_diff = abs(prev_energy - ith_energy)
460
+ delta_energy_max = max(next_energy_diff, prev_energy_diff)
461
+ delta_energy_min = min(next_energy_diff, prev_energy_diff)
462
+
463
+ # Uphill
464
+ if next_energy > ith_energy > prev_energy:
465
+ tangent = tangent_plus
466
+ # Downhill
467
+ elif next_energy < ith_energy < prev_energy:
468
+ tangent = tangent_minus
469
+ # Minimum or Maximum
470
+ else:
471
+ if next_energy >= prev_energy:
472
+ tangent = (
473
+ tangent_plus * delta_energy_max
474
+ + tangent_minus * delta_energy_min
475
+ )
476
+ # next_energy < prev_energy
477
+ else:
478
+ tangent = (
479
+ tangent_plus * delta_energy_min
480
+ + tangent_minus * delta_energy_max
481
+ )
482
+ elif kind == "lanczos":
483
+ # Calculating a lanczos tangent is costly, so we store the
484
+ # tangent in a dictionary. The current coordinates are
485
+ # stringified with precision=4 and then hashed. The tangent
486
+ # is stored/looked up with this hash.
487
+ cur_hash = hash_arr(ith_image.coords, precision=4)
488
+ try:
489
+ tangent = self.lanczos_tangents[cur_hash]
490
+ self.log(
491
+ "Returning previously calculated Lanczos tangent with "
492
+ f"hash={cur_hash}"
493
+ )
494
+ except KeyError:
495
+ # Try to use previous Lanczos tangent
496
+ guess = lanczos_guess
497
+ if (guess is None) and (self.prev_lanczos_hash is not None):
498
+ guess = self.lanczos_tangents[self.prev_lanczos_hash]
499
+ self.log(
500
+ f"Using tangent with hash={self.prev_lanczos_hash} "
501
+ "as initial guess for Lanczos algorithm."
502
+ )
503
+ w_min, tangent = geom_lanczos(
504
+ ith_image, guess=guess, logger=self.logger
505
+ )
506
+ self.lanczos_tangents[cur_hash] = tangent
507
+ # Update hash
508
+ self.prev_lanczos_hash = cur_hash
509
+
510
+ tangent /= np.linalg.norm(tangent)
511
+ return tangent
512
+
513
+ def get_tangents(self):
514
+ return np.array([self.get_tangent(i) for i in range(len(self.images))])
515
+
516
+ def as_xyz(self, comments=None):
517
+ return "\n".join([image.as_xyz() for image in self.images])
518
+
519
+ def get_dask_client(self):
520
+ return Client(self.scheduler)
521
+
522
+ def get_hei_index(self, energies=None):
523
+ """Return index of highest energy image."""
524
+ if energies is None:
525
+ energies = [image.energy for image in self.images]
526
+ return np.argmax(energies)
527
+
528
+ def prepare_opt_cycle(self, last_coords, last_energies, last_forces):
529
+ """Implements additional logic in preparation of the next
530
+ optimization cycle.
531
+
532
+ Should be called by the optimizer at the beginning of a new
533
+ optimization cycle. Can be used to implement additional logic
534
+ as needed for AdaptiveNEB etc.
535
+ """
536
+ self.coords_list.append(last_coords)
537
+ self.forces_list.append(last_forces)
538
+
539
+ # Return False if we don't want to climb or are already
540
+ # climbing.
541
+ already_climbing = self.started_climbing
542
+ if self.climb and not already_climbing:
543
+ self.started_climbing = self.check_for_climbing_start(self.climb_rms)
544
+ if self.started_climbing:
545
+ msg = "Will use climbing image(s) in next cycle."
546
+ self.log(msg)
547
+ print(msg)
548
+ # Determine climbing index/indices if not set, but requested.
549
+ if already_climbing and self.climb_fixed and (self.fixed_climb_indices is None):
550
+ self.fixed_climb_indices = self.get_climbing_indices()
551
+
552
+ already_climbing_lanczos = self.started_climbing_lanczos
553
+ if (
554
+ self.climb_lanczos
555
+ and self.started_climbing
556
+ and not already_climbing_lanczos
557
+ ):
558
+ self.started_climbing_lanczos = self.check_for_climbing_start(
559
+ self.climb_lanczos_rms
560
+ )
561
+ if self.started_climbing_lanczos:
562
+ msg = "Will use Lanczos algorithm for HEI tangent in next cycle."
563
+ self.log(msg)
564
+ print(msg)
565
+
566
+ return not already_climbing and self.started_climbing
567
+
568
+ def rms(self, arr):
569
+ """Root mean square
570
+
571
+ Returns the root mean square of the given array.
572
+
573
+ Parameters
574
+ ----------
575
+ arr : iterable of numbers
576
+
577
+ Returns
578
+ -------
579
+ rms : float
580
+ Root mean square of the given array.
581
+ """
582
+ return np.sqrt(np.mean(np.square(arr)))
583
+
584
+ def check_for_climbing_start(self, ref_rms):
585
+ # Only initiate climbing on a sufficiently converged MEP.
586
+ # This can be determined from a supplied threshold for the
587
+ # RMS force (rms_force) or from a multiple of the
588
+ # RMS force convergence threshold (rms_multiple, default).
589
+ rms_forces = self.rms(self.forces_list[-1])
590
+ # Only start climbing when the COS is fully grown. This
591
+ # attribute may not be defined in all subclasses, so it
592
+ # defaults to True here.
593
+ try:
594
+ fully_grown = self.fully_grown
595
+ except AttributeError:
596
+ fully_grown = True
597
+ start_climbing = (rms_forces <= ref_rms) and fully_grown
598
+ return start_climbing
599
+
600
+ def get_climbing_indices(self):
601
+ # Index of the highest energy image (HEI)
602
+ hei_index = self.get_hei_index()
603
+
604
+ move_inds = self.moving_indices
605
+ # Don't climb if not yet enabled or requested.
606
+ if not (self.climb and self.started_climbing):
607
+ climb_indices = tuple()
608
+ elif self.fixed_climb_indices is not None:
609
+ climb_indices = self.fixed_climb_indices
610
+ _ = "index" if len(climb_indices) == 1 else "indices"
611
+ self.log(f"Returning fixed climbing {_}.")
612
+ # Do one image climbing (C1) neb if explicitly requested or
613
+ # the HEI is the first or last item in moving_indices.
614
+ elif self.climb == "one" or ((hei_index == 1) or (hei_index == move_inds[-1])):
615
+ climb_indices = (hei_index,)
616
+ # We can do two climbing (C2) neb if the highest energy image (HEI)
617
+ # is in moving_indices but not the first or last item in this list.
618
+ # elif self.climb != "one" and hei_index in move_inds[1:-1]:
619
+ elif hei_index in move_inds[1:-1]:
620
+ climb_indices = (hei_index - 1, hei_index + 1)
621
+ # climb_indices = (hei_index,)
622
+ # Don't climb when the HEI is the first or last image of the whole
623
+ # NEB.
624
+ else:
625
+ climb_indices = tuple()
626
+ self.log("Want to climb but can't. HEI is first or last image!")
627
+ # self.log(f"Climbing indices: {climb_indices}")
628
+ return climb_indices
629
+
630
+ def get_climbing_forces(self, ind):
631
+ climbing_image = self.images[ind]
632
+ ci_forces = climbing_image.forces
633
+ tangent = self.get_tangent(ind)
634
+ climbing_forces = ci_forces - 2 * ci_forces.dot(tangent) * tangent
635
+
636
+ return climbing_forces, climbing_image.energy
637
+
638
+ def set_climbing_forces(self, forces):
639
+ # Avoids calling the other methods with their logging output etc.
640
+ if not self.started_climbing:
641
+ return forces
642
+
643
+ for i in self.get_climbing_indices():
644
+ climb_forces, climb_en = self.get_climbing_forces(i)
645
+ forces[i] = climb_forces
646
+ norm = np.linalg.norm(climb_forces)
647
+ self.log(
648
+ f"Climbing with image {i}, E = {climb_en:.6f} au, "
649
+ f"norm(forces)={norm:.6f}"
650
+ )
651
+ return forces
652
+
653
+ def get_splined_hei(self):
654
+ self.log("Splining HEI")
655
+ # Interpolate energies
656
+ cart_coords = align_coords([image.cart_coords for image in self.images])
657
+ coord_diffs = get_coords_diffs(cart_coords)
658
+ self.log(f"\tCoordinate differences: {coord_diffs}")
659
+ energies = np.array(self.energy)
660
+ energies_spline = interp1d(coord_diffs, energies, kind="cubic")
661
+ x_fine = np.linspace(0, 1, 500)
662
+ energies_fine = energies_spline(x_fine)
663
+ # Determine index that yields the highest energy
664
+ hei_ind = energies_fine.argmax()
665
+ hei_x = x_fine[hei_ind]
666
+ self.log(f"Found splined HEI at x={hei_x:.4f}")
667
+ hei_frac_index = hei_x * (len(self.images) - 1)
668
+ hei_energy = energies_fine[hei_ind]
669
+
670
+ reshaped = cart_coords.reshape(-1, self.cart_coords_length)
671
+ # To use splprep we have to transpose the coords.
672
+ transp_coords = reshaped.transpose()
673
+ tcks, us = zip(
674
+ *[
675
+ splprep(transp_coords[i : i + 9], s=0, k=3, u=coord_diffs)
676
+ for i in range(0, len(transp_coords), 9)
677
+ ]
678
+ )
679
+
680
+ # Reparametrize mesh
681
+ hei_coords = np.vstack(
682
+ [
683
+ # WTF, Black? This looks horrible.
684
+ splev(
685
+ [
686
+ hei_x,
687
+ ],
688
+ tck,
689
+ )
690
+ for tck in tcks
691
+ ]
692
+ )
693
+ hei_coords = hei_coords.flatten()
694
+
695
+ # Actually it looks like that splined tangents are really bad approximations
696
+ # to the actual imaginary mode. The Cartesian upwinding tangent is usually
697
+ # much much better. In 'run_tsopt_from_cos' we actually mix two "normal" tangents
698
+ # to obtain the HEI tangent.
699
+ hei_tangent = np.vstack(
700
+ [
701
+ # WTF, Black? This looks horrible.
702
+ splev(
703
+ [
704
+ hei_x,
705
+ ],
706
+ tck,
707
+ der=1,
708
+ )
709
+ for tck in tcks
710
+ ]
711
+ ).T
712
+ hei_tangent = hei_tangent.flatten()
713
+ hei_tangent /= np.linalg.norm(hei_tangent)
714
+ return hei_coords, hei_energy, hei_tangent, hei_frac_index
715
+
716
+ def get_image_calc_counter_sum(self):
717
+ return sum([image.calculator.calc_counter for image in self.images])
718
+
719
+ def describe(self):
720
+ imgs = self.images
721
+ img = imgs[0]
722
+ return f"ChainOfStates, {len(imgs)} images, ({img.sum_formula}, {len(img.atoms)} atoms) per image"
723
+
724
+ def __str__(self):
725
+ return self.__class__.__name__