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/irc.py ADDED
@@ -0,0 +1,638 @@
1
+ # mlmm/irc.py
2
+
3
+ """
4
+ ML/MM IRC calculation using the EulerPC predictor-corrector integrator.
5
+
6
+ Example:
7
+ mlmm irc -i ts.pdb --parm real.parm7 --model-pdb ml_region.pdb -q 0
8
+
9
+ For detailed documentation, see: docs/irc.md
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Any, Dict, Optional, List
16
+
17
+ import gc
18
+ import logging
19
+ import sys
20
+ import textwrap
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ import click
25
+ import time
26
+ import torch
27
+
28
+ from pysisyphus.helpers import geom_loader
29
+ from pysisyphus.irc.EulerPC import EulerPC
30
+ from .mlmm_calc import mlmm
31
+ from .freq import _torch_device, _calc_full_hessian_torch, _align_three_layer_hessian_targets
32
+ from .defaults import (
33
+ GEOM_KW_DEFAULT,
34
+ MLMM_CALC_KW as _UMA_CALC_KW,
35
+ IRC_KW,
36
+ )
37
+ from .utils import (
38
+ apply_ref_pdb_override,
39
+ apply_layer_freeze_constraints,
40
+ convert_xyz_to_pdb,
41
+ set_convert_file_enabled,
42
+ is_convert_file_enabled,
43
+ convert_xyz_like_outputs,
44
+ load_yaml_dict,
45
+ deep_update,
46
+ apply_yaml_overrides,
47
+ pretty_block,
48
+ strip_inherited_keys,
49
+ filter_calc_for_echo,
50
+ format_freeze_atoms_for_echo,
51
+ format_elapsed,
52
+ merge_freeze_atom_indices,
53
+ prepare_input_structure,
54
+ resolve_charge_spin_or_raise,
55
+ parse_indices_string,
56
+ build_model_pdb_from_bfactors,
57
+ build_model_pdb_from_indices,
58
+ yaml_section_has_key,
59
+ )
60
+ from .cli_utils import resolve_yaml_sources, load_merged_yaml_cfg, make_is_param_explicit
61
+
62
+
63
+ # --------------------------
64
+ # Default configuration
65
+ # --------------------------
66
+
67
+ CALC_KW_DEFAULT: Dict[str, Any] = dict(_UMA_CALC_KW)
68
+
69
+ IRC_KW_DEFAULT: Dict[str, Any] = {
70
+ **IRC_KW,
71
+ "dump_fn": "irc_data.h5",
72
+ "dump_every": 5,
73
+ }
74
+
75
+
76
+ def _echo_convert_trj_to_pdb_if_exists(trj_path: Path, ref_pdb: Path, out_path: Path) -> None:
77
+ if not is_convert_file_enabled():
78
+ return
79
+ if trj_path.exists():
80
+ try:
81
+ convert_xyz_to_pdb(trj_path, ref_pdb, out_path)
82
+ click.echo(f"[convert] Wrote '{out_path}'.")
83
+ except Exception as e:
84
+ logger.debug("Failed to convert %s to PDB", trj_path.name, exc_info=True)
85
+ click.echo(f"[convert] WARNING: Failed to convert '{trj_path.name}' to PDB: {e}", err=True)
86
+
87
+
88
+ # --------------------------
89
+ # CLI
90
+ # --------------------------
91
+
92
+ @click.command(
93
+ help="Run an IRC calculation with EulerPC. Only the documented CLI options are accepted; all other settings come from YAML.",
94
+ context_settings={"help_option_names": ["-h", "--help"]},
95
+ )
96
+ @click.option(
97
+ "-i", "--input",
98
+ "input_path",
99
+ type=click.Path(path_type=Path, exists=True, dir_okay=False),
100
+ required=True,
101
+ help="Input structure file (.pdb, .xyz, _trj.xyz, etc.).",
102
+ )
103
+ @click.option(
104
+ "--parm",
105
+ "real_parm7",
106
+ type=click.Path(path_type=Path, exists=True, dir_okay=False),
107
+ required=False,
108
+ help="Amber parm7 topology for the whole enzyme (MM region). "
109
+ "If omitted, must be provided in YAML as calc.real_parm7.",
110
+ )
111
+ @click.option(
112
+ "--model-pdb",
113
+ "model_pdb",
114
+ type=click.Path(path_type=Path, exists=True, dir_okay=False),
115
+ required=False,
116
+ help="PDB defining atoms belonging to the ML region. Optional when --detect-layer is enabled.",
117
+ )
118
+ @click.option(
119
+ "--model-indices",
120
+ "model_indices_str",
121
+ type=str,
122
+ default=None,
123
+ show_default=False,
124
+ help="Comma-separated atom indices for the ML region (ranges allowed like 1-5). "
125
+ "Used when --model-pdb is omitted.",
126
+ )
127
+ @click.option(
128
+ "--model-indices-one-based/--model-indices-zero-based",
129
+ "model_indices_one_based",
130
+ default=True,
131
+ show_default=True,
132
+ help="Interpret --model-indices as 1-based (default) or 0-based.",
133
+ )
134
+ @click.option(
135
+ "--detect-layer/--no-detect-layer",
136
+ "detect_layer",
137
+ default=True,
138
+ show_default=True,
139
+ help="Detect ML/MM layers from input PDB B-factors (B=0/10/20). "
140
+ "If disabled, you must provide --model-pdb or --model-indices.",
141
+ )
142
+ @click.option("-q", "--charge", type=int, required=False,
143
+ help="Total charge; overrides calc.charge from YAML. Required unless --ligand-charge is provided.")
144
+ @click.option("-l", "--ligand-charge", type=str, default=None, show_default=False,
145
+ help="Total charge or per-resname mapping (e.g., GPP:-3,SAM:1) used to derive "
146
+ "charge when -q is omitted (requires PDB input or --ref-pdb).")
147
+ @click.option(
148
+ "-m",
149
+ "--multiplicity",
150
+ "spin",
151
+ type=int,
152
+ default=None,
153
+ show_default=False,
154
+ help="Spin multiplicity (2S+1); overrides calc.spin from YAML.",
155
+ )
156
+ @click.option(
157
+ "--max-cycles", type=int, default=None, help="Maximum number of IRC steps; overrides irc.max_cycles from YAML."
158
+ )
159
+ @click.option("--step-size", type=float, default=None, help="Step length in mass-weighted coordinates; overrides irc.step_length from YAML.")
160
+ @click.option("--root", type=int, default=None, help="Imaginary mode index used for the initial displacement; overrides irc.root from YAML.")
161
+ @click.option(
162
+ "--forward/--no-forward",
163
+ "forward",
164
+ default=None,
165
+ help="Run the forward IRC; overrides irc.forward from YAML.",
166
+ )
167
+ @click.option(
168
+ "--backward/--no-backward",
169
+ "backward",
170
+ default=None,
171
+ help="Run the backward IRC; overrides irc.backward from YAML.",
172
+ )
173
+ @click.option("-o", "--out-dir", type=str, default="./result_irc/", show_default=True, help="Output directory; overrides irc.out_dir from YAML.")
174
+ @click.option(
175
+ "--hessian-calc-mode",
176
+ type=click.Choice(["Analytical", "FiniteDifference"], case_sensitive=False),
177
+ default=None,
178
+ help="How the ML backend builds the Hessian (Analytical or FiniteDifference); overrides calc.hessian_calc_mode from YAML. Default: 'FiniteDifference'. Use 'Analytical' when VRAM is sufficient.",
179
+ )
180
+ @click.option(
181
+ "--config",
182
+ "config_yaml",
183
+ type=click.Path(path_type=Path, exists=True, dir_okay=False),
184
+ default=None,
185
+ help="Base YAML configuration file applied before explicit CLI options.",
186
+ )
187
+ @click.option(
188
+ "--show-config/--no-show-config",
189
+ "show_config",
190
+ default=False,
191
+ show_default=True,
192
+ help="Print resolved configuration and continue execution.",
193
+ )
194
+ @click.option(
195
+ "--dry-run/--no-dry-run",
196
+ "dry_run",
197
+ default=False,
198
+ show_default=True,
199
+ help="Validate options and print the execution plan without running IRC.",
200
+ )
201
+ @click.option(
202
+ "--ref-pdb",
203
+ type=click.Path(path_type=Path, exists=True, dir_okay=False),
204
+ default=None,
205
+ help="Reference PDB topology to use when --input is XYZ (keeps XYZ coordinates).",
206
+ )
207
+ @click.option(
208
+ "--convert-files/--no-convert-files",
209
+ "convert_files",
210
+ default=True,
211
+ show_default=True,
212
+ help="Convert XYZ/TRJ outputs into PDB companions based on the input format.",
213
+ )
214
+ @click.option(
215
+ "-b", "--backend",
216
+ type=click.Choice(["uma", "orb", "mace", "aimnet2"], case_sensitive=False),
217
+ default=None,
218
+ show_default=False,
219
+ help="ML backend for the ONIOM high-level region (default: uma).",
220
+ )
221
+ @click.option(
222
+ "--embedcharge/--no-embedcharge",
223
+ "embedcharge",
224
+ default=False,
225
+ show_default=True,
226
+ help="Enable xTB point-charge embedding correction for MM→ML environmental effects.",
227
+ )
228
+ @click.option(
229
+ "--embedcharge-cutoff",
230
+ "embedcharge_cutoff",
231
+ type=float,
232
+ default=None,
233
+ show_default=False,
234
+ help="Distance cutoff (Å) from ML region for MM point charges in xTB embedding. "
235
+ "Default: 12.0 Å when --embedcharge is enabled.",
236
+ )
237
+ @click.pass_context
238
+ def cli(
239
+ ctx: click.Context,
240
+ input_path: Path,
241
+ real_parm7: Optional[Path],
242
+ model_pdb: Optional[Path],
243
+ model_indices_str: Optional[str],
244
+ model_indices_one_based: bool,
245
+ detect_layer: bool,
246
+ charge: Optional[int],
247
+ ligand_charge: Optional[str],
248
+ spin: Optional[int],
249
+ max_cycles: Optional[int],
250
+ step_size: Optional[float],
251
+ root: Optional[int],
252
+ forward: Optional[bool],
253
+ backward: Optional[bool],
254
+ out_dir: str,
255
+ hessian_calc_mode: Optional[str],
256
+ config_yaml: Optional[Path],
257
+ show_config: bool,
258
+ dry_run: bool,
259
+ ref_pdb: Optional[Path],
260
+ convert_files: bool,
261
+ backend: Optional[str],
262
+ embedcharge: bool,
263
+ embedcharge_cutoff: Optional[float],
264
+ ) -> None:
265
+ set_convert_file_enabled(convert_files)
266
+ _is_param_explicit = make_is_param_explicit(ctx)
267
+
268
+ config_yaml, override_yaml, used_legacy_yaml = resolve_yaml_sources(
269
+ config_yaml=config_yaml,
270
+ override_yaml=None,
271
+ args_yaml_legacy=None,
272
+ )
273
+ merged_yaml_cfg, _, _ = load_merged_yaml_cfg(
274
+ config_yaml=config_yaml,
275
+ override_yaml=None,
276
+ )
277
+
278
+ prepared_input = prepare_input_structure(input_path)
279
+ try:
280
+ apply_ref_pdb_override(prepared_input, ref_pdb)
281
+ except click.BadParameter as e:
282
+ click.echo(f"ERROR: {e}", err=True)
283
+ prepared_input.cleanup()
284
+ sys.exit(1)
285
+ geom_input_path = prepared_input.geom_path
286
+ source_path = prepared_input.source_path
287
+ charge, spin = resolve_charge_spin_or_raise(
288
+ prepared_input, charge, spin,
289
+ ligand_charge=ligand_charge, prefix="[irc]",
290
+ )
291
+
292
+ model_indices: Optional[List[int]] = None
293
+ if model_indices_str:
294
+ try:
295
+ model_indices = parse_indices_string(model_indices_str, one_based=model_indices_one_based)
296
+ except click.BadParameter as e:
297
+ click.echo(f"ERROR: {e}", err=True)
298
+ prepared_input.cleanup()
299
+ sys.exit(1)
300
+ try:
301
+ time_start = time.perf_counter()
302
+
303
+ # --------------------------
304
+ # 1) Assemble configuration: defaults < config < CLI(explicit) < override
305
+ # --------------------------
306
+ config_layer_cfg = load_yaml_dict(config_yaml)
307
+ override_layer_cfg = load_yaml_dict(override_yaml)
308
+
309
+ geom_cfg: Dict[str, Any] = dict(GEOM_KW_DEFAULT)
310
+ calc_cfg: Dict[str, Any] = dict(CALC_KW_DEFAULT)
311
+ irc_cfg: Dict[str, Any] = dict(IRC_KW_DEFAULT)
312
+ # Keep command-level default for detect-layer unless YAML/explicit CLI overrides it.
313
+ calc_cfg["use_bfactor_layers"] = bool(detect_layer)
314
+
315
+ apply_yaml_overrides(
316
+ config_layer_cfg,
317
+ [
318
+ (geom_cfg, (("geom",),)),
319
+ (calc_cfg, (("calc",), ("mlmm",))),
320
+ (irc_cfg, (("irc",),)),
321
+ ],
322
+ )
323
+
324
+ # CLI explicit overrides (after config YAML, before override YAML)
325
+ if backend is not None:
326
+ calc_cfg["backend"] = str(backend).lower()
327
+ if _is_param_explicit("embedcharge"):
328
+ calc_cfg["embedcharge"] = bool(embedcharge)
329
+ if _is_param_explicit("embedcharge_cutoff"):
330
+ calc_cfg["embedcharge_cutoff"] = embedcharge_cutoff
331
+
332
+ if _is_param_explicit("hessian_calc_mode") and hessian_calc_mode is not None:
333
+ calc_cfg["hessian_calc_mode"] = str(hessian_calc_mode)
334
+ if _is_param_explicit("max_cycles") and max_cycles is not None:
335
+ irc_cfg["max_cycles"] = int(max_cycles)
336
+ if _is_param_explicit("step_size") and step_size is not None:
337
+ irc_cfg["step_length"] = float(step_size)
338
+ if _is_param_explicit("root") and root is not None:
339
+ irc_cfg["root"] = int(root)
340
+ if _is_param_explicit("forward") and forward is not None:
341
+ irc_cfg["forward"] = bool(forward)
342
+ if _is_param_explicit("backward") and backward is not None:
343
+ irc_cfg["backward"] = bool(backward)
344
+ if _is_param_explicit("out_dir"):
345
+ irc_cfg["out_dir"] = str(out_dir)
346
+ if _is_param_explicit("detect_layer"):
347
+ calc_cfg["use_bfactor_layers"] = bool(detect_layer)
348
+
349
+ model_charge_value = calc_cfg.get("model_charge", charge)
350
+ if model_charge_value is None:
351
+ model_charge_value = charge
352
+ calc_cfg["model_charge"] = int(model_charge_value)
353
+ if _is_param_explicit("charge"):
354
+ calc_cfg["model_charge"] = int(charge)
355
+
356
+ model_mult_value = calc_cfg.get("model_mult", spin)
357
+ if model_mult_value is None:
358
+ model_mult_value = spin
359
+ calc_cfg["model_mult"] = int(model_mult_value)
360
+ if _is_param_explicit("spin"):
361
+ calc_cfg["model_mult"] = int(spin)
362
+
363
+ calc_cfg["input_pdb"] = str(source_path)
364
+ if real_parm7 is not None:
365
+ calc_cfg["real_parm7"] = str(real_parm7)
366
+ if model_pdb is not None:
367
+ calc_cfg["model_pdb"] = str(model_pdb)
368
+
369
+ apply_yaml_overrides(
370
+ override_layer_cfg,
371
+ [
372
+ (geom_cfg, (("geom",),)),
373
+ (calc_cfg, (("calc",), ("mlmm",))),
374
+ (irc_cfg, (("irc",),)),
375
+ ],
376
+ )
377
+ calc_paths = (("calc",), ("mlmm",))
378
+ partial_explicit = (
379
+ yaml_section_has_key(config_layer_cfg, calc_paths, "return_partial_hessian")
380
+ or yaml_section_has_key(override_layer_cfg, calc_paths, "return_partial_hessian")
381
+ )
382
+ if not partial_explicit:
383
+ calc_cfg["return_partial_hessian"] = True
384
+
385
+ # Normalize any existing freeze list from YAML before wiring it to UMA
386
+ merge_freeze_atom_indices(geom_cfg)
387
+ calc_cfg["freeze_atoms"] = list(geom_cfg.get("freeze_atoms", []))
388
+ if not calc_cfg.get("real_parm7"):
389
+ raise click.BadParameter("Missing --parm (or calc.real_parm7 in YAML).")
390
+
391
+ out_dir_path = Path(irc_cfg["out_dir"]).resolve()
392
+ layer_source_pdb = source_path
393
+ detect_layer_enabled = bool(calc_cfg.get("use_bfactor_layers", True))
394
+ model_pdb_cfg = calc_cfg.get("model_pdb")
395
+
396
+ if show_config:
397
+ click.echo(
398
+ pretty_block(
399
+ "yaml_layers",
400
+ {
401
+ "config": None if config_yaml is None else str(config_yaml),
402
+ "override_yaml": None if override_yaml is None else str(override_yaml),
403
+ "merged_keys": sorted(merged_yaml_cfg.keys()),
404
+ },
405
+ )
406
+ )
407
+
408
+ if dry_run:
409
+ model_region_source = "bfactor"
410
+ if not detect_layer_enabled:
411
+ if model_pdb_cfg is not None:
412
+ model_region_source = "model_pdb"
413
+ elif model_indices:
414
+ model_region_source = "model_indices"
415
+ else:
416
+ raise click.BadParameter("Provide --model-pdb or --model-indices when --no-detect-layer.")
417
+ if detect_layer_enabled and layer_source_pdb.suffix.lower() != ".pdb":
418
+ raise click.BadParameter("--detect-layer requires a PDB input (or --ref-pdb).")
419
+ if (
420
+ not detect_layer_enabled
421
+ and model_pdb_cfg is None
422
+ and model_indices
423
+ and layer_source_pdb.suffix.lower() != ".pdb"
424
+ ):
425
+ raise click.BadParameter("--model-indices requires a PDB input (or --ref-pdb).")
426
+ click.echo(
427
+ pretty_block(
428
+ "dry_run_plan",
429
+ {
430
+ "input_geometry": str(geom_input_path),
431
+ "output_dir": str(out_dir_path),
432
+ "detect_layer": bool(detect_layer_enabled),
433
+ "model_region_source": model_region_source,
434
+ "model_indices_count": 0 if not model_indices else len(model_indices),
435
+ "will_run_irc": True,
436
+ "will_write_trajectories": True,
437
+ "backend": calc_cfg.get("backend", "uma"),
438
+ "embedcharge": bool(calc_cfg.get("embedcharge", False)),
439
+ },
440
+ )
441
+ )
442
+ click.echo("[dry-run] Validation complete. IRC execution was skipped.")
443
+ return
444
+
445
+ out_dir_path.mkdir(parents=True, exist_ok=True)
446
+
447
+ if detect_layer_enabled and layer_source_pdb.suffix.lower() != ".pdb":
448
+ raise click.BadParameter("--detect-layer requires a PDB input (or --ref-pdb).")
449
+
450
+ model_pdb_path: Optional[Path] = None
451
+ layer_info: Optional[Dict[str, List[int]]] = None
452
+
453
+ if detect_layer_enabled:
454
+ try:
455
+ model_pdb_path, layer_info = build_model_pdb_from_bfactors(layer_source_pdb, out_dir_path)
456
+ calc_cfg["use_bfactor_layers"] = True
457
+ click.echo(
458
+ f"[layer] Detected B-factor layers: ML={len(layer_info.get('ml_indices', []))}, "
459
+ f"MovableMM={len(layer_info.get('movable_mm_indices', []))}, "
460
+ f"FrozenMM={len(layer_info.get('frozen_indices', []))}"
461
+ )
462
+ except Exception as e:
463
+ if model_pdb_cfg is None and not model_indices:
464
+ raise click.BadParameter(str(e))
465
+ click.echo(f"[layer] WARNING: {e} Falling back to explicit ML region.", err=True)
466
+ detect_layer_enabled = False
467
+
468
+ if not detect_layer_enabled:
469
+ if model_pdb_cfg is None and not model_indices:
470
+ raise click.BadParameter("Provide --model-pdb or --model-indices when --no-detect-layer.")
471
+ if model_pdb_cfg is not None:
472
+ model_pdb_path = Path(model_pdb_cfg)
473
+ else:
474
+ if layer_source_pdb.suffix.lower() != ".pdb":
475
+ raise click.BadParameter("--model-indices requires a PDB input (or --ref-pdb).")
476
+ try:
477
+ model_pdb_path = build_model_pdb_from_indices(layer_source_pdb, out_dir_path, model_indices or [])
478
+ except Exception as e:
479
+ raise click.BadParameter(str(e))
480
+ calc_cfg["use_bfactor_layers"] = False
481
+
482
+ if model_pdb_path is None:
483
+ raise click.BadParameter("Failed to resolve model PDB for the ML region.")
484
+
485
+ calc_cfg["model_pdb"] = str(model_pdb_path)
486
+ _ = apply_layer_freeze_constraints(
487
+ geom_cfg,
488
+ calc_cfg,
489
+ layer_info,
490
+ echo_fn=click.echo,
491
+ )
492
+ _align_three_layer_hessian_targets(calc_cfg, echo_fn=click.echo)
493
+
494
+ # Pretty-print configuration (expand freeze_atoms for readability)
495
+ click.echo(pretty_block("geom", format_freeze_atoms_for_echo(geom_cfg, key="freeze_atoms")))
496
+ echo_calc = format_freeze_atoms_for_echo(filter_calc_for_echo(calc_cfg), key="freeze_atoms")
497
+ click.echo(pretty_block("calc", echo_calc))
498
+ echo_irc = strip_inherited_keys({**irc_cfg, "out_dir": str(out_dir_path)}, IRC_KW_DEFAULT, mode="same")
499
+ click.echo(pretty_block("irc", echo_irc))
500
+
501
+ # --------------------------
502
+ # 2) Load geometry and configure UMA calculator
503
+ # --------------------------
504
+ coord_type = geom_cfg.get("coord_type", "cart")
505
+ coord_kwargs = dict(geom_cfg)
506
+ coord_kwargs.pop("coord_type", None)
507
+
508
+ geometry = geom_loader(geom_input_path, coord_type=coord_type, **coord_kwargs)
509
+
510
+ # Create mlmm calculator
511
+ calc = mlmm(**calc_cfg)
512
+ geometry.set_calculator(calc)
513
+
514
+ # Seed the initial Hessian — reuse cached TS Hessian when available.
515
+ from .hessian_cache import load as _hess_load, store as _hess_store
516
+ hess_device = _torch_device(calc_cfg.get("ml_device", "auto"))
517
+ cached = _hess_load("ts")
518
+ if cached is not None:
519
+ click.echo("[irc] Reusing cached TS Hessian from tsopt.")
520
+ active_dofs = cached.get("active_dofs")
521
+ h_raw = cached["hessian"]
522
+ if isinstance(h_raw, torch.Tensor):
523
+ h_init = h_raw.to(device=hess_device)
524
+ else:
525
+ h_init = torch.as_tensor(h_raw, dtype=torch.float64, device=hess_device)
526
+ if active_dofs is not None:
527
+ geometry.within_partial_hessian = {
528
+ "active_n_dof": len(active_dofs),
529
+ "full_n_dof": geometry.cart_coords.size,
530
+ "active_dofs": active_dofs,
531
+ "active_atoms": sorted(set(d // 3 for d in active_dofs)),
532
+ }
533
+ else:
534
+ click.echo("[irc] Seeding initial Hessian via shared freq backend.")
535
+ h_init, _ = _calc_full_hessian_torch(
536
+ geometry,
537
+ calc_cfg,
538
+ hess_device,
539
+ refresh_geom_meta=True,
540
+ )
541
+
542
+ geometry.cart_hessian = h_init
543
+ click.echo(f"[irc] Initial Hessian seeded (shape={h_init.shape[0]}x{h_init.shape[1]}).")
544
+ del h_init
545
+
546
+ # --------------------------
547
+ # 3) Construct and run EulerPC
548
+ # --------------------------
549
+ # EulerPC.__init__ forwards **kwargs directly to IRC.__init__
550
+ eulerpc = EulerPC(geometry, **irc_cfg)
551
+
552
+ click.echo("\n=== IRC (EulerPC) started ===\n")
553
+ eulerpc.run()
554
+ click.echo("\n=== IRC (EulerPC) finished ===\n")
555
+
556
+ # Cache IRC endpoint Hessians (Bofill-updated mw → Cartesian)
557
+ def _unmw_and_store(mw_H, key):
558
+ """Un-mass-weight active-DOF Hessian on device, store partial on CPU."""
559
+ import numpy as np
560
+ act = eulerpc._act_dofs
561
+ m_sqrt = geometry.masses_rep ** 0.5
562
+ ms_act = m_sqrt[act]
563
+ if isinstance(mw_H, torch.Tensor):
564
+ ms_t = torch.as_tensor(ms_act, dtype=mw_H.dtype, device=mw_H.device)
565
+ H_cart_act = ms_t.unsqueeze(1) * mw_H * ms_t.unsqueeze(0)
566
+ H_cart_act_np = H_cart_act.detach().cpu().numpy()
567
+ else:
568
+ H_cart_act_np = np.diag(ms_act) @ mw_H @ np.diag(ms_act)
569
+ _hess_store(key, H_cart_act_np, active_dofs=list(act))
570
+
571
+ if getattr(eulerpc, "forward_mw_hessian", None) is not None:
572
+ _unmw_and_store(eulerpc.forward_mw_hessian, "irc_left")
573
+ click.echo("[irc] Cached forward endpoint Hessian as 'irc_left'.")
574
+ if getattr(eulerpc, "mw_hessian", None) is not None:
575
+ _unmw_and_store(eulerpc.mw_hessian, "irc_right")
576
+ click.echo("[irc] Cached backward endpoint Hessian as 'irc_right'.")
577
+
578
+ # --------------------------
579
+ # 4) Convert trajectories to PDB when the input was PDB (or --ref-pdb provided)
580
+ # --------------------------
581
+ if source_path.suffix.lower() == ".pdb":
582
+ ref_pdb_path = source_path.resolve()
583
+
584
+ # Whole IRC trajectory
585
+ _echo_convert_trj_to_pdb_if_exists(
586
+ out_dir_path / f"{irc_cfg.get('prefix','')}{'finished_irc_trj.xyz'}",
587
+ ref_pdb_path,
588
+ out_dir_path / f"{irc_cfg.get('prefix','')}{'finished_irc.pdb'}",
589
+ )
590
+ # Forward/backward trajectories
591
+ _echo_convert_trj_to_pdb_if_exists(
592
+ out_dir_path / f"{irc_cfg.get('prefix','')}{'forward_irc_trj.xyz'}",
593
+ ref_pdb_path,
594
+ out_dir_path / f"{irc_cfg.get('prefix','')}{'forward_irc.pdb'}",
595
+ )
596
+ _echo_convert_trj_to_pdb_if_exists(
597
+ out_dir_path / f"{irc_cfg.get('prefix','')}{'backward_irc_trj.xyz'}",
598
+ ref_pdb_path,
599
+ out_dir_path / f"{irc_cfg.get('prefix','')}{'backward_irc.pdb'}",
600
+ )
601
+ # Single-frame endpoint PDBs (forward_last, backward_last)
602
+ prefix = irc_cfg.get("prefix", "")
603
+ for tag in ("forward_last", "backward_last"):
604
+ endpoint_xyz = out_dir_path / f"{prefix}{tag}.xyz"
605
+ endpoint_pdb = out_dir_path / f"{prefix}{tag}.pdb"
606
+ if endpoint_xyz.exists() and not endpoint_pdb.exists():
607
+ try:
608
+ convert_xyz_to_pdb(endpoint_xyz, ref_pdb_path, endpoint_pdb)
609
+ click.echo(f"[convert] Wrote '{endpoint_pdb}'.")
610
+ except Exception as e:
611
+ logger.debug("Failed to convert %s to PDB", endpoint_xyz.name, exc_info=True)
612
+ click.echo(f"[convert] WARNING: Failed to convert '{tag}.xyz' to PDB: {e}", err=True)
613
+
614
+ # summary.md and key_* outputs are disabled.
615
+ click.echo(format_elapsed("[time] Elapsed Time for IRC", time_start))
616
+
617
+ except KeyboardInterrupt:
618
+ click.echo("\nInterrupted by user.", err=True)
619
+ sys.exit(130)
620
+ except click.BadParameter as e:
621
+ click.echo(f"ERROR: {e}", err=True)
622
+ sys.exit(1)
623
+ except Exception as e:
624
+ tb = textwrap.indent("".join(__import__("traceback").format_exception(type(e), e, e.__traceback__)), " ")
625
+ click.echo("Unhandled exception during IRC:\n" + tb, err=True)
626
+ sys.exit(1)
627
+ finally:
628
+ prepared_input.cleanup()
629
+ # Release GPU memory (model + Hessian) so subsequent stages don't OOM
630
+ calc = eulerpc = geometry = None
631
+ gc.collect() # break cyclic refs inside torch.nn.Module
632
+ if torch.cuda.is_available():
633
+ torch.cuda.empty_cache()
634
+
635
+
636
+ # Script entry point
637
+ if __name__ == "__main__":
638
+ cli()