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/define_layer.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# mlmm/define_layer.py
|
|
2
|
+
|
|
3
|
+
"""Define 3-layer ML/MM system based on distance from the ML region.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
mlmm define-layer -i system.pdb --model-pdb ml_region.pdb -o labeled.pdb
|
|
7
|
+
|
|
8
|
+
For detailed documentation, see: docs/define_layer.md
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from .defaults import (
|
|
19
|
+
BFACTOR_ML,
|
|
20
|
+
BFACTOR_MOVABLE_MM,
|
|
21
|
+
BFACTOR_FROZEN,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Default radii for layer definition
|
|
25
|
+
# NOTE: reserved in 3-layer mode (no-op).
|
|
26
|
+
DEFAULT_RADIUS_PARTIAL_HESSIAN = 0.0 # Å
|
|
27
|
+
DEFAULT_RADIUS_FREEZE = 8.0 # Å
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_pdb_atoms(pdb_path: Path) -> List[Dict[str, Any]]:
|
|
31
|
+
"""
|
|
32
|
+
Parse ATOM/HETATM records from a PDB file.
|
|
33
|
+
|
|
34
|
+
Returns a list of dictionaries with atom information:
|
|
35
|
+
- idx: 0-based atom index
|
|
36
|
+
- atom_name: atom name
|
|
37
|
+
- res_name: residue name
|
|
38
|
+
- chain_id: chain identifier
|
|
39
|
+
- res_seq: residue sequence number
|
|
40
|
+
- icode: insertion code
|
|
41
|
+
- coord: (x, y, z) coordinates as numpy array
|
|
42
|
+
- element: element symbol
|
|
43
|
+
- res_key: (chain_id, res_seq, icode) tuple for residue identification
|
|
44
|
+
"""
|
|
45
|
+
atoms = []
|
|
46
|
+
with open(pdb_path, "r") as f:
|
|
47
|
+
atom_idx = 0
|
|
48
|
+
for line in f:
|
|
49
|
+
if not line.startswith(("ATOM ", "HETATM")):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
atom_name = line[12:16].strip()
|
|
53
|
+
res_name = line[17:20].strip()
|
|
54
|
+
chain_id = line[21:22].strip()
|
|
55
|
+
res_seq_str = line[22:26].strip()
|
|
56
|
+
try:
|
|
57
|
+
res_seq = int(res_seq_str)
|
|
58
|
+
except ValueError:
|
|
59
|
+
res_seq = 0
|
|
60
|
+
icode = line[26:27].strip()
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
x = float(line[30:38])
|
|
64
|
+
y = float(line[38:46])
|
|
65
|
+
z = float(line[46:54])
|
|
66
|
+
except ValueError:
|
|
67
|
+
x, y, z = 0.0, 0.0, 0.0
|
|
68
|
+
|
|
69
|
+
element = line[76:78].strip() if len(line) >= 78 else ""
|
|
70
|
+
|
|
71
|
+
atoms.append({
|
|
72
|
+
"idx": atom_idx,
|
|
73
|
+
"atom_name": atom_name,
|
|
74
|
+
"res_name": res_name,
|
|
75
|
+
"chain_id": chain_id,
|
|
76
|
+
"res_seq": res_seq,
|
|
77
|
+
"icode": icode,
|
|
78
|
+
"coord": np.array([x, y, z]),
|
|
79
|
+
"element": element,
|
|
80
|
+
"res_key": (chain_id, res_seq, icode),
|
|
81
|
+
})
|
|
82
|
+
atom_idx += 1
|
|
83
|
+
|
|
84
|
+
return atoms
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_ml_indices_from_model_pdb(
|
|
88
|
+
input_atoms: List[Dict[str, Any]],
|
|
89
|
+
model_pdb_path: Path,
|
|
90
|
+
) -> List[int]:
|
|
91
|
+
"""
|
|
92
|
+
Get ML region atom indices by matching atoms from model_pdb to input atoms.
|
|
93
|
+
|
|
94
|
+
Matching is done by (atom_name, res_name, res_seq) triplet.
|
|
95
|
+
"""
|
|
96
|
+
# Parse model PDB
|
|
97
|
+
model_atoms = _parse_pdb_atoms(model_pdb_path)
|
|
98
|
+
|
|
99
|
+
# Create a set of ML atom identifiers
|
|
100
|
+
ml_ids = set()
|
|
101
|
+
for atom in model_atoms:
|
|
102
|
+
ml_id = f"{atom['atom_name']} {atom['res_name']} {atom['res_seq']}"
|
|
103
|
+
ml_ids.add(ml_id)
|
|
104
|
+
|
|
105
|
+
# Find matching atoms in input
|
|
106
|
+
ml_indices = []
|
|
107
|
+
for atom in input_atoms:
|
|
108
|
+
atom_id = f"{atom['atom_name']} {atom['res_name']} {atom['res_seq']}"
|
|
109
|
+
if atom_id in ml_ids:
|
|
110
|
+
ml_indices.append(atom["idx"])
|
|
111
|
+
|
|
112
|
+
return sorted(ml_indices)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _parse_indices_string(
|
|
116
|
+
indices_str: str,
|
|
117
|
+
one_based: bool,
|
|
118
|
+
) -> List[int]:
|
|
119
|
+
"""Parse comma-separated indices string into a list of 0-based integers."""
|
|
120
|
+
indices = []
|
|
121
|
+
for token in indices_str.split(","):
|
|
122
|
+
token = token.strip()
|
|
123
|
+
if not token:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Handle range notation (e.g., "1-5")
|
|
127
|
+
if "-" in token and not token.startswith("-"):
|
|
128
|
+
parts = token.split("-")
|
|
129
|
+
if len(parts) == 2:
|
|
130
|
+
try:
|
|
131
|
+
start = int(parts[0])
|
|
132
|
+
end = int(parts[1])
|
|
133
|
+
for i in range(start, end + 1):
|
|
134
|
+
idx = i - 1 if one_based else i
|
|
135
|
+
indices.append(idx)
|
|
136
|
+
continue
|
|
137
|
+
except ValueError:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
idx = int(token)
|
|
142
|
+
if one_based:
|
|
143
|
+
idx -= 1
|
|
144
|
+
indices.append(idx)
|
|
145
|
+
except ValueError:
|
|
146
|
+
raise click.BadParameter(f"Invalid index: {token}")
|
|
147
|
+
|
|
148
|
+
return sorted(set(indices))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def compute_layer_indices_by_residue(
|
|
152
|
+
atoms: List[Dict[str, Any]],
|
|
153
|
+
ml_indices: List[int],
|
|
154
|
+
radius_partial_hessian: float,
|
|
155
|
+
radius_freeze: float,
|
|
156
|
+
) -> Dict[str, List[int]]:
|
|
157
|
+
"""
|
|
158
|
+
Compute 3-layer indices based on distance from ML region.
|
|
159
|
+
|
|
160
|
+
For residues WITHOUT ML atoms:
|
|
161
|
+
The minimum distance from any atom in the residue to any ML atom is computed.
|
|
162
|
+
The entire residue is assigned to the layer based on this minimum distance.
|
|
163
|
+
|
|
164
|
+
For residues WITH ML atoms:
|
|
165
|
+
ML atoms stay in Layer 1. Non-ML atoms (e.g., backbone) are classified
|
|
166
|
+
individually based on their distance to the nearest ML atom.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
atoms : List[Dict[str, Any]]
|
|
171
|
+
List of atom dictionaries from _parse_pdb_atoms
|
|
172
|
+
ml_indices : List[int]
|
|
173
|
+
0-based indices of ML region atoms
|
|
174
|
+
radius_partial_hessian : float
|
|
175
|
+
Deprecated in 3-layer mode. Ignored by the 3-layer assignment.
|
|
176
|
+
radius_freeze : float
|
|
177
|
+
Distance cutoff (Å) for movable MM atoms
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
Dict[str, List[int]]
|
|
182
|
+
Dictionary with keys: ml_indices, hess_mm_indices (empty in 3-layer mode),
|
|
183
|
+
movable_mm_indices, frozen_indices
|
|
184
|
+
"""
|
|
185
|
+
ml_set = set(ml_indices)
|
|
186
|
+
n_atoms = len(atoms)
|
|
187
|
+
|
|
188
|
+
# Get ML region coordinates
|
|
189
|
+
ml_coords = np.array([atoms[i]["coord"] for i in ml_indices if i < n_atoms])
|
|
190
|
+
if ml_coords.size == 0:
|
|
191
|
+
raise ValueError("No valid ML atoms found in the input structure")
|
|
192
|
+
|
|
193
|
+
# Group atoms by residue
|
|
194
|
+
residue_atoms: Dict[Tuple, List[int]] = {}
|
|
195
|
+
for atom in atoms:
|
|
196
|
+
res_key = atom["res_key"]
|
|
197
|
+
if res_key not in residue_atoms:
|
|
198
|
+
residue_atoms[res_key] = []
|
|
199
|
+
residue_atoms[res_key].append(atom["idx"])
|
|
200
|
+
|
|
201
|
+
# Classify atoms based on distance to ML region
|
|
202
|
+
# For residues WITHOUT ML atoms: classify entire residue by minimum distance
|
|
203
|
+
# For residues WITH ML atoms: classify non-ML atoms individually by distance
|
|
204
|
+
movable_mm_indices: List[int] = []
|
|
205
|
+
frozen_indices: List[int] = []
|
|
206
|
+
|
|
207
|
+
for res_key, atom_indices in residue_atoms.items():
|
|
208
|
+
# Check if this residue contains any ML atoms
|
|
209
|
+
has_ml_atoms = any(idx in ml_set for idx in atom_indices)
|
|
210
|
+
|
|
211
|
+
if has_ml_atoms:
|
|
212
|
+
# This residue contains ML atoms
|
|
213
|
+
# Classify non-ML atoms individually by their distance to ML region
|
|
214
|
+
for idx in atom_indices:
|
|
215
|
+
if idx in ml_set:
|
|
216
|
+
# Already an ML atom, skip
|
|
217
|
+
continue
|
|
218
|
+
# Compute distance from this atom to nearest ML atom
|
|
219
|
+
coord = atoms[idx]["coord"]
|
|
220
|
+
dists = np.linalg.norm(ml_coords - coord, axis=1)
|
|
221
|
+
min_dist = np.min(dists)
|
|
222
|
+
|
|
223
|
+
if min_dist <= radius_freeze:
|
|
224
|
+
movable_mm_indices.append(idx)
|
|
225
|
+
else:
|
|
226
|
+
frozen_indices.append(idx)
|
|
227
|
+
else:
|
|
228
|
+
# No ML atoms in this residue
|
|
229
|
+
# Compute minimum distance from any atom in this residue to any ML atom
|
|
230
|
+
residue_coords = np.array([atoms[i]["coord"] for i in atom_indices])
|
|
231
|
+
min_dist = float("inf")
|
|
232
|
+
for coord in residue_coords:
|
|
233
|
+
dists = np.linalg.norm(ml_coords - coord, axis=1)
|
|
234
|
+
min_dist = min(min_dist, np.min(dists))
|
|
235
|
+
|
|
236
|
+
# Assign entire residue to a layer
|
|
237
|
+
if min_dist <= radius_freeze:
|
|
238
|
+
movable_mm_indices.extend(atom_indices)
|
|
239
|
+
else:
|
|
240
|
+
frozen_indices.extend(atom_indices)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"ml_indices": sorted(ml_indices),
|
|
244
|
+
# Compatibility key for existing downstream codepaths.
|
|
245
|
+
"hess_mm_indices": [],
|
|
246
|
+
"movable_mm_indices": sorted(movable_mm_indices),
|
|
247
|
+
"frozen_indices": sorted(frozen_indices),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def write_layered_pdb(
|
|
252
|
+
input_pdb_path: Path,
|
|
253
|
+
output_pdb_path: Path,
|
|
254
|
+
layer_indices: Dict[str, List[int]],
|
|
255
|
+
) -> None:
|
|
256
|
+
"""
|
|
257
|
+
Write a PDB file with B-factors set according to layer assignments.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
input_pdb_path : Path
|
|
262
|
+
Input PDB file
|
|
263
|
+
output_pdb_path : Path
|
|
264
|
+
Output PDB file
|
|
265
|
+
layer_indices : Dict[str, List[int]]
|
|
266
|
+
Dictionary with ml_indices, movable_mm_indices, frozen_indices
|
|
267
|
+
"""
|
|
268
|
+
ml_set = set(layer_indices["ml_indices"])
|
|
269
|
+
movable_mm_set = set(layer_indices["movable_mm_indices"])
|
|
270
|
+
frozen_set = set(layer_indices["frozen_indices"])
|
|
271
|
+
|
|
272
|
+
lines_out = []
|
|
273
|
+
atom_idx = 0
|
|
274
|
+
|
|
275
|
+
with open(input_pdb_path, "r") as f:
|
|
276
|
+
for line in f:
|
|
277
|
+
if line.startswith("MODEL"):
|
|
278
|
+
atom_idx = 0
|
|
279
|
+
lines_out.append(line)
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
if line.startswith(("ATOM ", "HETATM")):
|
|
283
|
+
# Determine B-factor for this atom
|
|
284
|
+
if atom_idx in ml_set:
|
|
285
|
+
bfac = BFACTOR_ML
|
|
286
|
+
elif atom_idx in movable_mm_set:
|
|
287
|
+
bfac = BFACTOR_MOVABLE_MM
|
|
288
|
+
elif atom_idx in frozen_set:
|
|
289
|
+
bfac = BFACTOR_FROZEN
|
|
290
|
+
else:
|
|
291
|
+
# Default: treat as movable MM
|
|
292
|
+
bfac = BFACTOR_MOVABLE_MM
|
|
293
|
+
|
|
294
|
+
# Replace B-factor (columns 61-66)
|
|
295
|
+
if len(line) >= 66:
|
|
296
|
+
new_line = line[:60] + f"{bfac:6.2f}" + line[66:]
|
|
297
|
+
else:
|
|
298
|
+
padded = line.rstrip("\n").ljust(66)
|
|
299
|
+
new_line = padded[:60] + f"{bfac:6.2f}" + "\n"
|
|
300
|
+
lines_out.append(new_line)
|
|
301
|
+
atom_idx += 1
|
|
302
|
+
else:
|
|
303
|
+
lines_out.append(line)
|
|
304
|
+
|
|
305
|
+
with open(output_pdb_path, "w") as f:
|
|
306
|
+
f.writelines(lines_out)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def define_layers(
|
|
310
|
+
input_pdb: Path,
|
|
311
|
+
output_pdb: Path,
|
|
312
|
+
model_pdb: Optional[Path] = None,
|
|
313
|
+
model_indices: Optional[List[int]] = None,
|
|
314
|
+
radius_partial_hessian: float = DEFAULT_RADIUS_PARTIAL_HESSIAN,
|
|
315
|
+
radius_freeze: float = DEFAULT_RADIUS_FREEZE,
|
|
316
|
+
) -> Dict[str, List[int]]:
|
|
317
|
+
"""
|
|
318
|
+
Main function to define 3-layer ML/MM system and write output PDB.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
input_pdb : Path
|
|
323
|
+
Input PDB file with full system
|
|
324
|
+
output_pdb : Path
|
|
325
|
+
Output PDB file with B-factors set to layer values
|
|
326
|
+
model_pdb : Optional[Path]
|
|
327
|
+
PDB file defining ML region atoms
|
|
328
|
+
model_indices : Optional[List[int]]
|
|
329
|
+
Explicit 0-based indices of ML region atoms (takes precedence over model_pdb)
|
|
330
|
+
radius_partial_hessian : float
|
|
331
|
+
Deprecated in 3-layer mode (ignored).
|
|
332
|
+
radius_freeze : float
|
|
333
|
+
Distance cutoff (Å) for movable MM atoms (default: 8.0)
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
Dict[str, List[int]]
|
|
338
|
+
Layer indices dictionary
|
|
339
|
+
"""
|
|
340
|
+
# Parse input PDB
|
|
341
|
+
atoms = _parse_pdb_atoms(input_pdb)
|
|
342
|
+
if not atoms:
|
|
343
|
+
raise ValueError(f"No atoms found in {input_pdb}")
|
|
344
|
+
|
|
345
|
+
# Get ML indices
|
|
346
|
+
if model_indices is not None:
|
|
347
|
+
ml_indices = sorted(set(model_indices))
|
|
348
|
+
elif model_pdb is not None:
|
|
349
|
+
ml_indices = _get_ml_indices_from_model_pdb(atoms, model_pdb)
|
|
350
|
+
else:
|
|
351
|
+
raise ValueError("Either model_pdb or model_indices must be provided")
|
|
352
|
+
|
|
353
|
+
if not ml_indices:
|
|
354
|
+
raise ValueError("No ML region atoms found")
|
|
355
|
+
|
|
356
|
+
# Validate indices
|
|
357
|
+
n_atoms = len(atoms)
|
|
358
|
+
invalid = [i for i in ml_indices if i < 0 or i >= n_atoms]
|
|
359
|
+
if invalid:
|
|
360
|
+
raise ValueError(f"Invalid ML indices (out of range 0-{n_atoms-1}): {invalid}")
|
|
361
|
+
|
|
362
|
+
# Compute layer indices
|
|
363
|
+
layer_indices = compute_layer_indices_by_residue(
|
|
364
|
+
atoms,
|
|
365
|
+
ml_indices,
|
|
366
|
+
radius_partial_hessian,
|
|
367
|
+
radius_freeze,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Write output PDB
|
|
371
|
+
write_layered_pdb(input_pdb, output_pdb, layer_indices)
|
|
372
|
+
|
|
373
|
+
return layer_indices
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# -----------------------------------------------
|
|
377
|
+
# CLI
|
|
378
|
+
# -----------------------------------------------
|
|
379
|
+
|
|
380
|
+
@click.command(
|
|
381
|
+
help="Define 3-layer ML/MM system based on distance from ML region.",
|
|
382
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
383
|
+
)
|
|
384
|
+
@click.option(
|
|
385
|
+
"-i", "--input",
|
|
386
|
+
"input_pdb",
|
|
387
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
388
|
+
required=True,
|
|
389
|
+
help="Input PDB file containing the full system.",
|
|
390
|
+
)
|
|
391
|
+
@click.option(
|
|
392
|
+
"--model-pdb",
|
|
393
|
+
"model_pdb",
|
|
394
|
+
type=click.Path(path_type=Path, exists=True, dir_okay=False),
|
|
395
|
+
default=None,
|
|
396
|
+
help="PDB file defining atoms in the ML region.",
|
|
397
|
+
)
|
|
398
|
+
@click.option(
|
|
399
|
+
"--model-indices",
|
|
400
|
+
"model_indices_str",
|
|
401
|
+
type=str,
|
|
402
|
+
default=None,
|
|
403
|
+
help="Comma-separated atom indices for ML region (e.g., '0,1,2,3' or '1-10,15,20-25'). "
|
|
404
|
+
"Takes precedence over --model-pdb.",
|
|
405
|
+
)
|
|
406
|
+
@click.option(
|
|
407
|
+
"--radius-partial-hessian",
|
|
408
|
+
"radius_partial_hessian",
|
|
409
|
+
type=float,
|
|
410
|
+
default=DEFAULT_RADIUS_PARTIAL_HESSIAN,
|
|
411
|
+
show_default=True,
|
|
412
|
+
help="Deprecated in 3-layer mode (ignored).",
|
|
413
|
+
)
|
|
414
|
+
@click.option(
|
|
415
|
+
"--radius-freeze",
|
|
416
|
+
"radius_freeze",
|
|
417
|
+
type=float,
|
|
418
|
+
default=DEFAULT_RADIUS_FREEZE,
|
|
419
|
+
show_default=True,
|
|
420
|
+
help="Distance cutoff (Å) from ML region for movable MM atoms. "
|
|
421
|
+
"Atoms beyond this distance are frozen.",
|
|
422
|
+
)
|
|
423
|
+
@click.option(
|
|
424
|
+
"-o", "--output",
|
|
425
|
+
"output_pdb",
|
|
426
|
+
type=click.Path(path_type=Path, dir_okay=False),
|
|
427
|
+
default=None,
|
|
428
|
+
help="Output PDB file with B-factors set to layer values. "
|
|
429
|
+
"Defaults to '<input>_layered.pdb'.",
|
|
430
|
+
)
|
|
431
|
+
@click.option(
|
|
432
|
+
"--one-based/--zero-based",
|
|
433
|
+
"one_based",
|
|
434
|
+
default=True,
|
|
435
|
+
show_default=True,
|
|
436
|
+
help="Interpret --model-indices as 1-based (default) or 0-based.",
|
|
437
|
+
)
|
|
438
|
+
def cli(
|
|
439
|
+
input_pdb: Path,
|
|
440
|
+
model_pdb: Optional[Path],
|
|
441
|
+
model_indices_str: Optional[str],
|
|
442
|
+
radius_partial_hessian: float,
|
|
443
|
+
radius_freeze: float,
|
|
444
|
+
output_pdb: Optional[Path],
|
|
445
|
+
one_based: bool,
|
|
446
|
+
) -> None:
|
|
447
|
+
"""Define 3-layer ML/MM system based on distance from ML region."""
|
|
448
|
+
|
|
449
|
+
# Validate input
|
|
450
|
+
if model_pdb is None and model_indices_str is None:
|
|
451
|
+
click.echo("ERROR: Either --model-pdb or --model-indices must be provided.", err=True)
|
|
452
|
+
sys.exit(1)
|
|
453
|
+
|
|
454
|
+
# Validate radii
|
|
455
|
+
if radius_partial_hessian < 0:
|
|
456
|
+
click.echo("ERROR: --radius-partial-hessian must be non-negative.", err=True)
|
|
457
|
+
sys.exit(1)
|
|
458
|
+
if radius_freeze < 0:
|
|
459
|
+
click.echo("ERROR: --radius-freeze must be non-negative.", err=True)
|
|
460
|
+
sys.exit(1)
|
|
461
|
+
|
|
462
|
+
# Parse model indices if provided
|
|
463
|
+
model_indices = None
|
|
464
|
+
if model_indices_str is not None:
|
|
465
|
+
try:
|
|
466
|
+
model_indices = _parse_indices_string(model_indices_str, one_based)
|
|
467
|
+
except click.BadParameter as e:
|
|
468
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
469
|
+
sys.exit(1)
|
|
470
|
+
|
|
471
|
+
# Default output path
|
|
472
|
+
if output_pdb is None:
|
|
473
|
+
output_pdb = input_pdb.parent / f"{input_pdb.stem}_layered.pdb"
|
|
474
|
+
|
|
475
|
+
# Echo configuration
|
|
476
|
+
click.echo("\n=== Define Layer Configuration ===\n")
|
|
477
|
+
click.echo(f"Input PDB: {input_pdb}")
|
|
478
|
+
if model_pdb is not None:
|
|
479
|
+
click.echo(f"Model PDB: {model_pdb}")
|
|
480
|
+
if model_indices is not None:
|
|
481
|
+
click.echo(f"Model indices (0-based): {len(model_indices)} atoms")
|
|
482
|
+
click.echo(f"Radius (partial Hessian): {radius_partial_hessian} Å")
|
|
483
|
+
click.echo(f"Radius (freeze): {radius_freeze} Å")
|
|
484
|
+
if abs(radius_partial_hessian) > 1.0e-12:
|
|
485
|
+
click.echo(
|
|
486
|
+
"[warn] --radius-partial-hessian is ignored in 3-layer mode.",
|
|
487
|
+
err=True,
|
|
488
|
+
)
|
|
489
|
+
click.echo(f"Output PDB: {output_pdb}")
|
|
490
|
+
click.echo()
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
layer_indices = define_layers(
|
|
494
|
+
input_pdb=input_pdb,
|
|
495
|
+
output_pdb=output_pdb,
|
|
496
|
+
model_pdb=model_pdb,
|
|
497
|
+
model_indices=model_indices,
|
|
498
|
+
radius_partial_hessian=radius_partial_hessian,
|
|
499
|
+
radius_freeze=radius_freeze,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Print summary
|
|
503
|
+
click.echo("=== Layer Summary ===\n")
|
|
504
|
+
click.echo(f"Layer 1 (ML, B={BFACTOR_ML:.0f}): {len(layer_indices['ml_indices']):6d} atoms")
|
|
505
|
+
click.echo(f"Layer 2 (Movable MM, B={BFACTOR_MOVABLE_MM:.0f}): {len(layer_indices['movable_mm_indices']):6d} atoms")
|
|
506
|
+
click.echo(f"Layer 3 (Frozen MM, B={BFACTOR_FROZEN:.0f}): {len(layer_indices['frozen_indices']):6d} atoms")
|
|
507
|
+
click.echo()
|
|
508
|
+
total = (
|
|
509
|
+
len(layer_indices['ml_indices']) +
|
|
510
|
+
len(layer_indices['movable_mm_indices']) +
|
|
511
|
+
len(layer_indices['frozen_indices'])
|
|
512
|
+
)
|
|
513
|
+
click.echo(f"Total atoms: {total}")
|
|
514
|
+
click.echo()
|
|
515
|
+
click.echo(f"[output] Wrote '{output_pdb}'")
|
|
516
|
+
|
|
517
|
+
except ValueError as e:
|
|
518
|
+
click.echo(f"ERROR: {e}", err=True)
|
|
519
|
+
sys.exit(1)
|
|
520
|
+
except Exception as e:
|
|
521
|
+
click.echo(f"ERROR: Unexpected error: {e}", err=True)
|
|
522
|
+
sys.exit(1)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
if __name__ == "__main__":
|
|
526
|
+
cli()
|