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/scan.py
ADDED
|
@@ -0,0 +1,1047 @@
|
|
|
1
|
+
# mlmm/scan.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
ML/MM staged bond-length scan with harmonic restraints.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
mlmm scan -i pocket.pdb --parm real.parm7 --model-pdb ml_region.pdb -q 0 --scan-lists "[(12,45,2.20)]"
|
|
8
|
+
|
|
9
|
+
For detailed documentation, see: docs/scan.md
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from copy import deepcopy
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
17
|
+
|
|
18
|
+
import gc
|
|
19
|
+
import logging
|
|
20
|
+
import math
|
|
21
|
+
import sys
|
|
22
|
+
import textwrap
|
|
23
|
+
import traceback
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
import numpy as np
|
|
29
|
+
import time
|
|
30
|
+
import torch
|
|
31
|
+
|
|
32
|
+
from pysisyphus.helpers import geom_loader
|
|
33
|
+
from pysisyphus.optimizers.LBFGS import LBFGS
|
|
34
|
+
from pysisyphus.optimizers.exceptions import OptimizationError, ZeroStepLength
|
|
35
|
+
from pysisyphus.constants import BOHR2ANG, ANG2BOHR
|
|
36
|
+
|
|
37
|
+
from .mlmm_calc import mlmm
|
|
38
|
+
from .defaults import (
|
|
39
|
+
BIAS_KW as _BIAS_KW_DEFAULT,
|
|
40
|
+
BOND_KW as _BOND_KW_DEFAULT,
|
|
41
|
+
)
|
|
42
|
+
from .opt import (
|
|
43
|
+
GEOM_KW as _OPT_GEOM_KW,
|
|
44
|
+
CALC_KW as _OPT_CALC_KW,
|
|
45
|
+
OPT_BASE_KW as _OPT_BASE_KW,
|
|
46
|
+
LBFGS_KW as _OPT_LBFGS_KW,
|
|
47
|
+
HarmonicBiasCalculator,
|
|
48
|
+
_parse_freeze_atoms,
|
|
49
|
+
_normalize_geom_freeze,
|
|
50
|
+
)
|
|
51
|
+
from .utils import (
|
|
52
|
+
apply_ref_pdb_override,
|
|
53
|
+
apply_layer_freeze_constraints,
|
|
54
|
+
convert_xyz_to_pdb,
|
|
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
|
+
collect_single_option_values,
|
|
68
|
+
load_pdb_atom_metadata,
|
|
69
|
+
parse_scan_list_triples,
|
|
70
|
+
parse_scan_spec_stages,
|
|
71
|
+
is_scan_spec_file,
|
|
72
|
+
PDB_ATOM_META_HEADER,
|
|
73
|
+
format_pdb_atom_metadata,
|
|
74
|
+
parse_indices_string,
|
|
75
|
+
build_model_pdb_from_bfactors,
|
|
76
|
+
build_model_pdb_from_indices,
|
|
77
|
+
snapshot_geometry,
|
|
78
|
+
)
|
|
79
|
+
from .bond_changes import compare_structures, summarize_changes
|
|
80
|
+
from .cli_utils import resolve_yaml_sources, load_merged_yaml_cfg, make_is_param_explicit
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --------------------------------------------------------------------------------------
|
|
84
|
+
# Defaults (merge order: defaults ← YAML ← CLI)
|
|
85
|
+
# --------------------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
# Geometry handling (Cartesian recommended for scans)
|
|
88
|
+
GEOM_KW: Dict[str, Any] = deepcopy(_OPT_GEOM_KW)
|
|
89
|
+
|
|
90
|
+
# ML/MM calculator defaults (shared with opt/path_*)
|
|
91
|
+
CALC_KW: Dict[str, Any] = deepcopy(_OPT_CALC_KW)
|
|
92
|
+
|
|
93
|
+
# Optimizer base (convergence, dumping, etc.)
|
|
94
|
+
OPT_BASE_KW: Dict[str, Any] = deepcopy(_OPT_BASE_KW)
|
|
95
|
+
OPT_BASE_KW.update({
|
|
96
|
+
"out_dir": "./result_scan/",
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
# LBFGS specifics
|
|
100
|
+
LBFGS_KW: Dict[str, Any] = deepcopy(_OPT_LBFGS_KW)
|
|
101
|
+
LBFGS_KW.update({
|
|
102
|
+
"out_dir": "./result_scan/",
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
# Bias (harmonic well) defaults; can be overridden via YAML: section "bias"
|
|
106
|
+
BIAS_KW: Dict[str, Any] = deepcopy(_BIAS_KW_DEFAULT)
|
|
107
|
+
|
|
108
|
+
# Bond-change detection (as in path_search)
|
|
109
|
+
BOND_KW: Dict[str, Any] = deepcopy(_BOND_KW_DEFAULT)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _coords3d_to_xyz_string(geom, energy: Optional[float] = None) -> str:
|
|
113
|
+
s = geom.as_xyz()
|
|
114
|
+
lines = s.splitlines()
|
|
115
|
+
if energy is not None and len(lines) >= 2 and lines[0].strip().isdigit():
|
|
116
|
+
lines[1] = f"{energy:.12f}"
|
|
117
|
+
s = "\n".join(lines)
|
|
118
|
+
if not s.endswith("\n"):
|
|
119
|
+
s += "\n"
|
|
120
|
+
return s
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _pair_distances(coords_ang: np.ndarray, pairs: Iterable[Tuple[int, int]]) -> List[float]:
|
|
124
|
+
"""
|
|
125
|
+
coords_ang: (N,3) in Å; returns a list of distances (Å) for the given pairs.
|
|
126
|
+
"""
|
|
127
|
+
dists: List[float] = []
|
|
128
|
+
for i, j in pairs:
|
|
129
|
+
v = coords_ang[i] - coords_ang[j]
|
|
130
|
+
d = float(np.linalg.norm(v))
|
|
131
|
+
dists.append(d)
|
|
132
|
+
return dists
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _schedule_for_stage(
|
|
136
|
+
coords_ang: np.ndarray,
|
|
137
|
+
tuples: List[Tuple[int, int, float]],
|
|
138
|
+
max_step_size_ang: float,
|
|
139
|
+
) -> Tuple[int, List[float], List[float], List[float]]:
|
|
140
|
+
"""
|
|
141
|
+
Given current *Å* coords and stage tuples, compute:
|
|
142
|
+
N: number of steps
|
|
143
|
+
r0: initial distances per tuple (Å)
|
|
144
|
+
rT: target distances per tuple (Å)
|
|
145
|
+
step_widths: δ_k per tuple (Å, signed)
|
|
146
|
+
"""
|
|
147
|
+
pairs = [(i, j) for (i, j, _) in tuples]
|
|
148
|
+
r0 = _pair_distances(coords_ang, pairs)
|
|
149
|
+
rT = [t for (_, _, t) in tuples]
|
|
150
|
+
deltas = [RT - R0 for (R0, RT) in zip(r0, rT)]
|
|
151
|
+
d_max = max((abs(d) for d in deltas), default=0.0)
|
|
152
|
+
if d_max <= 0.0:
|
|
153
|
+
return 0, r0, rT, [0.0] * len(tuples)
|
|
154
|
+
if max_step_size_ang <= 0.0:
|
|
155
|
+
raise click.BadParameter("--max-step-size must be > 0.")
|
|
156
|
+
N = int(math.ceil(d_max / max_step_size_ang))
|
|
157
|
+
step_widths = [d / N for d in deltas]
|
|
158
|
+
return N, r0, rT, step_widths
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --------------------------------------------------------------------------------------
|
|
162
|
+
# Bond‑change helpers
|
|
163
|
+
# --------------------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def _has_bond_change(x, y, bond_cfg: Dict[str, Any]) -> Tuple[bool, str]:
|
|
166
|
+
"""
|
|
167
|
+
Return for covalent bonds forming/breaking between `x` and `y`.
|
|
168
|
+
"""
|
|
169
|
+
res = compare_structures(
|
|
170
|
+
x, y,
|
|
171
|
+
device=bond_cfg.get("device", "cuda"),
|
|
172
|
+
bond_factor=float(bond_cfg.get("bond_factor", 1.20)),
|
|
173
|
+
margin_fraction=float(bond_cfg.get("margin_fraction", 0.05)),
|
|
174
|
+
delta_fraction=float(bond_cfg.get("delta_fraction", 0.05)),
|
|
175
|
+
)
|
|
176
|
+
formed = len(getattr(res, "formed_covalent", [])) > 0
|
|
177
|
+
broken = len(getattr(res, "broken_covalent", [])) > 0
|
|
178
|
+
summary = summarize_changes(x, res, one_based=True)
|
|
179
|
+
return (formed or broken), summary
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _snapshot_geometry(g) -> Any:
|
|
183
|
+
"""Create an independent pysisyphus Geometry snapshot from the given Geometry."""
|
|
184
|
+
return snapshot_geometry(g, coord_type_default="cart")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@click.command(
|
|
188
|
+
help="Bond-length driven scan with staged harmonic restraints and relaxation (ML/MM).",
|
|
189
|
+
context_settings={
|
|
190
|
+
"help_option_names": ["-h", "--help"],
|
|
191
|
+
"ignore_unknown_options": True,
|
|
192
|
+
"allow_extra_args": True,
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
@click.option(
|
|
196
|
+
"-i", "--input",
|
|
197
|
+
"input_path",
|
|
198
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
199
|
+
required=True,
|
|
200
|
+
help="Full-enzyme PDB used by the ML/MM calculator and as reference for conversions.",
|
|
201
|
+
)
|
|
202
|
+
@click.option(
|
|
203
|
+
"--parm",
|
|
204
|
+
"real_parm7",
|
|
205
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
206
|
+
required=True,
|
|
207
|
+
help="Amber parm7 topology covering the entire enzyme complex.",
|
|
208
|
+
)
|
|
209
|
+
@click.option(
|
|
210
|
+
"--model-pdb",
|
|
211
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
212
|
+
required=False,
|
|
213
|
+
help="PDB defining the ML-region atoms for ML/MM. Optional when --detect-layer is enabled.",
|
|
214
|
+
)
|
|
215
|
+
@click.option(
|
|
216
|
+
"--model-indices",
|
|
217
|
+
"model_indices_str",
|
|
218
|
+
type=str,
|
|
219
|
+
default=None,
|
|
220
|
+
show_default=False,
|
|
221
|
+
help="Comma-separated atom indices for the ML region (ranges allowed like 1-5). "
|
|
222
|
+
"Used when --model-pdb is omitted.",
|
|
223
|
+
)
|
|
224
|
+
@click.option(
|
|
225
|
+
"--model-indices-one-based/--model-indices-zero-based",
|
|
226
|
+
"model_indices_one_based",
|
|
227
|
+
default=True,
|
|
228
|
+
show_default=True,
|
|
229
|
+
help="Interpret --model-indices as 1-based (default) or 0-based.",
|
|
230
|
+
)
|
|
231
|
+
@click.option(
|
|
232
|
+
"--detect-layer/--no-detect-layer",
|
|
233
|
+
"detect_layer",
|
|
234
|
+
default=True,
|
|
235
|
+
show_default=True,
|
|
236
|
+
help="Detect ML/MM layers from input PDB B-factors (B=0/10/20). "
|
|
237
|
+
"If disabled, you must provide --model-pdb or --model-indices.",
|
|
238
|
+
)
|
|
239
|
+
@click.option("-q", "--charge", type=int, required=False,
|
|
240
|
+
help="ML region charge. Required unless --ligand-charge is provided.")
|
|
241
|
+
@click.option("-l", "--ligand-charge", type=str, default=None, show_default=False,
|
|
242
|
+
help="Total charge or per-resname mapping (e.g., GPP:-3,SAM:1) used to derive "
|
|
243
|
+
"charge when -q is omitted (requires PDB input or --ref-pdb).")
|
|
244
|
+
@click.option(
|
|
245
|
+
"-m",
|
|
246
|
+
"--multiplicity",
|
|
247
|
+
"spin",
|
|
248
|
+
type=int,
|
|
249
|
+
default=None,
|
|
250
|
+
show_default=False,
|
|
251
|
+
help="Spin multiplicity (2S+1) for the ML region. Defaults to 1 when omitted.",
|
|
252
|
+
)
|
|
253
|
+
@click.option(
|
|
254
|
+
"--freeze-atoms",
|
|
255
|
+
"freeze_atoms_cli",
|
|
256
|
+
type=str,
|
|
257
|
+
default=None,
|
|
258
|
+
show_default=False,
|
|
259
|
+
help="Comma-separated 1-based atom indices to freeze (e.g., '1,3,5').",
|
|
260
|
+
)
|
|
261
|
+
@click.option(
|
|
262
|
+
"--hess-cutoff",
|
|
263
|
+
"hess_cutoff",
|
|
264
|
+
type=float,
|
|
265
|
+
default=None,
|
|
266
|
+
show_default=False,
|
|
267
|
+
help="Distance cutoff (Å) from ML region for MM atoms to include in Hessian calculation. "
|
|
268
|
+
"Applied to movable MM atoms and can be combined with --detect-layer.",
|
|
269
|
+
)
|
|
270
|
+
@click.option(
|
|
271
|
+
"--movable-cutoff",
|
|
272
|
+
"movable_cutoff",
|
|
273
|
+
type=float,
|
|
274
|
+
default=None,
|
|
275
|
+
show_default=False,
|
|
276
|
+
help="Distance cutoff (Å) from ML region for movable MM atoms. MM atoms beyond this are frozen. "
|
|
277
|
+
"Providing --movable-cutoff disables --detect-layer.",
|
|
278
|
+
)
|
|
279
|
+
@click.option(
|
|
280
|
+
"-s", "--scan-lists",
|
|
281
|
+
"scan_lists_raw",
|
|
282
|
+
type=str,
|
|
283
|
+
multiple=True,
|
|
284
|
+
required=False,
|
|
285
|
+
help="Scan targets: inline Python literal (e.g. '[(1,5,1.4)]') or a YAML/JSON spec file path. "
|
|
286
|
+
"Multiple inline literals define sequential stages.",
|
|
287
|
+
)
|
|
288
|
+
@click.option("--one-based/--zero-based", "one_based", default=True, show_default=True,
|
|
289
|
+
help="Interpret (i,j) indices in --scan-lists as 1-based (default) or 0-based.")
|
|
290
|
+
@click.option(
|
|
291
|
+
"--print-parsed/--no-print-parsed",
|
|
292
|
+
"print_parsed",
|
|
293
|
+
default=False,
|
|
294
|
+
show_default=True,
|
|
295
|
+
help="Print parsed scan targets after resolving -s/--scan-lists.",
|
|
296
|
+
)
|
|
297
|
+
@click.option("--max-step-size", type=float, default=0.20, show_default=True,
|
|
298
|
+
help="Maximum change in any scanned bond length per step [Å].")
|
|
299
|
+
@click.option("--bias-k", type=float, default=300, show_default=True,
|
|
300
|
+
help="Harmonic well strength k [eV/Å^2].")
|
|
301
|
+
@click.option(
|
|
302
|
+
"--opt-mode",
|
|
303
|
+
type=click.Choice(["grad", "hess", "lbfgs", "rfo", "light", "heavy"], case_sensitive=False),
|
|
304
|
+
default=None,
|
|
305
|
+
show_default=False,
|
|
306
|
+
help="Compatibility option for mlmm all forwarding. "
|
|
307
|
+
"scan relaxations currently use LBFGS regardless of this value.",
|
|
308
|
+
)
|
|
309
|
+
@click.option(
|
|
310
|
+
"--max-cycles",
|
|
311
|
+
type=int,
|
|
312
|
+
default=10000,
|
|
313
|
+
show_default=True,
|
|
314
|
+
help="Maximum LBFGS cycles per biased step and per (pre|end)opt stage.",
|
|
315
|
+
)
|
|
316
|
+
@click.option(
|
|
317
|
+
"--relax-max-cycles",
|
|
318
|
+
type=int,
|
|
319
|
+
default=None,
|
|
320
|
+
show_default=False,
|
|
321
|
+
help="Compatibility alias of --max-cycles (overrides it when provided).",
|
|
322
|
+
)
|
|
323
|
+
@click.option(
|
|
324
|
+
"--dump/--no-dump",
|
|
325
|
+
"dump",
|
|
326
|
+
default=False,
|
|
327
|
+
show_default=True,
|
|
328
|
+
help="Write per-step optimizer trajectory files. "
|
|
329
|
+
"scan_trj.xyz and scan.pdb are always written to out-dir regardless of this flag.",
|
|
330
|
+
)
|
|
331
|
+
@click.option("-o", "--out-dir", type=str, default="./result_scan/", show_default=True,
|
|
332
|
+
help="Base output directory.")
|
|
333
|
+
@click.option(
|
|
334
|
+
"--thresh",
|
|
335
|
+
type=click.Choice(["gau_loose", "gau", "gau_tight", "gau_vtight", "baker", "never"], case_sensitive=False),
|
|
336
|
+
default=None,
|
|
337
|
+
help="Convergence preset for relaxations.",
|
|
338
|
+
)
|
|
339
|
+
@click.option(
|
|
340
|
+
"--config",
|
|
341
|
+
"config_yaml",
|
|
342
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
343
|
+
default=None,
|
|
344
|
+
help="Base YAML configuration file applied before explicit CLI options.",
|
|
345
|
+
)
|
|
346
|
+
@click.option(
|
|
347
|
+
"--ref-pdb",
|
|
348
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
349
|
+
default=None,
|
|
350
|
+
help="Reference PDB topology to use when --input is XYZ (keeps XYZ coordinates).",
|
|
351
|
+
)
|
|
352
|
+
@click.option(
|
|
353
|
+
"--preopt/--no-preopt",
|
|
354
|
+
"preopt",
|
|
355
|
+
default=False,
|
|
356
|
+
show_default=True,
|
|
357
|
+
help="Pre-optimize initial structure without bias before the scan.",
|
|
358
|
+
)
|
|
359
|
+
@click.option(
|
|
360
|
+
"--endopt/--no-endopt",
|
|
361
|
+
"endopt",
|
|
362
|
+
default=False,
|
|
363
|
+
show_default=True,
|
|
364
|
+
help="After each stage, run an additional unbiased optimization of the stage result.",
|
|
365
|
+
)
|
|
366
|
+
@click.option(
|
|
367
|
+
"--dry-run/--no-dry-run",
|
|
368
|
+
"dry_run",
|
|
369
|
+
default=False,
|
|
370
|
+
show_default=True,
|
|
371
|
+
help="Validate options and print the execution plan without running the scan.",
|
|
372
|
+
)
|
|
373
|
+
@click.option(
|
|
374
|
+
"--convert-files/--no-convert-files",
|
|
375
|
+
"convert_files",
|
|
376
|
+
default=True,
|
|
377
|
+
show_default=True,
|
|
378
|
+
help="Convert XYZ/TRJ outputs into PDB companions based on the input format.",
|
|
379
|
+
)
|
|
380
|
+
@click.option(
|
|
381
|
+
"-b", "--backend",
|
|
382
|
+
type=click.Choice(["uma", "orb", "mace", "aimnet2"], case_sensitive=False),
|
|
383
|
+
default=None,
|
|
384
|
+
show_default=False,
|
|
385
|
+
help="ML backend for the ONIOM high-level region (default: uma).",
|
|
386
|
+
)
|
|
387
|
+
@click.option(
|
|
388
|
+
"--embedcharge/--no-embedcharge",
|
|
389
|
+
"embedcharge",
|
|
390
|
+
default=False,
|
|
391
|
+
show_default=True,
|
|
392
|
+
help="Enable xTB point-charge embedding correction for MM→ML environmental effects.",
|
|
393
|
+
)
|
|
394
|
+
@click.option(
|
|
395
|
+
"--embedcharge-cutoff",
|
|
396
|
+
"embedcharge_cutoff",
|
|
397
|
+
type=float,
|
|
398
|
+
default=None,
|
|
399
|
+
show_default=False,
|
|
400
|
+
help="Distance cutoff (Å) from ML region for MM point charges in xTB embedding. "
|
|
401
|
+
"Default: 12.0 Å when --embedcharge is enabled.",
|
|
402
|
+
)
|
|
403
|
+
@click.pass_context
|
|
404
|
+
def cli(
|
|
405
|
+
ctx: click.Context,
|
|
406
|
+
input_path: Path,
|
|
407
|
+
real_parm7: Path,
|
|
408
|
+
model_pdb: Optional[Path],
|
|
409
|
+
model_indices_str: Optional[str],
|
|
410
|
+
model_indices_one_based: bool,
|
|
411
|
+
detect_layer: bool,
|
|
412
|
+
charge: Optional[int],
|
|
413
|
+
ligand_charge: Optional[str],
|
|
414
|
+
spin: Optional[int],
|
|
415
|
+
freeze_atoms_cli: Optional[str],
|
|
416
|
+
hess_cutoff: Optional[float],
|
|
417
|
+
movable_cutoff: Optional[float],
|
|
418
|
+
scan_lists_raw: Sequence[str],
|
|
419
|
+
one_based: bool,
|
|
420
|
+
print_parsed: bool,
|
|
421
|
+
max_step_size: float,
|
|
422
|
+
bias_k: Optional[float],
|
|
423
|
+
opt_mode: Optional[str],
|
|
424
|
+
max_cycles: int,
|
|
425
|
+
relax_max_cycles: Optional[int],
|
|
426
|
+
dump: bool,
|
|
427
|
+
out_dir: str,
|
|
428
|
+
thresh: Optional[str],
|
|
429
|
+
config_yaml: Optional[Path],
|
|
430
|
+
ref_pdb: Optional[Path],
|
|
431
|
+
preopt: bool,
|
|
432
|
+
endopt: bool,
|
|
433
|
+
dry_run: bool,
|
|
434
|
+
convert_files: bool,
|
|
435
|
+
backend: Optional[str],
|
|
436
|
+
embedcharge: bool,
|
|
437
|
+
embedcharge_cutoff: Optional[float],
|
|
438
|
+
) -> None:
|
|
439
|
+
_is_param_explicit = make_is_param_explicit(ctx)
|
|
440
|
+
|
|
441
|
+
set_convert_file_enabled(convert_files)
|
|
442
|
+
time_start = time.perf_counter()
|
|
443
|
+
config_yaml, override_yaml, used_legacy_yaml = resolve_yaml_sources(
|
|
444
|
+
config_yaml=config_yaml,
|
|
445
|
+
override_yaml=None,
|
|
446
|
+
args_yaml_legacy=None,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if relax_max_cycles is not None:
|
|
450
|
+
max_cycles = int(relax_max_cycles)
|
|
451
|
+
if max_cycles <= 0:
|
|
452
|
+
raise click.BadParameter("--max-cycles must be > 0.")
|
|
453
|
+
if opt_mode is not None and str(opt_mode).lower() not in {"lbfgs", "light", "grad"}:
|
|
454
|
+
click.echo(
|
|
455
|
+
f"[scan] NOTE: --opt-mode={opt_mode} is accepted for compatibility, "
|
|
456
|
+
"but scan relaxations use LBFGS.",
|
|
457
|
+
err=True,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Validate input format: PDB directly, or XYZ with --ref-pdb
|
|
461
|
+
suffix = input_path.suffix.lower()
|
|
462
|
+
if suffix not in (".pdb", ".xyz"):
|
|
463
|
+
click.echo("ERROR: --input must be a PDB or XYZ file.", err=True)
|
|
464
|
+
sys.exit(1)
|
|
465
|
+
if suffix == ".xyz" and ref_pdb is None:
|
|
466
|
+
click.echo("ERROR: --ref-pdb is required when --input is an XYZ file.", err=True)
|
|
467
|
+
sys.exit(1)
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
with prepare_input_structure(input_path) as prepared_input:
|
|
471
|
+
try:
|
|
472
|
+
apply_ref_pdb_override(prepared_input, ref_pdb)
|
|
473
|
+
except click.BadParameter as e:
|
|
474
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
475
|
+
sys.exit(1)
|
|
476
|
+
geom_input_path = prepared_input.geom_path
|
|
477
|
+
source_path = prepared_input.source_path
|
|
478
|
+
charge, spin = resolve_charge_spin_or_raise(
|
|
479
|
+
prepared_input, charge, spin,
|
|
480
|
+
ligand_charge=ligand_charge, prefix="[scan]",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
freeze_atoms_list = _parse_freeze_atoms(freeze_atoms_cli)
|
|
485
|
+
except click.BadParameter as e:
|
|
486
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
487
|
+
sys.exit(1)
|
|
488
|
+
|
|
489
|
+
model_indices: Optional[List[int]] = None
|
|
490
|
+
if model_indices_str:
|
|
491
|
+
try:
|
|
492
|
+
model_indices = parse_indices_string(model_indices_str, one_based=model_indices_one_based)
|
|
493
|
+
except click.BadParameter as e:
|
|
494
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
495
|
+
sys.exit(1)
|
|
496
|
+
|
|
497
|
+
yaml_cfg, _, _ = load_merged_yaml_cfg(
|
|
498
|
+
config_yaml=config_yaml,
|
|
499
|
+
override_yaml=None,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
geom_cfg = dict(GEOM_KW)
|
|
503
|
+
calc_cfg = dict(CALC_KW)
|
|
504
|
+
opt_cfg = dict(OPT_BASE_KW)
|
|
505
|
+
lbfgs_cfg = dict(LBFGS_KW)
|
|
506
|
+
bias_cfg = dict(BIAS_KW)
|
|
507
|
+
bond_cfg = dict(BOND_KW)
|
|
508
|
+
|
|
509
|
+
apply_yaml_overrides(
|
|
510
|
+
yaml_cfg,
|
|
511
|
+
[
|
|
512
|
+
(geom_cfg, (("geom",),)),
|
|
513
|
+
(calc_cfg, (("calc",), ("mlmm",))),
|
|
514
|
+
(opt_cfg, (("opt",),)),
|
|
515
|
+
(lbfgs_cfg, (("lbfgs",), ("opt", "lbfgs"))),
|
|
516
|
+
(bias_cfg, (("bias",),)),
|
|
517
|
+
(bond_cfg, (("bond",),)),
|
|
518
|
+
],
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
geom_freeze = _normalize_geom_freeze(geom_cfg.get("freeze_atoms"))
|
|
523
|
+
except click.BadParameter as e:
|
|
524
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
525
|
+
sys.exit(1)
|
|
526
|
+
geom_cfg["freeze_atoms"] = geom_freeze
|
|
527
|
+
if freeze_atoms_list:
|
|
528
|
+
merge_freeze_atom_indices(geom_cfg, freeze_atoms_list)
|
|
529
|
+
freeze_atoms_final = list(geom_cfg.get("freeze_atoms") or [])
|
|
530
|
+
calc_cfg["freeze_atoms"] = freeze_atoms_final
|
|
531
|
+
|
|
532
|
+
opt_cfg["out_dir"] = out_dir
|
|
533
|
+
opt_cfg["dump"] = False
|
|
534
|
+
opt_cfg["max_cycles"] = int(max_cycles)
|
|
535
|
+
if thresh is not None:
|
|
536
|
+
opt_cfg["thresh"] = str(thresh)
|
|
537
|
+
lbfgs_cfg["max_cycles"] = int(max_cycles)
|
|
538
|
+
|
|
539
|
+
if bias_k is not None:
|
|
540
|
+
bias_cfg["k"] = float(bias_k)
|
|
541
|
+
|
|
542
|
+
out_dir_path = Path(out_dir).resolve()
|
|
543
|
+
|
|
544
|
+
calc_cfg["model_charge"] = int(charge)
|
|
545
|
+
calc_cfg["model_mult"] = int(spin)
|
|
546
|
+
calc_cfg["input_pdb"] = str(source_path)
|
|
547
|
+
calc_cfg["real_parm7"] = str(real_parm7)
|
|
548
|
+
if backend is not None:
|
|
549
|
+
calc_cfg["backend"] = str(backend).lower()
|
|
550
|
+
if _is_param_explicit("embedcharge"):
|
|
551
|
+
calc_cfg["embedcharge"] = bool(embedcharge)
|
|
552
|
+
if _is_param_explicit("embedcharge_cutoff"):
|
|
553
|
+
calc_cfg["embedcharge_cutoff"] = embedcharge_cutoff
|
|
554
|
+
|
|
555
|
+
# movable_cutoff implies full distance-based layer assignment.
|
|
556
|
+
# hess_cutoff alone can be combined with --detect-layer.
|
|
557
|
+
if movable_cutoff is not None:
|
|
558
|
+
if detect_layer:
|
|
559
|
+
click.echo("[layer] --movable-cutoff provided; disabling --detect-layer.", err=True)
|
|
560
|
+
detect_layer = False
|
|
561
|
+
|
|
562
|
+
layer_source_pdb = source_path
|
|
563
|
+
if detect_layer and layer_source_pdb.suffix.lower() != ".pdb":
|
|
564
|
+
click.echo("ERROR: --detect-layer requires a PDB input (or --ref-pdb).", err=True)
|
|
565
|
+
sys.exit(1)
|
|
566
|
+
|
|
567
|
+
model_pdb_path: Optional[Path] = None
|
|
568
|
+
layer_info: Optional[Dict[str, List[int]]] = None
|
|
569
|
+
|
|
570
|
+
if detect_layer:
|
|
571
|
+
try:
|
|
572
|
+
model_pdb_path, layer_info = build_model_pdb_from_bfactors(layer_source_pdb, out_dir_path)
|
|
573
|
+
calc_cfg["use_bfactor_layers"] = True
|
|
574
|
+
click.echo(
|
|
575
|
+
f"[layer] Detected B-factor layers: ML={len(layer_info.get('ml_indices', []))}, "
|
|
576
|
+
f"MovableMM={len(layer_info.get('movable_mm_indices', []))}, "
|
|
577
|
+
f"FrozenMM={len(layer_info.get('frozen_indices', []))}"
|
|
578
|
+
)
|
|
579
|
+
except Exception as e:
|
|
580
|
+
if model_pdb is None and not model_indices:
|
|
581
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
582
|
+
sys.exit(1)
|
|
583
|
+
click.echo(f"[layer] WARNING: {e} Falling back to explicit ML region.", err=True)
|
|
584
|
+
detect_layer = False
|
|
585
|
+
|
|
586
|
+
if not detect_layer:
|
|
587
|
+
if model_pdb is None and not model_indices:
|
|
588
|
+
click.echo("ERROR: Provide --model-pdb or --model-indices when --no-detect-layer.", err=True)
|
|
589
|
+
sys.exit(1)
|
|
590
|
+
if model_pdb is not None:
|
|
591
|
+
model_pdb_path = Path(model_pdb)
|
|
592
|
+
else:
|
|
593
|
+
if layer_source_pdb.suffix.lower() != ".pdb":
|
|
594
|
+
click.echo("ERROR: --model-indices requires a PDB input (or --ref-pdb).", err=True)
|
|
595
|
+
sys.exit(1)
|
|
596
|
+
try:
|
|
597
|
+
model_pdb_path = build_model_pdb_from_indices(layer_source_pdb, out_dir_path, model_indices or [])
|
|
598
|
+
except Exception as e:
|
|
599
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
600
|
+
sys.exit(1)
|
|
601
|
+
calc_cfg["use_bfactor_layers"] = False
|
|
602
|
+
|
|
603
|
+
if model_pdb_path is None:
|
|
604
|
+
click.echo("ERROR: Failed to resolve model PDB for the ML region.", err=True)
|
|
605
|
+
sys.exit(1)
|
|
606
|
+
|
|
607
|
+
calc_cfg["model_pdb"] = str(model_pdb_path)
|
|
608
|
+
freeze_atoms_final = apply_layer_freeze_constraints(
|
|
609
|
+
geom_cfg,
|
|
610
|
+
calc_cfg,
|
|
611
|
+
layer_info,
|
|
612
|
+
echo_fn=click.echo,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Distance-based overrides for Hessian-target and movable MM selection.
|
|
616
|
+
if hess_cutoff is not None:
|
|
617
|
+
calc_cfg["hess_cutoff"] = hess_cutoff
|
|
618
|
+
if movable_cutoff is not None:
|
|
619
|
+
calc_cfg["movable_cutoff"] = movable_cutoff
|
|
620
|
+
calc_cfg["use_bfactor_layers"] = False
|
|
621
|
+
|
|
622
|
+
for key in ("input_pdb", "real_parm7", "model_pdb", "mm_fd_dir"):
|
|
623
|
+
val = calc_cfg.get(key)
|
|
624
|
+
if val:
|
|
625
|
+
calc_cfg[key] = str(Path(val).expanduser().resolve())
|
|
626
|
+
echo_geom = format_freeze_atoms_for_echo(geom_cfg, key="freeze_atoms")
|
|
627
|
+
echo_calc = format_freeze_atoms_for_echo(filter_calc_for_echo(calc_cfg), key="freeze_atoms")
|
|
628
|
+
echo_opt = strip_inherited_keys({**opt_cfg, "out_dir": str(out_dir_path)}, OPT_BASE_KW, mode="same")
|
|
629
|
+
# Show only lbfgs-specific settings, not inherited from opt_cfg
|
|
630
|
+
echo_lbfgs = strip_inherited_keys(lbfgs_cfg, opt_cfg)
|
|
631
|
+
click.echo(pretty_block("geom", echo_geom))
|
|
632
|
+
click.echo(pretty_block("calc", echo_calc))
|
|
633
|
+
click.echo(pretty_block("opt", echo_opt))
|
|
634
|
+
click.echo(pretty_block("lbfgs", echo_lbfgs))
|
|
635
|
+
click.echo(pretty_block("bias", bias_cfg))
|
|
636
|
+
click.echo(pretty_block("bond", bond_cfg))
|
|
637
|
+
|
|
638
|
+
pdb_atom_meta: List[Dict[str, Any]] = []
|
|
639
|
+
if source_path.suffix.lower() == ".pdb":
|
|
640
|
+
pdb_atom_meta = load_pdb_atom_metadata(source_path)
|
|
641
|
+
|
|
642
|
+
cli_scan_values = collect_single_option_values(
|
|
643
|
+
sys.argv[1:], ("-s", "--scan-lists"), "--scan-lists"
|
|
644
|
+
)
|
|
645
|
+
if not cli_scan_values:
|
|
646
|
+
raise click.BadParameter("--scan-lists is required.")
|
|
647
|
+
|
|
648
|
+
stages: List[List[Tuple[int, int, float]]]
|
|
649
|
+
scan_one_based = bool(one_based)
|
|
650
|
+
scan_source = "--scan-lists"
|
|
651
|
+
# Bidirectional scan support (4-tuple): track which stages
|
|
652
|
+
# need geometry snapshot/reset.
|
|
653
|
+
_bidir_reset_before: set = set()
|
|
654
|
+
_bidir_snapshot_before: set = set()
|
|
655
|
+
# Auto-detect: single value that is a YAML/JSON file → spec mode
|
|
656
|
+
if len(cli_scan_values) == 1 and is_scan_spec_file(cli_scan_values[0]):
|
|
657
|
+
spec_path = Path(cli_scan_values[0])
|
|
658
|
+
stages, scan_one_based = parse_scan_spec_stages(
|
|
659
|
+
spec_path,
|
|
660
|
+
one_based_default=one_based,
|
|
661
|
+
atom_meta=pdb_atom_meta,
|
|
662
|
+
option_name="--scan-lists",
|
|
663
|
+
)
|
|
664
|
+
scan_source = f"--scan-lists ({spec_path})"
|
|
665
|
+
else:
|
|
666
|
+
stages = []
|
|
667
|
+
for idx, raw in enumerate(cli_scan_values, start=1):
|
|
668
|
+
parsed, _ = parse_scan_list_triples(
|
|
669
|
+
raw,
|
|
670
|
+
one_based=scan_one_based,
|
|
671
|
+
atom_meta=pdb_atom_meta,
|
|
672
|
+
option_name=f"--scan-lists #{idx}",
|
|
673
|
+
)
|
|
674
|
+
for t in parsed:
|
|
675
|
+
for dist in t[2:]:
|
|
676
|
+
if dist <= 0.0:
|
|
677
|
+
raise click.BadParameter(
|
|
678
|
+
f"Non-positive target length in --scan-lists #{idx}: {t}."
|
|
679
|
+
)
|
|
680
|
+
# Expand 4-tuples into two stages with reset marker
|
|
681
|
+
has_4tuple = any(len(t) == 4 for t in parsed)
|
|
682
|
+
if has_4tuple:
|
|
683
|
+
for t in parsed:
|
|
684
|
+
if len(t) == 4:
|
|
685
|
+
i, j, start, end = t
|
|
686
|
+
stage_a_idx = len(stages)
|
|
687
|
+
stages.append([(i, j, start)])
|
|
688
|
+
_bidir_snapshot_before.add(stage_a_idx)
|
|
689
|
+
_bidir_reset_before.add(stage_a_idx + 1)
|
|
690
|
+
stages.append([(i, j, end)])
|
|
691
|
+
else:
|
|
692
|
+
stages.append([t])
|
|
693
|
+
else:
|
|
694
|
+
stages.append(parsed)
|
|
695
|
+
K = len(stages)
|
|
696
|
+
click.echo(f"[scan] Received {K} stage(s).")
|
|
697
|
+
if print_parsed:
|
|
698
|
+
click.echo(
|
|
699
|
+
pretty_block(
|
|
700
|
+
"scan-parsed",
|
|
701
|
+
{
|
|
702
|
+
"source": scan_source,
|
|
703
|
+
"one_based": bool(scan_one_based),
|
|
704
|
+
"stages_0based": stages,
|
|
705
|
+
},
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
if dry_run:
|
|
710
|
+
model_region_source = "bfactor"
|
|
711
|
+
if not detect_layer:
|
|
712
|
+
if model_pdb is not None:
|
|
713
|
+
model_region_source = "model_pdb"
|
|
714
|
+
elif model_indices:
|
|
715
|
+
model_region_source = "model_indices"
|
|
716
|
+
click.echo(
|
|
717
|
+
pretty_block(
|
|
718
|
+
"dry_run_plan",
|
|
719
|
+
{
|
|
720
|
+
"input_geometry": str(geom_input_path),
|
|
721
|
+
"output_dir": str(out_dir_path),
|
|
722
|
+
"detect_layer": bool(detect_layer),
|
|
723
|
+
"model_region_source": model_region_source,
|
|
724
|
+
"num_stages": len(stages),
|
|
725
|
+
"stages_0based": stages,
|
|
726
|
+
"preopt": bool(preopt),
|
|
727
|
+
"endopt": bool(endopt),
|
|
728
|
+
"bias_k": float(bias_cfg["k"]),
|
|
729
|
+
"max_step_size": float(max_step_size),
|
|
730
|
+
"max_cycles": int(max_cycles),
|
|
731
|
+
"backend": calc_cfg.get("backend", "uma"),
|
|
732
|
+
"embedcharge": bool(calc_cfg.get("embedcharge", False)),
|
|
733
|
+
},
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
click.echo("[dry-run] Validation complete. Scan execution was skipped.")
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
if pdb_atom_meta:
|
|
740
|
+
click.echo("[scan] PDB atom details for scanned pairs:")
|
|
741
|
+
legend = PDB_ATOM_META_HEADER
|
|
742
|
+
click.echo(f" legend: {legend}")
|
|
743
|
+
for stage_idx, tuples in enumerate(stages, start=1):
|
|
744
|
+
click.echo(f" Stage {stage_idx}:")
|
|
745
|
+
for pair_idx, (i, j, _) in enumerate(tuples, start=1):
|
|
746
|
+
click.echo(
|
|
747
|
+
f" pair {pair_idx} i: {format_pdb_atom_metadata(pdb_atom_meta, i)}"
|
|
748
|
+
)
|
|
749
|
+
click.echo(
|
|
750
|
+
f" j: {format_pdb_atom_metadata(pdb_atom_meta, j)}"
|
|
751
|
+
)
|
|
752
|
+
stages_summary: List[Dict[str, Any]] = []
|
|
753
|
+
|
|
754
|
+
out_dir_path.mkdir(parents=True, exist_ok=True)
|
|
755
|
+
coord_type = geom_cfg.get("coord_type", "cart")
|
|
756
|
+
geom = geom_loader(geom_input_path, coord_type=coord_type)
|
|
757
|
+
|
|
758
|
+
freeze = list(geom_cfg.get("freeze_atoms") or [])
|
|
759
|
+
if freeze:
|
|
760
|
+
try:
|
|
761
|
+
geom.freeze_atoms = np.array(freeze, dtype=int)
|
|
762
|
+
except Exception:
|
|
763
|
+
logger.debug("Failed to set freeze_atoms on geometry", exc_info=True)
|
|
764
|
+
|
|
765
|
+
base_calc = mlmm(**calc_cfg)
|
|
766
|
+
|
|
767
|
+
max_step_bohr = float(max_step_size) * ANG2BOHR
|
|
768
|
+
|
|
769
|
+
def _make_lbfgs(_out_dir: Path, _prefix: str) -> LBFGS:
|
|
770
|
+
common = dict(opt_cfg)
|
|
771
|
+
common["out_dir"] = str(_out_dir)
|
|
772
|
+
common["prefix"] = _prefix
|
|
773
|
+
args = {**lbfgs_cfg, **common}
|
|
774
|
+
args["max_step"] = min(float(lbfgs_cfg.get("max_step", 0.30)), max_step_bohr)
|
|
775
|
+
return LBFGS(geom, **args)
|
|
776
|
+
|
|
777
|
+
if preopt:
|
|
778
|
+
pre_dir = out_dir_path / "preopt"
|
|
779
|
+
pre_dir.mkdir(parents=True, exist_ok=True)
|
|
780
|
+
geom.set_calculator(base_calc)
|
|
781
|
+
click.echo("[preopt] Unbiased relaxation (LBFGS) ...")
|
|
782
|
+
optimizer0 = _make_lbfgs(pre_dir, "preopt")
|
|
783
|
+
try:
|
|
784
|
+
optimizer0.run()
|
|
785
|
+
except ZeroStepLength:
|
|
786
|
+
click.echo("[preopt] ZeroStepLength — continuing.", err=True)
|
|
787
|
+
except OptimizationError as e:
|
|
788
|
+
click.echo(f"[preopt] OptimizationError — {e}", err=True)
|
|
789
|
+
|
|
790
|
+
pre_xyz = pre_dir / "result.xyz"
|
|
791
|
+
with open(pre_xyz, "w") as f:
|
|
792
|
+
f.write(_coords3d_to_xyz_string(geom))
|
|
793
|
+
click.echo(f"[write] Wrote '{pre_xyz}'.")
|
|
794
|
+
try:
|
|
795
|
+
convert_xyz_to_pdb(pre_xyz, source_path.resolve(), pre_dir / "result.pdb")
|
|
796
|
+
click.echo(f"[convert] Wrote '{pre_dir / 'result.pdb'}'.")
|
|
797
|
+
except Exception as e:
|
|
798
|
+
click.echo(f"[convert] WARNING: Failed to convert preopt result to PDB: {e}", err=True)
|
|
799
|
+
|
|
800
|
+
biased = HarmonicBiasCalculator(base_calc, k=float(bias_cfg["k"]))
|
|
801
|
+
geom.set_calculator(biased)
|
|
802
|
+
|
|
803
|
+
all_trj_blocks: List[str] = []
|
|
804
|
+
# For bidirectional 4-tuple scans: save geometry before pass 1,
|
|
805
|
+
# restore before pass 2, and reverse pass 1 trajectory.
|
|
806
|
+
_bidir_saved_geom = None
|
|
807
|
+
_bidir_pass1_trj: List[str] = []
|
|
808
|
+
|
|
809
|
+
for k, tuples in enumerate(stages, start=1):
|
|
810
|
+
# Bidirectional support: snapshot before pass 1
|
|
811
|
+
stage_idx_0 = k - 1 # 0-based
|
|
812
|
+
if stage_idx_0 in _bidir_snapshot_before:
|
|
813
|
+
_bidir_saved_geom = _snapshot_geometry(geom)
|
|
814
|
+
_bidir_pass1_trj = []
|
|
815
|
+
# Bidirectional support: restore geometry before pass 2
|
|
816
|
+
if stage_idx_0 in _bidir_reset_before and _bidir_saved_geom is not None:
|
|
817
|
+
click.echo("[bidir] Restoring initial geometry for reverse-direction pass.")
|
|
818
|
+
geom.coords = _bidir_saved_geom.coords.copy()
|
|
819
|
+
|
|
820
|
+
stage_dir = out_dir_path / f"stage_{k:02d}"
|
|
821
|
+
stage_dir.mkdir(parents=True, exist_ok=True)
|
|
822
|
+
click.echo(f"\n--- Stage {k}/{K} ---")
|
|
823
|
+
click.echo(f"Targets (i,j,target Å): {tuples}")
|
|
824
|
+
|
|
825
|
+
start_geom_for_stage = _snapshot_geometry(geom)
|
|
826
|
+
|
|
827
|
+
R_bohr = np.array(geom.coords3d, dtype=float)
|
|
828
|
+
R_ang = R_bohr * BOHR2ANG
|
|
829
|
+
Nsteps, r0, rT, step_widths = _schedule_for_stage(R_ang, tuples, float(max_step_size))
|
|
830
|
+
click.echo(f"[stage {k}] initial distances (Å) = {['{:.3f}'.format(x) for x in r0]}")
|
|
831
|
+
click.echo(f"[stage {k}] target distances (Å) = {['{:.3f}'.format(x) for x in rT]}")
|
|
832
|
+
click.echo(f"[stage {k}] steps N = {Nsteps}")
|
|
833
|
+
|
|
834
|
+
srec: Dict[str, Any] = {
|
|
835
|
+
"index": int(k),
|
|
836
|
+
"pairs_1based": [(int(i) + 1, int(j) + 1) for (i, j, _) in tuples],
|
|
837
|
+
"initial_distances_A": [float(f"{x:.3f}") for x in r0],
|
|
838
|
+
"target_distances_A": [float(f"{x:.3f}") for x in rT],
|
|
839
|
+
"per_pair_step_A": [float(f"{x:.3f}") for x in step_widths],
|
|
840
|
+
"num_steps": int(Nsteps),
|
|
841
|
+
"bond_change": {"changed": None, "summary": ""},
|
|
842
|
+
}
|
|
843
|
+
stages_summary.append(srec)
|
|
844
|
+
|
|
845
|
+
trj_blocks: List[str] = []
|
|
846
|
+
stage_trj_path = stage_dir / "scan_trj.xyz"
|
|
847
|
+
stage_trj_path.write_text("")
|
|
848
|
+
pairs = [(i, j) for (i, j, _) in tuples]
|
|
849
|
+
|
|
850
|
+
if Nsteps == 0:
|
|
851
|
+
if endopt:
|
|
852
|
+
geom.set_calculator(base_calc)
|
|
853
|
+
click.echo(f"[stage {k}] endopt (unbiased) ...")
|
|
854
|
+
try:
|
|
855
|
+
end_optimizer = _make_lbfgs(stage_dir, "endopt")
|
|
856
|
+
end_optimizer.run()
|
|
857
|
+
except ZeroStepLength:
|
|
858
|
+
click.echo(f"[stage {k}] endopt ZeroStepLength — continuing.", err=True)
|
|
859
|
+
except OptimizationError as e:
|
|
860
|
+
click.echo(f"[stage {k}] endopt OptimizationError — {e}", err=True)
|
|
861
|
+
finally:
|
|
862
|
+
geom.set_calculator(biased)
|
|
863
|
+
|
|
864
|
+
try:
|
|
865
|
+
changed, summary = _has_bond_change(start_geom_for_stage, geom, bond_cfg)
|
|
866
|
+
click.echo(f"[stage {k}] Covalent-bond changes (start vs final): {'Yes' if changed else 'No'}")
|
|
867
|
+
if changed and summary and summary.strip():
|
|
868
|
+
click.echo(textwrap.indent(summary.strip(), prefix=" "))
|
|
869
|
+
if not changed:
|
|
870
|
+
click.echo(" (no covalent changes detected)")
|
|
871
|
+
try:
|
|
872
|
+
srec["bond_change"]["changed"] = bool(changed)
|
|
873
|
+
srec["bond_change"]["summary"] = (summary.strip() if (summary and summary.strip()) else "")
|
|
874
|
+
except Exception:
|
|
875
|
+
logger.debug("Failed to store bond_change record", exc_info=True)
|
|
876
|
+
except Exception as e:
|
|
877
|
+
click.echo(f"[stage {k}] WARNING: Failed to evaluate bond changes: {e}", err=True)
|
|
878
|
+
|
|
879
|
+
final_xyz = stage_dir / "result.xyz"
|
|
880
|
+
with open(final_xyz, "w") as f:
|
|
881
|
+
f.write(_coords3d_to_xyz_string(geom))
|
|
882
|
+
click.echo(f"[write] Wrote '{final_xyz}'.")
|
|
883
|
+
try:
|
|
884
|
+
convert_xyz_to_pdb(final_xyz, source_path.resolve(), stage_dir / "result.pdb")
|
|
885
|
+
click.echo(f"[convert] Wrote '{stage_dir / 'result.pdb'}'.")
|
|
886
|
+
except Exception as e:
|
|
887
|
+
click.echo(f"[convert] WARNING: Failed to convert stage result to PDB: {e}", err=True)
|
|
888
|
+
continue
|
|
889
|
+
|
|
890
|
+
for s in range(1, Nsteps + 1):
|
|
891
|
+
step_targets = [r0_i + s * dw for (r0_i, dw) in zip(r0, step_widths)]
|
|
892
|
+
biased.set_pairs([(i, j, t) for ((i, j), t) in zip(pairs, step_targets)])
|
|
893
|
+
geom.set_calculator(biased)
|
|
894
|
+
|
|
895
|
+
prefix = f"scan_s{s:04d}"
|
|
896
|
+
optimizer = _make_lbfgs(stage_dir, prefix)
|
|
897
|
+
click.echo(f"[stage {k}] step {s}/{Nsteps}: relaxation (LBFGS) ...")
|
|
898
|
+
try:
|
|
899
|
+
optimizer.run()
|
|
900
|
+
except ZeroStepLength:
|
|
901
|
+
click.echo(f"[stage {k}] step {s}: ZeroStepLength — continuing to next step.", err=True)
|
|
902
|
+
except OptimizationError as e:
|
|
903
|
+
click.echo(f"[stage {k}] step {s}: OptimizationError — {e}", err=True)
|
|
904
|
+
|
|
905
|
+
trj_blocks.append(_coords3d_to_xyz_string(geom))
|
|
906
|
+
with open(stage_trj_path, "a") as _tf:
|
|
907
|
+
_tf.write(trj_blocks[-1])
|
|
908
|
+
|
|
909
|
+
if endopt:
|
|
910
|
+
geom.set_calculator(base_calc)
|
|
911
|
+
click.echo(f"[stage {k}] endopt (unbiased) ...")
|
|
912
|
+
try:
|
|
913
|
+
end_optimizer = _make_lbfgs(stage_dir, "endopt")
|
|
914
|
+
end_optimizer.run()
|
|
915
|
+
except ZeroStepLength:
|
|
916
|
+
click.echo(f"[stage {k}] endopt ZeroStepLength — continuing.", err=True)
|
|
917
|
+
except OptimizationError as e:
|
|
918
|
+
click.echo(f"[stage {k}] endopt OptimizationError — {e}", err=True)
|
|
919
|
+
finally:
|
|
920
|
+
geom.set_calculator(biased)
|
|
921
|
+
|
|
922
|
+
try:
|
|
923
|
+
changed, summary = _has_bond_change(start_geom_for_stage, geom, bond_cfg)
|
|
924
|
+
click.echo(f"[stage {k}] Covalent-bond changes (start vs final): {'Yes' if changed else 'No'}")
|
|
925
|
+
if changed and summary and summary.strip():
|
|
926
|
+
click.echo(textwrap.indent(summary.strip(), prefix=" "))
|
|
927
|
+
if not changed:
|
|
928
|
+
click.echo(" (no covalent changes detected)")
|
|
929
|
+
try:
|
|
930
|
+
srec["bond_change"]["changed"] = bool(changed)
|
|
931
|
+
srec["bond_change"]["summary"] = (summary.strip() if (summary and summary.strip()) else "")
|
|
932
|
+
except Exception:
|
|
933
|
+
logger.debug("Failed to store bond_change record", exc_info=True)
|
|
934
|
+
except Exception as e:
|
|
935
|
+
click.echo(f"[stage {k}] WARNING: Failed to evaluate bond changes: {e}", err=True)
|
|
936
|
+
|
|
937
|
+
if trj_blocks:
|
|
938
|
+
click.echo(f"[write] Wrote '{stage_trj_path}'.")
|
|
939
|
+
# Bidirectional trajectory assembly:
|
|
940
|
+
# pass 1 (initial→start) is saved; pass 2 (initial→end)
|
|
941
|
+
# triggers assembly: reversed(pass1) + pass2 → start→initial→end
|
|
942
|
+
if stage_idx_0 in _bidir_snapshot_before:
|
|
943
|
+
_bidir_pass1_trj = list(trj_blocks)
|
|
944
|
+
elif stage_idx_0 in _bidir_reset_before:
|
|
945
|
+
all_trj_blocks.extend(reversed(_bidir_pass1_trj))
|
|
946
|
+
all_trj_blocks.extend(trj_blocks)
|
|
947
|
+
_bidir_pass1_trj = []
|
|
948
|
+
else:
|
|
949
|
+
all_trj_blocks.extend(trj_blocks)
|
|
950
|
+
try:
|
|
951
|
+
convert_xyz_to_pdb(stage_trj_path, source_path.resolve(), stage_dir / "scan.pdb")
|
|
952
|
+
click.echo(f"[convert] Wrote '{stage_dir / 'scan.pdb'}'.")
|
|
953
|
+
except Exception as e:
|
|
954
|
+
click.echo(f"[convert] WARNING: Failed to convert stage trajectory to PDB: {e}", err=True)
|
|
955
|
+
|
|
956
|
+
final_xyz = stage_dir / "result.xyz"
|
|
957
|
+
with open(final_xyz, "w") as f:
|
|
958
|
+
f.write(_coords3d_to_xyz_string(geom))
|
|
959
|
+
click.echo(f"[write] Wrote '{final_xyz}'.")
|
|
960
|
+
try:
|
|
961
|
+
convert_xyz_to_pdb(final_xyz, source_path.resolve(), stage_dir / "result.pdb")
|
|
962
|
+
click.echo(f"[convert] Wrote '{stage_dir / 'result.pdb'}'.")
|
|
963
|
+
except Exception as e:
|
|
964
|
+
click.echo(f"[convert] WARNING: Failed to convert stage result to PDB: {e}", err=True)
|
|
965
|
+
|
|
966
|
+
# ------------------------------------------------------------------
|
|
967
|
+
# 4b) Write combined scan_trj.xyz + scan.pdb to out_dir
|
|
968
|
+
# ------------------------------------------------------------------
|
|
969
|
+
if all_trj_blocks:
|
|
970
|
+
combined_trj = out_dir_path / "scan_trj.xyz"
|
|
971
|
+
with open(combined_trj, "w") as f:
|
|
972
|
+
f.write("".join(all_trj_blocks))
|
|
973
|
+
click.echo(f"[write] Wrote '{combined_trj}'.")
|
|
974
|
+
try:
|
|
975
|
+
convert_xyz_to_pdb(combined_trj, source_path.resolve(), out_dir_path / "scan.pdb")
|
|
976
|
+
click.echo(f"[convert] Wrote '{out_dir_path / 'scan.pdb'}'.")
|
|
977
|
+
except Exception as e:
|
|
978
|
+
click.echo(f"[convert] WARNING: Failed to convert combined trajectory to PDB: {e}", err=True)
|
|
979
|
+
|
|
980
|
+
# ------------------------------------------------------------------
|
|
981
|
+
# 5) Final summary echo (human‑friendly)
|
|
982
|
+
# ------------------------------------------------------------------
|
|
983
|
+
def _echo_human_summary(_stages: List[Dict[str, Any]], _max_step_size: float) -> None:
|
|
984
|
+
"""
|
|
985
|
+
Print a readable end-of-run summary like the requested example.
|
|
986
|
+
"""
|
|
987
|
+
def _fmt_target_value(x: float) -> str:
|
|
988
|
+
# 2.600 -> "2.6", 1.500 -> "1.5"
|
|
989
|
+
s = f"{x:.3f}".rstrip("0").rstrip(".")
|
|
990
|
+
return s
|
|
991
|
+
|
|
992
|
+
def _targets_triplet_str(pairs_1based: List[Tuple[int, int]], targets: List[float]) -> str:
|
|
993
|
+
triples = [f"({i}, {j}, {_fmt_target_value(t)})" for (i, j), t in zip(pairs_1based, targets)]
|
|
994
|
+
return "[" + ", ".join(triples) + "]"
|
|
995
|
+
|
|
996
|
+
def _list_of_str_3f(values: List[float]) -> str:
|
|
997
|
+
return "[" + ", ".join(f"'{v:.3f}'" for v in values) + "]"
|
|
998
|
+
|
|
999
|
+
click.echo("\nSummary")
|
|
1000
|
+
click.echo("------------------")
|
|
1001
|
+
for s in _stages:
|
|
1002
|
+
idx = int(s.get("index", 0))
|
|
1003
|
+
pairs_1b = list(s.get("pairs_1based", []))
|
|
1004
|
+
r0 = list(s.get("initial_distances_A", []))
|
|
1005
|
+
rT = list(s.get("target_distances_A", []))
|
|
1006
|
+
dA = list(s.get("per_pair_step_A", []))
|
|
1007
|
+
N = int(s.get("num_steps", 0))
|
|
1008
|
+
bchg = s.get("bond_change", {}) or {}
|
|
1009
|
+
changed = bool(bchg.get("changed"))
|
|
1010
|
+
summary_txt = (bchg.get("summary") or "").strip()
|
|
1011
|
+
|
|
1012
|
+
click.echo(f"[stage {idx}] Targets (i,j,target Å): { _targets_triplet_str(pairs_1b, rT) }")
|
|
1013
|
+
click.echo(f"[stage {idx}] initial distances (Å) = { _list_of_str_3f(r0) }")
|
|
1014
|
+
click.echo(f"[stage {idx}] target distances (Å) = { _list_of_str_3f(rT) }")
|
|
1015
|
+
click.echo(f"[stage {idx}] per_pair_step (Å) = { _list_of_str_3f(dA) }")
|
|
1016
|
+
click.echo(f"[stage {idx}] steps N = {N}")
|
|
1017
|
+
click.echo(f"[stage {idx}] Covalent-bond changes (start vs final): {'Yes' if changed else 'No'}")
|
|
1018
|
+
if changed and summary_txt:
|
|
1019
|
+
click.echo(textwrap.indent(summary_txt, prefix=" "))
|
|
1020
|
+
if not changed:
|
|
1021
|
+
click.echo(" (no covalent changes detected)")
|
|
1022
|
+
click.echo("") # blank line between stages
|
|
1023
|
+
|
|
1024
|
+
_echo_human_summary(stages_summary, float(max_step_size))
|
|
1025
|
+
# ------------------------------------------------------------------
|
|
1026
|
+
|
|
1027
|
+
click.echo("\n=== Scan finished ===\n")
|
|
1028
|
+
|
|
1029
|
+
click.echo(format_elapsed("[time] Elapsed Time for Scan", time_start))
|
|
1030
|
+
|
|
1031
|
+
except KeyboardInterrupt:
|
|
1032
|
+
click.echo("\nInterrupted by user.", err=True)
|
|
1033
|
+
sys.exit(130)
|
|
1034
|
+
except Exception as e:
|
|
1035
|
+
tb = "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
1036
|
+
click.echo("Unhandled error during scan:\n" + textwrap.indent(tb, " "), err=True)
|
|
1037
|
+
sys.exit(1)
|
|
1038
|
+
finally:
|
|
1039
|
+
# Release GPU memory so subsequent pipeline stages don't OOM
|
|
1040
|
+
base_calc = biased = geom = optimizer = optimizer0 = end_optimizer = None
|
|
1041
|
+
gc.collect() # break cyclic refs inside torch.nn.Module
|
|
1042
|
+
if torch.cuda.is_available():
|
|
1043
|
+
torch.cuda.empty_cache()
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
if __name__ == "__main__":
|
|
1047
|
+
cli()
|