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
mlmm/trj2fig.py ADDED
@@ -0,0 +1,448 @@
1
+ # mlmm/trj2fig.py
2
+
3
+ """
4
+ Energy-profile utility: extract energies from XYZ trajectories and export Plotly figures / CSV.
5
+
6
+ Example:
7
+ mlmm trj2fig -i traj.xyz -o energy.png energy.csv --unit kcal
8
+
9
+ For detailed documentation, see: docs/trj2fig.md
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import csv
16
+ import re
17
+ from pathlib import Path
18
+ from typing import List, Optional, Sequence, Tuple
19
+
20
+ import click
21
+ import plotly.graph_objs as go
22
+ from ase import Atoms
23
+ from ase.io import read
24
+ from pysisyphus.constants import AU2EV, AU2KCALPERMOL
25
+
26
+ AXIS_WIDTH = 3 # axis and tick thickness
27
+ FONT_SIZE = 18 # tick-label font size
28
+ AXIS_TITLE_SIZE = 20 # axis-title font size
29
+ LINE_WIDTH = 2 # curve width
30
+ MARKER_SIZE = 6 # marker size
31
+
32
+
33
+ # ---------------------------------------------------------------------
34
+ # File helpers
35
+ # ---------------------------------------------------------------------
36
+ def read_energies_xyz(fname: Path | str) -> List[float]:
37
+ """
38
+ Extract Hartree energies from the second-line comment of each XYZ frame.
39
+ """
40
+ energies: List[float] = []
41
+ with open(fname, encoding="utf-8") as fh:
42
+ while (hdr := fh.readline()):
43
+ try:
44
+ nat = int(hdr.strip())
45
+ except ValueError: # reached a non-XYZ header
46
+ break
47
+ comment = fh.readline().strip()
48
+ m = re.search(r"(-?\d+(?:\.\d+)?)", comment)
49
+ if not m:
50
+ raise RuntimeError(f"Energy not found in comment: {comment}")
51
+ energies.append(float(m.group(1)))
52
+ for _ in range(nat): # skip coordinates
53
+ fh.readline()
54
+ if not energies:
55
+ raise RuntimeError(f"No energy data in {fname}")
56
+ return energies
57
+
58
+
59
+ def recompute_energies(
60
+ traj_path: Path,
61
+ charge: Optional[int],
62
+ multiplicity: Optional[int],
63
+ ) -> List[float]:
64
+ try:
65
+ import torch
66
+ from fairchem.core import pretrained_mlip
67
+ from fairchem.core.datasets import data_list_collater
68
+ from fairchem.core.datasets.atomic_data import AtomicData
69
+ except Exception as exc:
70
+ raise RuntimeError(
71
+ "Energy recomputation requires fairchem-core and torch."
72
+ ) from exc
73
+
74
+ frames_obj = read(traj_path, index=":", format="xyz")
75
+ frames = [frames_obj] if isinstance(frames_obj, Atoms) else list(frames_obj)
76
+ if not frames:
77
+ raise RuntimeError(f"No frames found in {traj_path}")
78
+
79
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
80
+ predictor = pretrained_mlip.get_predict_unit("uma-s-1p1", device=str(device))
81
+ predictor.model.eval()
82
+ backbone = getattr(getattr(predictor.model, "module", predictor.model), "backbone", None)
83
+ uma_max_neigh = getattr(backbone, "max_neighbors", None)
84
+ uma_radius = getattr(backbone, "cutoff", None)
85
+
86
+ q = int(charge if charge is not None else 0)
87
+ mult = int(multiplicity if multiplicity is not None else 1)
88
+ energies_h: List[float] = []
89
+ for atoms in frames:
90
+ atoms.info.update({"charge": q, "spin": mult})
91
+ data = AtomicData.from_ase(
92
+ atoms,
93
+ max_neigh=uma_max_neigh,
94
+ radius=uma_radius,
95
+ r_edges=False,
96
+ ).to(device)
97
+ data.dataset = "omol"
98
+ batch = data_list_collater([data], otf_graph=True).to(device)
99
+ with torch.no_grad():
100
+ res = predictor.predict(batch)
101
+ energy_ev = float(res["energy"].squeeze().detach().item())
102
+ energies_h.append(energy_ev / AU2EV)
103
+ return energies_h
104
+
105
+
106
+ # ---------------------------------------------------------------------
107
+ # Transformation
108
+ # ---------------------------------------------------------------------
109
+ def _parse_reference_spec(spec: str | None) -> str | int | None:
110
+ """
111
+ Normalize the reference specification:
112
+ - "init" (case-insensitive) -> "init"
113
+ - "none"/"null" -> None
114
+ - integer-like string -> int
115
+ """
116
+ if spec is None:
117
+ return "init"
118
+ s = str(spec).strip()
119
+ lower = s.lower()
120
+ if lower in {"none", "null"}:
121
+ return None
122
+ if lower == "init":
123
+ return "init"
124
+ try:
125
+ return int(s)
126
+ except ValueError:
127
+ raise ValueError(
128
+ f'Invalid -r/--reference: {spec!r}. Use "init", "None", or an integer index.'
129
+ )
130
+
131
+
132
+ def _resolve_reference_index(
133
+ n_frames: int, ref_spec: str | int | None, reverse_x: bool
134
+ ) -> Tuple[Optional[int], bool]:
135
+ """
136
+ Decide the reference index and whether to compute a ΔE series.
137
+
138
+ Returns (reference_index or None, is_delta)
139
+ """
140
+ if ref_spec is None:
141
+ return None, False # absolute energies
142
+ if ref_spec == "init":
143
+ idx = 0 if not reverse_x else n_frames - 1
144
+ return idx, True
145
+ # integer index
146
+ idx = int(ref_spec)
147
+ if idx < 0 or idx >= n_frames:
148
+ raise IndexError(f"Reference index {idx} out of range (0..{n_frames-1}).")
149
+ return idx, True
150
+
151
+
152
+ def transform_series(
153
+ energies_hartree: Sequence[float],
154
+ ref_spec_raw: str | None,
155
+ unit: str,
156
+ reverse_x: bool,
157
+ ) -> Tuple[List[float], str, bool]:
158
+ """
159
+ Compute the y-series and its axis label.
160
+
161
+ Returns (values, ylabel, is_delta)
162
+ """
163
+ ref_spec = _parse_reference_spec(ref_spec_raw)
164
+ ref_idx, is_delta = _resolve_reference_index(len(energies_hartree), ref_spec, reverse_x)
165
+
166
+ scale = AU2KCALPERMOL if unit == "kcal" else 1.0
167
+ if is_delta:
168
+ base = energies_hartree[ref_idx] # type: ignore[index]
169
+ values = [float((e - base) * scale) for e in energies_hartree]
170
+ ylabel = f"ΔE ({'kcal/mol' if unit == 'kcal' else 'hartree'})"
171
+ else:
172
+ values = [float(e * scale) for e in energies_hartree]
173
+ ylabel = f"E ({'kcal/mol' if unit == 'kcal' else 'hartree'})"
174
+
175
+ return values, ylabel, is_delta
176
+
177
+
178
+ # ---------------------------------------------------------------------
179
+ # Plotting
180
+ # ---------------------------------------------------------------------
181
+ def _axis_template() -> dict:
182
+ return dict(
183
+ showline=True,
184
+ linewidth=AXIS_WIDTH,
185
+ linecolor="#1C1C1C",
186
+ mirror=True,
187
+ ticks="inside",
188
+ tickwidth=AXIS_WIDTH,
189
+ tickcolor="#1C1C1C",
190
+ tickfont=dict(size=FONT_SIZE, color="#1C1C1C"),
191
+ gridcolor="lightgrey",
192
+ gridwidth=0.5,
193
+ zeroline=False,
194
+ )
195
+
196
+
197
+ def build_figure(delta_or_abs: Sequence[float], ylabel: str, reverse_x: bool) -> go.Figure:
198
+ """
199
+ Build a Plotly figure without a title.
200
+ """
201
+ fig = go.Figure(
202
+ go.Scatter(
203
+ x=list(range(len(delta_or_abs)))),
204
+ )
205
+ fig.data[0].update(
206
+ y=list(delta_or_abs),
207
+ mode="lines+markers",
208
+ marker=dict(size=MARKER_SIZE),
209
+ line=dict(shape="spline", smoothing=1.0, width=LINE_WIDTH),
210
+ )
211
+
212
+ xaxis_conf = _axis_template() | {
213
+ "title": dict(text="Frame", font=dict(size=AXIS_TITLE_SIZE, color="#1C1C1C"))
214
+ }
215
+ if reverse_x:
216
+ xaxis_conf["autorange"] = "reversed"
217
+
218
+ fig.update_layout(
219
+ xaxis=xaxis_conf,
220
+ yaxis=_axis_template() | {
221
+ "title": dict(text=ylabel, font=dict(size=AXIS_TITLE_SIZE, color="#1C1C1C"))
222
+ },
223
+ plot_bgcolor="white",
224
+ paper_bgcolor="white",
225
+ margin=dict(l=80, r=40, t=40, b=80),
226
+ )
227
+ return fig
228
+
229
+
230
+ def save_outputs(
231
+ outs: Sequence[Path],
232
+ fig: Optional[go.Figure],
233
+ energies: Sequence[float],
234
+ values: Sequence[float],
235
+ unit: str,
236
+ is_delta: bool,
237
+ ) -> None:
238
+ """
239
+ Write all requested outputs.
240
+ """
241
+ for out in outs:
242
+ ext = out.suffix.lower()
243
+ if ext == ".csv":
244
+ write_csv(out, energies, values, unit, is_delta)
245
+ elif ext == ".html":
246
+ assert fig is not None
247
+ fig.write_html(out)
248
+ click.echo(f"[trj2fig] Saved figure -> {out}")
249
+ elif ext in {".png", ".jpg", ".jpeg", ".pdf", ".svg"}:
250
+ assert fig is not None
251
+ kw = {"engine": "kaleido"}
252
+ if ext == ".png":
253
+ kw["scale"] = 2 # high-resolution PNG
254
+ fig.write_image(out, **kw)
255
+ click.echo(f"[trj2fig] Saved figure -> {out}")
256
+ else:
257
+ raise ValueError(f"Unsupported format: {ext}")
258
+
259
+
260
+ def write_csv(
261
+ out: Path,
262
+ energies_hartree: Sequence[float],
263
+ series: Sequence[float],
264
+ unit: str,
265
+ is_delta: bool,
266
+ ) -> None:
267
+ """
268
+ Save energies (hartree) and ΔE/E series to CSV.
269
+ """
270
+ colname = (f"delta_{unit}" if is_delta else f"energy_{unit}")
271
+ with out.open("w", newline="", encoding="utf-8") as fh:
272
+ w = csv.writer(fh)
273
+ w.writerow(["frame", "energy_hartree", colname])
274
+ for i, (eh, y) in enumerate(zip(energies_hartree, series)):
275
+ w.writerow([i, f"{eh:.8f}", f"{y:.6f}"])
276
+ click.echo(f"[trj2fig] Saved CSV -> {out}")
277
+
278
+
279
+ # ---------------------------------------------------------------------
280
+ # CLI (argparse)
281
+ # ---------------------------------------------------------------------
282
+ def parse_cli() -> argparse.Namespace:
283
+ p = argparse.ArgumentParser(
284
+ prog="trj2fig",
285
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
286
+ description="Plot ΔE or E from an XYZ trajectory and export a figure and/or CSV (no title).",
287
+ )
288
+ p.add_argument("-i", "--input", required=True, help="XYZ trajectory file")
289
+ p.add_argument(
290
+ "-o",
291
+ "--out",
292
+ nargs="+",
293
+ default=["energy.png"],
294
+ help="Output file(s) [.png/.html/.svg/.pdf/.csv]. Multiple names allowed.",
295
+ )
296
+ p.add_argument("--unit", choices=["kcal", "hartree"], default="kcal", help="Energy unit")
297
+ p.add_argument(
298
+ "-r",
299
+ "--reference",
300
+ default="init",
301
+ help='Reference: "init" (initial frame; last frame if --reverse-x), "None" (absolute E), or an integer index.',
302
+ )
303
+ p.add_argument(
304
+ "-q",
305
+ "--charge",
306
+ type=int,
307
+ required=False,
308
+ help="Total charge. Recompute energies when supplied.",
309
+ )
310
+ p.add_argument(
311
+ "-m",
312
+ "--multiplicity",
313
+ type=int,
314
+ required=False,
315
+ help="Spin multiplicity (2S+1). Recompute energies when supplied.",
316
+ )
317
+ p.add_argument(
318
+ "--reverse-x",
319
+ action="store_true",
320
+ help="Reverse the x-axis (last frame on the left).",
321
+ )
322
+ return p.parse_args()
323
+
324
+
325
+ def run_trj2fig(
326
+ input_path: Path,
327
+ outs: Sequence[Path],
328
+ unit: str,
329
+ reference: str,
330
+ reverse_x: bool,
331
+ charge: Optional[int] = None,
332
+ multiplicity: Optional[int] = None,
333
+ ) -> None:
334
+ traj = input_path.expanduser().resolve()
335
+ if not traj.is_file():
336
+ raise FileNotFoundError(traj)
337
+
338
+ if charge is None and multiplicity is None:
339
+ energies = read_energies_xyz(traj)
340
+ else:
341
+ click.echo("[trj2fig] Recomputing energies with UMA model ...")
342
+ energies = recompute_energies(traj, charge, multiplicity)
343
+ values, ylabel, is_delta = transform_series(energies, reference, unit, reverse_x)
344
+
345
+ need_plot = any(Path(o).suffix.lower() != ".csv" for o in outs)
346
+ fig = build_figure(values, ylabel, reverse_x) if need_plot else None
347
+
348
+ out_paths = [Path(o).expanduser().resolve() for o in outs]
349
+ save_outputs(out_paths, fig, energies, values, unit, is_delta)
350
+
351
+
352
+ def main() -> None:
353
+ args = parse_cli()
354
+ run_trj2fig(
355
+ Path(args.input),
356
+ args.out,
357
+ args.unit,
358
+ args.reference,
359
+ args.reverse_x,
360
+ args.charge,
361
+ args.multiplicity,
362
+ )
363
+
364
+
365
+ # ---------------------------------------------------------------------
366
+ # Click wrapper for package CLI integration
367
+ # ---------------------------------------------------------------------
368
+ @click.command(
369
+ name="trj2fig",
370
+ help="Plot ΔE or E from an XYZ trajectory and export figure/CSV.",
371
+ context_settings={"help_option_names": ["-h", "--help"]},
372
+ )
373
+ @click.option(
374
+ "-i",
375
+ "--input",
376
+ "input_path", # explicit internal argument name
377
+ required=True,
378
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
379
+ help="XYZ trajectory file",
380
+ )
381
+ @click.option(
382
+ "-o",
383
+ "--out",
384
+ "outs",
385
+ multiple=True, # allow repeating -o
386
+ default=(), # default is empty (we inject the fallback later)
387
+ type=click.Path(dir_okay=False, path_type=Path),
388
+ help="Output file(s). You can repeat -o, and/or list extra filenames after options "
389
+ "(.png/.html/.svg/.pdf/.csv). If nothing is given, defaults to energy.png.",
390
+ )
391
+ @click.argument(
392
+ "extra_outs", # also accept extra filenames provided positionally after options
393
+ nargs=-1,
394
+ type=click.Path(dir_okay=False, path_type=Path),
395
+ )
396
+ @click.option(
397
+ "--unit",
398
+ type=click.Choice(["kcal", "hartree"]),
399
+ default="kcal",
400
+ help="Energy unit.",
401
+ )
402
+ @click.option(
403
+ "-r",
404
+ "--reference",
405
+ default="init",
406
+ help='Reference: "init" (initial frame; last frame if --reverse-x), "None" (absolute E), or an integer index.',
407
+ )
408
+ @click.option(
409
+ "-q",
410
+ "--charge",
411
+ type=int,
412
+ default=None,
413
+ help="Total charge. Recompute energies when supplied.",
414
+ )
415
+ @click.option(
416
+ "-m",
417
+ "--multiplicity",
418
+ type=int,
419
+ default=None,
420
+ help="Spin multiplicity (2S+1). Recompute energies when supplied.",
421
+ )
422
+ @click.option(
423
+ "--reverse-x/--no-reverse-x",
424
+ "reverse_x",
425
+ default=False,
426
+ show_default=True,
427
+ help="Reverse the x-axis (last frame on the left).",
428
+ )
429
+ def cli(
430
+ input_path: Path,
431
+ outs: Tuple[Path, ...],
432
+ extra_outs: Tuple[Path, ...],
433
+ unit: str,
434
+ reference: str,
435
+ charge: Optional[int],
436
+ multiplicity: Optional[int],
437
+ reverse_x: bool,
438
+ ) -> None:
439
+ # Combine outputs from -o with positional filenames that follow the options
440
+ all_outs: List[Path] = list(outs) + list(extra_outs)
441
+ if not all_outs:
442
+ # Use the default when nothing is specified
443
+ all_outs = [Path("energy.png")]
444
+ run_trj2fig(input_path, all_outs, unit, reference, reverse_x, charge, multiplicity)
445
+
446
+
447
+ if __name__ == "__main__":
448
+ main()