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.
- hessian_ff/__init__.py +50 -0
- hessian_ff/analytical_hessian.py +609 -0
- hessian_ff/constants.py +46 -0
- hessian_ff/forcefield.py +339 -0
- hessian_ff/loaders.py +608 -0
- hessian_ff/native/Makefile +8 -0
- hessian_ff/native/__init__.py +28 -0
- hessian_ff/native/analytical_hessian.py +88 -0
- hessian_ff/native/analytical_hessian_ext.cpp +258 -0
- hessian_ff/native/bonded.py +82 -0
- hessian_ff/native/bonded_ext.cpp +640 -0
- hessian_ff/native/loader.py +349 -0
- hessian_ff/native/nonbonded.py +118 -0
- hessian_ff/native/nonbonded_ext.cpp +1150 -0
- hessian_ff/prmtop_parmed.py +23 -0
- hessian_ff/system.py +107 -0
- hessian_ff/terms/__init__.py +14 -0
- hessian_ff/terms/angle.py +73 -0
- hessian_ff/terms/bond.py +44 -0
- hessian_ff/terms/cmap.py +406 -0
- hessian_ff/terms/dihedral.py +141 -0
- hessian_ff/terms/nonbonded.py +209 -0
- hessian_ff/tests/__init__.py +0 -0
- hessian_ff/tests/conftest.py +75 -0
- hessian_ff/tests/data/small/complex.parm7 +1346 -0
- hessian_ff/tests/data/small/complex.pdb +125 -0
- hessian_ff/tests/data/small/complex.rst7 +63 -0
- hessian_ff/tests/test_coords_input.py +44 -0
- hessian_ff/tests/test_energy_force.py +49 -0
- hessian_ff/tests/test_hessian.py +137 -0
- hessian_ff/tests/test_smoke.py +18 -0
- hessian_ff/tests/test_validation.py +40 -0
- hessian_ff/workflows.py +889 -0
- mlmm/__init__.py +36 -0
- mlmm/__main__.py +7 -0
- mlmm/_version.py +34 -0
- mlmm/add_elem_info.py +374 -0
- mlmm/advanced_help.py +91 -0
- mlmm/align_freeze_atoms.py +601 -0
- mlmm/all.py +3535 -0
- mlmm/bond_changes.py +231 -0
- mlmm/bool_compat.py +223 -0
- mlmm/cli.py +574 -0
- mlmm/cli_utils.py +166 -0
- mlmm/default_group.py +337 -0
- mlmm/defaults.py +467 -0
- mlmm/define_layer.py +526 -0
- mlmm/dft.py +1041 -0
- mlmm/energy_diagram.py +253 -0
- mlmm/extract.py +2213 -0
- mlmm/fix_altloc.py +464 -0
- mlmm/freq.py +1406 -0
- mlmm/harmonic_constraints.py +140 -0
- mlmm/hessian_cache.py +44 -0
- mlmm/hessian_calc.py +174 -0
- mlmm/irc.py +638 -0
- mlmm/mlmm_calc.py +2262 -0
- mlmm/mm_parm.py +945 -0
- mlmm/oniom_export.py +1983 -0
- mlmm/oniom_import.py +457 -0
- mlmm/opt.py +1742 -0
- mlmm/path_opt.py +1353 -0
- mlmm/path_search.py +2299 -0
- mlmm/preflight.py +88 -0
- mlmm/py.typed +1 -0
- mlmm/pysis_runner.py +45 -0
- mlmm/scan.py +1047 -0
- mlmm/scan2d.py +1226 -0
- mlmm/scan3d.py +1265 -0
- mlmm/scan_common.py +184 -0
- mlmm/summary_log.py +736 -0
- mlmm/trj2fig.py +448 -0
- mlmm/tsopt.py +2871 -0
- mlmm/utils.py +2309 -0
- mlmm/xtb_embedcharge_correction.py +475 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/METADATA +1159 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/RECORD +372 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/WHEEL +5 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/entry_points.txt +2 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/licenses/LICENSE +674 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/top_level.txt +4 -0
- pysisyphus/Geometry.py +1667 -0
- pysisyphus/LICENSE +674 -0
- pysisyphus/TableFormatter.py +63 -0
- pysisyphus/TablePrinter.py +74 -0
- pysisyphus/__init__.py +12 -0
- pysisyphus/calculators/AFIR.py +452 -0
- pysisyphus/calculators/AnaPot.py +20 -0
- pysisyphus/calculators/AnaPot2.py +48 -0
- pysisyphus/calculators/AnaPot3.py +12 -0
- pysisyphus/calculators/AnaPot4.py +20 -0
- pysisyphus/calculators/AnaPotBase.py +337 -0
- pysisyphus/calculators/AnaPotCBM.py +25 -0
- pysisyphus/calculators/AtomAtomTransTorque.py +154 -0
- pysisyphus/calculators/CFOUR.py +250 -0
- pysisyphus/calculators/Calculator.py +844 -0
- pysisyphus/calculators/CerjanMiller.py +24 -0
- pysisyphus/calculators/Composite.py +123 -0
- pysisyphus/calculators/ConicalIntersection.py +171 -0
- pysisyphus/calculators/DFTBp.py +430 -0
- pysisyphus/calculators/DFTD3.py +66 -0
- pysisyphus/calculators/DFTD4.py +84 -0
- pysisyphus/calculators/Dalton.py +61 -0
- pysisyphus/calculators/Dimer.py +681 -0
- pysisyphus/calculators/Dummy.py +20 -0
- pysisyphus/calculators/EGO.py +76 -0
- pysisyphus/calculators/EnergyMin.py +224 -0
- pysisyphus/calculators/ExternalPotential.py +264 -0
- pysisyphus/calculators/FakeASE.py +35 -0
- pysisyphus/calculators/FourWellAnaPot.py +28 -0
- pysisyphus/calculators/FreeEndNEBPot.py +39 -0
- pysisyphus/calculators/Gaussian09.py +18 -0
- pysisyphus/calculators/Gaussian16.py +726 -0
- pysisyphus/calculators/HardSphere.py +159 -0
- pysisyphus/calculators/IDPPCalculator.py +49 -0
- pysisyphus/calculators/IPIClient.py +133 -0
- pysisyphus/calculators/IPIServer.py +234 -0
- pysisyphus/calculators/LEPSBase.py +24 -0
- pysisyphus/calculators/LEPSExpr.py +139 -0
- pysisyphus/calculators/LennardJones.py +80 -0
- pysisyphus/calculators/MOPAC.py +219 -0
- pysisyphus/calculators/MullerBrownSympyPot.py +51 -0
- pysisyphus/calculators/MultiCalc.py +85 -0
- pysisyphus/calculators/NFK.py +45 -0
- pysisyphus/calculators/OBabel.py +87 -0
- pysisyphus/calculators/ONIOMv2.py +1129 -0
- pysisyphus/calculators/ORCA.py +893 -0
- pysisyphus/calculators/ORCA5.py +6 -0
- pysisyphus/calculators/OpenMM.py +88 -0
- pysisyphus/calculators/OpenMolcas.py +281 -0
- pysisyphus/calculators/OverlapCalculator.py +908 -0
- pysisyphus/calculators/Psi4.py +218 -0
- pysisyphus/calculators/PyPsi4.py +37 -0
- pysisyphus/calculators/PySCF.py +341 -0
- pysisyphus/calculators/PyXTB.py +73 -0
- pysisyphus/calculators/QCEngine.py +106 -0
- pysisyphus/calculators/Rastrigin.py +22 -0
- pysisyphus/calculators/Remote.py +76 -0
- pysisyphus/calculators/Rosenbrock.py +15 -0
- pysisyphus/calculators/SocketCalc.py +97 -0
- pysisyphus/calculators/TIP3P.py +111 -0
- pysisyphus/calculators/TransTorque.py +161 -0
- pysisyphus/calculators/Turbomole.py +965 -0
- pysisyphus/calculators/VRIPot.py +37 -0
- pysisyphus/calculators/WFOWrapper.py +333 -0
- pysisyphus/calculators/WFOWrapper2.py +341 -0
- pysisyphus/calculators/XTB.py +418 -0
- pysisyphus/calculators/__init__.py +81 -0
- pysisyphus/calculators/cosmo_data.py +139 -0
- pysisyphus/calculators/parser.py +150 -0
- pysisyphus/color.py +19 -0
- pysisyphus/config.py +133 -0
- pysisyphus/constants.py +65 -0
- pysisyphus/cos/AdaptiveNEB.py +230 -0
- pysisyphus/cos/ChainOfStates.py +725 -0
- pysisyphus/cos/FreeEndNEB.py +25 -0
- pysisyphus/cos/FreezingString.py +103 -0
- pysisyphus/cos/GrowingChainOfStates.py +71 -0
- pysisyphus/cos/GrowingNT.py +309 -0
- pysisyphus/cos/GrowingString.py +508 -0
- pysisyphus/cos/NEB.py +189 -0
- pysisyphus/cos/SimpleZTS.py +64 -0
- pysisyphus/cos/__init__.py +22 -0
- pysisyphus/cos/stiffness.py +199 -0
- pysisyphus/drivers/__init__.py +17 -0
- pysisyphus/drivers/afir.py +855 -0
- pysisyphus/drivers/barriers.py +271 -0
- pysisyphus/drivers/birkholz.py +138 -0
- pysisyphus/drivers/cluster.py +318 -0
- pysisyphus/drivers/diabatization.py +133 -0
- pysisyphus/drivers/merge.py +368 -0
- pysisyphus/drivers/merge_mol2.py +322 -0
- pysisyphus/drivers/opt.py +375 -0
- pysisyphus/drivers/perf.py +91 -0
- pysisyphus/drivers/pka.py +52 -0
- pysisyphus/drivers/precon_pos_rot.py +669 -0
- pysisyphus/drivers/rates.py +480 -0
- pysisyphus/drivers/replace.py +219 -0
- pysisyphus/drivers/scan.py +212 -0
- pysisyphus/drivers/spectrum.py +166 -0
- pysisyphus/drivers/thermo.py +31 -0
- pysisyphus/dynamics/Gaussian.py +103 -0
- pysisyphus/dynamics/__init__.py +20 -0
- pysisyphus/dynamics/colvars.py +136 -0
- pysisyphus/dynamics/driver.py +297 -0
- pysisyphus/dynamics/helpers.py +256 -0
- pysisyphus/dynamics/lincs.py +105 -0
- pysisyphus/dynamics/mdp.py +364 -0
- pysisyphus/dynamics/rattle.py +121 -0
- pysisyphus/dynamics/thermostats.py +128 -0
- pysisyphus/dynamics/wigner.py +266 -0
- pysisyphus/elem_data.py +3473 -0
- pysisyphus/exceptions.py +2 -0
- pysisyphus/filtertrj.py +69 -0
- pysisyphus/helpers.py +623 -0
- pysisyphus/helpers_pure.py +649 -0
- pysisyphus/init_logging.py +50 -0
- pysisyphus/intcoords/Bend.py +69 -0
- pysisyphus/intcoords/Bend2.py +25 -0
- pysisyphus/intcoords/BondedFragment.py +32 -0
- pysisyphus/intcoords/Cartesian.py +41 -0
- pysisyphus/intcoords/CartesianCoords.py +140 -0
- pysisyphus/intcoords/Coords.py +56 -0
- pysisyphus/intcoords/DLC.py +197 -0
- pysisyphus/intcoords/DistanceFunction.py +34 -0
- pysisyphus/intcoords/DummyImproper.py +70 -0
- pysisyphus/intcoords/DummyTorsion.py +72 -0
- pysisyphus/intcoords/LinearBend.py +105 -0
- pysisyphus/intcoords/LinearDisplacement.py +80 -0
- pysisyphus/intcoords/OutOfPlane.py +59 -0
- pysisyphus/intcoords/PrimTypes.py +286 -0
- pysisyphus/intcoords/Primitive.py +137 -0
- pysisyphus/intcoords/RedundantCoords.py +659 -0
- pysisyphus/intcoords/RobustTorsion.py +59 -0
- pysisyphus/intcoords/Rotation.py +147 -0
- pysisyphus/intcoords/Stretch.py +31 -0
- pysisyphus/intcoords/Torsion.py +101 -0
- pysisyphus/intcoords/Torsion2.py +25 -0
- pysisyphus/intcoords/Translation.py +45 -0
- pysisyphus/intcoords/__init__.py +61 -0
- pysisyphus/intcoords/augment_bonds.py +126 -0
- pysisyphus/intcoords/derivatives.py +10512 -0
- pysisyphus/intcoords/eval.py +80 -0
- pysisyphus/intcoords/exceptions.py +37 -0
- pysisyphus/intcoords/findiffs.py +48 -0
- pysisyphus/intcoords/generate_derivatives.py +414 -0
- pysisyphus/intcoords/helpers.py +235 -0
- pysisyphus/intcoords/logging_conf.py +10 -0
- pysisyphus/intcoords/mp_derivatives.py +10836 -0
- pysisyphus/intcoords/setup.py +962 -0
- pysisyphus/intcoords/setup_fast.py +176 -0
- pysisyphus/intcoords/update.py +272 -0
- pysisyphus/intcoords/valid.py +89 -0
- pysisyphus/interpolate/Geodesic.py +93 -0
- pysisyphus/interpolate/IDPP.py +55 -0
- pysisyphus/interpolate/Interpolator.py +116 -0
- pysisyphus/interpolate/LST.py +70 -0
- pysisyphus/interpolate/Redund.py +152 -0
- pysisyphus/interpolate/__init__.py +9 -0
- pysisyphus/interpolate/helpers.py +34 -0
- pysisyphus/io/__init__.py +22 -0
- pysisyphus/io/aomix.py +178 -0
- pysisyphus/io/cjson.py +24 -0
- pysisyphus/io/crd.py +101 -0
- pysisyphus/io/cube.py +220 -0
- pysisyphus/io/fchk.py +184 -0
- pysisyphus/io/hdf5.py +49 -0
- pysisyphus/io/hessian.py +72 -0
- pysisyphus/io/mol2.py +146 -0
- pysisyphus/io/molden.py +293 -0
- pysisyphus/io/orca.py +189 -0
- pysisyphus/io/pdb.py +269 -0
- pysisyphus/io/psf.py +79 -0
- pysisyphus/io/pubchem.py +31 -0
- pysisyphus/io/qcschema.py +34 -0
- pysisyphus/io/sdf.py +29 -0
- pysisyphus/io/xyz.py +61 -0
- pysisyphus/io/zmat.py +175 -0
- pysisyphus/irc/DWI.py +108 -0
- pysisyphus/irc/DampedVelocityVerlet.py +134 -0
- pysisyphus/irc/Euler.py +22 -0
- pysisyphus/irc/EulerPC.py +345 -0
- pysisyphus/irc/GonzalezSchlegel.py +187 -0
- pysisyphus/irc/IMKMod.py +164 -0
- pysisyphus/irc/IRC.py +878 -0
- pysisyphus/irc/IRCDummy.py +10 -0
- pysisyphus/irc/Instanton.py +307 -0
- pysisyphus/irc/LQA.py +53 -0
- pysisyphus/irc/ModeKill.py +136 -0
- pysisyphus/irc/ParamPlot.py +53 -0
- pysisyphus/irc/RK4.py +36 -0
- pysisyphus/irc/__init__.py +31 -0
- pysisyphus/irc/initial_displ.py +219 -0
- pysisyphus/linalg.py +411 -0
- pysisyphus/line_searches/Backtracking.py +88 -0
- pysisyphus/line_searches/HagerZhang.py +184 -0
- pysisyphus/line_searches/LineSearch.py +232 -0
- pysisyphus/line_searches/StrongWolfe.py +108 -0
- pysisyphus/line_searches/__init__.py +9 -0
- pysisyphus/line_searches/interpol.py +15 -0
- pysisyphus/modefollow/NormalMode.py +40 -0
- pysisyphus/modefollow/__init__.py +10 -0
- pysisyphus/modefollow/davidson.py +199 -0
- pysisyphus/modefollow/lanczos.py +95 -0
- pysisyphus/optimizers/BFGS.py +99 -0
- pysisyphus/optimizers/BacktrackingOptimizer.py +113 -0
- pysisyphus/optimizers/ConjugateGradient.py +98 -0
- pysisyphus/optimizers/CubicNewton.py +75 -0
- pysisyphus/optimizers/FIRE.py +113 -0
- pysisyphus/optimizers/HessianOptimizer.py +1176 -0
- pysisyphus/optimizers/LBFGS.py +228 -0
- pysisyphus/optimizers/LayerOpt.py +411 -0
- pysisyphus/optimizers/MicroOptimizer.py +169 -0
- pysisyphus/optimizers/NCOptimizer.py +90 -0
- pysisyphus/optimizers/Optimizer.py +1084 -0
- pysisyphus/optimizers/PreconLBFGS.py +260 -0
- pysisyphus/optimizers/PreconSteepestDescent.py +7 -0
- pysisyphus/optimizers/QuickMin.py +74 -0
- pysisyphus/optimizers/RFOptimizer.py +181 -0
- pysisyphus/optimizers/RSA.py +99 -0
- pysisyphus/optimizers/StabilizedQNMethod.py +248 -0
- pysisyphus/optimizers/SteepestDescent.py +23 -0
- pysisyphus/optimizers/StringOptimizer.py +173 -0
- pysisyphus/optimizers/__init__.py +41 -0
- pysisyphus/optimizers/closures.py +301 -0
- pysisyphus/optimizers/cls_map.py +58 -0
- pysisyphus/optimizers/exceptions.py +6 -0
- pysisyphus/optimizers/gdiis.py +280 -0
- pysisyphus/optimizers/guess_hessians.py +311 -0
- pysisyphus/optimizers/hessian_updates.py +355 -0
- pysisyphus/optimizers/poly_fit.py +285 -0
- pysisyphus/optimizers/precon.py +153 -0
- pysisyphus/optimizers/restrict_step.py +24 -0
- pysisyphus/pack.py +172 -0
- pysisyphus/peakdetect.py +948 -0
- pysisyphus/plot.py +1031 -0
- pysisyphus/run.py +2106 -0
- pysisyphus/socket_helper.py +74 -0
- pysisyphus/stocastic/FragmentKick.py +132 -0
- pysisyphus/stocastic/Kick.py +81 -0
- pysisyphus/stocastic/Pipeline.py +303 -0
- pysisyphus/stocastic/__init__.py +21 -0
- pysisyphus/stocastic/align.py +127 -0
- pysisyphus/testing.py +96 -0
- pysisyphus/thermo.py +156 -0
- pysisyphus/trj.py +824 -0
- pysisyphus/tsoptimizers/RSIRFOptimizer.py +56 -0
- pysisyphus/tsoptimizers/RSPRFOptimizer.py +182 -0
- pysisyphus/tsoptimizers/TRIM.py +59 -0
- pysisyphus/tsoptimizers/TSHessianOptimizer.py +463 -0
- pysisyphus/tsoptimizers/__init__.py +23 -0
- pysisyphus/wavefunction/Basis.py +239 -0
- pysisyphus/wavefunction/DIIS.py +76 -0
- pysisyphus/wavefunction/__init__.py +25 -0
- pysisyphus/wavefunction/build_ext.py +42 -0
- pysisyphus/wavefunction/cart2sph.py +190 -0
- pysisyphus/wavefunction/diabatization.py +304 -0
- pysisyphus/wavefunction/excited_states.py +435 -0
- pysisyphus/wavefunction/gen_ints.py +1811 -0
- pysisyphus/wavefunction/helpers.py +104 -0
- pysisyphus/wavefunction/ints/__init__.py +0 -0
- pysisyphus/wavefunction/ints/boys.py +193 -0
- pysisyphus/wavefunction/ints/boys_table_N_64_xasym_27.1_step_0.01.npy +0 -0
- pysisyphus/wavefunction/ints/cart_gto3d.py +176 -0
- pysisyphus/wavefunction/ints/coulomb3d.py +25928 -0
- pysisyphus/wavefunction/ints/diag_quadrupole3d.py +10036 -0
- pysisyphus/wavefunction/ints/dipole3d.py +8762 -0
- pysisyphus/wavefunction/ints/int2c2e3d.py +7198 -0
- pysisyphus/wavefunction/ints/int3c2e3d_sph.py +65040 -0
- pysisyphus/wavefunction/ints/kinetic3d.py +8240 -0
- pysisyphus/wavefunction/ints/ovlp3d.py +3777 -0
- pysisyphus/wavefunction/ints/quadrupole3d.py +15054 -0
- pysisyphus/wavefunction/ints/self_ovlp3d.py +198 -0
- pysisyphus/wavefunction/localization.py +458 -0
- pysisyphus/wavefunction/multipole.py +159 -0
- pysisyphus/wavefunction/normalization.py +36 -0
- pysisyphus/wavefunction/pop_analysis.py +134 -0
- pysisyphus/wavefunction/shells.py +1171 -0
- pysisyphus/wavefunction/wavefunction.py +504 -0
- pysisyphus/wrapper/__init__.py +11 -0
- pysisyphus/wrapper/exceptions.py +2 -0
- pysisyphus/wrapper/jmol.py +120 -0
- pysisyphus/wrapper/mwfn.py +169 -0
- pysisyphus/wrapper/packmol.py +71 -0
- pysisyphus/xyzloader.py +168 -0
- pysisyphus/yaml_mods.py +45 -0
- thermoanalysis/LICENSE +674 -0
- thermoanalysis/QCData.py +244 -0
- thermoanalysis/__init__.py +0 -0
- thermoanalysis/config.py +3 -0
- thermoanalysis/constants.py +20 -0
- thermoanalysis/thermo.py +1011 -0
mlmm/summary_log.py
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
# mlmm/summary_log.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
User-friendly summary log writer used by ``path_search`` and ``all``.
|
|
5
|
+
|
|
6
|
+
The goal is to provide a compact, readable ``summary.log`` alongside the
|
|
7
|
+
``summary.yaml``. The log aggregates MEP details, segment barriers,
|
|
8
|
+
post-processing energies, 3-layer system info, and key output paths.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import textwrap
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence
|
|
16
|
+
|
|
17
|
+
from pysisyphus.constants import AU2KCALPERMOL
|
|
18
|
+
from . import __version__
|
|
19
|
+
from .defaults import MLMM_CALC_KW, BFACTOR_ML, BFACTOR_MOVABLE_MM, BFACTOR_FROZEN
|
|
20
|
+
|
|
21
|
+
REQUIRED_SUMMARY_PAYLOAD_KEYS: tuple[str, ...] = (
|
|
22
|
+
"root_out_dir",
|
|
23
|
+
"path_module_dir",
|
|
24
|
+
"pipeline_mode",
|
|
25
|
+
"segments",
|
|
26
|
+
"energy_diagrams",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_summary_payload(payload: Dict[str, Any] | None) -> Dict[str, Any]:
|
|
31
|
+
"""Return a defensive payload with stable defaults for summary rendering."""
|
|
32
|
+
raw = payload if isinstance(payload, dict) else {}
|
|
33
|
+
out: Dict[str, Any] = dict(raw)
|
|
34
|
+
out.setdefault("root_out_dir", "-")
|
|
35
|
+
out.setdefault("path_module_dir", "-")
|
|
36
|
+
out.setdefault("pipeline_mode", "-")
|
|
37
|
+
out.setdefault("segments", [])
|
|
38
|
+
out.setdefault("post_segments", [])
|
|
39
|
+
out.setdefault("energy_diagrams", [])
|
|
40
|
+
out.setdefault("mep", {})
|
|
41
|
+
out.setdefault("key_files", {})
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _fmt_bool(val: Optional[Any]) -> str:
|
|
46
|
+
if val is None:
|
|
47
|
+
return "-"
|
|
48
|
+
return "True" if bool(val) else "False"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _shorten_path(path: Optional[Path], root_out: Optional[Path]) -> str:
|
|
52
|
+
"""Return a path string, preferring a relative form to ``root_out`` or its parent."""
|
|
53
|
+
if not path:
|
|
54
|
+
return "(not available)"
|
|
55
|
+
|
|
56
|
+
path_obj = Path(path)
|
|
57
|
+
|
|
58
|
+
if root_out:
|
|
59
|
+
for base in (root_out, root_out.parent):
|
|
60
|
+
try:
|
|
61
|
+
return str(path_obj.relative_to(base))
|
|
62
|
+
except ValueError:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
return str(path_obj)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _format_energy_rows(
|
|
69
|
+
labels: Sequence[str],
|
|
70
|
+
energies_au: Optional[Sequence[Optional[float]]],
|
|
71
|
+
energies_kcal: Optional[Sequence[Optional[float]]],
|
|
72
|
+
) -> List[str]:
|
|
73
|
+
rows: List[str] = []
|
|
74
|
+
try:
|
|
75
|
+
energies_au_list = list(energies_au) if energies_au is not None else []
|
|
76
|
+
except Exception:
|
|
77
|
+
energies_au_list = []
|
|
78
|
+
try:
|
|
79
|
+
energies_kcal_list = list(energies_kcal) if energies_kcal is not None else []
|
|
80
|
+
except Exception:
|
|
81
|
+
energies_kcal_list = []
|
|
82
|
+
base_e = energies_au_list[0] if energies_au_list else None
|
|
83
|
+
|
|
84
|
+
for i, lab in enumerate(labels):
|
|
85
|
+
abs_e = energies_au_list[i] if i < len(energies_au_list) else None
|
|
86
|
+
rel_e = energies_kcal_list[i] if i < len(energies_kcal_list) else None
|
|
87
|
+
if rel_e is None and abs_e is not None and base_e is not None:
|
|
88
|
+
rel_e = (abs_e - base_e) * AU2KCALPERMOL
|
|
89
|
+
|
|
90
|
+
abs_txt = f"{abs_e:14.6f}" if abs_e is not None else f"{'n/a':>14}"
|
|
91
|
+
rel_txt = f"{rel_e:14.4f}" if rel_e is not None else f"{'n/a':>14}"
|
|
92
|
+
rows.append(f" {lab:<8}{abs_txt} {rel_txt}")
|
|
93
|
+
return rows
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _format_bond_changes(text: str, indent: int = 6) -> List[str]:
|
|
97
|
+
if not text:
|
|
98
|
+
return ["".rjust(indent) + "(no covalent changes detected)"]
|
|
99
|
+
blocks = [ln.rstrip() for ln in textwrap.dedent(text).splitlines() if ln.strip()]
|
|
100
|
+
return ["".rjust(indent) + ln for ln in blocks]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _format_ts_imag_info(ts_info: Any) -> List[str]:
|
|
104
|
+
if ts_info is None:
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
lines: List[str] = [" TS imaginary freq:"]
|
|
108
|
+
n_imag: Optional[int] = None
|
|
109
|
+
nu_imag: Optional[float] = None
|
|
110
|
+
min_abs: Optional[float] = None
|
|
111
|
+
|
|
112
|
+
if isinstance(ts_info, dict):
|
|
113
|
+
n_imag = ts_info.get("n_imag")
|
|
114
|
+
nu_imag = ts_info.get("nu_imag_max_cm") or ts_info.get("nu_imag_cm")
|
|
115
|
+
min_abs = ts_info.get("min_abs_imag_cm")
|
|
116
|
+
if nu_imag is None and ts_info.get("ts_imag_freq_cm"):
|
|
117
|
+
nu_imag = ts_info.get("ts_imag_freq_cm")
|
|
118
|
+
else:
|
|
119
|
+
try:
|
|
120
|
+
nu_imag = float(ts_info)
|
|
121
|
+
n_imag = 1 if nu_imag is not None else None
|
|
122
|
+
except Exception:
|
|
123
|
+
nu_imag = None
|
|
124
|
+
|
|
125
|
+
n_imag_txt = str(n_imag) if n_imag is not None else "-"
|
|
126
|
+
lines.append(f" n_imag : {n_imag_txt}")
|
|
127
|
+
|
|
128
|
+
nu_label = "\u03bd_imag (max)"
|
|
129
|
+
if nu_imag is not None:
|
|
130
|
+
lines.append(f" {nu_label} : {nu_imag:.1f} cm^-1")
|
|
131
|
+
else:
|
|
132
|
+
lines.append(f" {nu_label} : -")
|
|
133
|
+
|
|
134
|
+
magnitude = min_abs if min_abs is not None else (abs(nu_imag) if nu_imag is not None else None)
|
|
135
|
+
note: Optional[str] = None
|
|
136
|
+
if n_imag is not None:
|
|
137
|
+
if n_imag == 1:
|
|
138
|
+
if magnitude is not None and magnitude < 100.0:
|
|
139
|
+
note = "WARNING : Imaginary frequency magnitude is small; TS may be poorly optimized."
|
|
140
|
+
else:
|
|
141
|
+
note = "NOTE : OK (single imaginary mode)"
|
|
142
|
+
elif n_imag == 0:
|
|
143
|
+
note = "WARNING : No imaginary frequency; structure may not be a TS."
|
|
144
|
+
else:
|
|
145
|
+
note = "WARNING : Multiple imaginary frequencies; TS may be poorly optimized."
|
|
146
|
+
elif nu_imag is not None:
|
|
147
|
+
if magnitude is not None and magnitude < 100.0:
|
|
148
|
+
note = "WARNING : Imaginary frequency magnitude is small; TS may be poorly optimized."
|
|
149
|
+
else:
|
|
150
|
+
note = "NOTE : Single imaginary frequency (count unavailable)"
|
|
151
|
+
|
|
152
|
+
if note:
|
|
153
|
+
lines.append(f" {note}")
|
|
154
|
+
|
|
155
|
+
return lines
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _format_layer_info(payload: Dict[str, Any]) -> List[str]:
|
|
159
|
+
"""Format 3-layer ML/MM system information with Hessian-target MM subset info."""
|
|
160
|
+
lines: List[str] = []
|
|
161
|
+
|
|
162
|
+
ml_atoms = payload.get("ml_atoms")
|
|
163
|
+
hess_mm_atoms = payload.get("hess_mm_atoms")
|
|
164
|
+
movable_mm_atoms = payload.get("movable_mm_atoms")
|
|
165
|
+
frozen_atoms = payload.get("frozen_atoms")
|
|
166
|
+
|
|
167
|
+
n_ml = len(ml_atoms) if ml_atoms else 0
|
|
168
|
+
n_hess = len(hess_mm_atoms) if hess_mm_atoms else 0
|
|
169
|
+
n_movable = len(movable_mm_atoms) if movable_mm_atoms else 0
|
|
170
|
+
n_frozen = len(frozen_atoms) if frozen_atoms else 0
|
|
171
|
+
|
|
172
|
+
n_movable_total = n_hess + n_movable
|
|
173
|
+
lines.append(" 3-Layer ML/MM System:")
|
|
174
|
+
lines.append(f" ML (B={BFACTOR_ML:.0f}) : {n_ml:6d} atoms")
|
|
175
|
+
lines.append(f" Movable MM (B={BFACTOR_MOVABLE_MM:.0f}) : {n_movable_total:6d} atoms")
|
|
176
|
+
lines.append(f" Hessian-target subset : {n_hess:6d} atoms")
|
|
177
|
+
lines.append(f" Frozen MM (B={BFACTOR_FROZEN:.0f}) : {n_frozen:6d} atoms")
|
|
178
|
+
lines.append(f" Total : {n_ml + n_movable_total + n_frozen:6d} atoms")
|
|
179
|
+
|
|
180
|
+
hess_cutoff = payload.get("hess_cutoff")
|
|
181
|
+
movable_cutoff = payload.get("movable_cutoff")
|
|
182
|
+
use_bfactor = payload.get("use_bfactor_layers")
|
|
183
|
+
|
|
184
|
+
if hess_cutoff is not None or movable_cutoff is not None:
|
|
185
|
+
lines.append(
|
|
186
|
+
f" hess_cutoff : {hess_cutoff} A"
|
|
187
|
+
if hess_cutoff is not None
|
|
188
|
+
else " hess_cutoff : -"
|
|
189
|
+
)
|
|
190
|
+
lines.append(
|
|
191
|
+
f" movable_cutoff : {movable_cutoff} A"
|
|
192
|
+
if movable_cutoff is not None
|
|
193
|
+
else " movable_cutoff : -"
|
|
194
|
+
)
|
|
195
|
+
elif use_bfactor:
|
|
196
|
+
lines.append(" Layer source : B-factor based")
|
|
197
|
+
|
|
198
|
+
return lines
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _emit_energy_block(
|
|
202
|
+
lines: List[str],
|
|
203
|
+
title: str,
|
|
204
|
+
payload: Optional[Dict[str, Any]],
|
|
205
|
+
root_out: Optional[Path],
|
|
206
|
+
) -> None:
|
|
207
|
+
if not payload:
|
|
208
|
+
return
|
|
209
|
+
labels: Sequence[str] = payload.get("labels") or ["R", "TS", "P"]
|
|
210
|
+
energies_au = payload.get("energies_au")
|
|
211
|
+
energies_kcal = payload.get("energies_kcal")
|
|
212
|
+
lines.append(f" -- {title} --")
|
|
213
|
+
lines.append(" State Abs [Eh] Rel [kcal/mol]")
|
|
214
|
+
lines.extend(_format_energy_rows(labels, energies_au, energies_kcal))
|
|
215
|
+
|
|
216
|
+
diagram = payload.get("diagram") or payload.get("image")
|
|
217
|
+
if diagram:
|
|
218
|
+
lines.append(f" Diagram : {_shorten_path(diagram, root_out)}")
|
|
219
|
+
structs: Dict[str, Any] = payload.get("structures", {})
|
|
220
|
+
if structs:
|
|
221
|
+
lines.append(" Structures:")
|
|
222
|
+
for key in ("R", "TS", "P"):
|
|
223
|
+
if key in structs:
|
|
224
|
+
lines.append(f" {key}: {_shorten_path(structs.get(key), root_out)}")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _tree_rel_path(root: Path, p: Path) -> str:
|
|
228
|
+
try:
|
|
229
|
+
return p.relative_to(root).as_posix()
|
|
230
|
+
except ValueError:
|
|
231
|
+
return p.name
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _tree_annotate(annotations: Dict[str, str], rel: str) -> str:
|
|
235
|
+
note = annotations.get(rel)
|
|
236
|
+
return f" # {note}" if note else ""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _tree_leaf_files(dir_path: Path) -> Optional[List[str]]:
|
|
240
|
+
try:
|
|
241
|
+
inner_children = sorted(dir_path.iterdir(), key=lambda p: p.name.lower())
|
|
242
|
+
except Exception:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if any(p.is_dir() for p in inner_children):
|
|
246
|
+
return None
|
|
247
|
+
return [p.name for p in inner_children if p.is_file()]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _walk_directory_tree(
|
|
251
|
+
dir_path: Path,
|
|
252
|
+
prefix: str,
|
|
253
|
+
depth: int,
|
|
254
|
+
*,
|
|
255
|
+
root: Path,
|
|
256
|
+
annotations: Dict[str, str],
|
|
257
|
+
max_depth: int,
|
|
258
|
+
max_entries: int,
|
|
259
|
+
lines: List[str],
|
|
260
|
+
entries_seen_ref: List[int],
|
|
261
|
+
) -> bool:
|
|
262
|
+
try:
|
|
263
|
+
children = sorted(
|
|
264
|
+
dir_path.iterdir(),
|
|
265
|
+
key=lambda p: (p.is_file(), p.name.lower()),
|
|
266
|
+
)
|
|
267
|
+
except Exception:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
for idx, child in enumerate(children):
|
|
271
|
+
connector = "\u2514\u2500" if idx == len(children) - 1 else "\u251c\u2500"
|
|
272
|
+
rel = _tree_rel_path(root, child)
|
|
273
|
+
if child.is_dir():
|
|
274
|
+
leaf_names = _tree_leaf_files(child) if depth < max_depth else None
|
|
275
|
+
if leaf_names is not None:
|
|
276
|
+
lines.append(f"{prefix}{connector} {child.name}/{_tree_annotate(annotations, rel)}")
|
|
277
|
+
entries_seen_ref[0] += 1
|
|
278
|
+
if entries_seen_ref[0] >= max_entries:
|
|
279
|
+
lines.append(
|
|
280
|
+
f"{prefix} ... (truncated after {max_entries} entries)"
|
|
281
|
+
)
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
next_prefix = prefix + (" " if idx == len(children) - 1 else "\u2502 ")
|
|
285
|
+
grouped = ",".join(leaf_names)
|
|
286
|
+
lines.append(f"{next_prefix}\u2514\u2500 {{{grouped}}}")
|
|
287
|
+
entries_seen_ref[0] += 1
|
|
288
|
+
if entries_seen_ref[0] >= max_entries:
|
|
289
|
+
lines.append(
|
|
290
|
+
f"{next_prefix} ... (truncated after {max_entries} entries)"
|
|
291
|
+
)
|
|
292
|
+
return True
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
name = child.name + ("/" if child.is_dir() else "")
|
|
296
|
+
lines.append(f"{prefix}{connector} {name}{_tree_annotate(annotations, rel)}")
|
|
297
|
+
entries_seen_ref[0] += 1
|
|
298
|
+
if entries_seen_ref[0] >= max_entries:
|
|
299
|
+
lines.append(f"{prefix} ... (truncated after {max_entries} entries)")
|
|
300
|
+
return True
|
|
301
|
+
if child.is_dir() and depth < max_depth:
|
|
302
|
+
next_prefix = prefix + (" " if idx == len(children) - 1 else "\u2502 ")
|
|
303
|
+
if _walk_directory_tree(
|
|
304
|
+
child,
|
|
305
|
+
next_prefix,
|
|
306
|
+
depth + 1,
|
|
307
|
+
root=root,
|
|
308
|
+
annotations=annotations,
|
|
309
|
+
max_depth=max_depth,
|
|
310
|
+
max_entries=max_entries,
|
|
311
|
+
lines=lines,
|
|
312
|
+
entries_seen_ref=entries_seen_ref,
|
|
313
|
+
):
|
|
314
|
+
return True
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _format_directory_tree(
|
|
319
|
+
root: Path,
|
|
320
|
+
annotations: Dict[str, str],
|
|
321
|
+
max_depth: int = 4,
|
|
322
|
+
max_entries: int = 200,
|
|
323
|
+
) -> List[str]:
|
|
324
|
+
"""Render a compact directory tree rooted at ``root``."""
|
|
325
|
+
|
|
326
|
+
lines: List[str] = []
|
|
327
|
+
entries_seen_ref = [0]
|
|
328
|
+
lines.append(f" {root.name}/" + _tree_annotate(annotations, "."))
|
|
329
|
+
_walk_directory_tree(
|
|
330
|
+
root,
|
|
331
|
+
" ",
|
|
332
|
+
1,
|
|
333
|
+
root=root,
|
|
334
|
+
annotations=annotations,
|
|
335
|
+
max_depth=max_depth,
|
|
336
|
+
max_entries=max_entries,
|
|
337
|
+
lines=lines,
|
|
338
|
+
entries_seen_ref=entries_seen_ref,
|
|
339
|
+
)
|
|
340
|
+
return lines
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _segment_table_value(entry: Dict[str, Any], key: str, col_width: int) -> str:
|
|
344
|
+
if entry.get("kind") == "bridge" and not key.startswith("mep_"):
|
|
345
|
+
return "---".rjust(col_width)
|
|
346
|
+
val = entry.get(key)
|
|
347
|
+
if val is None:
|
|
348
|
+
return "---".rjust(col_width)
|
|
349
|
+
return f"{val:>{col_width}.2f}"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _classify_diagram_method(diag: Dict[str, Any]) -> str:
|
|
353
|
+
name = str(diag.get("name", "")).lower()
|
|
354
|
+
ylabel_txt = str(diag.get("ylabel", "")).lower()
|
|
355
|
+
|
|
356
|
+
if "g_dft" in name or "gibbs_dft" in name or ("gibbs" in ylabel_txt and "dft" in name):
|
|
357
|
+
return "gibbs_dft_uma"
|
|
358
|
+
if "dft" in name:
|
|
359
|
+
return "dft"
|
|
360
|
+
if "g_uma" in name or "gibbs" in name or "gibbs" in ylabel_txt:
|
|
361
|
+
return "gibbs_uma"
|
|
362
|
+
if "uma" in name:
|
|
363
|
+
return "uma"
|
|
364
|
+
return "mep"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _format_diag_row(
|
|
368
|
+
diag: Optional[Dict[str, Any]],
|
|
369
|
+
label: str,
|
|
370
|
+
col_width: int,
|
|
371
|
+
states: Sequence[str],
|
|
372
|
+
label_width: int,
|
|
373
|
+
) -> str:
|
|
374
|
+
if not diag:
|
|
375
|
+
values = " ".join("---".rjust(col_width) for _ in states)
|
|
376
|
+
return f" {label:<{label_width}} {values}"
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
labels_iter = list(diag.get("labels", []))
|
|
380
|
+
except Exception:
|
|
381
|
+
labels_iter = []
|
|
382
|
+
labels_map = {lab: i for i, lab in enumerate(labels_iter)}
|
|
383
|
+
energies_raw = diag.get("energies_kcal", [])
|
|
384
|
+
try:
|
|
385
|
+
energies = list(energies_raw) if energies_raw is not None else []
|
|
386
|
+
except Exception:
|
|
387
|
+
energies = []
|
|
388
|
+
row_vals: List[str] = []
|
|
389
|
+
for st in states:
|
|
390
|
+
idx = labels_map.get(st)
|
|
391
|
+
val = energies[idx] if idx is not None and idx < len(energies) else None
|
|
392
|
+
row_vals.append(f"{val:>{col_width}.2f}" if val is not None else "---".rjust(col_width))
|
|
393
|
+
return f" {label:<{label_width}} {' '.join(row_vals)}"
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def write_summary_log(dest: Path, payload: Dict[str, Any]) -> None:
|
|
397
|
+
"""Write a user-friendly summary.log at ``dest`` from a pre-collected payload."""
|
|
398
|
+
missing_required = [
|
|
399
|
+
key for key in REQUIRED_SUMMARY_PAYLOAD_KEYS if not isinstance(payload, dict) or key not in payload
|
|
400
|
+
]
|
|
401
|
+
payload = normalize_summary_payload(payload)
|
|
402
|
+
|
|
403
|
+
root_out = payload.get("root_out_dir") or "-"
|
|
404
|
+
root_out_path = Path(root_out) if root_out not in (None, "-") else None
|
|
405
|
+
path_module = payload.get("path_module_dir") or "-"
|
|
406
|
+
pipeline_mode = payload.get("pipeline_mode") or "-"
|
|
407
|
+
charge = payload.get("charge")
|
|
408
|
+
spin = payload.get("spin")
|
|
409
|
+
command = payload.get("command") or payload.get("cli_command")
|
|
410
|
+
|
|
411
|
+
lines: List[str] = []
|
|
412
|
+
lines.append("========================================================================")
|
|
413
|
+
lines.append("mlmm summary.log")
|
|
414
|
+
lines.append("========================================================================")
|
|
415
|
+
if command:
|
|
416
|
+
lines.append(f"Input : {command}")
|
|
417
|
+
if missing_required:
|
|
418
|
+
lines.append(
|
|
419
|
+
"Preflight note : missing payload keys replaced with defaults -> "
|
|
420
|
+
+ ", ".join(missing_required)
|
|
421
|
+
)
|
|
422
|
+
lines.append(f"Root out_dir : {root_out}")
|
|
423
|
+
path_module_disp = (
|
|
424
|
+
_shorten_path(path_module, root_out_path)
|
|
425
|
+
if path_module not in (None, "-")
|
|
426
|
+
else path_module
|
|
427
|
+
)
|
|
428
|
+
lines.append(f"Path module dir : {path_module_disp}")
|
|
429
|
+
lines.append(f"Pipeline mode : {pipeline_mode}")
|
|
430
|
+
lines.append(f"refine-path : {_fmt_bool(payload.get('refine_path'))}")
|
|
431
|
+
lines.append(f"TSOPT/IRC : {_fmt_bool(payload.get('tsopt'))}")
|
|
432
|
+
lines.append(f"Thermochemistry : {_fmt_bool(payload.get('thermo'))}")
|
|
433
|
+
lines.append(f"DFT single-point : {_fmt_bool(payload.get('dft'))}")
|
|
434
|
+
opt_mode_disp = payload.get("opt_mode") or "-"
|
|
435
|
+
lines.append(
|
|
436
|
+
f"Opt mode : {opt_mode_disp} (grad: lbfgs/dimer; hess: rfo/rsirfo)"
|
|
437
|
+
)
|
|
438
|
+
lines.append(f"MEP mode : {payload.get('mep_mode') or '-'}")
|
|
439
|
+
|
|
440
|
+
version_base = payload.get("code_version") or __version__
|
|
441
|
+
version_txt = f"mlmm {version_base}"
|
|
442
|
+
lines.append(f"Code version : {version_txt}")
|
|
443
|
+
uma_model = payload.get("uma_model") or MLMM_CALC_KW.get("uma_model") or "-"
|
|
444
|
+
lines.append(f"UMA model : {uma_model}")
|
|
445
|
+
lines.append(f"Total charge (ML) : {charge if charge is not None else '-'}")
|
|
446
|
+
lines.append(f"Multiplicity (2S+1): {spin if spin is not None else '-'}")
|
|
447
|
+
|
|
448
|
+
freeze_atoms_raw = payload.get("freeze_atoms")
|
|
449
|
+
if freeze_atoms_raw is None:
|
|
450
|
+
freeze_atoms_iter: List[Any] = []
|
|
451
|
+
else:
|
|
452
|
+
try:
|
|
453
|
+
freeze_atoms_iter = list(freeze_atoms_raw)
|
|
454
|
+
except Exception:
|
|
455
|
+
freeze_atoms_iter = []
|
|
456
|
+
try:
|
|
457
|
+
freeze_atoms_list = sorted({int(i) for i in freeze_atoms_iter})
|
|
458
|
+
except Exception:
|
|
459
|
+
freeze_atoms_list = []
|
|
460
|
+
if freeze_atoms_list:
|
|
461
|
+
lines.append(
|
|
462
|
+
"Freeze atoms (0-based): " + ",".join(map(str, freeze_atoms_list))
|
|
463
|
+
)
|
|
464
|
+
lines.append("")
|
|
465
|
+
|
|
466
|
+
# 3-layer ML/MM system info
|
|
467
|
+
if any(payload.get(k) for k in ["ml_atoms", "hess_mm_atoms", "movable_mm_atoms", "frozen_atoms"]):
|
|
468
|
+
lines.extend(_format_layer_info(payload))
|
|
469
|
+
lines.append("")
|
|
470
|
+
|
|
471
|
+
delta = "\u0394"
|
|
472
|
+
dagger = "\u2021"
|
|
473
|
+
|
|
474
|
+
mep = payload.get("mep", {}) or {}
|
|
475
|
+
diag = mep.get("diagram") or {}
|
|
476
|
+
lines.append("[1] Global MEP overview")
|
|
477
|
+
lines.append(f" Number of MEP images : {mep.get('n_images', '-')}")
|
|
478
|
+
lines.append(f" Number of segments : {mep.get('n_segments', '-')}")
|
|
479
|
+
if mep.get("traj_pdb"):
|
|
480
|
+
lines.append(
|
|
481
|
+
f" MEP trajectory (PDB) : {_shorten_path(mep.get('traj_pdb'), root_out_path)}"
|
|
482
|
+
)
|
|
483
|
+
if mep.get("mep_plot"):
|
|
484
|
+
lines.append(
|
|
485
|
+
f" MEP energy plot : {_shorten_path(mep.get('mep_plot'), root_out_path)}"
|
|
486
|
+
)
|
|
487
|
+
lines.append("")
|
|
488
|
+
lines.append(f" MEP energy diagram ({delta}E, kcal/mol)")
|
|
489
|
+
if diag:
|
|
490
|
+
if diag.get("image"):
|
|
491
|
+
lines.append(
|
|
492
|
+
f" Image : {_shorten_path(diag.get('image'), root_out_path)}"
|
|
493
|
+
)
|
|
494
|
+
lines.append(f" State {delta}E [kcal/mol]")
|
|
495
|
+
labels = diag.get("labels", [])
|
|
496
|
+
energies = diag.get("energies_kcal", [])
|
|
497
|
+
for i, lab in enumerate(labels):
|
|
498
|
+
rel = energies[i] if i < len(energies) else None
|
|
499
|
+
rel_txt = f"{rel:9.4f}" if rel is not None else " n/a"
|
|
500
|
+
lines.append(f" {lab:<8}{rel_txt}")
|
|
501
|
+
else:
|
|
502
|
+
lines.append(" (no diagram available)")
|
|
503
|
+
|
|
504
|
+
segments: Iterable[Dict[str, Any]] = payload.get("segments", []) or []
|
|
505
|
+
lines.append("")
|
|
506
|
+
lines.append("[2] Segment-level MEP summary (ML/MM path)")
|
|
507
|
+
if segments:
|
|
508
|
+
for seg in segments:
|
|
509
|
+
idx = int(seg.get("index", 0) or 0)
|
|
510
|
+
tag = seg.get("tag", f"seg_{idx:03d}")
|
|
511
|
+
kind = seg.get("kind", "seg")
|
|
512
|
+
lines.append(f" - Segment {idx:02d} [{kind}] tag={tag}")
|
|
513
|
+
barrier = seg.get("barrier_kcal")
|
|
514
|
+
delta_e = seg.get("delta_kcal")
|
|
515
|
+
b_txt = f"{barrier:7.2f}" if barrier is not None else " n/a"
|
|
516
|
+
d_txt = f"{delta_e:7.2f}" if delta_e is not None else " n/a"
|
|
517
|
+
lines.append(f" {delta}E{dagger} = {b_txt} kcal/mol, {delta}E = {d_txt} kcal/mol")
|
|
518
|
+
lines.append(" Bond changes:")
|
|
519
|
+
lines.extend(_format_bond_changes(str(seg.get("bond_changes", ""))))
|
|
520
|
+
else:
|
|
521
|
+
lines.append(" (no segment reports)")
|
|
522
|
+
|
|
523
|
+
post_segments: Iterable[Dict[str, Any]] = payload.get("post_segments", []) or []
|
|
524
|
+
segment_entries: Dict[int, Dict[str, Any]] = {}
|
|
525
|
+
for seg in segments:
|
|
526
|
+
idx = int(seg.get("index", 0) or 0)
|
|
527
|
+
tag = seg.get("tag", f"seg_{idx:03d}")
|
|
528
|
+
kind = seg.get("kind", "seg")
|
|
529
|
+
entry = segment_entries.setdefault(
|
|
530
|
+
idx, {"index": idx, "tag": tag, "kind": kind}
|
|
531
|
+
)
|
|
532
|
+
entry.setdefault("tag", tag)
|
|
533
|
+
entry.setdefault("kind", kind)
|
|
534
|
+
if seg.get("barrier_kcal") is not None:
|
|
535
|
+
entry["mep_barrier"] = seg.get("barrier_kcal")
|
|
536
|
+
if seg.get("delta_kcal") is not None:
|
|
537
|
+
entry["mep_delta"] = seg.get("delta_kcal")
|
|
538
|
+
lines.append("")
|
|
539
|
+
lines.append("[3] Per-segment post-processing (TSOPT / Thermo / DFT)")
|
|
540
|
+
if post_segments:
|
|
541
|
+
for seg in post_segments:
|
|
542
|
+
idx = int(seg.get("index", 0) or 0)
|
|
543
|
+
tag = seg.get("tag", f"seg_{idx:02d}")
|
|
544
|
+
kind = seg.get("kind", "seg")
|
|
545
|
+
lines.append(f" === Segment {idx:02d} ({kind}) tag={tag} ===")
|
|
546
|
+
if seg.get("post_dir"):
|
|
547
|
+
lines.append(
|
|
548
|
+
f" Post-process dir : {_shorten_path(seg.get('post_dir'), root_out_path)}"
|
|
549
|
+
)
|
|
550
|
+
ts_imag = seg.get("ts_imag") or seg.get("ts_imag_freq_cm")
|
|
551
|
+
lines.extend(_format_ts_imag_info(ts_imag))
|
|
552
|
+
if seg.get("irc_plot"):
|
|
553
|
+
lines.append(
|
|
554
|
+
f" IRC plot : {_shorten_path(seg.get('irc_plot'), root_out_path)}"
|
|
555
|
+
)
|
|
556
|
+
if seg.get("irc_traj"):
|
|
557
|
+
lines.append(
|
|
558
|
+
f" IRC trajectory : {_shorten_path(seg.get('irc_traj'), root_out_path)}"
|
|
559
|
+
)
|
|
560
|
+
_emit_energy_block(
|
|
561
|
+
lines, "ML/MM energies (TSOPT+IRC)", seg.get("uma"), root_out_path
|
|
562
|
+
)
|
|
563
|
+
_emit_energy_block(lines, "ML/MM Gibbs (thermo)", seg.get("gibbs_uma"), root_out_path)
|
|
564
|
+
_emit_energy_block(lines, "DFT single-point", seg.get("dft"), root_out_path)
|
|
565
|
+
_emit_energy_block(
|
|
566
|
+
lines, "DFT//ML/MM Gibbs", seg.get("gibbs_dft_uma"), root_out_path
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
entry = segment_entries.setdefault(
|
|
570
|
+
idx, {"index": idx, "tag": tag, "kind": kind}
|
|
571
|
+
)
|
|
572
|
+
entry.setdefault("tag", tag)
|
|
573
|
+
entry.setdefault("kind", kind)
|
|
574
|
+
if seg.get("mep_barrier_kcal") is not None:
|
|
575
|
+
entry["mep_barrier"] = seg.get("mep_barrier_kcal")
|
|
576
|
+
if seg.get("mep_delta_kcal") is not None:
|
|
577
|
+
entry["mep_delta"] = seg.get("mep_delta_kcal")
|
|
578
|
+
if seg.get("uma"):
|
|
579
|
+
uma_payload = seg.get("uma") or {}
|
|
580
|
+
if uma_payload.get("barrier_kcal") is not None:
|
|
581
|
+
entry["uma_barrier"] = uma_payload.get("barrier_kcal")
|
|
582
|
+
if uma_payload.get("delta_kcal") is not None:
|
|
583
|
+
entry["uma_delta"] = uma_payload.get("delta_kcal")
|
|
584
|
+
if seg.get("gibbs_uma"):
|
|
585
|
+
g_payload = seg.get("gibbs_uma") or {}
|
|
586
|
+
if g_payload.get("barrier_kcal") is not None:
|
|
587
|
+
entry["gibbs_uma_barrier"] = g_payload.get("barrier_kcal")
|
|
588
|
+
if g_payload.get("delta_kcal") is not None:
|
|
589
|
+
entry["gibbs_uma_delta"] = g_payload.get("delta_kcal")
|
|
590
|
+
if seg.get("dft"):
|
|
591
|
+
dft_payload = seg.get("dft") or {}
|
|
592
|
+
if dft_payload.get("barrier_kcal") is not None:
|
|
593
|
+
entry["dft_barrier"] = dft_payload.get("barrier_kcal")
|
|
594
|
+
if dft_payload.get("delta_kcal") is not None:
|
|
595
|
+
entry["dft_delta"] = dft_payload.get("delta_kcal")
|
|
596
|
+
if seg.get("gibbs_dft_uma"):
|
|
597
|
+
gd_payload = seg.get("gibbs_dft_uma") or {}
|
|
598
|
+
if gd_payload.get("barrier_kcal") is not None:
|
|
599
|
+
entry["gibbs_dft_uma_barrier"] = gd_payload.get("barrier_kcal")
|
|
600
|
+
if gd_payload.get("delta_kcal") is not None:
|
|
601
|
+
entry["gibbs_dft_uma_delta"] = gd_payload.get("delta_kcal")
|
|
602
|
+
else:
|
|
603
|
+
lines.append(" (no post-processing results)")
|
|
604
|
+
|
|
605
|
+
if segment_entries:
|
|
606
|
+
table_rows = [
|
|
607
|
+
(f"MEP {delta}E{dagger} [kcal/mol]", "mep_barrier"),
|
|
608
|
+
(f"MEP {delta}E [kcal/mol]", "mep_delta"),
|
|
609
|
+
(f"UMA {delta}E{dagger} [kcal/mol]", "uma_barrier"),
|
|
610
|
+
(f"UMA {delta}E [kcal/mol]", "uma_delta"),
|
|
611
|
+
(f"UMA {delta}G{dagger} [kcal/mol]", "gibbs_uma_barrier"),
|
|
612
|
+
(f"UMA {delta}G [kcal/mol]", "gibbs_uma_delta"),
|
|
613
|
+
(f"DFT//UMA {delta}E{dagger} [kcal/mol]", "dft_barrier"),
|
|
614
|
+
(f"DFT//UMA {delta}E [kcal/mol]", "dft_delta"),
|
|
615
|
+
(f"DFT//UMA {delta}G{dagger} [kcal/mol]", "gibbs_dft_uma_barrier"),
|
|
616
|
+
(f"DFT//UMA {delta}G [kcal/mol]", "gibbs_dft_uma_delta"),
|
|
617
|
+
]
|
|
618
|
+
sorted_entries = [segment_entries[k] for k in sorted(segment_entries.keys())]
|
|
619
|
+
headers = [f"{int(e.get('index', 0)):d}({e.get('tag', '-')})" for e in sorted_entries]
|
|
620
|
+
label_width = max(len(label) for label, _ in table_rows) + 2
|
|
621
|
+
col_width = max(max(len(h) for h in headers), 8)
|
|
622
|
+
|
|
623
|
+
lines.append("")
|
|
624
|
+
lines.append(" Segment overview table")
|
|
625
|
+
lines.append(
|
|
626
|
+
" "
|
|
627
|
+
+ f"{'Seg':<{label_width}} "
|
|
628
|
+
+ " ".join(f"{h:>{col_width}}" for h in headers)
|
|
629
|
+
)
|
|
630
|
+
for label, key in table_rows:
|
|
631
|
+
values = " ".join(_segment_table_value(entry, key, col_width) for entry in sorted_entries)
|
|
632
|
+
lines.append(f" {label:<{label_width}} {values}")
|
|
633
|
+
|
|
634
|
+
lines.append("")
|
|
635
|
+
lines.append("[4] Energy diagrams (overview)")
|
|
636
|
+
diagrams: Iterable[Dict[str, Any]] = payload.get("energy_diagrams", []) or []
|
|
637
|
+
diag_by_method: Dict[str, Dict[str, Any]] = {}
|
|
638
|
+
state_order: List[str] = []
|
|
639
|
+
|
|
640
|
+
if diagrams:
|
|
641
|
+
for diag_payload in diagrams:
|
|
642
|
+
image_path = diag_payload.get("image") or diag_payload.get("diagram")
|
|
643
|
+
if image_path and ("post_seg" in str(image_path) or "tsopt_seg_" in str(image_path)):
|
|
644
|
+
continue
|
|
645
|
+
|
|
646
|
+
name = diag_payload.get("name", "diagram")
|
|
647
|
+
ylabel = diag_payload.get("ylabel", f"{delta}E (kcal/mol)")
|
|
648
|
+
lines.append(f" {name} (ylabel: {ylabel})")
|
|
649
|
+
labels = diag_payload.get("labels", [])
|
|
650
|
+
energies = diag_payload.get("energies_kcal", [])
|
|
651
|
+
energy_label = f"{delta}G [kcal/mol]" if f"{delta}G" in str(ylabel) else f"{delta}E [kcal/mol]"
|
|
652
|
+
lines.append(f" State {energy_label}")
|
|
653
|
+
for i, lab in enumerate(labels):
|
|
654
|
+
rel = energies[i] if i < len(energies) else None
|
|
655
|
+
rel_txt = f"{rel:7.3f}" if rel is not None else " n/a"
|
|
656
|
+
lines.append(f" {lab:<8}{rel_txt}")
|
|
657
|
+
if diag_payload.get("image"):
|
|
658
|
+
lines.append(
|
|
659
|
+
f" Image : {_shorten_path(diag_payload.get('image'), root_out_path)}"
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
method_key = _classify_diagram_method(diag_payload)
|
|
663
|
+
diag_by_method.setdefault(method_key, diag_payload)
|
|
664
|
+
if not state_order and labels:
|
|
665
|
+
state_order = list(labels)
|
|
666
|
+
else:
|
|
667
|
+
lines.append(" (no energy diagrams recorded)")
|
|
668
|
+
|
|
669
|
+
if state_order and diag_by_method:
|
|
670
|
+
lines.append("")
|
|
671
|
+
lines.append(" Energy diagram overview table")
|
|
672
|
+
|
|
673
|
+
table_rows = [
|
|
674
|
+
(f"MEP {delta}E [kcal/mol]", "mep"),
|
|
675
|
+
(f"UMA {delta}E [kcal/mol]", "uma"),
|
|
676
|
+
(f"UMA {delta}G [kcal/mol]", "gibbs_uma"),
|
|
677
|
+
(f"DFT//UMA {delta}E [kcal/mol]", "dft"),
|
|
678
|
+
(f"DFT//UMA {delta}G [kcal/mol]", "gibbs_dft_uma"),
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
label_width = max(len(label) for label, _ in table_rows) + 2
|
|
682
|
+
col_width = max(max(len(st) for st in state_order), 7)
|
|
683
|
+
|
|
684
|
+
lines.append(
|
|
685
|
+
" "
|
|
686
|
+
+ f"{'State':<{label_width}} "
|
|
687
|
+
+ " ".join(f"{st:>{col_width}}" for st in state_order)
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
for label, method in table_rows:
|
|
691
|
+
diag_payload = diag_by_method.get(method)
|
|
692
|
+
lines.append(_format_diag_row(diag_payload, label, col_width, state_order, label_width))
|
|
693
|
+
|
|
694
|
+
lines.append("")
|
|
695
|
+
lines.append("[5] Output directory structure")
|
|
696
|
+
|
|
697
|
+
key_files = payload.get("key_files") or {}
|
|
698
|
+
annotations: Dict[str, str] = {Path(k).as_posix(): v for k, v in key_files.items()}
|
|
699
|
+
|
|
700
|
+
default_notes = {
|
|
701
|
+
"pockets": "Extracted pocket PDBs",
|
|
702
|
+
"scan": "Staged scan outputs",
|
|
703
|
+
"path_search": "Recursive GSM outputs",
|
|
704
|
+
"path_opt": "Single-pass GSM outputs",
|
|
705
|
+
"tsopt_single": "Single-structure TSOPT-only outputs",
|
|
706
|
+
"mep_plot.png": "ML/MM MEP energy plot",
|
|
707
|
+
"energy_diagram_MEP.png": "Compressed MEP diagram",
|
|
708
|
+
"energy_diagram_UMA_all.png": "UMA R-TS-P energies (all segments)",
|
|
709
|
+
"energy_diagram_G_UMA_all.png": "UMA Gibbs R-TS-P (all segments)",
|
|
710
|
+
"energy_diagram_DFT_all.png": "DFT R-TS-P (all segments)",
|
|
711
|
+
"energy_diagram_G_DFT_plus_UMA_all.png": "DFT//UMA Gibbs R-TS-P (all segments)",
|
|
712
|
+
"irc_plot_all.png": "Aggregated IRC plot",
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if root_out_path:
|
|
716
|
+
path_dir = payload.get("path_dir")
|
|
717
|
+
if path_dir:
|
|
718
|
+
try:
|
|
719
|
+
rel = Path(path_dir).relative_to(root_out_path).as_posix()
|
|
720
|
+
annotations.setdefault(rel, "Primary path module outputs")
|
|
721
|
+
except ValueError:
|
|
722
|
+
pass
|
|
723
|
+
|
|
724
|
+
for rel, desc in default_notes.items():
|
|
725
|
+
if (root_out_path / rel).exists():
|
|
726
|
+
annotations.setdefault(rel, desc)
|
|
727
|
+
|
|
728
|
+
if root_out_path.exists():
|
|
729
|
+
lines.extend(_format_directory_tree(root_out_path, annotations))
|
|
730
|
+
else:
|
|
731
|
+
lines.append(" (root output directory not found on disk)")
|
|
732
|
+
else:
|
|
733
|
+
lines.append(" (root output directory unknown)")
|
|
734
|
+
|
|
735
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
736
|
+
dest.write_text("\n".join(lines) + "\n", encoding="utf-8")
|