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,508 @@
1
+ import numpy as np
2
+ from scipy.interpolate import splprep, splev
3
+
4
+ from pysisyphus.constants import AU2KCALPERMOL
5
+ from pysisyphus.intcoords.exceptions import (
6
+ DifferentCoordLengthsException,
7
+ DifferentPrimitivesException,
8
+ RebuiltInternalsException,
9
+ )
10
+ from pysisyphus.cos.ChainOfStates import ChainOfStates
11
+ from pysisyphus.cos.GrowingChainOfStates import GrowingChainOfStates
12
+
13
+
14
+ # [1] https://aip.scitation.org/doi/abs/10.1063/1.1691018
15
+ # Peters, 2004
16
+ # [2] https://aip.scitation.org/doi/abs/10.1063/1.4804162
17
+ # Zimmerman, 2013
18
+
19
+
20
+ class GrowingString(GrowingChainOfStates):
21
+ def __init__(
22
+ self,
23
+ images,
24
+ calc_getter,
25
+ perp_thresh=0.05,
26
+ param="equi",
27
+ reparam_every=2,
28
+ reparam_every_full=3,
29
+ reparam_tol=None,
30
+ reparam_check="rms",
31
+ max_micro_cycles=5,
32
+ reset_dlc=True,
33
+ climb=False,
34
+ **kwargs,
35
+ ):
36
+ assert len(images) >= 2, "Need at least 2 images for GrowingString."
37
+ if len(images) > 2:
38
+ images = [images[0], images[-1]]
39
+ print("More than 2 images given. Will only use first and last image!")
40
+ if climb:
41
+ climb = "one"
42
+ super().__init__(images, calc_getter, climb=climb, **kwargs)
43
+
44
+ self.perp_thresh = perp_thresh
45
+ self.param = param
46
+ self.reparam_every = int(reparam_every)
47
+ self.reparam_every_full = int(reparam_every_full)
48
+ assert (
49
+ self.reparam_every >= 1 and self.reparam_every_full >= 1
50
+ ), "reparam_every and reparam_every_full must be positive integers!"
51
+ if reparam_tol is not None:
52
+ self.reparam_tol = float(reparam_tol)
53
+ assert self.reparam_tol > 0
54
+ else:
55
+ self.reparam_tol = 1 / (self.max_nodes + 2) / 2
56
+ self.log(f"Using reparametrization tolerance of {self.reparam_tol:.4f}")
57
+ self.reparam_check = reparam_check
58
+ assert self.reparam_check in ("norm", "rms")
59
+ self.max_micro_cycles = int(max_micro_cycles)
60
+ self.reset_dlc = bool(reset_dlc)
61
+
62
+ left_img, right_img = self.images
63
+
64
+ self.left_string = [
65
+ left_img,
66
+ ]
67
+ self.right_string = [
68
+ right_img,
69
+ ]
70
+
71
+ # The desired spacing of the nodes in the final string on the
72
+ # normalized arclength.
73
+ self.sk = 1 / (self.max_nodes + 1)
74
+
75
+ self.reparam_in = reparam_every
76
+ self._tangents = None
77
+ self.tangent_list = list()
78
+ self.perp_forces_list = list()
79
+ self.coords_list = list()
80
+
81
+ left_frontier = self.get_new_image(self.lf_ind)
82
+ self.left_string.append(left_frontier)
83
+ right_frontier = self.get_new_image(self.rf_ind)
84
+ self.right_string.append(right_frontier)
85
+ self.new_image_inds = list()
86
+
87
+ def get_cur_param_density(self, kind=None):
88
+ diffs = [
89
+ image - self.images[max(i - 1, 0)] for i, image in enumerate(self.images)
90
+ ]
91
+
92
+ norms = np.linalg.norm(diffs, axis=1)
93
+ param_density = np.cumsum(norms)
94
+ self.log(f"Current string length={param_density[-1]:.6f}")
95
+
96
+ # Energy weighted parametrization density
97
+ if kind == "energy":
98
+ prev_energies = np.array(self.all_energies[-1])
99
+
100
+ if len(prev_energies) != len(self.images):
101
+ return None
102
+
103
+ mean_energies = (prev_energies[1:] + prev_energies[:-1]) / 2
104
+ weights = mean_energies - prev_energies.min()
105
+ # This damps everything a bit.
106
+ weights = np.sqrt(weights)
107
+ param_density = [
108
+ 0,
109
+ ]
110
+ for weight, diff in zip(weights, norms[1:]):
111
+ assert weight > 0.0
112
+ param_density.append(param_density[-1] + weight * diff)
113
+
114
+ param_density = np.array(param_density)
115
+ param_density /= param_density[-1]
116
+
117
+ return param_density
118
+
119
+ def reset_geometries(self, ref_geometry):
120
+ ref_typed_prims = ref_geometry.internal.typed_prims
121
+ self.log(
122
+ f"Resetting image primitives. Got {len(ref_typed_prims)} typed primitives."
123
+ )
124
+ # Do multiple cycles as it may happen, that not all coordinates are valid
125
+ # at every node.
126
+ for i in range(3):
127
+ self.log(f"\tMicro cycle {i:d}")
128
+ intersect = set(self.images[0].internal.typed_prims)
129
+ for j, image in enumerate(self.images):
130
+ image.reset_coords(ref_typed_prims)
131
+ new_typed_prims = set(image.internal.typed_prims)
132
+ self.log(
133
+ f"\tImage {j:02d} now has {len(new_typed_prims)} typed primitives."
134
+ )
135
+ intersect = intersect & new_typed_prims
136
+
137
+ if intersect == set(ref_typed_prims):
138
+ ref_geometry.reset_coords(intersect)
139
+ break
140
+ ref_typed_prims = list(intersect)
141
+ else:
142
+ raise Exception("Too many reset cycles!")
143
+
144
+ def set_coords(self, image, coords):
145
+ try:
146
+ image.coords = coords
147
+ except RebuiltInternalsException:
148
+ print("Rebuilt internal coordinates!")
149
+ self.reset_geometries(image)
150
+
151
+ def get_new_image(self, ref_index):
152
+ """Get new image by taking a step from self.images[ref_index] towards
153
+ the center of the string."""
154
+ new_img = self.images[ref_index].copy(
155
+ coord_kwargs={
156
+ "check_bends": True,
157
+ }
158
+ )
159
+
160
+ if ref_index <= self.lf_ind:
161
+ tangent_ind = ref_index + 1
162
+ insert_ind = tangent_ind
163
+ else:
164
+ tangent_ind = ref_index - 1
165
+ insert_ind = ref_index
166
+ tangent_img = self.images[tangent_ind]
167
+
168
+ # (new_img - tangent_img) points from tangent_img towards new_img.
169
+ # As we want to derive a new image from new_img, we have to step
170
+ # against this vector, so we have to multiply by -1.
171
+ # Why don't we just use (tangent_img - new_img) to get the right
172
+ # direction? In DLC the resulting distance would then be given in
173
+ # the active set U of tangent_img, but we need it in the active set U
174
+ # of new_img.
175
+ # Formulated the other way around the same expression can be used for
176
+ # all coord types.
177
+ try:
178
+ distance = -(new_img - tangent_img)
179
+ except (DifferentCoordLengthsException, DifferentPrimitivesException):
180
+ self.reset_geometries(new_img)
181
+ distance = -(new_img - tangent_img)
182
+
183
+ # The desired step(_length) for the new image be can be easily determined
184
+ # from a simple rule of proportion by relating the actual distance between
185
+ # two images to their parametrization density difference on the normalized
186
+ # arclength and the desired spacing given by self.sk.
187
+ #
188
+ # Δparam_density / distance = self.sk / step
189
+ # step = self.sk / Δparam_density * distance
190
+ cpd = self.get_cur_param_density()
191
+ # As we always want to step in the direction of 'distance' we just take
192
+ # the absolute value of the difference, as we are not interested in the
193
+ # sign.
194
+ param_dens_diff = abs(cpd[ref_index] - cpd[tangent_ind])
195
+ step_length = self.sk / param_dens_diff
196
+ step = step_length * distance
197
+
198
+ new_coords = new_img.coords + step
199
+ self.set_coords(new_img, new_coords)
200
+
201
+ new_img.set_calculator(self.calc_getter())
202
+ ref_calc = self.images[ref_index].calculator
203
+ try:
204
+ chkfiles = ref_calc.get_chkfiles()
205
+ new_img.calculator.set_chkfiles(chkfiles)
206
+ self.log(
207
+ "Set checkfiles from calculator of node "
208
+ f"{ref_index:02d} on calculator of new node."
209
+ )
210
+ except AttributeError:
211
+ self.log("Calculator doesn't support 'get/set_chkfiles()'")
212
+ self.images.insert(insert_ind, new_img)
213
+ self.log(f"Created new image; inserted it before index {insert_ind}.")
214
+ return new_img
215
+
216
+ @property
217
+ def left_size(self):
218
+ return len(self.left_string)
219
+
220
+ @property
221
+ def right_size(self):
222
+ return len(self.right_string)
223
+
224
+ @property
225
+ def string_size(self):
226
+ return self.left_size + self.right_size
227
+
228
+ @property
229
+ def fully_grown(self):
230
+ """Returns wether the string is fully grown. Don't count the first
231
+ and last node."""
232
+ return not ((self.string_size - 2) < self.max_nodes)
233
+
234
+ @property
235
+ def nodes_missing(self):
236
+ """Returns the number of nodes to be grown."""
237
+ return (self.max_nodes + 2) - self.string_size
238
+
239
+ @property
240
+ def lf_ind(self):
241
+ """Index of the left frontier node in self.images."""
242
+ return len(self.left_string) - 1
243
+
244
+ @property
245
+ def rf_ind(self):
246
+ """Index of the right frontier node in self.images."""
247
+ return self.lf_ind + 1
248
+
249
+ @property
250
+ def full_string_image_inds(self):
251
+ left_inds = np.arange(self.left_size)
252
+ right_inds = np.arange(self.max_nodes + 2)[-self.right_size :]
253
+ image_inds = np.concatenate((left_inds, right_inds))
254
+ return image_inds
255
+
256
+ @property
257
+ def image_inds(self):
258
+ return self.full_string_image_inds
259
+
260
+ def spline(self, tangents=False):
261
+ if (not tangents) and (self.param == "energy") and self.fully_grown:
262
+ u = self.get_cur_param_density(kind="energy")
263
+ else:
264
+ u = self.get_cur_param_density()
265
+ reshaped = self.coords.reshape(-1, self.coords_length)
266
+ # To use splprep we have to transpose the coords.
267
+ transp_coords = reshaped.transpose()
268
+ # Spline in batches as scipy can't handle > 11 rows at once
269
+ tcks, us = zip(
270
+ *[
271
+ splprep(transp_coords[i : i + 9], s=0, k=3, u=u)
272
+ for i in range(0, len(transp_coords), 9)
273
+ ]
274
+ )
275
+ return tcks, us
276
+
277
+ def reparam_cart(self, desired_param_density):
278
+ tcks, us = self.spline()
279
+ # Reparametrize mesh
280
+ new_points = np.vstack([splev(desired_param_density, tck) for tck in tcks])
281
+ # Flatten along first dimension.
282
+ new_points = new_points.reshape(-1, len(self.images)).T
283
+ # With a climbing image we ignore the just splined coordinates for the CI
284
+ # and restore its original coordinates.
285
+ for index in self.get_climbing_indices():
286
+ new_points[index] = self.images[index].coords
287
+ self.log(f"Skipped reparametrization of climbing image with index {index}")
288
+ self.coords = new_points.flatten()
289
+ # In contrast to self.reparam_dlc() we don't check if the reparametrization
290
+ # succeeded because it can't fail ;)
291
+
292
+ def reparam_dlc(self, desired_param_density, thresh=1e-3):
293
+ climbing_indices = self.get_climbing_indices()
294
+ # Reparametrization will take place along the tangent between two
295
+ # images. The index of the tangent image depends on wether the image
296
+ # is above or below the desired param_density on the normalized arc.
297
+ #
298
+ # The reparametrization is done in micro cycles, until it is converged.
299
+ cur_param_density = self.get_cur_param_density()
300
+ self.log(f"Density before reparametrization: {cur_param_density}")
301
+ for i, reparam_image in enumerate(self.images[1:-1], 1):
302
+ if i in climbing_indices:
303
+ self.log(f"Skipped reparametrization of climbing image with index {i}")
304
+ continue
305
+ self.log(f"Reparametrizing node {i}")
306
+ for j in range(self.max_micro_cycles):
307
+ diff = (desired_param_density - cur_param_density)[i]
308
+ self.log(f"\t{j}: Δ={diff: .6f}")
309
+ # Do at least one pass
310
+ if (j > 0) and (abs(diff) < thresh):
311
+ break
312
+ # Negative sign: image is too far right and has to be shifted left.
313
+ # Positive sign: image is too far left and has to be shifted right.
314
+ sign = int(np.sign(diff))
315
+ # Index of the tangent image. reparam_image will be shifted along
316
+ # this direction to achieve the desired parametirzation density.
317
+ tangent_ind = i + sign
318
+ tangent_image = self.images[tangent_ind]
319
+ rl = "right" if sign > 0 else "left"
320
+ self.log(f"\t... shifting {rl} towards image {tangent_ind}")
321
+ distance = -(reparam_image - tangent_image)
322
+
323
+ param_dens_diff = abs(
324
+ cur_param_density[tangent_ind] - cur_param_density[i]
325
+ )
326
+ step_length = abs(diff) / param_dens_diff
327
+ step = step_length * distance
328
+ reparam_coords = reparam_image.coords + step
329
+ self.set_coords(reparam_image, reparam_coords)
330
+ cur_param_density = self.get_cur_param_density()
331
+ else:
332
+ self.log(
333
+ f"Reparametrization of node {i} did not converge after "
334
+ f"{self.max_micro_cycles} cycles. Breaking!"
335
+ )
336
+ break
337
+
338
+ cpd_str = np.array2string(cur_param_density, precision=4)
339
+ self.log(f"Param density after reparametrization: {cpd_str}")
340
+
341
+ # This check is disabled at it is not really applicable. While we reparametrize
342
+ # the images the string size may vary wildly, at least in the beginning. Lets
343
+ # say after reparametrization the distance vector between image 0 and 1 is of
344
+ # magnitude 1 and the overall string length is 10. Then image 1 is at 0.1 w.r.t.
345
+ # the parametrization density. If we reparametrize the remaining images the over-
346
+ # all string size may be 8, and now image 1 suddenly sits at 1/8 = 0.125, which
347
+ # may be already above the allowed threshold.
348
+ # Over time the string size will equilibrate and the desired parametrization
349
+ # density will actually be realized.
350
+ # try:
351
+ # # Dont check climbing images
352
+ # np.testing.assert_allclose(
353
+ # np.delete(cur_param_density, climbing_indices),
354
+ # np.delete(desired_param_density, climbing_indices),
355
+ # atol=self.reparam_tol
356
+ # )
357
+ # except AssertionError as err:
358
+ # trj_str = self.as_xyz()
359
+ # fn = "failed_reparametrization_trj.xyz"
360
+ # with open(fn, "w") as handle:
361
+ # handle.write(trj_str)
362
+ # print(f"Wrote coordinates of failed reparametrization to '{fn}'")
363
+ # raise err
364
+
365
+ # Regenerate active set after reparametrization
366
+ if self.reset_dlc and not self.fully_grown:
367
+ [image.internal.set_active_set() for image in self.moving_images]
368
+ self.log(f"Created new DLCs for {len(self.images)} string images.")
369
+ elif self.reset_dlc:
370
+ self.log("Skipping creation of new DLCs, as string is already fully grown.")
371
+
372
+ def get_tangent(self, i):
373
+ # Simple tangent, pointing at each other, for the frontier images.
374
+ if not self.fully_grown and i in (self.lf_ind, self.rf_ind):
375
+ next_ind = i + 1 if (i <= self.lf_ind) else i - 1
376
+ tangent = self.images[next_ind] - self.images[i]
377
+ tangent /= np.linalg.norm(tangent)
378
+ else:
379
+ tangent = super().get_tangent(i, kind="upwinding")
380
+
381
+ return tangent
382
+
383
+ @ChainOfStates.forces.getter
384
+ def forces(self):
385
+ if self._forces is None:
386
+ self.calculate_forces()
387
+ indices = range(len(self.images))
388
+ # In constrast to NEB calculations we only use the perpendicular component
389
+ # of the force, without any spring forces. A desired image distribution is
390
+ # achieved via periodic reparametrization.
391
+ perp_forces = np.array([self.get_perpendicular_forces(i) for i in indices])
392
+ self.perp_forces_list.append(perp_forces.copy().flatten())
393
+ # Add climbing forces
394
+ total_forces = self.set_climbing_forces(perp_forces)
395
+ self._forces = total_forces.flatten()
396
+ return self._forces
397
+
398
+ def reparametrize(self):
399
+ reparametrized = False
400
+ # If this counter reaches 0 reparametrization will occur.
401
+ self.reparam_in -= 1
402
+
403
+ self.new_image_inds = list()
404
+ # Check if new images can be added for incomplete strings.
405
+ if not self.fully_grown:
406
+ perp_forces = self.perp_forces_list[-1].reshape(len(self.images), -1)
407
+ # Calculate norm and rms of the perpendicular force for every
408
+ # node/image on the string.
409
+ to_check = {
410
+ "norm": np.linalg.norm(perp_forces, axis=1),
411
+ "rms": np.sqrt(np.mean(perp_forces**2, axis=1)),
412
+ }
413
+ self.log(
414
+ f"Checking frontier node convergence, threshold={self.perp_thresh:.6f}"
415
+ )
416
+ # We can add a new node if the norm/rms of the perpendicular force is below
417
+ # the threshold.
418
+ def converged(i):
419
+ cur_val = to_check[self.reparam_check][i]
420
+ is_converged = cur_val <= self.perp_thresh
421
+ conv_str = ", converged" if is_converged else ""
422
+ self.log(
423
+ f"\tnode {i:02d}: {self.reparam_check}(perp_forces)={cur_val:.6f}"
424
+ f"{conv_str}"
425
+ )
426
+ return is_converged
427
+
428
+ # New images are added with the same coordinates as the frontier image.
429
+ # We force reparametrization by setting self.reparam_in to 0 to get sane
430
+ # coordinates for the new image(s).
431
+ if converged(self.lf_ind):
432
+ # Insert at the end of the left string, just before the
433
+ # right frontier node.
434
+ new_left_frontier = self.get_new_image(self.lf_ind)
435
+ self.new_image_inds.append(self.left_size)
436
+ self.left_string.append(new_left_frontier)
437
+ self.log("Added new left frontier node.")
438
+ self.reparam_in = 0
439
+ # If an image was just grown in the left substring the string may now
440
+ # be fully grown, so we reavluate 'self.fully_grown' here.
441
+ if (not self.fully_grown) and converged(self.rf_ind):
442
+ # Insert at the end of the right string, just before the
443
+ # current right frontier node.
444
+ new_right_frontier = self.get_new_image(self.rf_ind)
445
+ self.new_image_inds.append(self.left_size)
446
+ self.right_string.append(new_right_frontier)
447
+ self.log("Added new right frontier node.")
448
+ self.reparam_in = 0
449
+ self.log(f"New image indices: {self.new_image_inds}")
450
+
451
+ self.log(
452
+ f"Current string size is {self.left_size}+{self.right_size}="
453
+ f"{self.string_size}. There are still {self.nodes_missing} "
454
+ "nodes to be grown."
455
+ if not self.fully_grown
456
+ else "String is fully grown."
457
+ )
458
+
459
+ if self.reparam_in > 0:
460
+ self.log(
461
+ "Skipping reparametrization. Next reparametrization in "
462
+ f"{self.reparam_in} cycles."
463
+ )
464
+ else:
465
+ # Prepare image reparametrization
466
+ desired_param_density = self.sk * self.full_string_image_inds
467
+ pd_str = np.array2string(desired_param_density, precision=4)
468
+ self.log(f"Desired param density: {pd_str}")
469
+
470
+ # Reparametrize images.
471
+ if self.coord_type == "cart":
472
+ self.reparam_cart(desired_param_density)
473
+ elif self.coord_type == "dlc":
474
+ self.reparam_dlc(desired_param_density, thresh=self.reparam_tol)
475
+ else:
476
+ raise Exception("How did you get here?")
477
+
478
+ self.reparam_in = (
479
+ self.reparam_every_full if self.fully_grown else self.reparam_every
480
+ )
481
+ reparametrized = True
482
+ # Writing is deactivated, as this does not respect an out_dir or
483
+ # something similar.
484
+ # with open("reparametrized_trj.xyz", "w") as handle:
485
+ # handle.write(self.as_xyz())
486
+
487
+ return reparametrized
488
+
489
+ def get_additional_print(self):
490
+ size_str = f"{self.left_size}+{self.right_size}"
491
+ if self.fully_grown:
492
+ size_str = " Full"
493
+ size_info = f"String={size_str: >5s}"
494
+ energies = np.array(self.all_energies[-1])
495
+ barrier = (energies.max() - energies[0]) * AU2KCALPERMOL
496
+ barrier_info = f"(E_hei-E_0)={barrier:6.1f} kcal/mol"
497
+ hei_ind = energies.argmax()
498
+ hei_norm = np.linalg.norm(self.all_true_forces[-1][hei_ind])
499
+ hei_info = f"norm(forces_true,hei)={hei_norm:.6f} E_h/a_0"
500
+ hei_str = f"HEI={hei_ind+1:02d}/{energies.size:02d}"
501
+
502
+ strs = (
503
+ size_info,
504
+ hei_str,
505
+ barrier_info,
506
+ hei_info,
507
+ )
508
+ return "\t" + " ".join(strs)
pysisyphus/cos/NEB.py ADDED
@@ -0,0 +1,189 @@
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+
5
+ from pysisyphus.cos.ChainOfStates import ChainOfStates
6
+ from pysisyphus.cos.stiffness import get_stiff_stress
7
+
8
+ # [1] http://aip.scitation.org/doi/pdf/10.1063/1.1323224
9
+ # 10.1063/1.1323224
10
+ # [2] http://onlinelibrary.wiley.com/doi/10.1002/jcc.20780/pdf
11
+ # 10.1002/jcc.20780
12
+ # [3] https://aip.scitation.org/doi/10.1063/1.2841941
13
+ # Sheppard, 2008
14
+ # [4] https://aip.scitation.org/doi/pdf/10.1063/1.1636455
15
+ # Trygubenko, 2004
16
+ # [5] Nudged Elastic Band Method for Finding Minimum Energy Paths of Transitions
17
+ # Hannes Jónsson , Greg Mills , Karsten W. Jacobsen
18
+ # https://github.com/cstein/neb/blob/master/neb/neb.py
19
+
20
+
21
+ class NEB(ChainOfStates):
22
+ def __init__(
23
+ self,
24
+ images,
25
+ variable_springs=False,
26
+ k_max=0.3,
27
+ k_min=0.1,
28
+ perp_spring_forces=None,
29
+ bandwidth: Optional[float] = None,
30
+ **kwargs,
31
+ ):
32
+ super(NEB, self).__init__(images, **kwargs)
33
+
34
+ assert k_max >= k_min, "k_max must be bigger or equal to k_min!"
35
+ self.variable_springs = variable_springs
36
+ self.k_max = k_max
37
+ self.k_min = k_min
38
+ self.perp_spring_forces = perp_spring_forces
39
+ self.bandwidth = bandwidth
40
+
41
+ self.delta_k = self.k_max - self.k_min
42
+ self.k = list()
43
+
44
+ def update_springs(self):
45
+ # Check if there are enough springs
46
+ if len(self.k) != len(self.images) - 1:
47
+ self.k = np.full(len(self.images) - 1, self.k_min)
48
+ if self.variable_springs:
49
+ self.set_variable_springs()
50
+
51
+ def set_variable_springs(self):
52
+ shifted_energies = self.energy - self.energy.min()
53
+ energy_max = max(shifted_energies)
54
+ energy_ref = 0.85 * energy_max
55
+ for i in range(len(self.k)):
56
+ # The ith spring connects images i-1 and i.
57
+ e_i = i + 1
58
+ ith_energy = max(shifted_energies[e_i], shifted_energies[e_i - 1])
59
+ if ith_energy < energy_ref:
60
+ self.k[i] = self.k_min
61
+ else:
62
+ self.k[i] = self.k_max - self.delta_k * (energy_max - ith_energy) / (
63
+ energy_max - energy_ref
64
+ )
65
+ self.log("updated springs: " + self.fmt_k())
66
+
67
+ def fmt_k(self):
68
+ return ", ".join([str(f"{k:.03f}") for k in self.k])
69
+
70
+ @property
71
+ def parallel_forces(self):
72
+ indices = range(len(self.images))
73
+ par_forces = [self.get_parallel_forces(i) for i in indices]
74
+ return np.array(par_forces).flatten()
75
+
76
+ def get_spring_forces(self, i):
77
+ if i not in self.moving_indices:
78
+ return self.zero_vec.copy()
79
+
80
+ if (i == 0) or (i == len(self.images) - 1):
81
+ # We can't use the last image index because there is one
82
+ # spring less than there are images.
83
+ spring_index = min(i, len(self.images) - 2)
84
+ return self.k[spring_index] * self.get_tangent(i)
85
+
86
+ prev_coords = self.images[i - 1].coords
87
+ ith_coords = self.images[i].coords
88
+ next_coords = self.images[i + 1].coords
89
+ spring_forces = self.k[i] * (next_coords - ith_coords) - (
90
+ ith_coords - prev_coords
91
+ )
92
+ return spring_forces
93
+
94
+ def get_quenched_dneb_forces(self, i):
95
+ """See [3], Sec. VI and [4] Sec. D."""
96
+ if not self.perp_spring_forces or (i not in self.moving_indices):
97
+ return self.zero_vec.copy()
98
+ forces = self.images[i].forces
99
+ tangent = self.get_tangent(i)
100
+ perp_forces = forces - forces.dot(tangent) * tangent
101
+ spring_forces = self.get_spring_forces(i)
102
+ tangent = self.get_tangent(i)
103
+ perp_spring_forces = spring_forces - spring_forces.dot(tangent) * tangent
104
+ dneb_forces = (
105
+ perp_spring_forces - perp_spring_forces.dot(perp_forces) * perp_forces
106
+ )
107
+ perp_norm = np.linalg.norm(perp_forces)
108
+ perp_spring_norm = np.linalg.norm(perp_spring_forces)
109
+
110
+ # Switching function to quench the dneb forces
111
+ # Eq. (15) in [3]
112
+ #
113
+ # If norm(perp_force) >> norm(perp_spring_forces): dneb_factor ~ 1
114
+ # If norm(perp_force) << norm(perp_spring_forces): dneb_factor ~ 0
115
+ #
116
+ # If the perpendicular spring force is much bigger than the
117
+ # perpendicular force the DNEB forces is nearly fully quenched.
118
+ dneb_factor = 2 / np.pi * np.arctan2(perp_norm**2, perp_spring_norm**2)
119
+ dneb_forces_quenched = dneb_factor * dneb_forces
120
+
121
+ # An alternative switchting function is given in [5], Eq. (10)
122
+ # f(phi) = 1/2 * (1 + cos(pi*cos(theta)))
123
+ # f -> 0 for a straight path (theta -> 0°)
124
+ # f -> 1 for a perpendicular path (theta -> 90°)
125
+ # cos(theta) = (R_(i+1) - R_i) * (R_i - R_(i-1)) / (norm of numerator)
126
+
127
+ return dneb_forces_quenched
128
+
129
+ def get_parallel_forces(self, i):
130
+ if i not in self.moving_indices:
131
+ return self.zero_vec.copy()
132
+
133
+ if (i == 0) or (i == len(self.images) - 1):
134
+ # We can't use the last image index because there is one
135
+ # spring less than there are images.
136
+ spring_index = min(i, len(self.images) - 2)
137
+ return self.k[spring_index] * self.get_tangent(i)
138
+
139
+ prev_coords = self.images[i - 1].coords
140
+ ith_coords = self.images[i].coords
141
+ next_coords = self.images[i + 1].coords
142
+ return (
143
+ self.k[i]
144
+ * (
145
+ np.linalg.norm(next_coords - ith_coords)
146
+ - np.linalg.norm(ith_coords - prev_coords)
147
+ )
148
+ * self.get_tangent(i)
149
+ )
150
+
151
+ # See https://stackoverflow.com/a/15786149
152
+ # This way we can reuse the parents setter.
153
+ @ChainOfStates.forces.getter
154
+ def forces(self):
155
+ if self._forces is not None:
156
+ return self._forces
157
+
158
+ org_results = self.calculate_forces()
159
+ self.update_springs()
160
+ indices = range(len(self.images))
161
+ total_forces = np.array(
162
+ [
163
+ self.get_parallel_forces(i)
164
+ + self.get_perpendicular_forces(i)
165
+ + self.get_quenched_dneb_forces(i)
166
+ for i in indices
167
+ ]
168
+ )
169
+ total_forces = self.set_climbing_forces(total_forces)
170
+ if self.bandwidth is not None:
171
+ # kappa = self.k
172
+ # breakpoint()
173
+ stiff_stress = get_stiff_stress(
174
+ bandwidth=self.bandwidth,
175
+ kappa=self.k,
176
+ image_coords=self.image_coords,
177
+ tangents=self.get_tangents(),
178
+ )
179
+ total_forces = total_forces + stiff_stress
180
+ total_forces[self.org_forces_indices] = org_results["forces"][
181
+ self.org_forces_indices
182
+ ]
183
+ if self.org_forces_indices:
184
+ self.log(
185
+ f"Returning unrpojected original forces for image(s): {self.org_forces_indices}."
186
+ )
187
+ self._forces = total_forces.flatten()
188
+
189
+ return self._forces