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/scan3d.py
ADDED
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ML/MM three-distance scan with harmonic restraints (d1, d2, d3 grid).
|
|
3
|
+
|
|
4
|
+
Example:
|
|
5
|
+
mlmm scan3d -i input.pdb --parm real.parm7 --model-pdb ml_region.pdb -q 0 \
|
|
6
|
+
--scan-lists "[(12,45,1.30,3.10),(10,55,1.20,3.20),(15,60,1.10,3.00)]"
|
|
7
|
+
|
|
8
|
+
For detailed documentation, see: docs/scan3d.md
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
from copy import deepcopy
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
import gc
|
|
19
|
+
import logging
|
|
20
|
+
import math
|
|
21
|
+
import shutil
|
|
22
|
+
import sys
|
|
23
|
+
import textwrap
|
|
24
|
+
import traceback
|
|
25
|
+
import tempfile
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
import click
|
|
31
|
+
import torch
|
|
32
|
+
import numpy as np
|
|
33
|
+
import pandas as pd
|
|
34
|
+
from scipy.interpolate import Rbf
|
|
35
|
+
import plotly.graph_objects as go
|
|
36
|
+
|
|
37
|
+
from pysisyphus.helpers import geom_loader
|
|
38
|
+
from pysisyphus.optimizers.LBFGS import LBFGS
|
|
39
|
+
from pysisyphus.optimizers.exceptions import OptimizationError, ZeroStepLength
|
|
40
|
+
from pysisyphus.constants import ANG2BOHR, AU2KCALPERMOL
|
|
41
|
+
|
|
42
|
+
from .mlmm_calc import mlmm
|
|
43
|
+
from .defaults import BIAS_KW as _BIAS_KW_DEFAULT
|
|
44
|
+
from .opt import (
|
|
45
|
+
GEOM_KW as _OPT_GEOM_KW,
|
|
46
|
+
CALC_KW as _OPT_CALC_KW,
|
|
47
|
+
OPT_BASE_KW as _OPT_BASE_KW,
|
|
48
|
+
LBFGS_KW as _OPT_LBFGS_KW,
|
|
49
|
+
HarmonicBiasCalculator,
|
|
50
|
+
_parse_freeze_atoms,
|
|
51
|
+
_normalize_geom_freeze,
|
|
52
|
+
)
|
|
53
|
+
from .utils import (
|
|
54
|
+
apply_ref_pdb_override,
|
|
55
|
+
apply_layer_freeze_constraints,
|
|
56
|
+
set_convert_file_enabled,
|
|
57
|
+
deep_update,
|
|
58
|
+
load_yaml_dict,
|
|
59
|
+
apply_yaml_overrides,
|
|
60
|
+
pretty_block,
|
|
61
|
+
strip_inherited_keys,
|
|
62
|
+
filter_calc_for_echo,
|
|
63
|
+
format_freeze_atoms_for_echo,
|
|
64
|
+
format_elapsed,
|
|
65
|
+
merge_freeze_atom_indices,
|
|
66
|
+
prepare_input_structure,
|
|
67
|
+
resolve_charge_spin_or_raise,
|
|
68
|
+
convert_xyz_to_pdb,
|
|
69
|
+
load_pdb_atom_metadata,
|
|
70
|
+
parse_scan_list_quads,
|
|
71
|
+
parse_scan_spec_quads,
|
|
72
|
+
is_scan_spec_file,
|
|
73
|
+
axis_label_csv,
|
|
74
|
+
axis_label_html,
|
|
75
|
+
PDB_ATOM_META_HEADER,
|
|
76
|
+
format_pdb_atom_metadata,
|
|
77
|
+
parse_indices_string,
|
|
78
|
+
build_model_pdb_from_bfactors,
|
|
79
|
+
build_model_pdb_from_indices,
|
|
80
|
+
ensure_dir,
|
|
81
|
+
distance_A_from_coords,
|
|
82
|
+
distance_tag,
|
|
83
|
+
values_from_bounds,
|
|
84
|
+
unbiased_energy_hartree,
|
|
85
|
+
snapshot_geometry,
|
|
86
|
+
convert_and_annotate_xyz_to_pdb,
|
|
87
|
+
)
|
|
88
|
+
from .cli_utils import resolve_yaml_sources, load_merged_yaml_cfg, make_is_param_explicit
|
|
89
|
+
|
|
90
|
+
# Shared defaults (copied from opt.py to keep ML/MM behaviour consistent)
|
|
91
|
+
GEOM_KW: Dict[str, Any] = deepcopy(_OPT_GEOM_KW)
|
|
92
|
+
CALC_KW: Dict[str, Any] = deepcopy(_OPT_CALC_KW)
|
|
93
|
+
OPT_BASE_KW: Dict[str, Any] = deepcopy(_OPT_BASE_KW)
|
|
94
|
+
OPT_BASE_KW.update(
|
|
95
|
+
{
|
|
96
|
+
"out_dir": "./result_scan3d/",
|
|
97
|
+
"dump": False,
|
|
98
|
+
"max_cycles": 10000,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
LBFGS_KW: Dict[str, Any] = deepcopy(_OPT_LBFGS_KW)
|
|
102
|
+
LBFGS_KW.update({"out_dir": "./result_scan3d/"})
|
|
103
|
+
BIAS_KW: Dict[str, Any] = deepcopy(_BIAS_KW_DEFAULT)
|
|
104
|
+
|
|
105
|
+
_VOLUME_GRID_N = 50 # 50×50×50 RBF interpolation grid
|
|
106
|
+
|
|
107
|
+
_snapshot_geometry = functools.partial(snapshot_geometry, coord_type_default="cart")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_axis_label(df: pd.DataFrame, column: str, fallback: Optional[str]) -> Optional[str]:
|
|
111
|
+
if column not in df.columns:
|
|
112
|
+
return fallback
|
|
113
|
+
values = df[column].dropna()
|
|
114
|
+
if values.empty:
|
|
115
|
+
return fallback
|
|
116
|
+
return str(values.iloc[0])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _make_lbfgs(
|
|
120
|
+
geom,
|
|
121
|
+
lbfgs_cfg: Dict[str, Any],
|
|
122
|
+
opt_cfg: Dict[str, Any],
|
|
123
|
+
*,
|
|
124
|
+
max_step_bohr: float,
|
|
125
|
+
relax_max_cycles: int,
|
|
126
|
+
out_dir: Path,
|
|
127
|
+
prefix: str,
|
|
128
|
+
) -> LBFGS:
|
|
129
|
+
common = dict(opt_cfg)
|
|
130
|
+
common["out_dir"] = str(out_dir)
|
|
131
|
+
common["prefix"] = prefix
|
|
132
|
+
args = {**lbfgs_cfg, **common}
|
|
133
|
+
args["max_step"] = min(float(lbfgs_cfg.get("max_step", 0.30)), max_step_bohr)
|
|
134
|
+
args["max_cycles"] = int(relax_max_cycles)
|
|
135
|
+
return LBFGS(geom, **args)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _finalize_surface_and_plot(
|
|
139
|
+
*,
|
|
140
|
+
df: pd.DataFrame,
|
|
141
|
+
final_dir: Path,
|
|
142
|
+
baseline: str,
|
|
143
|
+
zmin: Optional[float],
|
|
144
|
+
zmax: Optional[float],
|
|
145
|
+
d1_label_csv: Optional[str],
|
|
146
|
+
d2_label_csv: Optional[str],
|
|
147
|
+
d3_label_csv: Optional[str],
|
|
148
|
+
write_surface_csv: bool,
|
|
149
|
+
time_start: float,
|
|
150
|
+
) -> None:
|
|
151
|
+
if df.empty:
|
|
152
|
+
click.echo("No grid records produced; aborting.", err=True)
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
|
|
155
|
+
d1_label_csv = _extract_axis_label(df, "d1_label", d1_label_csv)
|
|
156
|
+
d2_label_csv = _extract_axis_label(df, "d2_label", d2_label_csv)
|
|
157
|
+
d3_label_csv = _extract_axis_label(df, "d3_label", d3_label_csv)
|
|
158
|
+
if d1_label_csv is None or d2_label_csv is None or d3_label_csv is None:
|
|
159
|
+
click.echo(
|
|
160
|
+
"[plot] WARNING: axis label metadata is missing in CSV; using generic labels.",
|
|
161
|
+
err=True,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
d1_label_html = axis_label_html(d1_label_csv) if d1_label_csv else "d1 (Å)"
|
|
165
|
+
d2_label_html = axis_label_html(d2_label_csv) if d2_label_csv else "d2 (Å)"
|
|
166
|
+
d3_label_html = axis_label_html(d3_label_csv) if d3_label_csv else "d3 (Å)"
|
|
167
|
+
|
|
168
|
+
if "energy_kcal" not in df.columns:
|
|
169
|
+
if "energy_hartree" not in df.columns:
|
|
170
|
+
click.echo(
|
|
171
|
+
"[baseline] energy_kcal is missing and energy_hartree is not available in CSV; aborting.",
|
|
172
|
+
err=True,
|
|
173
|
+
)
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
if baseline == "first":
|
|
177
|
+
ref_mask = (df["i"] == 0) & (df["j"] == 0) & (df["k"] == 0)
|
|
178
|
+
if not ref_mask.any():
|
|
179
|
+
click.echo(
|
|
180
|
+
"[baseline] 'first' requested but (i=0,j=0,k=0) missing; using global minimum instead.",
|
|
181
|
+
err=True,
|
|
182
|
+
)
|
|
183
|
+
ref_energy = float(df["energy_hartree"].min())
|
|
184
|
+
else:
|
|
185
|
+
ref_energy = float(df.loc[ref_mask, "energy_hartree"].iloc[0])
|
|
186
|
+
else:
|
|
187
|
+
ref_energy = float(df["energy_hartree"].min())
|
|
188
|
+
df["energy_kcal"] = (df["energy_hartree"] - ref_energy) * AU2KCALPERMOL
|
|
189
|
+
|
|
190
|
+
if write_surface_csv:
|
|
191
|
+
surface_csv = final_dir / "surface.csv"
|
|
192
|
+
df["d1_label"] = d1_label_csv
|
|
193
|
+
df["d2_label"] = d2_label_csv
|
|
194
|
+
df["d3_label"] = d3_label_csv
|
|
195
|
+
df.to_csv(surface_csv, index=False)
|
|
196
|
+
click.echo(f"[write] Wrote '{surface_csv}'.")
|
|
197
|
+
|
|
198
|
+
# ===== 3D RBF interpolation & visualization (isosurface) =====
|
|
199
|
+
d1_points = df["d1_A"].to_numpy(dtype=float)
|
|
200
|
+
d2_points = df["d2_A"].to_numpy(dtype=float)
|
|
201
|
+
d3_points = df["d3_A"].to_numpy(dtype=float)
|
|
202
|
+
z_points = df["energy_kcal"].to_numpy(dtype=float)
|
|
203
|
+
|
|
204
|
+
mask = (
|
|
205
|
+
np.isfinite(d1_points)
|
|
206
|
+
& np.isfinite(d2_points)
|
|
207
|
+
& np.isfinite(d3_points)
|
|
208
|
+
& np.isfinite(z_points)
|
|
209
|
+
)
|
|
210
|
+
if not np.any(mask):
|
|
211
|
+
click.echo("[plot] No finite data for plotting.", err=True)
|
|
212
|
+
sys.exit(1)
|
|
213
|
+
|
|
214
|
+
x_min, x_max = float(np.min(d1_points[mask])), float(np.max(d1_points[mask]))
|
|
215
|
+
y_min, y_max = float(np.min(d2_points[mask])), float(np.max(d2_points[mask]))
|
|
216
|
+
z_min_val, z_max_val = float(np.min(d3_points[mask])), float(np.max(d3_points[mask]))
|
|
217
|
+
|
|
218
|
+
xi = np.linspace(x_min, x_max, _VOLUME_GRID_N)
|
|
219
|
+
yi = np.linspace(y_min, y_max, _VOLUME_GRID_N)
|
|
220
|
+
zi = np.linspace(z_min_val, z_max_val, _VOLUME_GRID_N)
|
|
221
|
+
|
|
222
|
+
click.echo("[plot] 3D RBF interpolation on a 50×50×50 grid ...")
|
|
223
|
+
rbf3d = Rbf(
|
|
224
|
+
d1_points[mask],
|
|
225
|
+
d2_points[mask],
|
|
226
|
+
d3_points[mask],
|
|
227
|
+
z_points[mask],
|
|
228
|
+
function="multiquadric",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
XI, YI, ZI = np.meshgrid(xi, yi, zi, indexing="xy")
|
|
232
|
+
X_flat = XI.flatten()
|
|
233
|
+
Y_flat = YI.flatten()
|
|
234
|
+
Z_flat = ZI.flatten()
|
|
235
|
+
E_flat = rbf3d(X_flat, Y_flat, Z_flat)
|
|
236
|
+
|
|
237
|
+
vmin = float(np.nanmin(E_flat)) if zmin is None else float(zmin)
|
|
238
|
+
vmax = float(np.nanmax(E_flat)) if zmax is None else float(zmax)
|
|
239
|
+
if not np.isfinite(vmin) or not np.isfinite(vmax) or vmax <= vmin:
|
|
240
|
+
vmin, vmax = float(np.nanmin(E_flat)), float(np.nanmax(E_flat))
|
|
241
|
+
|
|
242
|
+
# Discrete isosurfaces with banded colors
|
|
243
|
+
n_levels = 8
|
|
244
|
+
level_values = np.linspace(vmin, vmax, n_levels + 2)[1:-1]
|
|
245
|
+
level_colors = [
|
|
246
|
+
"#0d0887",
|
|
247
|
+
"#5b02a3",
|
|
248
|
+
"#9c179e",
|
|
249
|
+
"#cb4679",
|
|
250
|
+
"#ed7953",
|
|
251
|
+
"#fb9f3a",
|
|
252
|
+
"#fdca26",
|
|
253
|
+
"#f0f921",
|
|
254
|
+
]
|
|
255
|
+
level_opacity = [
|
|
256
|
+
1.000, 0.667, 0.444, 0.296, 0.198, 0.132, 0.088, 0.059,
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
isosurfaces = []
|
|
260
|
+
for lvl, color, opacity_lvl in zip(level_values, level_colors, level_opacity):
|
|
261
|
+
trace = go.Isosurface(
|
|
262
|
+
x=X_flat,
|
|
263
|
+
y=Y_flat,
|
|
264
|
+
z=Z_flat,
|
|
265
|
+
value=E_flat,
|
|
266
|
+
isomin=lvl,
|
|
267
|
+
isomax=lvl,
|
|
268
|
+
surface_count=1,
|
|
269
|
+
opacity=opacity_lvl,
|
|
270
|
+
showscale=False,
|
|
271
|
+
colorscale=[[0.0, color], [1.0, color]],
|
|
272
|
+
caps=dict(x_show=False, y_show=False, z_show=False),
|
|
273
|
+
name=f"{lvl:.1f} kcal/mol",
|
|
274
|
+
)
|
|
275
|
+
isosurfaces.append(trace)
|
|
276
|
+
|
|
277
|
+
colorbar_colorscale = [
|
|
278
|
+
[idx / (len(level_colors) - 1), col]
|
|
279
|
+
for idx, col in enumerate(level_colors)
|
|
280
|
+
]
|
|
281
|
+
cb_tickvals = [float(v) for v in level_values]
|
|
282
|
+
cb_ticktext = [f"{v:.1f}" for v in level_values]
|
|
283
|
+
|
|
284
|
+
colorbar_trace = go.Scatter3d(
|
|
285
|
+
x=[x_min],
|
|
286
|
+
y=[y_min],
|
|
287
|
+
z=[z_min_val],
|
|
288
|
+
mode="markers",
|
|
289
|
+
marker=dict(
|
|
290
|
+
size=0,
|
|
291
|
+
opacity=0.0,
|
|
292
|
+
color=[vmin, vmax],
|
|
293
|
+
colorscale=colorbar_colorscale,
|
|
294
|
+
showscale=True,
|
|
295
|
+
colorbar=dict(
|
|
296
|
+
title=dict(text="(kcal/mol)", side="top", font=dict(size=16, color="#1C1C1C")),
|
|
297
|
+
tickfont=dict(size=14, color="#1C1C1C"),
|
|
298
|
+
ticks="inside",
|
|
299
|
+
ticklen=10,
|
|
300
|
+
tickcolor="#1C1C1C",
|
|
301
|
+
outlinecolor="#1C1C1C",
|
|
302
|
+
outlinewidth=2,
|
|
303
|
+
lenmode="fraction",
|
|
304
|
+
len=1.11,
|
|
305
|
+
x=1.05,
|
|
306
|
+
y=0.53,
|
|
307
|
+
xanchor="left",
|
|
308
|
+
yanchor="middle",
|
|
309
|
+
tickvals=cb_tickvals,
|
|
310
|
+
ticktext=cb_ticktext,
|
|
311
|
+
),
|
|
312
|
+
),
|
|
313
|
+
hoverinfo="none",
|
|
314
|
+
showlegend=False,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
fig3d = go.Figure(data=isosurfaces + [colorbar_trace])
|
|
318
|
+
fig3d.update_layout(
|
|
319
|
+
title="3D Energy Landscape (ML/MM)",
|
|
320
|
+
width=900,
|
|
321
|
+
height=800,
|
|
322
|
+
scene=dict(
|
|
323
|
+
bgcolor="rgba(0,0,0,0)",
|
|
324
|
+
xaxis=dict(
|
|
325
|
+
title=d1_label_html,
|
|
326
|
+
range=[x_min, x_max],
|
|
327
|
+
showline=True,
|
|
328
|
+
linewidth=4,
|
|
329
|
+
linecolor="#1C1C1C",
|
|
330
|
+
mirror=True,
|
|
331
|
+
ticks="inside",
|
|
332
|
+
tickwidth=4,
|
|
333
|
+
tickcolor="#1C1C1C",
|
|
334
|
+
gridcolor="rgba(0,0,0,0.1)",
|
|
335
|
+
zerolinecolor="rgba(0,0,0,0.1)",
|
|
336
|
+
showbackground=False,
|
|
337
|
+
),
|
|
338
|
+
yaxis=dict(
|
|
339
|
+
title=d2_label_html,
|
|
340
|
+
range=[y_min, y_max],
|
|
341
|
+
showline=True,
|
|
342
|
+
linewidth=4,
|
|
343
|
+
linecolor="#1C1C1C",
|
|
344
|
+
mirror=True,
|
|
345
|
+
ticks="inside",
|
|
346
|
+
tickwidth=4,
|
|
347
|
+
tickcolor="#1C1C1C",
|
|
348
|
+
gridcolor="rgba(0,0,0,0.1)",
|
|
349
|
+
zerolinecolor="rgba(0,0,0,0.1)",
|
|
350
|
+
showbackground=False,
|
|
351
|
+
),
|
|
352
|
+
zaxis=dict(
|
|
353
|
+
title=d3_label_html,
|
|
354
|
+
range=[z_min_val, z_max_val],
|
|
355
|
+
showline=True,
|
|
356
|
+
linewidth=4,
|
|
357
|
+
linecolor="#1C1C1C",
|
|
358
|
+
mirror=True,
|
|
359
|
+
ticks="inside",
|
|
360
|
+
tickwidth=4,
|
|
361
|
+
tickcolor="#1C1C1C",
|
|
362
|
+
gridcolor="rgba(0,0,0,0.1)",
|
|
363
|
+
zerolinecolor="rgba(0,0,0,0.1)",
|
|
364
|
+
showbackground=False,
|
|
365
|
+
),
|
|
366
|
+
aspectmode="cube",
|
|
367
|
+
),
|
|
368
|
+
margin=dict(l=10, r=20, b=10, t=40),
|
|
369
|
+
paper_bgcolor="white",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
html3d = final_dir / "scan3d_density.html"
|
|
373
|
+
fig3d.write_html(str(html3d))
|
|
374
|
+
click.echo(f"[plot] Wrote '{html3d}'.")
|
|
375
|
+
|
|
376
|
+
click.echo("\n=== 3D Scan finished ===\n")
|
|
377
|
+
click.echo(format_elapsed("[time] Elapsed Time for 3D Scan", time_start))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@click.command(
|
|
381
|
+
help="3D distance scan with harmonic restraints using the ML/MM calculator.",
|
|
382
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
383
|
+
)
|
|
384
|
+
@click.option(
|
|
385
|
+
"-i",
|
|
386
|
+
"--input",
|
|
387
|
+
"input_path",
|
|
388
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
389
|
+
required=False,
|
|
390
|
+
help="Input structure file (.pdb/.xyz). Required unless --csv is provided.",
|
|
391
|
+
)
|
|
392
|
+
@click.option(
|
|
393
|
+
"--parm",
|
|
394
|
+
"real_parm7",
|
|
395
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
396
|
+
required=False,
|
|
397
|
+
help="Amber parm7 topology for the enzyme. Required unless --csv is provided.",
|
|
398
|
+
)
|
|
399
|
+
@click.option(
|
|
400
|
+
"--model-pdb",
|
|
401
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
402
|
+
required=False,
|
|
403
|
+
help="PDB defining the ML region. Optional when --detect-layer is enabled.",
|
|
404
|
+
)
|
|
405
|
+
@click.option(
|
|
406
|
+
"--model-indices",
|
|
407
|
+
"model_indices_str",
|
|
408
|
+
type=str,
|
|
409
|
+
default=None,
|
|
410
|
+
show_default=False,
|
|
411
|
+
help="Comma-separated atom indices for the ML region (ranges allowed like 1-5). "
|
|
412
|
+
"Used when --model-pdb is omitted.",
|
|
413
|
+
)
|
|
414
|
+
@click.option(
|
|
415
|
+
"--model-indices-one-based/--model-indices-zero-based",
|
|
416
|
+
"model_indices_one_based",
|
|
417
|
+
default=True,
|
|
418
|
+
show_default=True,
|
|
419
|
+
help="Interpret --model-indices as 1-based (default) or 0-based.",
|
|
420
|
+
)
|
|
421
|
+
@click.option(
|
|
422
|
+
"--detect-layer/--no-detect-layer",
|
|
423
|
+
"detect_layer",
|
|
424
|
+
default=True,
|
|
425
|
+
show_default=True,
|
|
426
|
+
help="Detect ML/MM layers from input PDB B-factors (B=0/10/20). "
|
|
427
|
+
"If disabled, you must provide --model-pdb or --model-indices.",
|
|
428
|
+
)
|
|
429
|
+
@click.option("-q", "--charge", type=int, required=False,
|
|
430
|
+
help="ML-region total charge. Required unless --ligand-charge is provided.")
|
|
431
|
+
@click.option("-l", "--ligand-charge", type=str, default=None, show_default=False,
|
|
432
|
+
help="Total charge or per-resname mapping (e.g., GPP:-3,SAM:1) used to derive "
|
|
433
|
+
"charge when -q is omitted (requires PDB input or --ref-pdb).")
|
|
434
|
+
@click.option(
|
|
435
|
+
"-m",
|
|
436
|
+
"--multiplicity",
|
|
437
|
+
"spin",
|
|
438
|
+
type=int,
|
|
439
|
+
default=None,
|
|
440
|
+
show_default=False,
|
|
441
|
+
help="Spin multiplicity (2S+1) for the ML region. Defaults to 1 when omitted.",
|
|
442
|
+
)
|
|
443
|
+
@click.option(
|
|
444
|
+
"--freeze-atoms",
|
|
445
|
+
"freeze_atoms_cli",
|
|
446
|
+
type=str,
|
|
447
|
+
default=None,
|
|
448
|
+
show_default=False,
|
|
449
|
+
help='Comma-separated 1-based atom indices to freeze (e.g., "1,3,5").',
|
|
450
|
+
)
|
|
451
|
+
@click.option(
|
|
452
|
+
"--hess-cutoff",
|
|
453
|
+
"hess_cutoff",
|
|
454
|
+
type=float,
|
|
455
|
+
default=None,
|
|
456
|
+
show_default=False,
|
|
457
|
+
help="Distance cutoff (Å) from ML region for MM atoms to include in Hessian calculation. "
|
|
458
|
+
"Applied to movable MM atoms and can be combined with --detect-layer.",
|
|
459
|
+
)
|
|
460
|
+
@click.option(
|
|
461
|
+
"--movable-cutoff",
|
|
462
|
+
"movable_cutoff",
|
|
463
|
+
type=float,
|
|
464
|
+
default=None,
|
|
465
|
+
show_default=False,
|
|
466
|
+
help="Distance cutoff (Å) from ML region for movable MM atoms. MM atoms beyond this are frozen. "
|
|
467
|
+
"Providing --movable-cutoff disables --detect-layer.",
|
|
468
|
+
)
|
|
469
|
+
@click.option(
|
|
470
|
+
"-s", "--scan-lists",
|
|
471
|
+
"scan_list_raw",
|
|
472
|
+
type=str,
|
|
473
|
+
required=False,
|
|
474
|
+
help="Scan targets: inline Python literal or a YAML/JSON spec file path.",
|
|
475
|
+
)
|
|
476
|
+
@click.option(
|
|
477
|
+
"--csv",
|
|
478
|
+
"csv_path",
|
|
479
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
480
|
+
required=False,
|
|
481
|
+
help="Plot-only mode: load a precomputed surface.csv and skip the 3D scan.",
|
|
482
|
+
)
|
|
483
|
+
@click.option(
|
|
484
|
+
"--one-based/--zero-based",
|
|
485
|
+
"one_based",
|
|
486
|
+
default=True,
|
|
487
|
+
show_default=True,
|
|
488
|
+
help="Interpret (i,j) indices in --scan-lists as 1-based (default) or 0-based.",
|
|
489
|
+
)
|
|
490
|
+
@click.option(
|
|
491
|
+
"--print-parsed/--no-print-parsed",
|
|
492
|
+
"print_parsed",
|
|
493
|
+
default=False,
|
|
494
|
+
show_default=True,
|
|
495
|
+
help="Print parsed scan targets after resolving --scan-lists.",
|
|
496
|
+
)
|
|
497
|
+
@click.option(
|
|
498
|
+
"--max-step-size",
|
|
499
|
+
type=float,
|
|
500
|
+
default=0.20,
|
|
501
|
+
show_default=True,
|
|
502
|
+
help="Maximum spacing between successive distance targets [Å].",
|
|
503
|
+
)
|
|
504
|
+
@click.option("--bias-k", type=float, default=300.0, show_default=True, help="Harmonic well strength k [eV/Å^2].")
|
|
505
|
+
@click.option(
|
|
506
|
+
"--relax-max-cycles",
|
|
507
|
+
type=int,
|
|
508
|
+
default=10000,
|
|
509
|
+
show_default=True,
|
|
510
|
+
help="Maximum LBFGS cycles per biased relaxation (also used for preopt).",
|
|
511
|
+
)
|
|
512
|
+
@click.option(
|
|
513
|
+
"--dump/--no-dump",
|
|
514
|
+
"dump",
|
|
515
|
+
default=False,
|
|
516
|
+
show_default=True,
|
|
517
|
+
help="Write inner d3 scan TRJs per (d1,d2) slice.",
|
|
518
|
+
)
|
|
519
|
+
@click.option(
|
|
520
|
+
"-o", "--out-dir",
|
|
521
|
+
type=str,
|
|
522
|
+
default="./result_scan3d/",
|
|
523
|
+
show_default=True,
|
|
524
|
+
help="Base output directory.",
|
|
525
|
+
)
|
|
526
|
+
@click.option(
|
|
527
|
+
"--thresh",
|
|
528
|
+
type=click.Choice(["gau_loose", "gau", "gau_tight", "gau_vtight", "baker", "never"], case_sensitive=False),
|
|
529
|
+
default="baker",
|
|
530
|
+
show_default=True,
|
|
531
|
+
help="Convergence preset.",
|
|
532
|
+
)
|
|
533
|
+
@click.option(
|
|
534
|
+
"--config",
|
|
535
|
+
"config_yaml",
|
|
536
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
537
|
+
default=None,
|
|
538
|
+
show_default=False,
|
|
539
|
+
help="Base YAML configuration file applied before explicit CLI options.",
|
|
540
|
+
)
|
|
541
|
+
@click.option(
|
|
542
|
+
"--ref-pdb",
|
|
543
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
544
|
+
default=None,
|
|
545
|
+
help="Reference PDB topology to use when --input is XYZ (keeps XYZ coordinates).",
|
|
546
|
+
)
|
|
547
|
+
@click.option(
|
|
548
|
+
"--preopt/--no-preopt",
|
|
549
|
+
"preopt",
|
|
550
|
+
default=False,
|
|
551
|
+
show_default=True,
|
|
552
|
+
help="Run an unbiased pre-optimization.",
|
|
553
|
+
)
|
|
554
|
+
@click.option(
|
|
555
|
+
"--baseline",
|
|
556
|
+
type=click.Choice(["min", "first"]),
|
|
557
|
+
default="min",
|
|
558
|
+
show_default=True,
|
|
559
|
+
help="Reference for relative energy (kcal/mol): 'min' or 'first' (i=0,j=0,k=0).",
|
|
560
|
+
)
|
|
561
|
+
@click.option(
|
|
562
|
+
"--zmin",
|
|
563
|
+
type=float,
|
|
564
|
+
default=None,
|
|
565
|
+
show_default=False,
|
|
566
|
+
help="Lower bound of the color scale (kcal/mol).",
|
|
567
|
+
)
|
|
568
|
+
@click.option(
|
|
569
|
+
"--zmax",
|
|
570
|
+
type=float,
|
|
571
|
+
default=None,
|
|
572
|
+
show_default=False,
|
|
573
|
+
help="Upper bound of the color scale (kcal/mol).",
|
|
574
|
+
)
|
|
575
|
+
@click.option(
|
|
576
|
+
"--convert-files/--no-convert-files",
|
|
577
|
+
"convert_files",
|
|
578
|
+
default=True,
|
|
579
|
+
show_default=True,
|
|
580
|
+
help="Convert XYZ/TRJ outputs into PDB companions based on the input format.",
|
|
581
|
+
)
|
|
582
|
+
@click.option(
|
|
583
|
+
"-b", "--backend",
|
|
584
|
+
type=click.Choice(["uma", "orb", "mace", "aimnet2"], case_sensitive=False),
|
|
585
|
+
default=None,
|
|
586
|
+
show_default=False,
|
|
587
|
+
help="ML backend for the ONIOM high-level region (default: uma).",
|
|
588
|
+
)
|
|
589
|
+
@click.option(
|
|
590
|
+
"--embedcharge/--no-embedcharge",
|
|
591
|
+
"embedcharge",
|
|
592
|
+
default=False,
|
|
593
|
+
show_default=True,
|
|
594
|
+
help="Enable xTB point-charge embedding correction for MM→ML environmental effects.",
|
|
595
|
+
)
|
|
596
|
+
@click.option(
|
|
597
|
+
"--embedcharge-cutoff",
|
|
598
|
+
"embedcharge_cutoff",
|
|
599
|
+
type=float,
|
|
600
|
+
default=None,
|
|
601
|
+
show_default=False,
|
|
602
|
+
help="Distance cutoff (Å) from ML region for MM point charges in xTB embedding. "
|
|
603
|
+
"Default: 12.0 Å when --embedcharge is enabled.",
|
|
604
|
+
)
|
|
605
|
+
@click.pass_context
|
|
606
|
+
def cli(
|
|
607
|
+
ctx: click.Context,
|
|
608
|
+
input_path: Optional[Path],
|
|
609
|
+
real_parm7: Optional[Path],
|
|
610
|
+
model_pdb: Optional[Path],
|
|
611
|
+
model_indices_str: Optional[str],
|
|
612
|
+
model_indices_one_based: bool,
|
|
613
|
+
detect_layer: bool,
|
|
614
|
+
charge: Optional[int],
|
|
615
|
+
ligand_charge: Optional[str],
|
|
616
|
+
spin: Optional[int],
|
|
617
|
+
freeze_atoms_cli: Optional[str],
|
|
618
|
+
hess_cutoff: Optional[float],
|
|
619
|
+
movable_cutoff: Optional[float],
|
|
620
|
+
scan_list_raw: Optional[str],
|
|
621
|
+
csv_path: Optional[Path],
|
|
622
|
+
one_based: bool,
|
|
623
|
+
print_parsed: bool,
|
|
624
|
+
max_step_size: float,
|
|
625
|
+
bias_k: float,
|
|
626
|
+
relax_max_cycles: int,
|
|
627
|
+
dump: bool,
|
|
628
|
+
out_dir: str,
|
|
629
|
+
thresh: Optional[str],
|
|
630
|
+
config_yaml: Optional[Path],
|
|
631
|
+
ref_pdb: Optional[Path],
|
|
632
|
+
preopt: bool,
|
|
633
|
+
baseline: str,
|
|
634
|
+
zmin: Optional[float],
|
|
635
|
+
zmax: Optional[float],
|
|
636
|
+
convert_files: bool,
|
|
637
|
+
backend: Optional[str],
|
|
638
|
+
embedcharge: bool,
|
|
639
|
+
embedcharge_cutoff: Optional[float],
|
|
640
|
+
) -> None:
|
|
641
|
+
_is_param_explicit = make_is_param_explicit(ctx)
|
|
642
|
+
|
|
643
|
+
set_convert_file_enabled(convert_files)
|
|
644
|
+
time_start = time.perf_counter()
|
|
645
|
+
config_yaml, override_yaml, used_legacy_yaml = resolve_yaml_sources(
|
|
646
|
+
config_yaml=config_yaml,
|
|
647
|
+
override_yaml=None,
|
|
648
|
+
args_yaml_legacy=None,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
if csv_path is not None:
|
|
652
|
+
final_dir = Path(out_dir).resolve()
|
|
653
|
+
ensure_dir(final_dir)
|
|
654
|
+
resolved_csv = Path(csv_path).resolve()
|
|
655
|
+
try:
|
|
656
|
+
df = pd.read_csv(resolved_csv)
|
|
657
|
+
except Exception as exc:
|
|
658
|
+
click.echo(f"[read] Failed to read CSV '{resolved_csv}': {exc}", err=True)
|
|
659
|
+
sys.exit(1)
|
|
660
|
+
click.echo(f"[read] Loaded precomputed grid from '{resolved_csv}'.")
|
|
661
|
+
_finalize_surface_and_plot(
|
|
662
|
+
df=df,
|
|
663
|
+
final_dir=final_dir,
|
|
664
|
+
baseline=baseline,
|
|
665
|
+
zmin=zmin,
|
|
666
|
+
zmax=zmax,
|
|
667
|
+
d1_label_csv=None,
|
|
668
|
+
d2_label_csv=None,
|
|
669
|
+
d3_label_csv=None,
|
|
670
|
+
write_surface_csv=False,
|
|
671
|
+
time_start=time_start,
|
|
672
|
+
)
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
if input_path is None:
|
|
676
|
+
click.echo("ERROR: -i/--input is required unless --csv is provided.", err=True)
|
|
677
|
+
sys.exit(1)
|
|
678
|
+
if real_parm7 is None:
|
|
679
|
+
click.echo("ERROR: --parm is required unless --csv is provided.", err=True)
|
|
680
|
+
sys.exit(1)
|
|
681
|
+
|
|
682
|
+
suffix = input_path.suffix.lower()
|
|
683
|
+
if suffix not in (".pdb", ".xyz"):
|
|
684
|
+
click.echo("ERROR: --input must be a PDB or XYZ file.", err=True)
|
|
685
|
+
sys.exit(1)
|
|
686
|
+
if suffix == ".xyz" and ref_pdb is None:
|
|
687
|
+
click.echo("ERROR: --ref-pdb is required when --input is an XYZ file.", err=True)
|
|
688
|
+
sys.exit(1)
|
|
689
|
+
|
|
690
|
+
tmp_root = None
|
|
691
|
+
try:
|
|
692
|
+
with prepare_input_structure(input_path) as prepared_input:
|
|
693
|
+
try:
|
|
694
|
+
apply_ref_pdb_override(prepared_input, ref_pdb)
|
|
695
|
+
except click.BadParameter as e:
|
|
696
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
697
|
+
sys.exit(1)
|
|
698
|
+
geom_input_path = prepared_input.geom_path
|
|
699
|
+
source_path = prepared_input.source_path
|
|
700
|
+
charge, spin = resolve_charge_spin_or_raise(
|
|
701
|
+
prepared_input, charge, spin,
|
|
702
|
+
ligand_charge=ligand_charge, prefix="[scan3d]",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
try:
|
|
706
|
+
freeze_atoms_list = _parse_freeze_atoms(freeze_atoms_cli)
|
|
707
|
+
except click.BadParameter as exc:
|
|
708
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
709
|
+
sys.exit(1)
|
|
710
|
+
|
|
711
|
+
model_indices: Optional[List[int]] = None
|
|
712
|
+
if model_indices_str:
|
|
713
|
+
try:
|
|
714
|
+
model_indices = parse_indices_string(model_indices_str, one_based=model_indices_one_based)
|
|
715
|
+
except click.BadParameter as exc:
|
|
716
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
717
|
+
sys.exit(1)
|
|
718
|
+
|
|
719
|
+
yaml_cfg, _, _ = load_merged_yaml_cfg(
|
|
720
|
+
config_yaml=config_yaml,
|
|
721
|
+
override_yaml=None,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
geom_cfg = dict(GEOM_KW)
|
|
725
|
+
calc_cfg = dict(CALC_KW)
|
|
726
|
+
opt_cfg = dict(OPT_BASE_KW)
|
|
727
|
+
lbfgs_cfg = dict(LBFGS_KW)
|
|
728
|
+
bias_cfg = dict(BIAS_KW)
|
|
729
|
+
|
|
730
|
+
apply_yaml_overrides(
|
|
731
|
+
yaml_cfg,
|
|
732
|
+
[
|
|
733
|
+
(geom_cfg, (("geom",),)),
|
|
734
|
+
(calc_cfg, (("calc",), ("mlmm",))),
|
|
735
|
+
(opt_cfg, (("opt",),)),
|
|
736
|
+
(lbfgs_cfg, (("lbfgs",), ("opt", "lbfgs"))),
|
|
737
|
+
(bias_cfg, (("bias",),)),
|
|
738
|
+
],
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
try:
|
|
742
|
+
geom_freeze = _normalize_geom_freeze(geom_cfg.get("freeze_atoms"))
|
|
743
|
+
except click.BadParameter as exc:
|
|
744
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
745
|
+
sys.exit(1)
|
|
746
|
+
geom_cfg["freeze_atoms"] = geom_freeze
|
|
747
|
+
if freeze_atoms_list:
|
|
748
|
+
merge_freeze_atom_indices(geom_cfg, freeze_atoms_list)
|
|
749
|
+
freeze_atoms_final = list(geom_cfg.get("freeze_atoms") or [])
|
|
750
|
+
calc_cfg["freeze_atoms"] = freeze_atoms_final
|
|
751
|
+
|
|
752
|
+
opt_cfg["out_dir"] = out_dir
|
|
753
|
+
opt_cfg["dump"] = False
|
|
754
|
+
opt_cfg["max_cycles"] = int(relax_max_cycles)
|
|
755
|
+
if thresh is not None:
|
|
756
|
+
opt_cfg["thresh"] = str(thresh)
|
|
757
|
+
lbfgs_cfg["max_cycles"] = int(relax_max_cycles)
|
|
758
|
+
if bias_k is not None:
|
|
759
|
+
bias_cfg["k"] = float(bias_k)
|
|
760
|
+
|
|
761
|
+
out_dir_path = Path(opt_cfg["out_dir"]).resolve()
|
|
762
|
+
|
|
763
|
+
calc_cfg["model_charge"] = int(charge)
|
|
764
|
+
calc_cfg["model_mult"] = int(spin)
|
|
765
|
+
calc_cfg["input_pdb"] = str(source_path)
|
|
766
|
+
calc_cfg["real_parm7"] = str(real_parm7)
|
|
767
|
+
if backend is not None:
|
|
768
|
+
calc_cfg["backend"] = str(backend).lower()
|
|
769
|
+
if _is_param_explicit("embedcharge"):
|
|
770
|
+
calc_cfg["embedcharge"] = bool(embedcharge)
|
|
771
|
+
if _is_param_explicit("embedcharge_cutoff"):
|
|
772
|
+
calc_cfg["embedcharge_cutoff"] = embedcharge_cutoff
|
|
773
|
+
|
|
774
|
+
# movable_cutoff implies full distance-based layer assignment.
|
|
775
|
+
# hess_cutoff alone can be combined with --detect-layer.
|
|
776
|
+
if movable_cutoff is not None:
|
|
777
|
+
if detect_layer:
|
|
778
|
+
click.echo("[layer] --movable-cutoff provided; disabling --detect-layer.", err=True)
|
|
779
|
+
detect_layer = False
|
|
780
|
+
|
|
781
|
+
layer_source_pdb = source_path
|
|
782
|
+
if detect_layer and layer_source_pdb.suffix.lower() != ".pdb":
|
|
783
|
+
click.echo("ERROR: --detect-layer requires a PDB input (or --ref-pdb).", err=True)
|
|
784
|
+
sys.exit(1)
|
|
785
|
+
|
|
786
|
+
model_pdb_path: Optional[Path] = None
|
|
787
|
+
layer_info: Optional[Dict[str, List[int]]] = None
|
|
788
|
+
|
|
789
|
+
if detect_layer:
|
|
790
|
+
try:
|
|
791
|
+
model_pdb_path, layer_info = build_model_pdb_from_bfactors(layer_source_pdb, out_dir_path)
|
|
792
|
+
calc_cfg["use_bfactor_layers"] = True
|
|
793
|
+
click.echo(
|
|
794
|
+
f"[layer] Detected B-factor layers: ML={len(layer_info.get('ml_indices', []))}, "
|
|
795
|
+
f"MovableMM={len(layer_info.get('movable_mm_indices', []))}, "
|
|
796
|
+
f"FrozenMM={len(layer_info.get('frozen_indices', []))}"
|
|
797
|
+
)
|
|
798
|
+
except Exception as e:
|
|
799
|
+
if model_pdb is None and not model_indices:
|
|
800
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
801
|
+
sys.exit(1)
|
|
802
|
+
click.echo(f"[layer] WARNING: {e} Falling back to explicit ML region.", err=True)
|
|
803
|
+
detect_layer = False
|
|
804
|
+
|
|
805
|
+
if not detect_layer:
|
|
806
|
+
if model_pdb is None and not model_indices:
|
|
807
|
+
click.echo("ERROR: Provide --model-pdb or --model-indices when --no-detect-layer.", err=True)
|
|
808
|
+
sys.exit(1)
|
|
809
|
+
if model_pdb is not None:
|
|
810
|
+
model_pdb_path = Path(model_pdb)
|
|
811
|
+
else:
|
|
812
|
+
if layer_source_pdb.suffix.lower() != ".pdb":
|
|
813
|
+
click.echo("ERROR: --model-indices requires a PDB input (or --ref-pdb).", err=True)
|
|
814
|
+
sys.exit(1)
|
|
815
|
+
try:
|
|
816
|
+
model_pdb_path = build_model_pdb_from_indices(layer_source_pdb, out_dir_path, model_indices or [])
|
|
817
|
+
except Exception as e:
|
|
818
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
819
|
+
sys.exit(1)
|
|
820
|
+
calc_cfg["use_bfactor_layers"] = False
|
|
821
|
+
|
|
822
|
+
if model_pdb_path is None:
|
|
823
|
+
click.echo("ERROR: Failed to resolve model PDB for the ML region.", err=True)
|
|
824
|
+
sys.exit(1)
|
|
825
|
+
|
|
826
|
+
calc_cfg["model_pdb"] = str(model_pdb_path)
|
|
827
|
+
freeze_atoms_final = apply_layer_freeze_constraints(
|
|
828
|
+
geom_cfg,
|
|
829
|
+
calc_cfg,
|
|
830
|
+
layer_info,
|
|
831
|
+
echo_fn=click.echo,
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Distance-based overrides for Hessian-target and movable MM selection.
|
|
835
|
+
if hess_cutoff is not None:
|
|
836
|
+
calc_cfg["hess_cutoff"] = hess_cutoff
|
|
837
|
+
if movable_cutoff is not None:
|
|
838
|
+
calc_cfg["movable_cutoff"] = movable_cutoff
|
|
839
|
+
calc_cfg["use_bfactor_layers"] = False
|
|
840
|
+
|
|
841
|
+
for key in ("input_pdb", "real_parm7", "model_pdb", "mm_fd_dir"):
|
|
842
|
+
val = calc_cfg.get(key)
|
|
843
|
+
if val:
|
|
844
|
+
calc_cfg[key] = str(Path(val).expanduser().resolve())
|
|
845
|
+
ensure_dir(out_dir_path)
|
|
846
|
+
|
|
847
|
+
ref_pdb_resolve = source_path.resolve()
|
|
848
|
+
|
|
849
|
+
click.echo(pretty_block("geom", format_freeze_atoms_for_echo(geom_cfg, key="freeze_atoms")))
|
|
850
|
+
echo_calc = format_freeze_atoms_for_echo(filter_calc_for_echo(calc_cfg), key="freeze_atoms")
|
|
851
|
+
click.echo(pretty_block("calc", echo_calc))
|
|
852
|
+
echo_opt = strip_inherited_keys({**opt_cfg, "out_dir": str(out_dir_path)}, OPT_BASE_KW, mode="same")
|
|
853
|
+
click.echo(pretty_block("opt", echo_opt))
|
|
854
|
+
echo_lbfgs = strip_inherited_keys(lbfgs_cfg, opt_cfg)
|
|
855
|
+
click.echo(pretty_block("lbfgs", echo_lbfgs))
|
|
856
|
+
click.echo(pretty_block("bias", bias_cfg))
|
|
857
|
+
|
|
858
|
+
pdb_atom_meta: List[Dict[str, Any]] = []
|
|
859
|
+
if source_path.suffix.lower() == ".pdb":
|
|
860
|
+
pdb_atom_meta = load_pdb_atom_metadata(source_path)
|
|
861
|
+
|
|
862
|
+
if scan_list_raw is None:
|
|
863
|
+
raise click.BadParameter("--scan-lists is required.")
|
|
864
|
+
scan_one_based = bool(one_based)
|
|
865
|
+
scan_source = "--scan-lists"
|
|
866
|
+
if is_scan_spec_file(scan_list_raw):
|
|
867
|
+
spec_path = Path(scan_list_raw)
|
|
868
|
+
parsed, raw_pairs, scan_one_based = parse_scan_spec_quads(
|
|
869
|
+
spec_path,
|
|
870
|
+
expected_len=3,
|
|
871
|
+
one_based_default=one_based,
|
|
872
|
+
atom_meta=pdb_atom_meta,
|
|
873
|
+
option_name="--scan-lists",
|
|
874
|
+
)
|
|
875
|
+
scan_source = f"--scan-lists ({spec_path})"
|
|
876
|
+
else:
|
|
877
|
+
parsed, raw_pairs = parse_scan_list_quads(
|
|
878
|
+
scan_list_raw,
|
|
879
|
+
expected_len=3,
|
|
880
|
+
one_based=scan_one_based,
|
|
881
|
+
atom_meta=pdb_atom_meta,
|
|
882
|
+
option_name="--scan-lists",
|
|
883
|
+
)
|
|
884
|
+
(i1, j1, low1, high1), (i2, j2, low2, high2), (i3, j3, low3, high3) = parsed
|
|
885
|
+
d1_label_csv = axis_label_csv("d1", i1, j1, scan_one_based, pdb_atom_meta, raw_pairs[0])
|
|
886
|
+
d2_label_csv = axis_label_csv("d2", i2, j2, scan_one_based, pdb_atom_meta, raw_pairs[1])
|
|
887
|
+
d3_label_csv = axis_label_csv("d3", i3, j3, scan_one_based, pdb_atom_meta, raw_pairs[2])
|
|
888
|
+
if print_parsed:
|
|
889
|
+
click.echo(
|
|
890
|
+
pretty_block(
|
|
891
|
+
"scan-parsed",
|
|
892
|
+
{
|
|
893
|
+
"source": scan_source,
|
|
894
|
+
"one_based": bool(scan_one_based),
|
|
895
|
+
"pairs_0based": parsed,
|
|
896
|
+
},
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
click.echo(
|
|
900
|
+
pretty_block(
|
|
901
|
+
"scan-list (0-based)",
|
|
902
|
+
{
|
|
903
|
+
"d1": (i1, j1, low1, high1),
|
|
904
|
+
"d2": (i2, j2, low2, high2),
|
|
905
|
+
"d3": (i3, j3, low3, high3),
|
|
906
|
+
},
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
if pdb_atom_meta:
|
|
910
|
+
click.echo("[scan3d] PDB atom details for scanned pairs:")
|
|
911
|
+
legend = PDB_ATOM_META_HEADER
|
|
912
|
+
click.echo(f" legend: {legend}")
|
|
913
|
+
click.echo(f" d1 i: {format_pdb_atom_metadata(pdb_atom_meta, i1)}")
|
|
914
|
+
click.echo(f" j: {format_pdb_atom_metadata(pdb_atom_meta, j1)}")
|
|
915
|
+
click.echo(f" d2 i: {format_pdb_atom_metadata(pdb_atom_meta, i2)}")
|
|
916
|
+
click.echo(f" j: {format_pdb_atom_metadata(pdb_atom_meta, j2)}")
|
|
917
|
+
click.echo(f" d3 i: {format_pdb_atom_metadata(pdb_atom_meta, i3)}")
|
|
918
|
+
click.echo(f" j: {format_pdb_atom_metadata(pdb_atom_meta, j3)}")
|
|
919
|
+
|
|
920
|
+
# Directory layout
|
|
921
|
+
tmp_root = Path(tempfile.mkdtemp(prefix="scan3d_tmp_"))
|
|
922
|
+
grid_dir = out_dir_path / "grid"
|
|
923
|
+
tmp_opt_dir = tmp_root / "opt"
|
|
924
|
+
ensure_dir(grid_dir)
|
|
925
|
+
ensure_dir(tmp_opt_dir)
|
|
926
|
+
final_dir = out_dir_path
|
|
927
|
+
|
|
928
|
+
coord_type = geom_cfg.get("coord_type", "cart")
|
|
929
|
+
geom_outer = geom_loader(geom_input_path, coord_type=coord_type)
|
|
930
|
+
freeze = list(geom_cfg.get("freeze_atoms") or [])
|
|
931
|
+
if freeze:
|
|
932
|
+
try:
|
|
933
|
+
geom_outer.freeze_atoms = np.array(freeze, dtype=int)
|
|
934
|
+
except Exception:
|
|
935
|
+
logger.debug("Failed to set freeze_atoms on geometry", exc_info=True)
|
|
936
|
+
|
|
937
|
+
base_calc = mlmm(**calc_cfg)
|
|
938
|
+
biased = HarmonicBiasCalculator(base_calc, k=float(bias_cfg["k"]))
|
|
939
|
+
|
|
940
|
+
if preopt:
|
|
941
|
+
click.echo("[preopt] Unbiased relaxation of the initial structure ...")
|
|
942
|
+
geom_outer.set_calculator(base_calc)
|
|
943
|
+
optimizer0 = _make_lbfgs(
|
|
944
|
+
geom_outer,
|
|
945
|
+
lbfgs_cfg,
|
|
946
|
+
opt_cfg,
|
|
947
|
+
max_step_bohr=float(max_step_size) * ANG2BOHR,
|
|
948
|
+
relax_max_cycles=relax_max_cycles,
|
|
949
|
+
out_dir=tmp_opt_dir,
|
|
950
|
+
prefix="preopt",
|
|
951
|
+
)
|
|
952
|
+
try:
|
|
953
|
+
optimizer0.run()
|
|
954
|
+
except ZeroStepLength:
|
|
955
|
+
click.echo("[preopt] ZeroStepLength — continuing.", err=True)
|
|
956
|
+
except OptimizationError as exc:
|
|
957
|
+
click.echo(f"[preopt] OptimizationError — {exc}", err=True)
|
|
958
|
+
|
|
959
|
+
records: List[Dict[str, Any]] = []
|
|
960
|
+
|
|
961
|
+
# Measure reference distances on the (pre)optimized structure
|
|
962
|
+
coords_outer = np.asarray(geom_outer.coords).reshape(-1, 3)
|
|
963
|
+
d1_ref = distance_A_from_coords(coords_outer, i1, j1)
|
|
964
|
+
d2_ref = distance_A_from_coords(coords_outer, i2, j2)
|
|
965
|
+
d3_ref = distance_A_from_coords(coords_outer, i3, j3)
|
|
966
|
+
|
|
967
|
+
if math.isfinite(d1_ref) and math.isfinite(d2_ref) and math.isfinite(d3_ref):
|
|
968
|
+
click.echo(
|
|
969
|
+
f"[center] reference distances from (pre)optimized structure: "
|
|
970
|
+
f"d1 = {d1_ref:.3f} Å, d2 = {d2_ref:.3f} Å, d3 = {d3_ref:.3f} Å"
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
# Write preoptimized structure
|
|
974
|
+
d1_ref_tag = distance_tag(d1_ref)
|
|
975
|
+
d2_ref_tag = distance_tag(d2_ref)
|
|
976
|
+
d3_ref_tag = distance_tag(d3_ref)
|
|
977
|
+
preopt_xyz_path = grid_dir / f"preopt_i{d1_ref_tag}_j{d2_ref_tag}_k{d3_ref_tag}.xyz"
|
|
978
|
+
try:
|
|
979
|
+
xyz_pre = geom_outer.as_xyz()
|
|
980
|
+
if not xyz_pre.endswith("\n"):
|
|
981
|
+
xyz_pre += "\n"
|
|
982
|
+
with open(preopt_xyz_path, "w") as handle:
|
|
983
|
+
handle.write(xyz_pre)
|
|
984
|
+
|
|
985
|
+
convert_and_annotate_xyz_to_pdb(
|
|
986
|
+
preopt_xyz_path,
|
|
987
|
+
ref_pdb_resolve,
|
|
988
|
+
preopt_xyz_path.with_suffix(".pdb"),
|
|
989
|
+
model_pdb_path,
|
|
990
|
+
freeze_atoms_final,
|
|
991
|
+
)
|
|
992
|
+
except Exception as exc:
|
|
993
|
+
click.echo(
|
|
994
|
+
f"[write] WARNING: failed to write or convert {preopt_xyz_path.name}: {exc}",
|
|
995
|
+
err=True,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
preopt_energy_h = unbiased_energy_hartree(geom_outer, base_calc)
|
|
999
|
+
records.append(
|
|
1000
|
+
{
|
|
1001
|
+
"i": -1,
|
|
1002
|
+
"j": -1,
|
|
1003
|
+
"k": -1,
|
|
1004
|
+
"d1_A": float(d1_ref),
|
|
1005
|
+
"d2_A": float(d2_ref),
|
|
1006
|
+
"d3_A": float(d3_ref),
|
|
1007
|
+
"energy_hartree": preopt_energy_h,
|
|
1008
|
+
"bias_converged": True,
|
|
1009
|
+
"is_preopt": True,
|
|
1010
|
+
}
|
|
1011
|
+
)
|
|
1012
|
+
else:
|
|
1013
|
+
click.echo(
|
|
1014
|
+
"[center] WARNING: failed to determine reference distances; using grid order as-is.",
|
|
1015
|
+
err=True,
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
# Build distance grids and reorder so that scanning starts near the reference structure
|
|
1019
|
+
d1_values = values_from_bounds(low1, high1, float(max_step_size))
|
|
1020
|
+
d2_values = values_from_bounds(low2, high2, float(max_step_size))
|
|
1021
|
+
d3_values = values_from_bounds(low3, high3, float(max_step_size))
|
|
1022
|
+
|
|
1023
|
+
if math.isfinite(d1_ref):
|
|
1024
|
+
d1_values = np.array(sorted(d1_values, key=lambda v: abs(v - d1_ref)), dtype=float)
|
|
1025
|
+
if math.isfinite(d2_ref):
|
|
1026
|
+
d2_values = np.array(sorted(d2_values, key=lambda v: abs(v - d2_ref)), dtype=float)
|
|
1027
|
+
if math.isfinite(d3_ref):
|
|
1028
|
+
d3_values = np.array(sorted(d3_values, key=lambda v: abs(v - d3_ref)), dtype=float)
|
|
1029
|
+
|
|
1030
|
+
N1, N2, N3 = len(d1_values), len(d2_values), len(d3_values)
|
|
1031
|
+
click.echo(f"[grid] d1 steps = {N1} values(A)={list(map(lambda x: f'{x:.3f}', d1_values))}")
|
|
1032
|
+
click.echo(f"[grid] d2 steps = {N2} values(A)={list(map(lambda x: f'{x:.3f}', d2_values))}")
|
|
1033
|
+
click.echo(f"[grid] d3 steps = {N3} values(A)={list(map(lambda x: f'{x:.3f}', d3_values))}")
|
|
1034
|
+
click.echo(f"[grid] total grid points = {N1 * N2 * N3}")
|
|
1035
|
+
|
|
1036
|
+
max_step_bohr = float(max_step_size) * ANG2BOHR
|
|
1037
|
+
|
|
1038
|
+
# Caches for nearest-neighbor starting geometries
|
|
1039
|
+
d1_geoms: Dict[int, Any] = {}
|
|
1040
|
+
d2_geoms: Dict[int, Dict[int, Any]] = {}
|
|
1041
|
+
d3_geoms: Dict[Tuple[int, int], Dict[int, Any]] = {}
|
|
1042
|
+
|
|
1043
|
+
geom_outer_initial = _snapshot_geometry(geom_outer)
|
|
1044
|
+
|
|
1045
|
+
# ===== 3D nested scan: d1 (outer) → d2 (middle) → d3 (inner) =====
|
|
1046
|
+
for i_idx, d1_target in enumerate(d1_values):
|
|
1047
|
+
d1_tag = distance_tag(d1_target)
|
|
1048
|
+
click.echo(f"\n--- d1 step {i_idx + 1}/{N1} : target = {d1_target:.3f} Å ---")
|
|
1049
|
+
|
|
1050
|
+
# Choose initial geometry for this d1
|
|
1051
|
+
if not d1_geoms:
|
|
1052
|
+
geom_outer_i = _snapshot_geometry(geom_outer_initial)
|
|
1053
|
+
else:
|
|
1054
|
+
nearest_i = min(d1_geoms.keys(), key=lambda p: abs(d1_values[p] - d1_target))
|
|
1055
|
+
geom_outer_i = _snapshot_geometry(d1_geoms[nearest_i])
|
|
1056
|
+
|
|
1057
|
+
biased.set_pairs([(i1, j1, float(d1_target))])
|
|
1058
|
+
geom_outer_i.set_calculator(biased)
|
|
1059
|
+
|
|
1060
|
+
opt1 = _make_lbfgs(
|
|
1061
|
+
geom_outer_i,
|
|
1062
|
+
lbfgs_cfg,
|
|
1063
|
+
opt_cfg,
|
|
1064
|
+
max_step_bohr=max_step_bohr,
|
|
1065
|
+
relax_max_cycles=relax_max_cycles,
|
|
1066
|
+
out_dir=tmp_opt_dir,
|
|
1067
|
+
prefix=f"d1_{d1_tag}",
|
|
1068
|
+
)
|
|
1069
|
+
try:
|
|
1070
|
+
opt1.run()
|
|
1071
|
+
except ZeroStepLength:
|
|
1072
|
+
click.echo(f"[d1 {i_idx}] ZeroStepLength — continuing to d2/d3 scan.", err=True)
|
|
1073
|
+
except OptimizationError as exc:
|
|
1074
|
+
click.echo(f"[d1 {i_idx}] OptimizationError — {exc}", err=True)
|
|
1075
|
+
|
|
1076
|
+
geom_after_d1 = _snapshot_geometry(geom_outer_i)
|
|
1077
|
+
d1_geoms[i_idx] = geom_after_d1
|
|
1078
|
+
|
|
1079
|
+
if i_idx not in d2_geoms:
|
|
1080
|
+
d2_geoms[i_idx] = {}
|
|
1081
|
+
|
|
1082
|
+
for j_idx, d2_target in enumerate(d2_values):
|
|
1083
|
+
d2_tag = distance_tag(d2_target)
|
|
1084
|
+
click.echo(
|
|
1085
|
+
f" [stage] d1/d2 step ({i_idx + 1}/{N1}, {j_idx + 1}/{N2}): "
|
|
1086
|
+
f"targets = ({d1_target:.3f}, {d2_target:.3f}) Å"
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
# Choose initial geometry for this (d1,d2)
|
|
1090
|
+
d2_store = d2_geoms[i_idx]
|
|
1091
|
+
if not d2_store:
|
|
1092
|
+
geom_mid = _snapshot_geometry(geom_after_d1)
|
|
1093
|
+
else:
|
|
1094
|
+
nearest_j = min(d2_store.keys(), key=lambda p: abs(d2_values[p] - d2_target))
|
|
1095
|
+
geom_mid = _snapshot_geometry(d2_store[nearest_j])
|
|
1096
|
+
|
|
1097
|
+
biased.set_pairs([
|
|
1098
|
+
(i1, j1, float(d1_target)),
|
|
1099
|
+
(i2, j2, float(d2_target)),
|
|
1100
|
+
])
|
|
1101
|
+
geom_mid.set_calculator(biased)
|
|
1102
|
+
|
|
1103
|
+
opt2 = _make_lbfgs(
|
|
1104
|
+
geom_mid,
|
|
1105
|
+
lbfgs_cfg,
|
|
1106
|
+
opt_cfg,
|
|
1107
|
+
max_step_bohr=max_step_bohr,
|
|
1108
|
+
relax_max_cycles=relax_max_cycles,
|
|
1109
|
+
out_dir=tmp_opt_dir,
|
|
1110
|
+
prefix=f"d1_{d1_tag}_d2_{d2_tag}",
|
|
1111
|
+
)
|
|
1112
|
+
try:
|
|
1113
|
+
opt2.run()
|
|
1114
|
+
except ZeroStepLength:
|
|
1115
|
+
click.echo(f"[d1 {i_idx}, d2 {j_idx}] ZeroStepLength — continuing to d3 scan.", err=True)
|
|
1116
|
+
except OptimizationError as exc:
|
|
1117
|
+
click.echo(f"[d1 {i_idx}, d2 {j_idx}] OptimizationError — {exc}", err=True)
|
|
1118
|
+
|
|
1119
|
+
geom_after_d2 = _snapshot_geometry(geom_mid)
|
|
1120
|
+
d2_store[j_idx] = geom_after_d2
|
|
1121
|
+
|
|
1122
|
+
key_ij = (i_idx, j_idx)
|
|
1123
|
+
if key_ij not in d3_geoms:
|
|
1124
|
+
d3_geoms[key_ij] = {}
|
|
1125
|
+
d3_store = d3_geoms[key_ij]
|
|
1126
|
+
|
|
1127
|
+
trj_blocks = [] if dump else None
|
|
1128
|
+
|
|
1129
|
+
for k_idx, d3_target in enumerate(d3_values):
|
|
1130
|
+
d3_tag = distance_tag(d3_target)
|
|
1131
|
+
|
|
1132
|
+
# Choose initial geometry for this (d1,d2,d3)
|
|
1133
|
+
if not d3_store:
|
|
1134
|
+
geom_inner = _snapshot_geometry(geom_after_d2)
|
|
1135
|
+
else:
|
|
1136
|
+
nearest_k = min(d3_store.keys(), key=lambda p: abs(d3_values[p] - d3_target))
|
|
1137
|
+
geom_inner = _snapshot_geometry(d3_store[nearest_k])
|
|
1138
|
+
|
|
1139
|
+
biased.set_pairs([
|
|
1140
|
+
(i1, j1, float(d1_target)),
|
|
1141
|
+
(i2, j2, float(d2_target)),
|
|
1142
|
+
(i3, j3, float(d3_target)),
|
|
1143
|
+
])
|
|
1144
|
+
geom_inner.set_calculator(biased)
|
|
1145
|
+
|
|
1146
|
+
opt3 = _make_lbfgs(
|
|
1147
|
+
geom_inner,
|
|
1148
|
+
lbfgs_cfg,
|
|
1149
|
+
opt_cfg,
|
|
1150
|
+
max_step_bohr=max_step_bohr,
|
|
1151
|
+
relax_max_cycles=relax_max_cycles,
|
|
1152
|
+
out_dir=tmp_opt_dir,
|
|
1153
|
+
prefix=f"d1_{d1_tag}_d2_{d2_tag}_d3_{d3_tag}",
|
|
1154
|
+
)
|
|
1155
|
+
try:
|
|
1156
|
+
opt3.run()
|
|
1157
|
+
converged = True
|
|
1158
|
+
except ZeroStepLength:
|
|
1159
|
+
click.echo(
|
|
1160
|
+
f" [d1 {i_idx}, d2 {j_idx}, d3 {k_idx}] ZeroStepLength — recorded anyway.",
|
|
1161
|
+
err=True,
|
|
1162
|
+
)
|
|
1163
|
+
converged = False
|
|
1164
|
+
except OptimizationError as exc:
|
|
1165
|
+
click.echo(
|
|
1166
|
+
f" [d1 {i_idx}, d2 {j_idx}, d3 {k_idx}] OptimizationError — {exc}",
|
|
1167
|
+
err=True,
|
|
1168
|
+
)
|
|
1169
|
+
converged = False
|
|
1170
|
+
|
|
1171
|
+
# Cache final geometry for nearest-neighbor reuse
|
|
1172
|
+
d3_store[k_idx] = _snapshot_geometry(geom_inner)
|
|
1173
|
+
|
|
1174
|
+
energy_h = unbiased_energy_hartree(geom_inner, base_calc)
|
|
1175
|
+
|
|
1176
|
+
xyz_path = grid_dir / f"point_i{d1_tag}_j{d2_tag}_k{d3_tag}.xyz"
|
|
1177
|
+
try:
|
|
1178
|
+
xyz = geom_inner.as_xyz()
|
|
1179
|
+
if not xyz.endswith("\n"):
|
|
1180
|
+
xyz += "\n"
|
|
1181
|
+
with open(xyz_path, "w") as handle:
|
|
1182
|
+
handle.write(xyz)
|
|
1183
|
+
|
|
1184
|
+
convert_and_annotate_xyz_to_pdb(
|
|
1185
|
+
xyz_path,
|
|
1186
|
+
ref_pdb_resolve,
|
|
1187
|
+
xyz_path.with_suffix(".pdb"),
|
|
1188
|
+
model_pdb_path,
|
|
1189
|
+
freeze_atoms_final,
|
|
1190
|
+
)
|
|
1191
|
+
except Exception as exc:
|
|
1192
|
+
click.echo(
|
|
1193
|
+
f"[write] WARNING: failed to write or convert {xyz_path.name}: {exc}",
|
|
1194
|
+
err=True,
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
if dump and trj_blocks is not None:
|
|
1198
|
+
block = geom_inner.as_xyz()
|
|
1199
|
+
if not block.endswith("\n"):
|
|
1200
|
+
block += "\n"
|
|
1201
|
+
trj_blocks.append(block)
|
|
1202
|
+
|
|
1203
|
+
records.append(
|
|
1204
|
+
{
|
|
1205
|
+
"i": int(i_idx),
|
|
1206
|
+
"j": int(j_idx),
|
|
1207
|
+
"k": int(k_idx),
|
|
1208
|
+
"d1_A": float(d1_target),
|
|
1209
|
+
"d2_A": float(d2_target),
|
|
1210
|
+
"d3_A": float(d3_target),
|
|
1211
|
+
"energy_hartree": energy_h,
|
|
1212
|
+
"bias_converged": bool(converged),
|
|
1213
|
+
"is_preopt": False,
|
|
1214
|
+
}
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
if dump and trj_blocks:
|
|
1218
|
+
trj_path = grid_dir / f"inner_path_d1_{d1_tag}_d2_{d2_tag}_trj.xyz"
|
|
1219
|
+
try:
|
|
1220
|
+
with open(trj_path, "w") as handle:
|
|
1221
|
+
handle.write("".join(trj_blocks))
|
|
1222
|
+
click.echo(f"[write] Wrote '{trj_path}'.")
|
|
1223
|
+
|
|
1224
|
+
convert_and_annotate_xyz_to_pdb(
|
|
1225
|
+
trj_path,
|
|
1226
|
+
ref_pdb_resolve,
|
|
1227
|
+
trj_path.with_suffix(".pdb"),
|
|
1228
|
+
model_pdb_path,
|
|
1229
|
+
freeze_atoms_final,
|
|
1230
|
+
)
|
|
1231
|
+
except Exception as exc:
|
|
1232
|
+
click.echo(
|
|
1233
|
+
f"[write] WARNING: failed to write or convert '{trj_path}': {exc}",
|
|
1234
|
+
err=True,
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
df = pd.DataFrame.from_records(records)
|
|
1238
|
+
_finalize_surface_and_plot(
|
|
1239
|
+
df=df,
|
|
1240
|
+
final_dir=final_dir,
|
|
1241
|
+
baseline=baseline,
|
|
1242
|
+
zmin=zmin,
|
|
1243
|
+
zmax=zmax,
|
|
1244
|
+
d1_label_csv=d1_label_csv,
|
|
1245
|
+
d2_label_csv=d2_label_csv,
|
|
1246
|
+
d3_label_csv=d3_label_csv,
|
|
1247
|
+
write_surface_csv=True,
|
|
1248
|
+
time_start=time_start,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
except KeyboardInterrupt:
|
|
1252
|
+
click.echo("\nInterrupted by user.", err=True)
|
|
1253
|
+
sys.exit(130)
|
|
1254
|
+
except Exception as exc:
|
|
1255
|
+
tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
1256
|
+
click.echo("Unhandled exception during 3D scan:\n" + textwrap.indent(tb, " "), err=True)
|
|
1257
|
+
sys.exit(1)
|
|
1258
|
+
finally:
|
|
1259
|
+
if tmp_root is not None:
|
|
1260
|
+
shutil.rmtree(tmp_root, ignore_errors=True)
|
|
1261
|
+
# Release GPU memory so subsequent pipeline stages don't OOM
|
|
1262
|
+
base_calc = geom_outer = optimizer0 = None
|
|
1263
|
+
gc.collect() # break cyclic refs inside torch.nn.Module
|
|
1264
|
+
if torch.cuda.is_available():
|
|
1265
|
+
torch.cuda.empty_cache()
|