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/cli_utils.py ADDED
@@ -0,0 +1,166 @@
1
+ """CLI utilities for standardized exception handling and shared boilerplate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import gc
7
+ import os
8
+ import shutil
9
+ import sys
10
+ import textwrap
11
+ import traceback
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Dict, Optional, Tuple, Type
14
+
15
+ import click
16
+
17
+ from .utils import deep_update, load_yaml_dict
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Click parameter source helper
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def make_is_param_explicit(ctx: "click.Context"):
25
+ """Return a helper that checks whether a Click parameter was set explicitly."""
26
+ from click.core import ParameterSource
27
+ def _is_param_explicit(name: str) -> bool:
28
+ try:
29
+ source = ctx.get_parameter_source(name)
30
+ return source not in (None, ParameterSource.DEFAULT)
31
+ except Exception:
32
+ return False
33
+ return _is_param_explicit
34
+
35
+
36
+ _TRUE_VALUES = {"true", "1", "yes", "y", "t"}
37
+ _FALSE_VALUES = {"false", "0", "no", "n", "f"}
38
+
39
+
40
+ def parse_bool(value: Any) -> bool:
41
+ """Parse common boolean strings into bool; raise ValueError on invalid input."""
42
+ if value is None:
43
+ raise ValueError("Invalid boolean value: None. Use True/False.")
44
+ text = str(value).strip().lower()
45
+ if text in _TRUE_VALUES:
46
+ return True
47
+ if text in _FALSE_VALUES:
48
+ return False
49
+ raise ValueError(f"Invalid boolean value: {value!r}. Use True/False.")
50
+
51
+
52
+ def argparse_bool(value: str) -> bool:
53
+ """argparse-compatible boolean parser using parse_bool()."""
54
+ try:
55
+ return parse_bool(value)
56
+ except ValueError as e:
57
+ raise argparse.ArgumentTypeError(str(e))
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # YAML source resolution
62
+ # ---------------------------------------------------------------------------
63
+
64
+ def resolve_yaml_sources(
65
+ config_yaml: Optional[Path],
66
+ override_yaml: Optional[Path],
67
+ args_yaml_legacy: Optional[Path],
68
+ ) -> Tuple[Optional[Path], Optional[Path], bool]:
69
+ """Resolve which YAML files to use, raising on conflicting options."""
70
+ if override_yaml is not None and args_yaml_legacy is not None:
71
+ raise click.BadParameter(
72
+ "Use a single YAML source option."
73
+ )
74
+ if args_yaml_legacy is not None:
75
+ return config_yaml, args_yaml_legacy, True
76
+ return config_yaml, override_yaml, False
77
+
78
+
79
+ def load_merged_yaml_cfg(
80
+ config_yaml: Optional[Path],
81
+ override_yaml: Optional[Path],
82
+ ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
83
+ """Load and merge YAML config and override files.
84
+
85
+ Returns ``(merged, config_dict, override_dict)`` so that callers can
86
+ use the individual layers for staged ``apply_yaml_overrides`` and the
87
+ merged dict for ``show_config`` display without re-reading the files.
88
+ """
89
+ config_dict = load_yaml_dict(config_yaml)
90
+ override_dict = load_yaml_dict(override_yaml)
91
+ merged: Dict[str, Any] = {}
92
+ deep_update(merged, config_dict)
93
+ deep_update(merged, override_dict)
94
+ return merged, config_dict, override_dict
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # File helpers
99
+ # ---------------------------------------------------------------------------
100
+
101
+ def link_or_copy_file(src: Path, dst: Path) -> bool:
102
+ """Create a symlink when possible; fall back to copy."""
103
+ try:
104
+ if dst.exists() or dst.is_symlink():
105
+ if dst.is_dir():
106
+ return False
107
+ dst.unlink()
108
+ rel = os.path.relpath(src, start=dst.parent)
109
+ dst.symlink_to(rel)
110
+ return True
111
+ except OSError:
112
+ try:
113
+ shutil.copy2(src, dst)
114
+ return True
115
+ except OSError:
116
+ return False
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # CLI exception wrapper
121
+ # ---------------------------------------------------------------------------
122
+
123
+ def run_cli(
124
+ fn: Callable[[], None],
125
+ *,
126
+ label: str,
127
+ zero_step_exc: Optional[Type[BaseException]] = None,
128
+ zero_step_msg: Optional[str] = None,
129
+ opt_exc: Optional[Type[BaseException]] = None,
130
+ opt_msg: Optional[str] = None,
131
+ ) -> None:
132
+ """Standard CLI exception handling with consistent messaging."""
133
+ try:
134
+ fn()
135
+ except KeyboardInterrupt:
136
+ click.echo("Interrupted by user.", err=True)
137
+ sys.exit(130)
138
+ except Exception as e:
139
+ if zero_step_exc is not None and isinstance(e, zero_step_exc):
140
+ click.echo(
141
+ zero_step_msg
142
+ or "ERROR: Proposed step length dropped below the minimum allowed (ZeroStepLength).",
143
+ err=True,
144
+ )
145
+ sys.exit(2)
146
+ if opt_exc is not None and isinstance(e, opt_exc):
147
+ msg = opt_msg or "ERROR: Optimization failed - {e}"
148
+ click.echo(msg.format(e=e), err=True)
149
+ sys.exit(3)
150
+ tb = "".join(traceback.format_exception(type(e), e, e.__traceback__))
151
+ click.echo(
152
+ f"Unhandled error during {label}:\n" + textwrap.indent(tb, " "),
153
+ err=True,
154
+ )
155
+ sys.exit(1)
156
+ finally:
157
+ # Release GPU memory (model + Hessian) after CLI command finishes
158
+ # so that subsequent pipeline stages (e.g. tsopt → irc) don't OOM.
159
+ # gc.collect() breaks cyclic refs inside torch.nn.Module.
160
+ gc.collect()
161
+ try:
162
+ import torch
163
+ if torch.cuda.is_available():
164
+ torch.cuda.empty_cache()
165
+ except ImportError:
166
+ pass
mlmm/default_group.py ADDED
@@ -0,0 +1,337 @@
1
+ """Shared Click group helpers for lazy subcommand loading and bool normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from collections.abc import Callable, Mapping
7
+
8
+ import click
9
+
10
+ LazySubcommands = Mapping[str, tuple[str, str, str]]
11
+ BoolOptionsByCommand = Mapping[str, frozenset[str]]
12
+ BoolNegativeAliasesByCommand = Mapping[str, Mapping[str, str]]
13
+ BoolSingleFlagOptionsByCommand = Mapping[str, frozenset[str]]
14
+ PrimaryHelpOptionsByCommand = Mapping[str, frozenset[str]]
15
+ ParserWrapperBoolOptionProviders = Mapping[str, Callable[[], frozenset[str]]]
16
+
17
+ NormalizeBoolArgvFunc = Callable[
18
+ [
19
+ list[str],
20
+ BoolOptionsByCommand,
21
+ BoolOptionsByCommand,
22
+ BoolNegativeAliasesByCommand,
23
+ BoolSingleFlagOptionsByCommand,
24
+ ],
25
+ tuple[list[str], bool],
26
+ ]
27
+ EnsureHelpAdvancedOptionFunc = Callable[[click.Command], click.Command]
28
+ ConfigureSubcommandHelpVisibilityFunc = Callable[
29
+ [str, click.Command, PrimaryHelpOptionsByCommand], click.Command
30
+ ]
31
+ BuildUnavailableCommandFunc = Callable[[str, ImportError], click.Command]
32
+
33
+
34
+ def build_unavailable_command(command_name: str, exc: ImportError) -> click.Command:
35
+ """Return a placeholder command that reports import failure details at runtime."""
36
+ missing = exc.name if isinstance(exc, ModuleNotFoundError) else None
37
+ msg_lines = [
38
+ f"Command '{command_name}' is unavailable because the module could not be imported."
39
+ ]
40
+ if missing:
41
+ msg_lines.append(f"Missing dependency: {missing}")
42
+ msg_lines.append("Install the missing dependency in your runtime environment and retry.")
43
+ else:
44
+ msg_lines.append(f"Import error: {exc}")
45
+
46
+ help_text = (
47
+ f"[Unavailable] {command_name} command.\n"
48
+ "The command failed to import due to a missing dependency."
49
+ )
50
+
51
+ @click.command(name=command_name, help=help_text)
52
+ def _unavailable() -> None:
53
+ raise click.ClickException("\n".join(msg_lines))
54
+
55
+ return _unavailable
56
+
57
+
58
+ class DefaultGroup(click.Group):
59
+ """Click group with default subcommand + lazy loading + bool compatibility normalization."""
60
+
61
+ def __init__(
62
+ self,
63
+ *args,
64
+ default: str | None = None,
65
+ lazy_subcommands: LazySubcommands | None = None,
66
+ command_bool_value_options: BoolOptionsByCommand | None = None,
67
+ command_bool_toggle_options: BoolOptionsByCommand | None = None,
68
+ command_bool_toggle_negative_aliases: BoolNegativeAliasesByCommand | None = None,
69
+ command_bool_single_flag_options: BoolSingleFlagOptionsByCommand | None = None,
70
+ parser_wrapper_subcommands: frozenset[str] | None = None,
71
+ parser_wrapper_bool_option_providers: ParserWrapperBoolOptionProviders | None = None,
72
+ subcommand_primary_help_options: PrimaryHelpOptionsByCommand | None = None,
73
+ normalize_bool_argv: NormalizeBoolArgvFunc | None = None,
74
+ ensure_help_advanced_option: EnsureHelpAdvancedOptionFunc | None = None,
75
+ configure_subcommand_help_visibility: ConfigureSubcommandHelpVisibilityFunc | None = None,
76
+ build_unavailable_command: BuildUnavailableCommandFunc = build_unavailable_command,
77
+ **kwargs,
78
+ ):
79
+ super().__init__(*args, **kwargs)
80
+ if normalize_bool_argv is None:
81
+ raise ValueError("normalize_bool_argv is required")
82
+ if ensure_help_advanced_option is None:
83
+ raise ValueError("ensure_help_advanced_option is required")
84
+ if configure_subcommand_help_visibility is None:
85
+ raise ValueError("configure_subcommand_help_visibility is required")
86
+
87
+ self._default_cmd = default
88
+ self._lazy_subcommands = dict(lazy_subcommands or {})
89
+ self._lazy_cache: dict[str, click.Command] = {}
90
+ self._command_bool_value_options = dict(command_bool_value_options or {})
91
+ self._command_bool_toggle_options = dict(command_bool_toggle_options or {})
92
+ self._command_bool_toggle_negative_aliases = dict(
93
+ command_bool_toggle_negative_aliases or {}
94
+ )
95
+ self._command_bool_single_flag_options = dict(command_bool_single_flag_options or {})
96
+ self._resolved_bool_options_by_command: dict[
97
+ str, tuple[frozenset[str], frozenset[str], dict[str, str], frozenset[str]]
98
+ ] = {}
99
+ self._parser_wrapper_subcommands = set(parser_wrapper_subcommands or set())
100
+ self._parser_wrapper_bool_option_providers = dict(
101
+ parser_wrapper_bool_option_providers or {}
102
+ )
103
+ self._resolved_parser_wrapper_bool_options: dict[str, frozenset[str]] = {}
104
+ self._subcommand_primary_help_options = dict(subcommand_primary_help_options or {})
105
+ self._normalize_bool_argv = normalize_bool_argv
106
+ self._ensure_help_advanced_option = ensure_help_advanced_option
107
+ self._configure_subcommand_help_visibility = configure_subcommand_help_visibility
108
+ self._build_unavailable_command = build_unavailable_command
109
+
110
+ def _resolve_parser_wrapper_bool_options(self, command_name: str) -> frozenset[str]:
111
+ cached = self._resolved_parser_wrapper_bool_options.get(command_name)
112
+ if cached is not None:
113
+ return cached
114
+
115
+ provider = self._parser_wrapper_bool_option_providers.get(command_name)
116
+ if provider is None:
117
+ resolved = frozenset()
118
+ self._resolved_parser_wrapper_bool_options[command_name] = resolved
119
+ return resolved
120
+
121
+ try:
122
+ provided = provider()
123
+ except Exception:
124
+ resolved = frozenset()
125
+ self._resolved_parser_wrapper_bool_options[command_name] = resolved
126
+ return resolved
127
+
128
+ normalized: set[str] = set()
129
+ for name in provided:
130
+ if not name.startswith("--"):
131
+ continue
132
+ if name.startswith("--no-"):
133
+ normalized.add(f"--{name[5:]}")
134
+ else:
135
+ normalized.add(name)
136
+
137
+ resolved = frozenset(normalized)
138
+ self._resolved_parser_wrapper_bool_options[command_name] = resolved
139
+ return resolved
140
+
141
+ @staticmethod
142
+ def _long_option_names(option_names: list[str]) -> tuple[str, ...]:
143
+ return tuple(name for name in option_names if name.startswith("--"))
144
+
145
+ def _resolve_bool_options(
146
+ self, ctx: click.Context, command_name: str
147
+ ) -> tuple[frozenset[str], frozenset[str], dict[str, str], frozenset[str]]:
148
+ cached = self._resolved_bool_options_by_command.get(command_name)
149
+ if cached is not None:
150
+ return cached
151
+
152
+ bool_value_options = set(
153
+ self._command_bool_value_options.get(command_name, frozenset())
154
+ )
155
+ bool_toggle_options = set(
156
+ self._command_bool_toggle_options.get(command_name, frozenset())
157
+ )
158
+ bool_toggle_negative_aliases = dict(
159
+ self._command_bool_toggle_negative_aliases.get(command_name, {})
160
+ )
161
+ bool_single_flag_options = set(
162
+ self._command_bool_single_flag_options.get(command_name, frozenset())
163
+ )
164
+
165
+ bool_toggle_options.update(
166
+ self._resolve_parser_wrapper_bool_options(command_name)
167
+ )
168
+
169
+ command = self.get_command(ctx, command_name)
170
+ if command is not None:
171
+ for param in command.params:
172
+ if not isinstance(param, click.Option):
173
+ continue
174
+
175
+ positive_long_names = self._long_option_names(param.opts)
176
+ if not positive_long_names:
177
+ continue
178
+
179
+ negative_long_names = self._long_option_names(param.secondary_opts)
180
+ if param.is_bool_flag:
181
+ if negative_long_names:
182
+ bool_toggle_options.update(positive_long_names)
183
+ first_negative = negative_long_names[0]
184
+ for index, positive_name in enumerate(positive_long_names):
185
+ negative_name = (
186
+ negative_long_names[index]
187
+ if index < len(negative_long_names)
188
+ else first_negative
189
+ )
190
+ bool_toggle_negative_aliases.setdefault(
191
+ positive_name, negative_name
192
+ )
193
+ else:
194
+ bool_single_flag_options.update(positive_long_names)
195
+ continue
196
+
197
+ if isinstance(param.type, click.types.BoolParamType):
198
+ bool_value_options.update(positive_long_names)
199
+
200
+ resolved = (
201
+ frozenset(bool_value_options),
202
+ frozenset(bool_toggle_options),
203
+ bool_toggle_negative_aliases,
204
+ frozenset(bool_single_flag_options),
205
+ )
206
+ self._resolved_bool_options_by_command[command_name] = resolved
207
+ return resolved
208
+
209
+ def parse_args(self, ctx, args):
210
+ show_help_or_version = any(a in ("-h", "--help", "--version") for a in args)
211
+
212
+ if self._default_cmd is not None and not show_help_or_version:
213
+ if not args or args[0].startswith("-"):
214
+ args.insert(0, self._default_cmd)
215
+
216
+ bool_value_options = self._command_bool_value_options
217
+ bool_toggle_options = self._command_bool_toggle_options
218
+ bool_toggle_negative_aliases = self._command_bool_toggle_negative_aliases
219
+ bool_single_flag_options = self._command_bool_single_flag_options
220
+ if args and not args[0].startswith("-"):
221
+ command_name = args[0]
222
+ (
223
+ command_bool_value_options,
224
+ command_bool_toggle_options,
225
+ command_toggle_negative_aliases,
226
+ command_bool_single_flag_options,
227
+ ) = self._resolve_bool_options(ctx, command_name)
228
+ bool_value_options = {command_name: command_bool_value_options}
229
+ bool_toggle_options = {command_name: command_bool_toggle_options}
230
+ bool_toggle_negative_aliases = {
231
+ command_name: command_toggle_negative_aliases
232
+ }
233
+ bool_single_flag_options = {command_name: command_bool_single_flag_options}
234
+
235
+ args, _ = self._normalize_bool_argv(
236
+ args,
237
+ bool_value_options,
238
+ bool_toggle_options,
239
+ bool_toggle_negative_aliases,
240
+ bool_single_flag_options,
241
+ )
242
+ return super().parse_args(ctx, args)
243
+
244
+ @staticmethod
245
+ def _silence_pysisyphus_loggers():
246
+ """Remove pysisyphus file handlers and suppress log output.
247
+
248
+ pysisyphus creates FileHandler + StreamHandler in its various
249
+ ``__init__.py`` files at import time, overriding any prior
250
+ level settings. This method must run *after* the lazy import
251
+ has loaded the subcommand module (and thus pysisyphus).
252
+ """
253
+ import logging as _logging
254
+
255
+ _PYSIS_LOGGERS = (
256
+ "pysisyphus", "calculator", "cos", "dimer", "dynamics",
257
+ "gdiis", "internal_coords", "irc", "optimizer",
258
+ "tsoptimizer", "mwfn", "stocastic", "wfoverlap",
259
+ )
260
+ for name in _PYSIS_LOGGERS:
261
+ lg = _logging.getLogger(name)
262
+ lg.setLevel(_logging.CRITICAL + 1)
263
+ lg.propagate = False
264
+ for h in lg.handlers[:]:
265
+ lg.removeHandler(h)
266
+ try:
267
+ h.close()
268
+ except Exception:
269
+ pass
270
+
271
+ def invoke(self, ctx):
272
+ # Add a leading blank line for subcommands (except "all") to separate CLI tool logs.
273
+ if ctx.invoked_subcommand and ctx.invoked_subcommand != "all":
274
+ click.echo()
275
+ # Suppress pysisyphus loggers AFTER lazy import has loaded the
276
+ # subcommand module (which triggers pysisyphus __init__ file handlers).
277
+ self._silence_pysisyphus_loggers()
278
+ return super().invoke(ctx)
279
+
280
+ def list_commands(self, ctx):
281
+ cmds = set(super().list_commands(ctx))
282
+ cmds.update(self._lazy_subcommands.keys())
283
+ return sorted(cmds)
284
+
285
+ def get_command(self, ctx, cmd_name):
286
+ cmd = super().get_command(ctx, cmd_name)
287
+ if cmd is not None:
288
+ if cmd_name not in self._parser_wrapper_subcommands:
289
+ cmd = self._ensure_help_advanced_option(cmd)
290
+ return self._configure_subcommand_help_visibility(
291
+ cmd_name, cmd, self._subcommand_primary_help_options
292
+ )
293
+
294
+ lazy_spec = self._lazy_subcommands.get(cmd_name)
295
+ if lazy_spec is None:
296
+ return None
297
+
298
+ cached = self._lazy_cache.get(cmd_name)
299
+ if cached is not None:
300
+ return cached
301
+
302
+ module_name, attr_name, _ = lazy_spec
303
+ try:
304
+ module = importlib.import_module(module_name, package=__package__)
305
+ loaded_cmd = getattr(module, attr_name)
306
+ except (ModuleNotFoundError, ImportError) as exc:
307
+ loaded_cmd = self._build_unavailable_command(cmd_name, exc)
308
+
309
+ if cmd_name not in self._parser_wrapper_subcommands:
310
+ loaded_cmd = self._ensure_help_advanced_option(loaded_cmd)
311
+ loaded_cmd = self._configure_subcommand_help_visibility(
312
+ cmd_name, loaded_cmd, self._subcommand_primary_help_options
313
+ )
314
+ self._lazy_cache[cmd_name] = loaded_cmd
315
+ return loaded_cmd
316
+
317
+ def format_commands(self, ctx, formatter):
318
+ rows = []
319
+ for subcommand in self.list_commands(ctx):
320
+ lazy_spec = self._lazy_subcommands.get(subcommand)
321
+ if lazy_spec is not None:
322
+ rows.append((subcommand, lazy_spec[2]))
323
+ continue
324
+
325
+ cmd = super().get_command(ctx, subcommand)
326
+ if cmd is None or cmd.hidden:
327
+ continue
328
+ rows.append(
329
+ (
330
+ subcommand,
331
+ cmd.get_short_help_str(formatter.width - 6 - len(subcommand)),
332
+ )
333
+ )
334
+
335
+ if rows:
336
+ with formatter.section("Commands"):
337
+ formatter.write_dl(rows)