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,1084 @@
1
+ import abc
2
+ from dataclasses import dataclass
3
+ import functools
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+ import textwrap
9
+ import time
10
+ from typing import Literal, Optional, Tuple
11
+
12
+ import numpy as np
13
+ import yaml
14
+
15
+ from pysisyphus.cos.ChainOfStates import ChainOfStates
16
+ from pysisyphus.Geometry import Geometry
17
+ from pysisyphus.helpers import (
18
+ check_for_end_sign,
19
+ fit_rigid,
20
+ get_coords_diffs,
21
+ procrustes,
22
+ )
23
+ from pysisyphus.helpers_pure import highlight_text
24
+ from pysisyphus.intcoords.exceptions import RebuiltInternalsException
25
+ from pysisyphus.intcoords.helpers import interfragment_distance
26
+ from pysisyphus.io.hdf5 import get_h5_group, resize_h5_group
27
+ from pysisyphus.optimizers.exceptions import ZeroStepLength
28
+ from pysisyphus.TablePrinter import TablePrinter
29
+
30
+ import torch
31
+
32
+ def get_data_model(geometry, is_cos, max_cycles):
33
+ try:
34
+ # Attribute is only present in COS classes
35
+ image_num = geometry.max_image_num
36
+ dummy_geom = geometry.images[0]
37
+ except AttributeError:
38
+ image_num = 1
39
+ dummy_geom = geometry
40
+
41
+ # Define dataset shapes. As pysisyphus offers growing COS methods where
42
+ # the number of images changes along the optimization we have to define
43
+ # the shapes accordingly by considering the maximum number of images.
44
+ _1d = (max_cycles,)
45
+ _2d = (max_cycles, image_num * dummy_geom.coords.size)
46
+ _image_inds = (max_cycles, image_num)
47
+ # Number of cartesian coordinates is probably different from the number
48
+ # of internal coordinates.
49
+ _2d_cart = (max_cycles, image_num * dummy_geom.cart_coords.size)
50
+ # The dimensionality of energies depends on whether a COS is optimized or
51
+ # not. I know this is probably not the best idea...
52
+ _energy = _1d if (not is_cos) else (max_cycles, geometry.max_image_num)
53
+
54
+ data_model = {
55
+ "image_nums": _1d,
56
+ "image_inds": _image_inds,
57
+ "cart_coords": _2d_cart,
58
+ "coords": _2d,
59
+ "energies": _energy,
60
+ "forces": _2d,
61
+ # AFIR related
62
+ "true_energies": _energy,
63
+ "true_forces": _2d_cart,
64
+ "steps": _2d,
65
+ # Convergence related
66
+ "max_forces": _1d,
67
+ "rms_forces": _1d,
68
+ "max_steps": _1d,
69
+ "rms_steps": _1d,
70
+ # Misc
71
+ "cycle_times": _1d,
72
+ "modified_forces": _2d,
73
+ # COS specific
74
+ "tangents": _2d,
75
+ }
76
+
77
+ return data_model
78
+
79
+
80
+ CONV_THRESHS = {
81
+ # max_force, rms_force, max_step, rms_step
82
+ "nwchem_loose": (4.5e-3, 3.0e-3, 5.4e-3, 3.6e-3),
83
+ "gau_loose": (2.5e-3, 1.7e-3, 1.0e-2, 6.7e-3),
84
+ "gau": (4.5e-4, 3.0e-4, 1.8e-3, 1.2e-3),
85
+ "gau_tight": (1.5e-5, 1.0e-5, 6.0e-5, 4.0e-5),
86
+ "gau_vtight": (2.0e-6, 1.0e-6, 6.0e-6, 4.0e-6),
87
+ "baker": (3.0e-4, 2.0e-4, 3.0e-4, 2.0e-4),
88
+ # Dummy thresholds
89
+ "never": (2.0e-6, 1.0e-6, 6.0e-6, 4.0e-6),
90
+ }
91
+ Thresh = Literal["gau_loose", "gau", "gau_tight", "gau_vtight", "baker", "never"]
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class ConvInfo:
96
+ cur_cycle: int
97
+ energy_converged: bool
98
+ max_force_converged: bool
99
+ rms_force_converged: bool
100
+ max_step_converged: bool
101
+ rms_step_converged: bool
102
+ desired_eigval_structure: bool
103
+
104
+ def get_convergence(self):
105
+ return (
106
+ self.energy_converged,
107
+ self.max_force_converged,
108
+ self.rms_force_converged,
109
+ self.max_step_converged,
110
+ self.rms_step_converged,
111
+ self.desired_eigval_structure,
112
+ )
113
+
114
+ def is_converged(self):
115
+ return all(self.get_convergence())
116
+
117
+
118
+ class Optimizer(metaclass=abc.ABCMeta):
119
+ def __init__(
120
+ self,
121
+ geometry: Geometry,
122
+ thresh: Thresh = "gau_loose",
123
+ max_step: float = 0.04,
124
+ max_cycles: int = 150,
125
+ min_step_norm: float = 1e-8,
126
+ assert_min_step: bool = True,
127
+ rms_force: Optional[float] = None,
128
+ rms_force_only: bool = False,
129
+ max_force_only: bool = False,
130
+ force_only: bool = False,
131
+ converge_to_geom_rms_thresh: float = 0.05,
132
+ align: bool = False,
133
+ align_factor: float = 1.0,
134
+ dump: bool = False,
135
+ dump_restart: bool = False,
136
+ print_every: int = 1,
137
+ prefix: str = "",
138
+ reparam_thresh: float = 1e-3,
139
+ reparam_check_rms: bool = True,
140
+ reparam_when: Optional[Literal["before", "after"]] = "after",
141
+ overachieve_factor: float = 0.0,
142
+ check_eigval_structure: bool = False,
143
+ restart_info=None,
144
+ check_coord_diffs: bool = True,
145
+ coord_diff_thresh: float = 0.01,
146
+ fragments: Optional[Tuple] = None,
147
+ monitor_frag_dists: int = 0,
148
+ out_dir: str = ".",
149
+ h5_fn: str = "optimization.h5",
150
+ h5_group_name: str = "opt",
151
+ ) -> None:
152
+ """Optimizer baseclass. Meant to be subclassed.
153
+
154
+ Parameters
155
+ ----------
156
+ geometry
157
+ Geometry to be optimized.
158
+ thresh
159
+ Convergence threshold.
160
+ max_step
161
+ Maximum absolute component of the allowed step vector. Utilized in
162
+ optimizers that don't support a trust region or line search.
163
+ max_cycles
164
+ Maximum number of allowed optimization cycles.
165
+ min_step_norm
166
+ Minimum norm of an allowed step. If the step norm drops below
167
+ this value a ZeroStepLength-exception is raised. The unit depends
168
+ on the coordinate system of the supplied geometry.
169
+ assert_min_step
170
+ Flag that controls whether the norm of the proposed step is check
171
+ for being too small.
172
+ rms_force
173
+ Root-mean-square of the force from which user-defined thresholds
174
+ are derived. When 'rms_force' is given 'thresh' is ignored.
175
+ rms_force_only
176
+ When set, convergence is signalled only based on rms(forces).
177
+ max_force_only
178
+ When set, convergence is signalled only based on max(|forces|).
179
+ force_only
180
+ When set, convergence is signalled only based on max(|forces|) and rms(forces).
181
+ converge_to_geom_rms_thresh
182
+ Threshold for the RMSD with another geometry. When the RMSD drops
183
+ below this threshold convergence is signalled. Only used with
184
+ Growing Newton trajectories.
185
+ align
186
+ Flag that controls whether the geometry is aligned in every step
187
+ onto the coordinates of the previous step. Must not be used with
188
+ internal coordinates.
189
+ align_factor
190
+ Factor that controls the strength of the alignment. 1.0 means
191
+ full alignment, 0.0 means no alignment. The factor mixes the
192
+ rotation matrix of the alignment with the identity matrix.
193
+ dump
194
+ Flag to control dumping/writing of optimization progress to the
195
+ filesystem
196
+ dump_restart
197
+ Flag to control whether restart information is dumped to the
198
+ filesystem.
199
+ print_every
200
+ Report optimization progress every nth cycle.
201
+ prefix
202
+ Short string that is prepended to several files created by
203
+ the optimizer. Allows distinguishing several optimizations carried
204
+ out in the same directory.
205
+ reparam_thresh
206
+ Controls the minimal allowed similarity between coordinates
207
+ after two successive reparametrizations. Convergence is signalled
208
+ if the coordinates did not change significantly.
209
+ reparam_check_rms
210
+ Whether to check for (too) similar coordinates after reparametrization.
211
+ reparam_when
212
+ Reparametrize before or after calculating the step. Can also be turned
213
+ off by setting it to None.
214
+ overachieve_factor
215
+ Signal convergence when max(forces) and rms(forces) fall below the
216
+ chosen threshold, divided by this factor. Convergence of max(step) and
217
+ rms(step) is ignored.
218
+ check_eigval_structure
219
+ Check the eigenvalues of the modes we maximize along. Convergence requires
220
+ them to be negative. Useful if TS searches are started from geometries close
221
+ to a minimum.
222
+ restart_info
223
+ Restart information. Undocumented.
224
+ check_coord_diffs
225
+ Whether coordinates of chain-of-sates images are checked for being
226
+ too similar.
227
+ coord_diff_thresh
228
+ Unitless threshold for similary checking of COS image coordinates.
229
+ The first image is assigned 0, the last image is assigned to 1.
230
+ fragments
231
+ Tuple of lists containing atom indices, defining two fragments.
232
+ monitor_frag_dists
233
+ Monitor fragment distances for N cycles. The optimization is terminated
234
+ when the interfragment distances falls below the initial value after N
235
+ cycles.
236
+ out_dir
237
+ String poiting to a directory where optimization progress is
238
+ dumped.
239
+ h5_fn
240
+ Basename of the HDF5 file used for dumping.
241
+ h5_group_name
242
+ Groupname used for dumping of this optimization.
243
+ """
244
+ assert thresh in CONV_THRESHS.keys()
245
+
246
+ self.geometry = geometry
247
+ self.thresh = thresh
248
+ self.max_step = max_step
249
+ self.min_step_norm = min_step_norm
250
+ self.assert_min_step = assert_min_step
251
+ self.rms_force_only = rms_force_only
252
+ self.max_force_only = max_force_only
253
+ self.force_only = force_only
254
+ self.converge_to_geom_rms_thresh = converge_to_geom_rms_thresh
255
+ self.align = align
256
+ self.align_factor = align_factor
257
+ self.dump = dump
258
+ self.dump_restart = dump_restart
259
+ print_every = int(print_every)
260
+ assert print_every >= 1
261
+ self.print_every = print_every
262
+ self.prefix = f"{prefix}_" if prefix else prefix
263
+ self.reparam_thresh = reparam_thresh
264
+ self.reparam_check_rms = reparam_check_rms
265
+ self.reparam_when = reparam_when
266
+ assert self.reparam_when in ("after", "before", None)
267
+ self.overachieve_factor = float(overachieve_factor)
268
+ self.check_eigval_structure = check_eigval_structure
269
+ self.check_coord_diffs = check_coord_diffs
270
+ self.coord_diff_thresh = float(coord_diff_thresh)
271
+
272
+ self.logger = logging.getLogger("optimizer")
273
+ self.is_cos = issubclass(type(self.geometry), ChainOfStates)
274
+
275
+ # Set up convergence thresholds
276
+ self.convergence = self.make_conv_dict(
277
+ thresh, rms_force, rms_force_only, max_force_only, force_only
278
+ )
279
+ for key, value in self.convergence.items():
280
+ setattr(self, key, value)
281
+
282
+ if self.thresh == "never":
283
+ max_cycles = 1_000_000_000
284
+ self.dump = False
285
+ self.log(
286
+ f"Got threshold {self.thresh}, set 'max_cycles' to {max_cycles} "
287
+ "and disabled dumping!"
288
+ )
289
+ self.conv_dict = {}
290
+ try:
291
+ self.converge_to_geom = self.geometry.converge_to_geom
292
+ except AttributeError:
293
+ # TODO: log that attribute is not present at debug level
294
+ self.converge_to_geom = None
295
+ self.max_cycles = max_cycles
296
+
297
+ self.fragments = fragments
298
+ self.monitor_frag_dists = monitor_frag_dists
299
+ if self.monitor_frag_dists:
300
+ assert (
301
+ len(self.fragments) == 2
302
+ ), "Interfragment monitoring requires two fragments!"
303
+ assert all(
304
+ [len(frag) > 0 for frag in self.fragments]
305
+ ), "Fragments must not be empty!"
306
+ # Setting some default values
307
+ self.monitor_frag_dists_counter = self.monitor_frag_dists
308
+ self.interfrag_dists = list()
309
+ self.resetted = False
310
+ try:
311
+ out_dir = Path(out_dir)
312
+ except TypeError:
313
+ out_dir = Path(".")
314
+ self.out_dir = out_dir.resolve()
315
+ self.out_dir.mkdir(parents=True, exist_ok=True)
316
+
317
+ if self.is_cos:
318
+ moving_image_num = len(self.geometry.moving_indices)
319
+ print(f"Path with {moving_image_num} moving images.")
320
+
321
+ # Don't use prefix for this fn, as different optimizations
322
+ # can be distinguished according to their group in the HDF5 file.
323
+ self.h5_fn = self.get_path_for_fn(h5_fn, with_prefix=False)
324
+ self.h5_group_name = h5_group_name
325
+
326
+ current_fn = "current_geometries_trj.xyz" if self.is_cos else "current_geometry.xyz"
327
+ self.current_fn = self.get_path_for_fn(current_fn)
328
+ final_fn = "final_geometries_trj.xyz" if self.is_cos else "final_geometry.xyz"
329
+ self.final_fn = self.get_path_for_fn(final_fn)
330
+ self.hei_trj_fn = self.get_path_for_fn("cos_hei_trj.xyz")
331
+ try:
332
+ os.remove(self.hei_trj_fn)
333
+ except FileNotFoundError:
334
+ pass
335
+
336
+ # Setting some empty lists as default. The actual shape of the respective
337
+ # entries is not considered, which gives us some flexibility.
338
+ self.data_model = get_data_model(self.geometry, self.is_cos, self.max_cycles)
339
+ for la in self.data_model.keys():
340
+ setattr(self, la, list())
341
+
342
+ if self.dump:
343
+ out_trj_fn = self.get_path_for_fn("optimization_trj.xyz")
344
+ self.out_trj_handle = open(out_trj_fn, "w")
345
+ # Call with reset=True to delete remnants of previous calculations, unless
346
+ # the optimizer was restarted. Given a previous optimization with, e.g. 30
347
+ # cycles and a second restarted optimization with 20 cycles the last 10 cycles
348
+ # of the previous optimization would still be present.
349
+ reset = restart_info is None
350
+ # h5_group = get_h5_group(
351
+ # self.h5_fn, self.h5_group_name, self.data_model, reset=reset
352
+ # )
353
+ # h5_group.file.close()
354
+ if self.prefix:
355
+ self.log(f"Created optimizer with prefix {self.prefix}")
356
+
357
+ self.restarted = False
358
+ self.last_cycle = 0
359
+ self.cur_cycle = 0
360
+ if restart_info is not None:
361
+ if isinstance(restart_info, str):
362
+ restart_info = yaml.load(restart_info, Loader=yaml.SafeLoader)
363
+ self.set_restart_info(restart_info)
364
+ self.restarted = True
365
+
366
+ header = "cycle Δ(energy) max(|force|) rms(force) max(|step|) rms(step) s/cycle".split()
367
+ col_fmts = "int float float float float float float_short".split()
368
+ self.table = TablePrinter(header, col_fmts, width=12)
369
+ self.is_converged = False
370
+
371
+ def get_path_for_fn(self, fn, with_prefix=True):
372
+ prefix = self.prefix if with_prefix else ""
373
+ return self.out_dir / (prefix + fn)
374
+
375
+ def make_conv_dict(
376
+ self, key, rms_force=None, rms_force_only=False, max_force_only=False, force_only=False
377
+ ):
378
+ if not rms_force:
379
+ threshs = CONV_THRESHS[key]
380
+ else:
381
+ print(
382
+ "Deriving convergence threshold from supplied "
383
+ f"rms_force={rms_force}."
384
+ )
385
+ threshs = (
386
+ 1.5 * rms_force,
387
+ rms_force,
388
+ 6 * rms_force,
389
+ 4 * rms_force,
390
+ )
391
+ keys = keep_keys = [
392
+ "max_force_thresh",
393
+ "rms_force_thresh",
394
+ "max_step_thresh",
395
+ "rms_step_thresh",
396
+ ]
397
+ conv_dict = {k: v for k, v in zip(keys, threshs)}
398
+
399
+ # Only used gradient information for COS optimizations
400
+ if self.is_cos:
401
+ keep_keys = ["max_force_thresh", "rms_force_thresh"]
402
+
403
+ if rms_force_only:
404
+ self.log("Checking convergence with rms(forces) only!")
405
+ keep_keys = ["rms_force_thresh"]
406
+ elif max_force_only:
407
+ self.log("Checking convergence with max(forces) only!")
408
+ keep_keys = ["max_force_thresh"]
409
+ elif force_only:
410
+ self.log("Checking convergence with max(forces) and rms(forces) only!")
411
+ keep_keys = ["max_force_thresh", "rms_force_thresh"]
412
+
413
+ # The dictionary should only contain pairs that are needed
414
+ conv_dict = {key: value for key, value in conv_dict.items() if key in keep_keys}
415
+ return conv_dict
416
+
417
+ def report_conv_thresholds(self):
418
+ oaf = self.overachieve_factor
419
+
420
+ # Overachieved
421
+ def oa(val):
422
+ return f", ({val/oaf:.6f})" if oaf > 0.0 else ""
423
+
424
+ internal_coords = self.geometry.coord_type not in (
425
+ "cart",
426
+ "cartesian",
427
+ "mwcartesian",
428
+ )
429
+ fu = "E_h a_0⁻¹" + (" (rad⁻¹)" if internal_coords else "") # forces unit
430
+ su = "a_0" + (" (rad)" if internal_coords else "") # step unit
431
+
432
+ try:
433
+ rms_thresh = f"\tmax(|force|) <= {self.max_force_thresh:.6f}{oa(self.max_force_thresh)} {fu}"
434
+ except AttributeError:
435
+ rms_thresh = None
436
+ try:
437
+ max_thresh = f"\t rms(force) <= {self.rms_force_thresh:.6f}{oa(self.rms_force_thresh)} {fu}"
438
+ except AttributeError:
439
+ max_thresh = None
440
+ threshs = (rms_thresh, max_thresh)
441
+
442
+ if self.rms_force_only:
443
+ use_threshs = (threshs[1],)
444
+ elif self.max_force_only:
445
+ use_threshs = (threshs[0],)
446
+ elif self.force_only:
447
+ use_threshs = threshs
448
+ elif self.is_cos:
449
+ use_threshs = threshs
450
+ else:
451
+ use_threshs = threshs + (
452
+ f"\t max(|step|) <= {self.max_step_thresh:.6f} {su}",
453
+ f"\t rms(step) <= {self.rms_step_thresh:.6f} {su}",
454
+ )
455
+ if self.thresh == "baker":
456
+ use_threshs = use_threshs + ("\t Δ(energy) <= 0.000001 E_h",)
457
+ print(
458
+ "Convergence thresholds"
459
+ + (", (overachieved when)" if oaf > 0.0 else "")
460
+ + ":\n"
461
+ + "\n".join(use_threshs)
462
+ + f"\n\t'Superscript {self.table.mark}' indicates convergence"
463
+ + "\n"
464
+ )
465
+
466
+ def log(self, message, level=50):
467
+ self.logger.log(level, message)
468
+
469
+ def check_convergence(self, step=None, multiple=1.0, overachieve_factor=None):
470
+ """Check if the current convergence of the optimization
471
+ is equal to or below the required thresholds, or a multiple
472
+ thereof. The latter may be used in initiating the climbing image.
473
+ """
474
+
475
+ if step is None:
476
+ step = self.steps[-1]
477
+ if overachieve_factor is None:
478
+ overachieve_factor = self.overachieve_factor
479
+
480
+ if isinstance(step, torch.Tensor):
481
+ step = step.detach().cpu().numpy()
482
+
483
+ # When using a ChainOfStates method we are only interested
484
+ # in optimizing the forces perpendicular to the MEP.
485
+ if self.is_cos:
486
+ forces = self.geometry.perpendicular_forces
487
+ elif len(self.modified_forces) == len(self.forces):
488
+ self.log("Using modified forces to determine convergence!")
489
+ forces = self.modified_forces[-1]
490
+ else:
491
+ forces = self.forces[-1]
492
+
493
+ # The forces of fixed images may be zero and this may distort the RMS
494
+ # values. So we take into account the number of moving images with
495
+ # non-zero forces vectors.
496
+ if self.is_cos:
497
+ non_zero_elements = (
498
+ len(self.geometry.moving_indices) * self.geometry.coords_length
499
+ )
500
+ rms_force = np.sqrt(np.sum(np.square(forces)) / non_zero_elements)
501
+ rms_step = np.sqrt(np.sum(np.square(step)) / non_zero_elements)
502
+ else:
503
+ rms_force = np.sqrt(np.mean(np.square(forces)))
504
+ rms_step = np.sqrt(np.mean(np.square(step)))
505
+
506
+ max_force = np.abs(forces).max()
507
+ max_step = np.abs(step).max()
508
+
509
+ self.max_forces.append(max_force)
510
+ self.rms_forces.append(rms_force)
511
+ self.max_steps.append(max_step)
512
+ self.rms_steps.append(rms_step)
513
+
514
+ # Give geometry a chance to signal convergence, e.g. GrowingNT that
515
+ # is supposed to stop when a TS was passed.
516
+ try:
517
+ geom_converged = self.geometry.check_convergence()
518
+ except AttributeError:
519
+ geom_converged = False
520
+
521
+ converged_to_geom = False
522
+ if self.converge_to_geom is not None:
523
+ rmsd = np.sqrt(
524
+ np.mean((self.converge_to_geom.coords - self.geometry.coords) ** 2)
525
+ )
526
+ converged_to_geom = rmsd < self.converge_to_geom_rms_thresh
527
+
528
+ this_cycle = {
529
+ "max_force_thresh": max_force,
530
+ "rms_force_thresh": rms_force,
531
+ "max_step_thresh": max_step,
532
+ "rms_step_thresh": rms_step,
533
+ }
534
+
535
+ def check(key):
536
+ # Always return True if given key is not checked
537
+ key_is_checked = key in self.convergence
538
+ if key_is_checked:
539
+ result = this_cycle[key] <= getattr(self, key) * multiple
540
+ else:
541
+ result = True
542
+ return result
543
+
544
+ convergence = {
545
+ "energy_converged": True,
546
+ "max_force_converged": check("max_force_thresh"),
547
+ "rms_force_converged": check("rms_force_thresh"),
548
+ "max_step_converged": check("max_step_thresh"),
549
+ "rms_step_converged": check("rms_step_thresh"),
550
+ }
551
+ # For TS optimizations we also try to check the eigenvalue structure of the
552
+ # Hessian. A saddle point of order n must have exatcly only n significant negative
553
+ # eigenvalues. We try to check this below.
554
+ #
555
+ # Currently, this is not totally strict,
556
+ # as only the values in self.ts_mode_eigvals are checked but actually all eigenvalues
557
+ # would have to be checked.
558
+ desired_eigval_structure = True
559
+ if self.check_eigval_structure:
560
+ try:
561
+ desired_eigval_structure = (
562
+ # Acutally all eigenvalues would have to be checked, but
563
+ # currently they are not stored anywhere.
564
+ self.ts_mode_eigvals
565
+ < self.small_eigval_thresh
566
+ ).sum() == len(self.roots)
567
+ except AttributeError:
568
+ self.log(
569
+ "Skipping check of eigenvalue structure, as information is unavailable."
570
+ )
571
+ convergence["desired_eigval_structure"] = desired_eigval_structure
572
+ conv_info = ConvInfo(self.cur_cycle, **convergence)
573
+
574
+ # Check if force convergence is overachieved. If the eigenvalue-structure is
575
+ # checked, a wrong structure will prevent overachieved convergence.
576
+ overachieved = False
577
+ if overachieve_factor > 0 and desired_eigval_structure:
578
+ max_thresh = self.convergence.get("max_force_thresh", 0) / overachieve_factor
579
+ rms_thresh = self.convergence.get("rms_force_thresh", 0) / overachieve_factor
580
+ max_ = max_force < max_thresh
581
+ rms_ = rms_force < rms_thresh
582
+ overachieved = max_ and rms_
583
+ if max_:
584
+ self.log("max(force) is overachieved")
585
+ if rms_:
586
+ self.log("rms(force) is overachieved")
587
+ if max_ and rms_:
588
+ self.log("Force convergence overachieved!")
589
+
590
+ converged = all([val for val in convergence.values()])
591
+ not_never = self.thresh != "never"
592
+
593
+ if self.thresh == "baker":
594
+ energy_converged = False
595
+ if len(self.energies) >= 2:
596
+ cur_energy = np.asarray(self.energies[-1])
597
+ prev_energy = np.asarray(self.energies[-2])
598
+ if cur_energy.shape == prev_energy.shape:
599
+ energy_converged = np.all(np.abs(cur_energy - prev_energy) < 1e-6)
600
+
601
+ # Enforce at least one new cycle after (re)start and require energy convergence.
602
+ convergence["energy_converged"] = bool(energy_converged)
603
+ conv_info = ConvInfo(self.cur_cycle, **convergence)
604
+ converged = (self.cur_cycle > self.last_cycle) and all(convergence.values())
605
+ # Keep Baker strict: don't bypass the energy criterion via overachievement.
606
+ overachieved = False
607
+ return (
608
+ any((converged_to_geom, converged, overachieved, geom_converged))
609
+ and not_never,
610
+ conv_info,
611
+ )
612
+
613
+ def print_opt_progress(self, conv_info):
614
+ try:
615
+ energy_diff = self.energies[-1] - self.energies[-2]
616
+ # ValueError: maybe raised when the number of images differ in cycles
617
+ # IndexError: raised in first cycle when only one energy is present
618
+ except (ValueError, IndexError):
619
+ energy_diff = float("nan")
620
+
621
+ # Try to sum COS energies
622
+ try:
623
+ energy_diff = sum(energy_diff)
624
+ except TypeError:
625
+ pass
626
+
627
+ if (self.cur_cycle > 1) and (self.cur_cycle % 10 == 0):
628
+ self.table.print_sep()
629
+
630
+ # desired_eigval_structure is also returned, but currently not reported.
631
+ marks = [False, *conv_info.get_convergence()[:-1], False]
632
+ try:
633
+ cycle_time = self.cycle_times[-1]
634
+ except IndexError:
635
+ cycle_time = 0.0
636
+ self.table.print_row(
637
+ (
638
+ self.cur_cycle,
639
+ energy_diff,
640
+ self.max_forces[-1],
641
+ self.rms_forces[-1],
642
+ self.max_steps[-1],
643
+ self.rms_steps[-1],
644
+ cycle_time,
645
+ ),
646
+ marks=marks,
647
+ )
648
+ try:
649
+ # Geometries/ChainOfStates objects can also do some printing.
650
+ add_info = self.geometry.get_additional_print()
651
+ if add_info:
652
+ self.table.print(add_info)
653
+ except AttributeError:
654
+ pass
655
+
656
+ def fit_rigid(self, *, vectors=None, vector_lists=None, hessian=None):
657
+ return fit_rigid(
658
+ self.geometry,
659
+ vectors=vectors,
660
+ vector_lists=vector_lists,
661
+ hessian=hessian,
662
+ align_factor=self.align_factor,
663
+ )
664
+
665
+ def procrustes(self):
666
+ """Wrapper for procrustes that passes additional arguments along."""
667
+ procrustes(self.geometry, self.align_factor)
668
+
669
+ def scale_by_max_step(self, steps):
670
+ steps_max = np.abs(steps).max()
671
+ if steps_max > self.max_step:
672
+ steps *= self.max_step / steps_max
673
+ return steps
674
+
675
+ def prepare_opt(self):
676
+ pass
677
+
678
+ def postprocess_opt(self):
679
+ pass
680
+
681
+ @abc.abstractmethod
682
+ def optimize(self):
683
+ pass
684
+
685
+ def write_to_out_dir(self, out_fn, content, mode="w"):
686
+ out_path = self.out_dir / out_fn
687
+ with open(out_path, mode) as handle:
688
+ handle.write(content)
689
+
690
+ def write_image_trjs(self):
691
+ base_name = "image_{:03d}_trj.xyz"
692
+ for i, image in enumerate(self.geometry.images):
693
+ image_fn = base_name.format(i)
694
+ comment = f"cycle {self.cur_cycle}"
695
+ as_xyz = image.as_xyz(comment)
696
+ self.write_to_out_dir(image_fn, as_xyz + "\n", "a")
697
+
698
+ def write_results(self):
699
+ # Save results from the Optimizer to HDF5 file if requested
700
+ # h5_group = get_h5_group(self.h5_fn, self.h5_group_name)
701
+
702
+ # Some attributes never change and are only set in the first cycle
703
+ # if self.cur_cycle == 0:
704
+ # h5_group.attrs["is_cos"] = self.is_cos
705
+ # try:
706
+ # atoms = self.geometry.images[0].atoms
707
+ # coord_size = self.geometry.images[0].coords.size
708
+ # except AttributeError:
709
+ # atoms = self.geometry.atoms
710
+ # coord_size = self.geometry.coords.size
711
+ # h5_group.attrs["atoms"] = np.bytes_(atoms)
712
+ # h5_group.attrs["coord_type"] = self.geometry.coord_type
713
+ # h5_group.attrs["coord_size"] = coord_size
714
+ # h5_group.attrs["overachieve_factor"] = self.overachieve_factor
715
+ # for key in (
716
+ # "max_force_thresh",
717
+ # "rms_force_thresh",
718
+ # "max_step_thresh",
719
+ # "rms_step_thresh",
720
+ # ):
721
+ # try:
722
+ # h5_group.attrs[key] = self.convergence[key]
723
+ # # Step threshold may not be present
724
+ # except KeyError:
725
+ # pass
726
+
727
+ # Update changing attributes
728
+ # h5_group.attrs["cur_cycle"] = self.cur_cycle
729
+ # h5_group.attrs["is_converged"] = self.is_converged
730
+
731
+ # for key, shape in self.data_model.items():
732
+ # value = getattr(self, key)
733
+ # # Don't try to set empty values, e.g. 'tangents' are only present
734
+ # # for COS methods. 'modified_forces' are only present for NCOptimizer.
735
+ # if not value:
736
+ # continue
737
+ # if len(shape) > 1:
738
+ # h5_group[key][self.cur_cycle, : len(value[-1])] = value[-1]
739
+ # else:
740
+ # h5_group[key][self.cur_cycle] = value[-1]
741
+
742
+ # h5_group.file.close()
743
+ pass
744
+
745
+ def write_cycle_to_file(self):
746
+ as_xyz_str = self.geometry.as_xyz()
747
+
748
+ if self.is_cos:
749
+ out_fn = "cycle_{:03d}_trj.xyz".format(self.cur_cycle)
750
+ self.write_to_out_dir(out_fn, as_xyz_str)
751
+ # Also write separate _trj.xyz files for every image in the cos
752
+ self.write_image_trjs()
753
+
754
+ # Dump current HEI
755
+ max_ind = np.argmax(self.energies[-1])
756
+ with open(self.hei_trj_fn, "a") as handle:
757
+ handle.write(self.geometry.images[max_ind].as_xyz() + "\n")
758
+
759
+ else:
760
+ # Append to _trj.xyz file
761
+ self.out_trj_handle.write(as_xyz_str + "\n")
762
+ self.out_trj_handle.flush()
763
+ # Dump to HDF5
764
+ # self.write_results()
765
+
766
+ def final_summary(self):
767
+ # If the optimization was stopped _forces may not be set, so
768
+ # then we force a calculation if it was not already set.
769
+ _ = self.geometry.forces
770
+ cart_forces = self.geometry.cart_forces
771
+ max_cart_forces = np.abs(cart_forces).max()
772
+ rms_cart_forces = np.sqrt(np.mean(cart_forces**2))
773
+ int_str = ""
774
+ if self.geometry.coord_type not in ("cart", "cartesian", "mwcartesian"):
775
+ int_forces = self.geometry.forces
776
+ max_int_forces = np.abs(int_forces).max()
777
+ rms_int_forces = np.sqrt(np.mean(int_forces**2))
778
+ int_str = f"""
779
+ \tmax(forces, internal): {max_int_forces:.6f} hartree/(bohr,rad)
780
+ \trms(forces, internal): {rms_int_forces:.6f} hartree/(bohr,rad)"""
781
+ energy = self.geometry.energy
782
+ final_summary = f"""
783
+ Final summary:{int_str}
784
+ \tmax(forces,cartesian): {max_cart_forces:.6f} hartree/bohr
785
+ \trms(forces,cartesian): {rms_cart_forces:.6f} hartree/bohr
786
+ \tenergy: {energy:.8f} hartree
787
+ """
788
+ return textwrap.dedent(final_summary.strip())
789
+
790
+ def run(self):
791
+ print("If not specified otherwise, all quantities are given in au.\n")
792
+
793
+ if not self.restarted:
794
+ prep_start_time = time.time()
795
+ self.prepare_opt()
796
+ self.log(f"{self.geometry.coords.size} degrees of freedom.")
797
+ prep_end_time = time.time()
798
+ prep_time = prep_end_time - prep_start_time
799
+ self.report_conv_thresholds()
800
+ print(f"Spent {prep_time:.1f} s preparing the first cycle.")
801
+
802
+ self.table.print_header(with_sep=False)
803
+ self.stopped = False
804
+ # Actual optimization loop
805
+ for self.cur_cycle in range(self.last_cycle, self.max_cycles):
806
+ start_time = time.time()
807
+ self.log(highlight_text(f"Cycle {self.cur_cycle:03d}"))
808
+
809
+ if self.is_cos and self.check_coord_diffs:
810
+ image_coords = [image.cart_coords for image in self.geometry.images]
811
+ align = len(image_coords[0]) > 3
812
+ cds = get_coords_diffs(image_coords, align=align)
813
+ # Differences of coordinate differences ;)
814
+ cds_diffs = np.diff(cds)
815
+ min_ind = cds_diffs.argmin()
816
+ if cds_diffs[min_ind] < self.coord_diff_thresh:
817
+ similar_inds = min_ind, min_ind + 1
818
+ msg = (
819
+ f"Cartesian coordinates of images {similar_inds} are "
820
+ "too similar. Stopping optimization!"
821
+ )
822
+ # I should improve my logging :)
823
+ print(msg)
824
+ self.log(msg)
825
+ sim_fn = "too_similar_trj.xyz"
826
+ with open(sim_fn, "w") as handle:
827
+ handle.write(self.geometry.as_xyz())
828
+ print(f"Dumped latest coordinates to '{sim_fn}'.")
829
+ break
830
+
831
+ # Check if something considerably changed in the optimization,
832
+ # e.g. new images were added/interpolated. Then the optimizer
833
+ # should be reset.
834
+ reset_flag = False
835
+ if self.cur_cycle > 0 and self.is_cos:
836
+ reset_flag = self.geometry.prepare_opt_cycle(
837
+ self.coords[-1], self.energies[-1], self.forces[-1]
838
+ )
839
+ # Reset when number of coordinates changed
840
+ elif self.cur_cycle > 0:
841
+ reset_flag = reset_flag or (
842
+ self.geometry.coords.size != self.coords[-1].size
843
+ )
844
+
845
+ if reset_flag:
846
+ self.reset()
847
+
848
+ # Coordinates may be updated here.
849
+ if self.reparam_when == "before" and hasattr(
850
+ self.geometry, "reparametrize"
851
+ ):
852
+ # This call actually returns a bool, but right now we just drop it.
853
+ self.geometry.reparametrize()
854
+
855
+ self.coords.append(self.geometry.coords.copy())
856
+ self.cart_coords.append(self.geometry.cart_coords.copy())
857
+
858
+ # Determine and store number of currenctly actively optimized images
859
+ try:
860
+ image_inds = self.geometry.image_inds
861
+ image_num = len(image_inds)
862
+ except AttributeError:
863
+ image_inds = [
864
+ 0,
865
+ ]
866
+ image_num = 1
867
+ self.image_inds.append(image_inds)
868
+ self.image_nums.append(image_num)
869
+
870
+ # Here the actual step is obtained from the actual optimizer class.
871
+ step = self.optimize()
872
+ step_norm = np.linalg.norm(step)
873
+ self.log(f"norm(step)={step_norm:.6f} au (rad)")
874
+ for source, target in (
875
+ ("true_energy", "true_energies"),
876
+ ("true_forces", "true_forces"),
877
+ ):
878
+ try:
879
+ if (value := getattr(self.geometry, source)) is not None:
880
+ getattr(self, target).append(value)
881
+ except AttributeError:
882
+ pass
883
+
884
+ if step is None:
885
+ # Remove the previously added coords
886
+ self.coords.pop(-1)
887
+ self.cart_coords.pop(-1)
888
+ continue
889
+
890
+ if self.is_cos:
891
+ self.tangents.append(self.geometry.get_tangents().flatten())
892
+
893
+ self.steps.append(step)
894
+
895
+ # Convergence check
896
+ self.is_converged, conv_info = self.check_convergence()
897
+
898
+ end_time = time.time()
899
+ elapsed_seconds = end_time - start_time
900
+ self.cycle_times.append(elapsed_seconds)
901
+
902
+ if self.dump:
903
+ self.write_cycle_to_file()
904
+ with open(self.current_fn, "w") as handle:
905
+ handle.write(self.geometry.as_xyz())
906
+
907
+ if (
908
+ self.dump
909
+ and self.dump_restart
910
+ and (self.cur_cycle % self.dump_restart) == 0
911
+ ):
912
+ self.dump_restart_info()
913
+
914
+ if (self.cur_cycle % self.print_every) == 0 or self.is_converged:
915
+ self.print_opt_progress(conv_info)
916
+ if self.is_converged:
917
+ self.table.print("Converged!")
918
+ break
919
+ # Allow convergence, before checking for too small steps
920
+ elif self.assert_min_step and (step_norm <= self.min_step_norm):
921
+ raise ZeroStepLength
922
+
923
+ # Update coordinates
924
+ new_coords = self.geometry.coords.copy() + step
925
+ try:
926
+ self.geometry.coords = new_coords
927
+ # Use the actual step. It may differ from the proposed step
928
+ # when internal coordinates are used, as the internal-Cartesian
929
+ # transformation is done iteratively.
930
+ self.steps[-1] = self.geometry.coords - self.coords[-1]
931
+ except RebuiltInternalsException as exception:
932
+ print("Rebuilt internal coordinates!")
933
+ rebuilt_fn = self.get_path_for_fn("rebuilt_primitives.xyz")
934
+ with open(rebuilt_fn, "w") as handle:
935
+ handle.write(self.geometry.as_xyz())
936
+ if self.is_cos:
937
+ for image in self.geometry.images:
938
+ image.reset_coords(exception.typed_prims)
939
+ self.reset()
940
+
941
+ if self.dump:
942
+ self.data_model = get_data_model(
943
+ self.geometry, self.is_cos, self.max_cycles
944
+ )
945
+ self.h5_group_name += "_rebuilt"
946
+ # h5_group = get_h5_group(
947
+ # self.h5_fn,
948
+ # self.h5_group_name,
949
+ # self.data_model,
950
+ # reset=True,
951
+ # )
952
+ # h5_group.file.close()
953
+
954
+ # Coordinates may be updated here.
955
+ if (self.reparam_when == "after") and hasattr(
956
+ self.geometry, "reparametrize"
957
+ ):
958
+ reparametrized = self.geometry.reparametrize()
959
+ else:
960
+ reparametrized = False
961
+
962
+ cur_coords = self.geometry.coords
963
+ prev_coords = self.coords[-1]
964
+ if (
965
+ self.is_cos
966
+ and self.reparam_check_rms
967
+ and reparametrized
968
+ and (cur_coords.size == prev_coords.size)
969
+ ):
970
+ self.log("Did reparametrization")
971
+
972
+ rms = np.sqrt(np.mean((prev_coords - cur_coords) ** 2))
973
+ self.log(f"rms of coordinates after reparametrization={rms:.6f}")
974
+ self.is_converged = rms < self.reparam_thresh
975
+ if self.is_converged:
976
+ self.table.print(
977
+ "Insignificant coordinate change after "
978
+ "reparametrization. Signalling convergence!"
979
+ )
980
+ break
981
+
982
+ # Alternative: calcualte overlap of AFIR force and step. If this
983
+ # overlap is negative the step is taken against the AFIR force.
984
+ if self.monitor_frag_dists_counter > 0:
985
+ interfrag_dist = interfragment_distance(
986
+ *self.fragments, self.geometry.coords3d
987
+ )
988
+ try:
989
+ prev_interfrag_dist = self.interfrag_dists[-1]
990
+ if interfrag_dist > prev_interfrag_dist:
991
+ print("Interfragment distances increased!")
992
+ self.stopped = True # Can't use := in if clause
993
+ break
994
+ except IndexError:
995
+ pass
996
+ self.interfrag_dists.append(interfrag_dist)
997
+ self.monitor_frag_dists_counter -= 1
998
+
999
+ sys.stdout.flush()
1000
+ sign = check_for_end_sign()
1001
+ if sign == "stop":
1002
+ self.stopped = True
1003
+ break
1004
+ elif sign == "converged":
1005
+ self.converged = True
1006
+ self.table.print("Operator indicated convergence!")
1007
+ break
1008
+
1009
+ self.log("")
1010
+ else:
1011
+ self.table.print("Number of cycles exceeded!")
1012
+
1013
+ # Outside loop
1014
+ print()
1015
+ if self.dump:
1016
+ self.out_trj_handle.close()
1017
+
1018
+ if (not self.is_cos) and (not self.stopped):
1019
+ print(self.final_summary())
1020
+ # Remove 'current_geometry.xyz' file
1021
+ try:
1022
+ os.remove(self.current_fn)
1023
+ except FileNotFoundError:
1024
+ self.log(f"Tried to delete '{self.current_fn}'. Couldn't find it.")
1025
+ with open(self.final_fn, "w") as handle:
1026
+ handle.write(self.geometry.as_xyz())
1027
+ self.table.print(
1028
+ f"Wrote final, hopefully optimized, geometry to '{self.final_fn.name}'"
1029
+ )
1030
+ self.postprocess_opt()
1031
+ sys.stdout.flush()
1032
+
1033
+ def _get_opt_restart_info(self):
1034
+ """To be re-implemented in the derived classes."""
1035
+ return dict()
1036
+
1037
+ def _set_opt_restart_info(self, opt_restart_info):
1038
+ """To be re-implemented in the derived classes."""
1039
+ return
1040
+
1041
+ def get_restart_info(self):
1042
+ restart_info = {
1043
+ "geom_info": self.geometry.get_restart_info(),
1044
+ "last_cycle": self.cur_cycle,
1045
+ "max_cycles": self.max_cycles,
1046
+ "energies": self.energies,
1047
+ "coords": self.coords,
1048
+ "forces": [forces.tolist() for forces in self.forces],
1049
+ "steps": [step.tolist() for step in self.steps],
1050
+ }
1051
+ restart_info.update(self._get_opt_restart_info())
1052
+ return restart_info
1053
+
1054
+ def set_restart_info(self, restart_info):
1055
+ # Set restart information general to all optimizers
1056
+ self.last_cycle = restart_info["last_cycle"] + 1
1057
+
1058
+ must_resize = self.last_cycle >= self.max_cycles
1059
+ if must_resize:
1060
+ self.max_cycles += restart_info["max_cycles"]
1061
+ # Resize HDF5
1062
+ if self.dump:
1063
+ # h5_group = get_h5_group(self.h5_fn, self.h5_group_name)
1064
+ # resize_h5_group(h5_group, self.max_cycles)
1065
+ # h5_group.file.close()
1066
+ pass
1067
+
1068
+ self.coords = [np.array(coords) for coords in restart_info["coords"]]
1069
+ self.energies = restart_info["energies"]
1070
+ self.forces = [np.array(forces) for forces in restart_info["forces"]]
1071
+ self.steps = [np.array(step) for step in restart_info["steps"]]
1072
+
1073
+ # Set subclass specific information
1074
+ self._set_opt_restart_info(restart_info)
1075
+
1076
+ # Propagate restart information downwards to the geometry
1077
+ self.geometry.set_restart_info(restart_info["geom_info"])
1078
+
1079
+ def dump_restart_info(self):
1080
+ restart_info = self.get_restart_info()
1081
+
1082
+ restart_fn = f"restart_{self.cur_cycle:03d}.yaml"
1083
+ restart_yaml = yaml.dump(restart_info)
1084
+ self.write_to_out_dir(restart_fn, restart_yaml)