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