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,855 @@
1
+ # [1] https://doi.org/10.1002/jcc.23481
2
+ # Exploring transition state structures for intramolecular pathways
3
+ # by the artificial force induced reaction method
4
+ # Maeda, Morokuma et al, 2013
5
+ # [2] https://pubs.acs.org/doi/10.1021/ct200290m
6
+ # Finding Reaction Pathways of Type A + B → X: Toward Systematic
7
+ # Prediction of Reaction Mechanisms
8
+ # Maeda, Morokuma, 2011
9
+
10
+ from dataclasses import dataclass
11
+ import itertools as it
12
+ import logging
13
+ from functools import reduce
14
+ import os
15
+ from pathlib import Path
16
+ from pprint import pformat
17
+ import shutil
18
+ import traceback
19
+ from typing import Callable, Dict, List, Tuple, Optional
20
+
21
+ import numpy as np
22
+ from numpy.typing import NDArray
23
+ from rmsd import kabsch_rmsd
24
+ from scipy.spatial.distance import pdist
25
+ from scipy.optimize import least_squares
26
+
27
+ from pysisyphus import logger as pysis_logger
28
+ from pysisyphus.calculators.AFIR import AFIR, CovRadiiSumZero
29
+ from pysisyphus.calculators import HardSphere
30
+ from pysisyphus.config import AFIR_RMSD_THRESH, OUT_DIR_DEFAULT
31
+ from pysisyphus.constants import BOHR2ANG
32
+ from pysisyphus.cos.NEB import NEB
33
+ from pysisyphus.drivers.opt import run_opt
34
+ from pysisyphus.elem_data import COVALENT_RADII as CR
35
+ from pysisyphus.Geometry import Geometry
36
+ from pysisyphus.helpers import pick_image_inds, check_for_end_sign
37
+ from pysisyphus.helpers_pure import to_sets
38
+ from pysisyphus.intcoords.helpers import get_bond_difference
39
+ from pysisyphus.intcoords.setup import get_pair_covalent_radii
40
+ from pysisyphus.intcoords.setup_fast import find_bonds
41
+ from pysisyphus.optimizers.FIRE import FIRE
42
+ from pysisyphus.xyzloader import make_xyz_str
43
+
44
+
45
+ logger = pysis_logger.getChild("afir")
46
+ logger.setLevel(logging.DEBUG)
47
+ file_handler = logging.FileHandler("afir.log", mode="w", delay=True)
48
+ logger.addHandler(file_handler)
49
+
50
+
51
+ AFIR_BOND_FACTOR = 1.2
52
+
53
+
54
+ @dataclass
55
+ class AFIRPath:
56
+ atoms: tuple
57
+ cart_coords: np.ndarray
58
+ energies: np.ndarray
59
+ forces: np.ndarray
60
+ charge: int
61
+ mult: int
62
+ opt_is_converged: Optional[bool] = None
63
+ gamma: Optional[float] = None
64
+ path_indices: Optional[List[int]] = None
65
+
66
+ def compatible(self, other):
67
+ check = ("atoms", "charge", "mult")
68
+ return all([getattr(self, name) == getattr(other, name) for name in check])
69
+
70
+ def __add__(self, other):
71
+ """This assumes that the first item in other.cart_coords is the same as the
72
+ last item of self.cart_coords."""
73
+ assert self.compatible(other)
74
+
75
+ def conc(name):
76
+ return np.concatenate(
77
+ (getattr(self, name), getattr(other, name)[1:]), axis=0
78
+ )
79
+
80
+ cart_coords = conc("cart_coords")
81
+ energies = conc("energies")
82
+ forces = conc("forces")
83
+
84
+ if self.path_indices is None:
85
+ path_indices = [0] * len(self.cart_coords)
86
+ else:
87
+ path_indices = self.path_indices
88
+ path_indices += [path_indices[-1] + 1] * (len(other.cart_coords) - 1)
89
+
90
+ return AFIRPath(
91
+ atoms=self.atoms,
92
+ cart_coords=cart_coords,
93
+ energies=energies,
94
+ forces=forces,
95
+ charge=self.charge,
96
+ mult=self.mult,
97
+ path_indices=path_indices,
98
+ )
99
+
100
+ def dump_trj(self, fn):
101
+ geom = Geometry(self.atoms, self.cart_coords[0])
102
+ xyzs = [geom.as_xyz(cart_coords=cc) for cc in self.cart_coords]
103
+ with open(fn, "w") as handle:
104
+ handle.write("\n".join(xyzs))
105
+
106
+
107
+ def analyze_afir_path(energies):
108
+ energies = np.array(energies)
109
+ max_ind = energies.argmax()
110
+
111
+ local_minima = list()
112
+ local_maxima = list()
113
+ stationary_points = list()
114
+ for i, en in enumerate(energies[1:-1], 1):
115
+ prev_en = energies[i - 1]
116
+ next_en = energies[i + 1]
117
+ if is_minimum := prev_en > en < next_en:
118
+ local_minima.append(i)
119
+ if is_ts := prev_en < en > next_en:
120
+ local_maxima.append(i)
121
+ if is_minimum or is_ts:
122
+ stationary_points.append(i)
123
+
124
+ if max_ind in stationary_points:
125
+ ts_ind = np.where(stationary_points == max_ind)[0][0]
126
+ prev_min_ind = stationary_points[ts_ind - 1]
127
+ next_min_ind = stationary_points[ts_ind + 1]
128
+ sp_inds = [prev_min_ind, max_ind, next_min_ind]
129
+ else:
130
+ sp_inds = list()
131
+ return sp_inds
132
+
133
+
134
+ ##########################
135
+ # Multi-component AFIR #
136
+ # Helper #
137
+ ##########################
138
+
139
+
140
+ def generate_random_union(geoms, offset=2.0, rng=None):
141
+ """Unite fragments into one Geometry with random fragment orientations.
142
+
143
+ Center, rotate and translate from origin acoording to approximate radius
144
+ and an offset.
145
+ Displace along +x, -x, +y, -y, +z, -z.
146
+
147
+ Results for > 3 fragments don't look so pretty ;).
148
+ """
149
+
150
+ axis_inds = (0, 0, 1, 1, 2, 2)
151
+ axis_inds_num = len(axis_inds)
152
+ axis_translations = np.zeros(axis_inds_num)
153
+ randomized = list()
154
+ for i, geom in enumerate(geoms):
155
+ i_mod = i % axis_inds_num
156
+ geom = geom.copy()
157
+ geom.center()
158
+ geom.rotate(rng=rng)
159
+ axis = axis_inds[i_mod]
160
+ step_size = axis_translations[i_mod] + geom.approximate_radius() + offset
161
+ axis_translations[i_mod] = step_size
162
+ step = np.zeros(3)
163
+ # Alternate between negative and positive direction along x/y/z
164
+ step[axis] = (-1) ** i * step_size
165
+ geom.coords3d += step[None, :]
166
+ randomized.append(geom)
167
+ union = reduce(lambda geom1, geom2: geom1 + geom2, randomized)
168
+ return union
169
+
170
+
171
+ def generate_random_union_ref(geoms, rng=None, opt_kwargs=None):
172
+ """Unite fragments into one Geometry with random fragment orientations."""
173
+
174
+ geoms = [geom.copy() for geom in geoms]
175
+
176
+ if rng is None:
177
+ rng = np.random.default_rng()
178
+
179
+ if opt_kwargs is None:
180
+ opt_kwargs = {}
181
+
182
+ # Random rotations
183
+ for geom in geoms:
184
+ geom.center()
185
+ geom.rotate(rng=rng)
186
+
187
+ # HardSphere optimization to fix overlapping fragments
188
+ #
189
+ # Set up fragment lists.
190
+ fragments = list()
191
+ for geom in geoms:
192
+ geom_inds = np.arange(len(geom.atoms))
193
+ try:
194
+ geom_inds += fragments[-1][-1] + 1
195
+ except IndexError:
196
+ pass
197
+ fragments.append(geom_inds.tolist())
198
+ union = reduce(lambda geom1, geom2: geom1 + geom2, geoms)
199
+ calc = HardSphere(union, frags=fragments, permutations=False, kappa=1.0)
200
+ union.set_calculator(calc)
201
+ _opt_kwargs = {
202
+ "max_step": 0.2,
203
+ "max_cycles": 500,
204
+ }
205
+ _opt_kwargs.update(opt_kwargs)
206
+ opt = FIRE(union, **_opt_kwargs)
207
+ opt.run()
208
+ if not opt.is_converged:
209
+ union = None
210
+ else:
211
+ # Remove HardSphere calculator
212
+ union.clear()
213
+ del union.calculator
214
+
215
+ return union
216
+
217
+
218
+ def prepare_mc_afir(geoms, rng=None, **kwargs):
219
+ """Wrapper for generate_random_union(_ref)."""
220
+ union = generate_random_union(geoms, rng=rng, **kwargs)
221
+
222
+ # Set up list of fragments
223
+ i = 0
224
+ fragments = list()
225
+ for geom in geoms:
226
+ atom_num = len(geom.atoms)
227
+ fragments.append(np.arange(atom_num) + i)
228
+ i += atom_num
229
+
230
+ afir_kwargs = {
231
+ "fragment_indices": fragments,
232
+ }
233
+ broken_bonds = []
234
+ return union, afir_kwargs, broken_bonds
235
+
236
+
237
+ ###########################
238
+ # Single-component AFIR #
239
+ # Helper #
240
+ ###########################
241
+
242
+
243
+ def decrease_distance(coords3d, m, n, frac=0.8):
244
+ c3d_m = coords3d[m]
245
+ c3d_n = coords3d[n]
246
+ dist_vec = c3d_n - c3d_m
247
+ step = (1 - frac) / 2 * dist_vec
248
+ c3d_new = coords3d.copy()
249
+ c3d_new[m] += step
250
+ c3d_new[n] -= step
251
+ return c3d_new
252
+
253
+
254
+ def lstsqs_with_reference(coords3d, ref_coords3d, freeze_atoms=None):
255
+ """Least-squares w.r.t. reference coordinates while keeping some
256
+ atoms frozen."""
257
+
258
+ if freeze_atoms is None:
259
+ freeze_atoms = []
260
+ else:
261
+ freeze_atoms = list(freeze_atoms)
262
+ ref_dists = pdist(ref_coords3d)
263
+
264
+ mask = np.ones_like(coords3d[:, 0], dtype=bool)
265
+ mask[freeze_atoms] = False
266
+ # All atoms w/o the frozen atoms
267
+ coords = coords3d[mask].flatten()
268
+ x0 = coords
269
+
270
+ coords_full = coords3d.copy()
271
+
272
+ def fun(x):
273
+ # Consider all distances, including distances to the fixed atoms 'm' and 'n'.
274
+ coords_full[mask] = x.reshape(-1, 3)
275
+ dists = pdist(coords_full)
276
+ return dists - ref_dists
277
+
278
+ res = least_squares(fun, x0)
279
+ opt_coords = res.x
280
+ coords_full[mask] = opt_coords.reshape(-1, 3)
281
+ return res, coords_full
282
+
283
+
284
+ def weight_function(atoms, coords3d, i, j, p=6):
285
+ cr_sum = sum([CR[atoms[k].lower()] for k in (i, j)])
286
+ r_ij = np.linalg.norm(coords3d[i] - coords3d[j])
287
+ omega = (cr_sum / r_ij) ** p
288
+ return omega
289
+
290
+
291
+ def find_candidates(center, bond_sets):
292
+ center_set = {
293
+ center,
294
+ }
295
+ bonded_to_center = list()
296
+ for bond in bond_sets:
297
+ if center not in bond:
298
+ continue
299
+ bonded_to_center.append(*set(bond) - center_set)
300
+ return bonded_to_center
301
+
302
+
303
+ def automatic_fragmentation(
304
+ atoms, coords3d, frag1, frag2, cycles=2, p=6, bond_factor=1.25
305
+ ):
306
+ """Automatic fragmentation scheme as described in SC-AFIR paper [1]."""
307
+
308
+ frag1 = set(frag1)
309
+ frag2 = set(frag2)
310
+
311
+ def w(m, n):
312
+ """Shortcut for weight function"""
313
+ return weight_function(atoms, coords3d, m, n, p=p)
314
+
315
+ pairs = list(it.product(frag1, frag2))
316
+ weights = [w(m, n) for m, n in pairs]
317
+ max_weight = max(weights)
318
+
319
+ bonds = find_bonds(atoms, coords3d, bond_factor=bond_factor)
320
+ bond_sets = [set(bond) for bond in bonds.tolist()]
321
+
322
+ def filter_candidates(candidates, partners, max_weight):
323
+ to_keep = set()
324
+ for candidate in candidates:
325
+ for partner in partners:
326
+ if candidate == partner:
327
+ break
328
+ weight = w(candidate, partner)
329
+ if weight > max_weight:
330
+ break
331
+ else:
332
+ to_keep.add(candidate)
333
+ return to_keep
334
+
335
+ def grow_fragment(frag1, frag2):
336
+ # Find candidates that are bonded to atoms in frag1. Step 2 in [1].
337
+ candidates = [find_candidates(m, bond_sets) for m in frag1]
338
+ # Filter out candidates that are already contained in frag1
339
+ candidates = [c for c in it.chain(*candidates) if c not in frag1]
340
+ # Filter out candidates with weights that are too big. Step 3/4 in [1].
341
+ candidates = set(filter_candidates(candidates, frag2, max_weight))
342
+ return candidates
343
+
344
+ for _ in range(cycles):
345
+ f1_candidates = grow_fragment(frag1, frag2)
346
+ f2_candidates = grow_fragment(frag2, frag1)
347
+
348
+ # Step 5 in [1].
349
+ f1_candidates = filter_candidates(f1_candidates, f2_candidates, max_weight)
350
+ f2_candidates = filter_candidates(f2_candidates, f1_candidates, max_weight)
351
+
352
+ # Step 6 in [1].
353
+ frag1.update(f1_candidates)
354
+ frag2.update(f2_candidates)
355
+ assert frag1.isdisjoint(
356
+ frag2
357
+ ), "Overlapping fragments detected!" # Sanity check
358
+ return frag1, frag2
359
+
360
+
361
+ def prepare_sc_afir(geom, m, n, bond_factor=AFIR_BOND_FACTOR):
362
+ """Create perturbed geometry, determine fragments and set AFIR calculator."""
363
+ geom = geom.copy()
364
+ atoms = geom.atoms
365
+ org_coords3d = geom.coords3d.copy()
366
+
367
+ def bond_sets(coords3d):
368
+ bonds = find_bonds(atoms, coords3d, bond_factor=bond_factor)
369
+ return set([frozenset(bond) for bond in bonds.tolist()])
370
+
371
+ org_bond_sets = bond_sets(org_coords3d)
372
+
373
+ # Move target atoms closer together along distance vector (decrease distance)
374
+ decr_coords3d = decrease_distance(geom.coords3d, m, n)
375
+ # Optimize remaining coordinates using least-squares, while keeping target
376
+ # atom pair fixed.
377
+ _, opt_coords3d = lstsqs_with_reference(decr_coords3d.copy(), geom.coords3d, (m, n))
378
+ # Determine fragments, using the automated fragmentation
379
+ frag1, frag2 = automatic_fragmentation(atoms, opt_coords3d, [m], [n])
380
+ logger.debug(f"Fragments for target pair [{m}, {n}]: ({frag1}, {frag2})")
381
+ fragment_indices = [list(frag) for frag in (frag1, frag2)]
382
+ # Set lstsq-optimized coordinates and created wrapped calculator
383
+ geom.coords3d = opt_coords3d
384
+
385
+ opt_bond_sets = bond_sets(opt_coords3d)
386
+ broken_bonds = org_bond_sets - opt_bond_sets
387
+
388
+ afir_kwargs = {
389
+ "fragment_indices": fragment_indices,
390
+ # If 'complete_fragments' would be True, all remaining atom indices not
391
+ # present in 'fragment_indices' would be assigned to a third fragment.
392
+ "complete_fragments": False,
393
+ }
394
+
395
+ def set_atoms(inds, atom_type="X", mod_atoms=None):
396
+ if mod_atoms is None:
397
+ mod_atoms = list(atoms)
398
+ for i in inds:
399
+ mod_atoms[i] = atom_type
400
+ return mod_atoms
401
+
402
+ atoms_target = set_atoms((m, n))
403
+ atoms_fragments = set_atoms(frag1)
404
+ atoms_fragments = set_atoms(frag2, atom_type="Q", mod_atoms=atoms_fragments)
405
+
406
+ atoms_coords3d = {
407
+ "original": (atoms, org_coords3d),
408
+ "original w/ target atoms": (atoms_target, org_coords3d),
409
+ "decreased distance": (atoms, decr_coords3d),
410
+ "decreased distance w/ target atoms": (atoms_target, decr_coords3d),
411
+ "lstsq optimized": (atoms, opt_coords3d),
412
+ "lstsq optimized w/ target atoms": (atoms_target, opt_coords3d),
413
+ "lstsq optimized w/ fragments": (atoms_fragments, opt_coords3d),
414
+ }
415
+ trj = "\n".join(
416
+ [
417
+ make_xyz_str(atoms, BOHR2ANG * coords3d, comment=key)
418
+ for key, (atoms, coords3d) in atoms_coords3d.items()
419
+ ]
420
+ )
421
+ return geom, afir_kwargs, broken_bonds, trj
422
+
423
+
424
+ def determine_target_pairs(
425
+ atoms: Tuple[str],
426
+ coords3d: NDArray,
427
+ min_: float = 1.25,
428
+ max_: float = 5.0,
429
+ active_atoms=None,
430
+ ) -> List[Tuple[int]]:
431
+ """Determine possible target m, n atom pairs for SC-AFIR calculations."""
432
+ if active_atoms is None:
433
+ active_atoms = range(len(atoms))
434
+ active_atoms = set(active_atoms)
435
+
436
+ pair_cov_radii = get_pair_covalent_radii(atoms)
437
+ pair_dists = pdist(coords3d)
438
+ quots = pair_dists / pair_cov_radii
439
+ pair_inds = it.combinations(range(len(atoms)), 2)
440
+ target_pairs = list()
441
+ for pair_ind, quot in zip(pair_inds, quots):
442
+ if (min_ <= quot <= max_) and (set(pair_ind) & active_atoms):
443
+ target_pairs.append(pair_ind)
444
+ return target_pairs
445
+
446
+
447
+ def determine_target_pairs_for_geom(geom: Geometry, **kwargs) -> List[Tuple[int]]:
448
+ """Determine possible target m, n atom pairs for SC-AFIR calculations
449
+ from geom."""
450
+ target_pairs = determine_target_pairs(geom.atoms, geom.coords3d, **kwargs)
451
+ return target_pairs
452
+
453
+
454
+ def coordinates_similar(
455
+ test_coords3d: NDArray, ref_coords3d: List[NDArray], rmsd_thresh: float = 1e-2
456
+ ) -> Tuple[bool, int]:
457
+ # When the reference coordinates are an empty list.
458
+ if len(ref_coords3d) == 0:
459
+ return False, -1
460
+ test_centered3d = test_coords3d - test_coords3d.mean(axis=0)[None, :]
461
+ for i, rcoords3d in enumerate(ref_coords3d):
462
+ ref_centered3d = rcoords3d - rcoords3d.mean(axis=0)[None, :]
463
+ rmsd_ = kabsch_rmsd(test_centered3d, ref_centered3d)
464
+ if rmsd_ <= rmsd_thresh:
465
+ break
466
+ else:
467
+ return False, -1
468
+ return True, i
469
+
470
+
471
+ def geom_similar(test_geom: Geometry, ref_geoms: List[Geometry], **kwargs) -> bool:
472
+ return coordinates_similar(
473
+ test_geom.coords3d, [geom.coords3d for geom in ref_geoms], **kwargs
474
+ )
475
+
476
+
477
+ ########################
478
+ # Actual AFIR drivers #
479
+ ########################
480
+
481
+
482
+ def opt_afir_path(geom, calc_getter, afir_kwargs, opt_kwargs=None, out_dir=None):
483
+ """Minimize geometry with AFIR calculator."""
484
+ if opt_kwargs is None:
485
+ opt_kwargs = dict()
486
+ if out_dir is None:
487
+ out_dir = "."
488
+ out_dir = Path(out_dir)
489
+
490
+ actual_calc = calc_getter(out_dir=out_dir / OUT_DIR_DEFAULT)
491
+
492
+ def afir_calc_getter():
493
+ afir_calc = AFIR(actual_calc, out_dir=out_dir, **afir_kwargs)
494
+ return afir_calc
495
+
496
+ _opt_kwargs = {
497
+ "dump": True,
498
+ "out_dir": out_dir,
499
+ "prefix": "afir",
500
+ "max_cycles": 125,
501
+ "overachieve_factor": 3,
502
+ "hessian_update": "flowchart",
503
+ }
504
+ logger.debug(
505
+ "\n".join(
506
+ (
507
+ "afir_kwargs:",
508
+ "\t" + pformat(afir_kwargs),
509
+ "opt_kwargs:",
510
+ "\t" + pformat(_opt_kwargs),
511
+ )
512
+ )
513
+ )
514
+ _opt_kwargs.update(opt_kwargs)
515
+ opt_result = run_opt(geom, afir_calc_getter, opt_key="rfo", opt_kwargs=_opt_kwargs)
516
+ opt = opt_result.opt
517
+
518
+ afir_path = AFIRPath(
519
+ atoms=geom.atoms,
520
+ cart_coords=np.array(opt.cart_coords),
521
+ energies=np.array(opt.true_energies),
522
+ forces=np.array(opt.true_forces),
523
+ opt_is_converged=opt.is_converged,
524
+ charge=actual_calc.charge,
525
+ mult=actual_calc.mult,
526
+ gamma=afir_kwargs["gamma"],
527
+ )
528
+
529
+ return afir_path
530
+
531
+
532
+ def run_afir_path(
533
+ geom,
534
+ calc_getter,
535
+ out_dir,
536
+ gamma_max,
537
+ gamma_interval: Tuple[float, float],
538
+ rng,
539
+ ignore_bonds=None,
540
+ bond_factor=AFIR_BOND_FACTOR,
541
+ afir_kwargs=None,
542
+ opt_kwargs=None,
543
+ ):
544
+ """Driver for AFIR minimizations with increasing gamma values."""
545
+ if ignore_bonds is None:
546
+ ignore_bonds = list()
547
+ if afir_kwargs is None:
548
+ afir_kwargs = dict()
549
+ if opt_kwargs is None:
550
+ opt_kwargs = {}
551
+
552
+ if out_dir.exists():
553
+ dir_contents = os.listdir(out_dir)
554
+ for fn in dir_contents:
555
+ fn = out_dir / fn
556
+ try:
557
+ os.remove(fn)
558
+ except IsADirectoryError:
559
+ shutil.rmtree(fn)
560
+ else:
561
+ os.mkdir(out_dir)
562
+
563
+ # Decreasing the distance between two atoms in SC-AFIR may lead to broken
564
+ # bonds for these two bonds, e.g., hydrogen atoms "that are left behind".
565
+ # These bonds will be formed again when the AFIR function is minimized, but
566
+ # we are not interested in these changes. So they can be ignored here.
567
+ ignore_bonds = set([frozenset(bond) for bond in ignore_bonds])
568
+
569
+ ref_calc = calc_getter()
570
+ ref_energy = ref_calc.get_energy(geom.atoms, geom.cart_coords)["energy"]
571
+ geom_backup = geom.copy()
572
+
573
+ # By using (1.0, 1.0) as interval we can directly start at gamma_max, e.g.,
574
+ # in SC-AFIR.
575
+ gamma_low, gamma_high = gamma_interval
576
+ assert gamma_high >= gamma_low
577
+ gamma_spread = gamma_high - gamma_low
578
+ gamma_0 = (gamma_low + (gamma_spread * rng.random(1)[0])) * gamma_max
579
+ gamma_inrc = 0.1 * gamma_max
580
+
581
+ afir_paths = list()
582
+ best_afir_path = None
583
+ lowest_barrier = None
584
+ gamma = gamma_0
585
+
586
+ # Minimize AFIR functions until gamma exceeds gamma_max.
587
+ logger.info(f"New AFIR run with γ_max={gamma_max:.6f} au")
588
+ while gamma <= gamma_max:
589
+ gamma_ratio = gamma / gamma_max
590
+ logger.info(f"AFIR run with γ={gamma:.6f} au, γ/γ_max={gamma_ratio: >6.2%}")
591
+ _afir_kwargs = afir_kwargs.copy()
592
+ _afir_kwargs["gamma"] = gamma
593
+ try:
594
+ afir_path = opt_afir_path(
595
+ geom,
596
+ calc_getter,
597
+ afir_kwargs=_afir_kwargs,
598
+ opt_kwargs=opt_kwargs,
599
+ out_dir=out_dir,
600
+ )
601
+ # Can happen in SC-AFIR runs when fragments comprise only hydrogens.
602
+ except CovRadiiSumZero:
603
+ logger.warning("Sum of covalent radii is 0.0!")
604
+ best_afir_path = None
605
+ break
606
+ except Exception:
607
+ logger.error(f"Optimization crashed!\n{traceback.format_exc()}")
608
+ best_afir_path = None
609
+ break
610
+
611
+ afir_paths.append(afir_path)
612
+ true_energies = afir_path.energies
613
+
614
+ # Check for changes in bond topology by comparing the result of the current
615
+ # minimization to the initial geometry.
616
+ formed, broken = get_bond_difference(geom_backup, geom, bond_factor=bond_factor)
617
+ formed = to_sets(formed) - ignore_bonds
618
+ broken = to_sets(broken) - ignore_bonds
619
+ if formed or broken:
620
+ max_energy = true_energies.max()
621
+ barrier = max_energy - ref_energy
622
+
623
+ # Always store the first path that leads to a change in bond topology.
624
+ if lowest_barrier is None:
625
+ best_afir_path = afir_path
626
+ lowest_barrier = barrier
627
+ # Break when a path with a lower or higher barrier is detected. This
628
+ # should happen in the cycle after the first change in bond toplogy was
629
+ # detected.
630
+ if barrier < lowest_barrier:
631
+ best_afir_path = afir_path
632
+ lowest_barrier = barrier
633
+ break
634
+ elif barrier > lowest_barrier:
635
+ break
636
+ else:
637
+ barrier = None
638
+
639
+ # Update values for next cycle
640
+ gamma += gamma_inrc
641
+
642
+ # reduce(lambda ap1, ap2: ap1 + ap2, afir_paths).dump_trj("dumped_trj.xyz")
643
+
644
+ # Construct TS guess from highest energy point along the AFIR path.
645
+ if best_afir_path:
646
+ guess_ind = best_afir_path.energies.argmax()
647
+ guess_coords = best_afir_path.cart_coords[guess_ind]
648
+ ts_guess = Geometry(geom.atoms, guess_coords)
649
+ ts_guess.dump_xyz(out_dir / "ts_guess.xyz")
650
+ afir_path_merged = reduce(lambda ap1, ap2: ap1 + ap2, afir_paths)
651
+ else:
652
+ ts_guess = None
653
+ afir_path_merged = None
654
+ return ts_guess, afir_path_merged
655
+
656
+
657
+ def relax_afir_path(atoms, cart_coords, calc_getter, images=15, out_dir=None):
658
+ """Sample imagef from AFIR path and do COS relaxation."""
659
+ image_inds = pick_image_inds(cart_coords, images=images)
660
+ images = [Geometry(atoms, cart_coords[i]) for i in image_inds]
661
+
662
+ # Relax last image
663
+ opt_kwargs = {
664
+ "dump": True,
665
+ "prefix": "last",
666
+ "out_dir": out_dir,
667
+ }
668
+ last_image = images[-1]
669
+ last_image_backup = last_image.copy()
670
+ run_opt(last_image, calc_getter, opt_key="rfo", opt_kwargs=opt_kwargs)
671
+ _, broken = get_bond_difference(last_image, last_image_backup)
672
+ if broken:
673
+ return
674
+
675
+ cos_kwargs = {}
676
+ cos = NEB(images, **cos_kwargs)
677
+ cos_opt_kwargs = {
678
+ "align": True,
679
+ "dump": True,
680
+ "max_cycles": 30,
681
+ "out_dir": out_dir,
682
+ }
683
+ run_opt(cos, calc_getter, opt_key="lbfgs", opt_kwargs=cos_opt_kwargs)
684
+
685
+
686
+ def run_mc_afir_paths(
687
+ geoms: List,
688
+ calc_getter: Callable,
689
+ gamma_max: float,
690
+ rng,
691
+ N_max: int = 5,
692
+ gamma_interval: Tuple[float, float] = (0.0, 1.0),
693
+ afir_kwargs: Optional[Dict] = None,
694
+ opt_kwargs: Optional[Dict] = None,
695
+ ):
696
+ if afir_kwargs is None:
697
+ afir_kwargs = dict()
698
+
699
+ N = 0
700
+ N_0 = 0
701
+ fmt = " >4d"
702
+ while True:
703
+ geom, _afir_kwargs, *_ = prepare_mc_afir(geoms, rng=rng)
704
+ afir_kwargs = afir_kwargs.copy()
705
+ afir_kwargs.update(_afir_kwargs)
706
+ out_dir = Path(f"out_{N:03d}")
707
+
708
+ ts_guess, afir_path = run_afir_path(
709
+ geom,
710
+ calc_getter,
711
+ out_dir,
712
+ gamma_max,
713
+ gamma_interval,
714
+ rng=rng,
715
+ afir_kwargs=afir_kwargs,
716
+ opt_kwargs=opt_kwargs,
717
+ )
718
+ ts_guess_is_new = yield N, ts_guess, afir_path
719
+ if ts_guess_is_new:
720
+ N_0 = N
721
+ # Allow up to consecutive N_max failures
722
+ if (N - N_0) > N_max:
723
+ break
724
+ logger.debug(f"{N_0=:{fmt}}, {N=:{fmt}}, {N-N_0=:{fmt}}, {N_max=:{fmt}}, ")
725
+ N += 1
726
+
727
+
728
+ def run_sc_afir_paths(
729
+ geom: Geometry,
730
+ calc_getter: Callable,
731
+ gamma_max: float,
732
+ rng,
733
+ N_max: int = 5,
734
+ N_sample: int = 0,
735
+ gamma_interval: Tuple[float, float] = (1.0, 1.0), # Start directly with gamma_max
736
+ afir_kwargs: Optional[Dict] = None,
737
+ opt_kwargs: Optional[Dict] = None,
738
+ target_pairs: Optional[List] = None,
739
+ ):
740
+ if target_pairs is None:
741
+ target_pairs = determine_target_pairs_for_geom(geom)
742
+
743
+ if afir_kwargs is None:
744
+ afir_kwargs = dict()
745
+ if opt_kwargs is None:
746
+ opt_kwargs = dict()
747
+ _opt_kwargs = opt_kwargs.copy()
748
+
749
+ i = 0
750
+ while len(target_pairs) > 0:
751
+ m, n = target_pairs.pop(0)
752
+ logger.info(f"Running SC-AFIR with target_pair ({m}, {n}).")
753
+ # _afir_kwargs will contain the automatically determined fragments
754
+ geom_mod, _afir_kwargs, broken_bonds, trj = prepare_sc_afir(geom, m, n)
755
+ afir_kwargs = afir_kwargs.copy()
756
+ afir_kwargs.update(_afir_kwargs)
757
+
758
+ # _opt_kwargs.update({
759
+ # "fragments": [[m, ], [n, ]],
760
+ # "monitor_frag_dists": 5,
761
+ # })
762
+
763
+ out_dir = Path(f"out_{i:03d}")
764
+ ts_guess, afir_path = run_afir_path(
765
+ geom_mod,
766
+ calc_getter,
767
+ out_dir,
768
+ gamma_max,
769
+ gamma_interval,
770
+ rng=rng,
771
+ ignore_bonds=broken_bonds,
772
+ afir_kwargs=afir_kwargs,
773
+ opt_kwargs=_opt_kwargs,
774
+ )
775
+ ts_guess_is_new = yield i, ts_guess, afir_path
776
+ i += 1
777
+ # Here, TS optimization & IRC integration could take place, when the TS is new.
778
+
779
+
780
+ def run_afir_paths(
781
+ afir_key,
782
+ geoms,
783
+ calc_getter,
784
+ afir_kwargs=None,
785
+ opt_kwargs=None,
786
+ seed=None,
787
+ N_sample=None,
788
+ rmsd_thresh: float = AFIR_RMSD_THRESH,
789
+ **kwargs,
790
+ ):
791
+ if seed is None:
792
+ rng = np.random.default_rng()
793
+ seed = rng.integers(1_000_000_000_000)
794
+ logger.info(f"{seed=}")
795
+ rng = np.random.default_rng(seed)
796
+
797
+ if afir_key == "sc":
798
+ assert (
799
+ len(geoms) == 1
800
+ ), f"Expected only 1 geometry for SC-AFIR, but got {len(geoms)}!."
801
+ geoms = geoms[0]
802
+
803
+ afir_funcs = {
804
+ "mc": run_mc_afir_paths,
805
+ "sc": run_sc_afir_paths,
806
+ }
807
+ afir_func = afir_funcs[afir_key]
808
+ afir_coroutine = afir_func(
809
+ geoms,
810
+ calc_getter,
811
+ rng=rng,
812
+ afir_kwargs=afir_kwargs,
813
+ opt_kwargs=opt_kwargs,
814
+ **kwargs,
815
+ )
816
+ ts_guesses = list()
817
+ afir_paths = list()
818
+ stop_sign = "afir_stop"
819
+
820
+ i, ts_guess, afir_path = next(afir_coroutine)
821
+ while True:
822
+ prefix = f"{i:03d}"
823
+ logger.info(f"AFIR run {prefix}")
824
+ # Check if we found enough TS guesses
825
+ if N_sample and (len(ts_guesses) >= N_sample):
826
+ break
827
+
828
+ # Check similarity of TS guess to previously found TS guesses
829
+ try:
830
+ rmsds = [ts_guess.rmsd(guess) for guess in ts_guesses]
831
+ min_rmsd = min(rmsds)
832
+ except AttributeError: # Raised when ts_guess is None
833
+ min_rmsd = None
834
+ except ValueError:
835
+ min_rmsd = rmsd_thresh if (ts_guess is not None) else None
836
+
837
+ if ts_guess_is_new := ((min_rmsd is not None) and (min_rmsd >= rmsd_thresh)):
838
+ logger.debug(f"rmsds different enough! {min_rmsd=:.6f} au")
839
+ ts_guesses.append(ts_guess)
840
+ afir_paths.append(afir_path)
841
+ afir_path.dump_trj(f"{prefix}_afir_path_trj.xyz")
842
+ ts_guess.dump_xyz(f"{prefix}_ts_guess.xyz")
843
+ elif min_rmsd:
844
+ logger.debug(f"rmsds too similar! {min_rmsd=:.6f} au)")
845
+
846
+ sign = check_for_end_sign(add_signs=(stop_sign,))
847
+ if sign == stop_sign:
848
+ break
849
+ logger.info(f"{len(ts_guesses)=: >5d}")
850
+ try:
851
+ i, ts_guess, afir_path = afir_coroutine.send(ts_guess_is_new)
852
+ except StopIteration:
853
+ break
854
+
855
+ return ts_guesses, afir_paths