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.
Files changed (372) hide show
  1. hessian_ff/__init__.py +50 -0
  2. hessian_ff/analytical_hessian.py +609 -0
  3. hessian_ff/constants.py +46 -0
  4. hessian_ff/forcefield.py +339 -0
  5. hessian_ff/loaders.py +608 -0
  6. hessian_ff/native/Makefile +8 -0
  7. hessian_ff/native/__init__.py +28 -0
  8. hessian_ff/native/analytical_hessian.py +88 -0
  9. hessian_ff/native/analytical_hessian_ext.cpp +258 -0
  10. hessian_ff/native/bonded.py +82 -0
  11. hessian_ff/native/bonded_ext.cpp +640 -0
  12. hessian_ff/native/loader.py +349 -0
  13. hessian_ff/native/nonbonded.py +118 -0
  14. hessian_ff/native/nonbonded_ext.cpp +1150 -0
  15. hessian_ff/prmtop_parmed.py +23 -0
  16. hessian_ff/system.py +107 -0
  17. hessian_ff/terms/__init__.py +14 -0
  18. hessian_ff/terms/angle.py +73 -0
  19. hessian_ff/terms/bond.py +44 -0
  20. hessian_ff/terms/cmap.py +406 -0
  21. hessian_ff/terms/dihedral.py +141 -0
  22. hessian_ff/terms/nonbonded.py +209 -0
  23. hessian_ff/tests/__init__.py +0 -0
  24. hessian_ff/tests/conftest.py +75 -0
  25. hessian_ff/tests/data/small/complex.parm7 +1346 -0
  26. hessian_ff/tests/data/small/complex.pdb +125 -0
  27. hessian_ff/tests/data/small/complex.rst7 +63 -0
  28. hessian_ff/tests/test_coords_input.py +44 -0
  29. hessian_ff/tests/test_energy_force.py +49 -0
  30. hessian_ff/tests/test_hessian.py +137 -0
  31. hessian_ff/tests/test_smoke.py +18 -0
  32. hessian_ff/tests/test_validation.py +40 -0
  33. hessian_ff/workflows.py +889 -0
  34. mlmm/__init__.py +36 -0
  35. mlmm/__main__.py +7 -0
  36. mlmm/_version.py +34 -0
  37. mlmm/add_elem_info.py +374 -0
  38. mlmm/advanced_help.py +91 -0
  39. mlmm/align_freeze_atoms.py +601 -0
  40. mlmm/all.py +3535 -0
  41. mlmm/bond_changes.py +231 -0
  42. mlmm/bool_compat.py +223 -0
  43. mlmm/cli.py +574 -0
  44. mlmm/cli_utils.py +166 -0
  45. mlmm/default_group.py +337 -0
  46. mlmm/defaults.py +467 -0
  47. mlmm/define_layer.py +526 -0
  48. mlmm/dft.py +1041 -0
  49. mlmm/energy_diagram.py +253 -0
  50. mlmm/extract.py +2213 -0
  51. mlmm/fix_altloc.py +464 -0
  52. mlmm/freq.py +1406 -0
  53. mlmm/harmonic_constraints.py +140 -0
  54. mlmm/hessian_cache.py +44 -0
  55. mlmm/hessian_calc.py +174 -0
  56. mlmm/irc.py +638 -0
  57. mlmm/mlmm_calc.py +2262 -0
  58. mlmm/mm_parm.py +945 -0
  59. mlmm/oniom_export.py +1983 -0
  60. mlmm/oniom_import.py +457 -0
  61. mlmm/opt.py +1742 -0
  62. mlmm/path_opt.py +1353 -0
  63. mlmm/path_search.py +2299 -0
  64. mlmm/preflight.py +88 -0
  65. mlmm/py.typed +1 -0
  66. mlmm/pysis_runner.py +45 -0
  67. mlmm/scan.py +1047 -0
  68. mlmm/scan2d.py +1226 -0
  69. mlmm/scan3d.py +1265 -0
  70. mlmm/scan_common.py +184 -0
  71. mlmm/summary_log.py +736 -0
  72. mlmm/trj2fig.py +448 -0
  73. mlmm/tsopt.py +2871 -0
  74. mlmm/utils.py +2309 -0
  75. mlmm/xtb_embedcharge_correction.py +475 -0
  76. mlmm_toolkit-0.2.2.dev0.dist-info/METADATA +1159 -0
  77. mlmm_toolkit-0.2.2.dev0.dist-info/RECORD +372 -0
  78. mlmm_toolkit-0.2.2.dev0.dist-info/WHEEL +5 -0
  79. mlmm_toolkit-0.2.2.dev0.dist-info/entry_points.txt +2 -0
  80. mlmm_toolkit-0.2.2.dev0.dist-info/licenses/LICENSE +674 -0
  81. mlmm_toolkit-0.2.2.dev0.dist-info/top_level.txt +4 -0
  82. pysisyphus/Geometry.py +1667 -0
  83. pysisyphus/LICENSE +674 -0
  84. pysisyphus/TableFormatter.py +63 -0
  85. pysisyphus/TablePrinter.py +74 -0
  86. pysisyphus/__init__.py +12 -0
  87. pysisyphus/calculators/AFIR.py +452 -0
  88. pysisyphus/calculators/AnaPot.py +20 -0
  89. pysisyphus/calculators/AnaPot2.py +48 -0
  90. pysisyphus/calculators/AnaPot3.py +12 -0
  91. pysisyphus/calculators/AnaPot4.py +20 -0
  92. pysisyphus/calculators/AnaPotBase.py +337 -0
  93. pysisyphus/calculators/AnaPotCBM.py +25 -0
  94. pysisyphus/calculators/AtomAtomTransTorque.py +154 -0
  95. pysisyphus/calculators/CFOUR.py +250 -0
  96. pysisyphus/calculators/Calculator.py +844 -0
  97. pysisyphus/calculators/CerjanMiller.py +24 -0
  98. pysisyphus/calculators/Composite.py +123 -0
  99. pysisyphus/calculators/ConicalIntersection.py +171 -0
  100. pysisyphus/calculators/DFTBp.py +430 -0
  101. pysisyphus/calculators/DFTD3.py +66 -0
  102. pysisyphus/calculators/DFTD4.py +84 -0
  103. pysisyphus/calculators/Dalton.py +61 -0
  104. pysisyphus/calculators/Dimer.py +681 -0
  105. pysisyphus/calculators/Dummy.py +20 -0
  106. pysisyphus/calculators/EGO.py +76 -0
  107. pysisyphus/calculators/EnergyMin.py +224 -0
  108. pysisyphus/calculators/ExternalPotential.py +264 -0
  109. pysisyphus/calculators/FakeASE.py +35 -0
  110. pysisyphus/calculators/FourWellAnaPot.py +28 -0
  111. pysisyphus/calculators/FreeEndNEBPot.py +39 -0
  112. pysisyphus/calculators/Gaussian09.py +18 -0
  113. pysisyphus/calculators/Gaussian16.py +726 -0
  114. pysisyphus/calculators/HardSphere.py +159 -0
  115. pysisyphus/calculators/IDPPCalculator.py +49 -0
  116. pysisyphus/calculators/IPIClient.py +133 -0
  117. pysisyphus/calculators/IPIServer.py +234 -0
  118. pysisyphus/calculators/LEPSBase.py +24 -0
  119. pysisyphus/calculators/LEPSExpr.py +139 -0
  120. pysisyphus/calculators/LennardJones.py +80 -0
  121. pysisyphus/calculators/MOPAC.py +219 -0
  122. pysisyphus/calculators/MullerBrownSympyPot.py +51 -0
  123. pysisyphus/calculators/MultiCalc.py +85 -0
  124. pysisyphus/calculators/NFK.py +45 -0
  125. pysisyphus/calculators/OBabel.py +87 -0
  126. pysisyphus/calculators/ONIOMv2.py +1129 -0
  127. pysisyphus/calculators/ORCA.py +893 -0
  128. pysisyphus/calculators/ORCA5.py +6 -0
  129. pysisyphus/calculators/OpenMM.py +88 -0
  130. pysisyphus/calculators/OpenMolcas.py +281 -0
  131. pysisyphus/calculators/OverlapCalculator.py +908 -0
  132. pysisyphus/calculators/Psi4.py +218 -0
  133. pysisyphus/calculators/PyPsi4.py +37 -0
  134. pysisyphus/calculators/PySCF.py +341 -0
  135. pysisyphus/calculators/PyXTB.py +73 -0
  136. pysisyphus/calculators/QCEngine.py +106 -0
  137. pysisyphus/calculators/Rastrigin.py +22 -0
  138. pysisyphus/calculators/Remote.py +76 -0
  139. pysisyphus/calculators/Rosenbrock.py +15 -0
  140. pysisyphus/calculators/SocketCalc.py +97 -0
  141. pysisyphus/calculators/TIP3P.py +111 -0
  142. pysisyphus/calculators/TransTorque.py +161 -0
  143. pysisyphus/calculators/Turbomole.py +965 -0
  144. pysisyphus/calculators/VRIPot.py +37 -0
  145. pysisyphus/calculators/WFOWrapper.py +333 -0
  146. pysisyphus/calculators/WFOWrapper2.py +341 -0
  147. pysisyphus/calculators/XTB.py +418 -0
  148. pysisyphus/calculators/__init__.py +81 -0
  149. pysisyphus/calculators/cosmo_data.py +139 -0
  150. pysisyphus/calculators/parser.py +150 -0
  151. pysisyphus/color.py +19 -0
  152. pysisyphus/config.py +133 -0
  153. pysisyphus/constants.py +65 -0
  154. pysisyphus/cos/AdaptiveNEB.py +230 -0
  155. pysisyphus/cos/ChainOfStates.py +725 -0
  156. pysisyphus/cos/FreeEndNEB.py +25 -0
  157. pysisyphus/cos/FreezingString.py +103 -0
  158. pysisyphus/cos/GrowingChainOfStates.py +71 -0
  159. pysisyphus/cos/GrowingNT.py +309 -0
  160. pysisyphus/cos/GrowingString.py +508 -0
  161. pysisyphus/cos/NEB.py +189 -0
  162. pysisyphus/cos/SimpleZTS.py +64 -0
  163. pysisyphus/cos/__init__.py +22 -0
  164. pysisyphus/cos/stiffness.py +199 -0
  165. pysisyphus/drivers/__init__.py +17 -0
  166. pysisyphus/drivers/afir.py +855 -0
  167. pysisyphus/drivers/barriers.py +271 -0
  168. pysisyphus/drivers/birkholz.py +138 -0
  169. pysisyphus/drivers/cluster.py +318 -0
  170. pysisyphus/drivers/diabatization.py +133 -0
  171. pysisyphus/drivers/merge.py +368 -0
  172. pysisyphus/drivers/merge_mol2.py +322 -0
  173. pysisyphus/drivers/opt.py +375 -0
  174. pysisyphus/drivers/perf.py +91 -0
  175. pysisyphus/drivers/pka.py +52 -0
  176. pysisyphus/drivers/precon_pos_rot.py +669 -0
  177. pysisyphus/drivers/rates.py +480 -0
  178. pysisyphus/drivers/replace.py +219 -0
  179. pysisyphus/drivers/scan.py +212 -0
  180. pysisyphus/drivers/spectrum.py +166 -0
  181. pysisyphus/drivers/thermo.py +31 -0
  182. pysisyphus/dynamics/Gaussian.py +103 -0
  183. pysisyphus/dynamics/__init__.py +20 -0
  184. pysisyphus/dynamics/colvars.py +136 -0
  185. pysisyphus/dynamics/driver.py +297 -0
  186. pysisyphus/dynamics/helpers.py +256 -0
  187. pysisyphus/dynamics/lincs.py +105 -0
  188. pysisyphus/dynamics/mdp.py +364 -0
  189. pysisyphus/dynamics/rattle.py +121 -0
  190. pysisyphus/dynamics/thermostats.py +128 -0
  191. pysisyphus/dynamics/wigner.py +266 -0
  192. pysisyphus/elem_data.py +3473 -0
  193. pysisyphus/exceptions.py +2 -0
  194. pysisyphus/filtertrj.py +69 -0
  195. pysisyphus/helpers.py +623 -0
  196. pysisyphus/helpers_pure.py +649 -0
  197. pysisyphus/init_logging.py +50 -0
  198. pysisyphus/intcoords/Bend.py +69 -0
  199. pysisyphus/intcoords/Bend2.py +25 -0
  200. pysisyphus/intcoords/BondedFragment.py +32 -0
  201. pysisyphus/intcoords/Cartesian.py +41 -0
  202. pysisyphus/intcoords/CartesianCoords.py +140 -0
  203. pysisyphus/intcoords/Coords.py +56 -0
  204. pysisyphus/intcoords/DLC.py +197 -0
  205. pysisyphus/intcoords/DistanceFunction.py +34 -0
  206. pysisyphus/intcoords/DummyImproper.py +70 -0
  207. pysisyphus/intcoords/DummyTorsion.py +72 -0
  208. pysisyphus/intcoords/LinearBend.py +105 -0
  209. pysisyphus/intcoords/LinearDisplacement.py +80 -0
  210. pysisyphus/intcoords/OutOfPlane.py +59 -0
  211. pysisyphus/intcoords/PrimTypes.py +286 -0
  212. pysisyphus/intcoords/Primitive.py +137 -0
  213. pysisyphus/intcoords/RedundantCoords.py +659 -0
  214. pysisyphus/intcoords/RobustTorsion.py +59 -0
  215. pysisyphus/intcoords/Rotation.py +147 -0
  216. pysisyphus/intcoords/Stretch.py +31 -0
  217. pysisyphus/intcoords/Torsion.py +101 -0
  218. pysisyphus/intcoords/Torsion2.py +25 -0
  219. pysisyphus/intcoords/Translation.py +45 -0
  220. pysisyphus/intcoords/__init__.py +61 -0
  221. pysisyphus/intcoords/augment_bonds.py +126 -0
  222. pysisyphus/intcoords/derivatives.py +10512 -0
  223. pysisyphus/intcoords/eval.py +80 -0
  224. pysisyphus/intcoords/exceptions.py +37 -0
  225. pysisyphus/intcoords/findiffs.py +48 -0
  226. pysisyphus/intcoords/generate_derivatives.py +414 -0
  227. pysisyphus/intcoords/helpers.py +235 -0
  228. pysisyphus/intcoords/logging_conf.py +10 -0
  229. pysisyphus/intcoords/mp_derivatives.py +10836 -0
  230. pysisyphus/intcoords/setup.py +962 -0
  231. pysisyphus/intcoords/setup_fast.py +176 -0
  232. pysisyphus/intcoords/update.py +272 -0
  233. pysisyphus/intcoords/valid.py +89 -0
  234. pysisyphus/interpolate/Geodesic.py +93 -0
  235. pysisyphus/interpolate/IDPP.py +55 -0
  236. pysisyphus/interpolate/Interpolator.py +116 -0
  237. pysisyphus/interpolate/LST.py +70 -0
  238. pysisyphus/interpolate/Redund.py +152 -0
  239. pysisyphus/interpolate/__init__.py +9 -0
  240. pysisyphus/interpolate/helpers.py +34 -0
  241. pysisyphus/io/__init__.py +22 -0
  242. pysisyphus/io/aomix.py +178 -0
  243. pysisyphus/io/cjson.py +24 -0
  244. pysisyphus/io/crd.py +101 -0
  245. pysisyphus/io/cube.py +220 -0
  246. pysisyphus/io/fchk.py +184 -0
  247. pysisyphus/io/hdf5.py +49 -0
  248. pysisyphus/io/hessian.py +72 -0
  249. pysisyphus/io/mol2.py +146 -0
  250. pysisyphus/io/molden.py +293 -0
  251. pysisyphus/io/orca.py +189 -0
  252. pysisyphus/io/pdb.py +269 -0
  253. pysisyphus/io/psf.py +79 -0
  254. pysisyphus/io/pubchem.py +31 -0
  255. pysisyphus/io/qcschema.py +34 -0
  256. pysisyphus/io/sdf.py +29 -0
  257. pysisyphus/io/xyz.py +61 -0
  258. pysisyphus/io/zmat.py +175 -0
  259. pysisyphus/irc/DWI.py +108 -0
  260. pysisyphus/irc/DampedVelocityVerlet.py +134 -0
  261. pysisyphus/irc/Euler.py +22 -0
  262. pysisyphus/irc/EulerPC.py +345 -0
  263. pysisyphus/irc/GonzalezSchlegel.py +187 -0
  264. pysisyphus/irc/IMKMod.py +164 -0
  265. pysisyphus/irc/IRC.py +878 -0
  266. pysisyphus/irc/IRCDummy.py +10 -0
  267. pysisyphus/irc/Instanton.py +307 -0
  268. pysisyphus/irc/LQA.py +53 -0
  269. pysisyphus/irc/ModeKill.py +136 -0
  270. pysisyphus/irc/ParamPlot.py +53 -0
  271. pysisyphus/irc/RK4.py +36 -0
  272. pysisyphus/irc/__init__.py +31 -0
  273. pysisyphus/irc/initial_displ.py +219 -0
  274. pysisyphus/linalg.py +411 -0
  275. pysisyphus/line_searches/Backtracking.py +88 -0
  276. pysisyphus/line_searches/HagerZhang.py +184 -0
  277. pysisyphus/line_searches/LineSearch.py +232 -0
  278. pysisyphus/line_searches/StrongWolfe.py +108 -0
  279. pysisyphus/line_searches/__init__.py +9 -0
  280. pysisyphus/line_searches/interpol.py +15 -0
  281. pysisyphus/modefollow/NormalMode.py +40 -0
  282. pysisyphus/modefollow/__init__.py +10 -0
  283. pysisyphus/modefollow/davidson.py +199 -0
  284. pysisyphus/modefollow/lanczos.py +95 -0
  285. pysisyphus/optimizers/BFGS.py +99 -0
  286. pysisyphus/optimizers/BacktrackingOptimizer.py +113 -0
  287. pysisyphus/optimizers/ConjugateGradient.py +98 -0
  288. pysisyphus/optimizers/CubicNewton.py +75 -0
  289. pysisyphus/optimizers/FIRE.py +113 -0
  290. pysisyphus/optimizers/HessianOptimizer.py +1176 -0
  291. pysisyphus/optimizers/LBFGS.py +228 -0
  292. pysisyphus/optimizers/LayerOpt.py +411 -0
  293. pysisyphus/optimizers/MicroOptimizer.py +169 -0
  294. pysisyphus/optimizers/NCOptimizer.py +90 -0
  295. pysisyphus/optimizers/Optimizer.py +1084 -0
  296. pysisyphus/optimizers/PreconLBFGS.py +260 -0
  297. pysisyphus/optimizers/PreconSteepestDescent.py +7 -0
  298. pysisyphus/optimizers/QuickMin.py +74 -0
  299. pysisyphus/optimizers/RFOptimizer.py +181 -0
  300. pysisyphus/optimizers/RSA.py +99 -0
  301. pysisyphus/optimizers/StabilizedQNMethod.py +248 -0
  302. pysisyphus/optimizers/SteepestDescent.py +23 -0
  303. pysisyphus/optimizers/StringOptimizer.py +173 -0
  304. pysisyphus/optimizers/__init__.py +41 -0
  305. pysisyphus/optimizers/closures.py +301 -0
  306. pysisyphus/optimizers/cls_map.py +58 -0
  307. pysisyphus/optimizers/exceptions.py +6 -0
  308. pysisyphus/optimizers/gdiis.py +280 -0
  309. pysisyphus/optimizers/guess_hessians.py +311 -0
  310. pysisyphus/optimizers/hessian_updates.py +355 -0
  311. pysisyphus/optimizers/poly_fit.py +285 -0
  312. pysisyphus/optimizers/precon.py +153 -0
  313. pysisyphus/optimizers/restrict_step.py +24 -0
  314. pysisyphus/pack.py +172 -0
  315. pysisyphus/peakdetect.py +948 -0
  316. pysisyphus/plot.py +1031 -0
  317. pysisyphus/run.py +2106 -0
  318. pysisyphus/socket_helper.py +74 -0
  319. pysisyphus/stocastic/FragmentKick.py +132 -0
  320. pysisyphus/stocastic/Kick.py +81 -0
  321. pysisyphus/stocastic/Pipeline.py +303 -0
  322. pysisyphus/stocastic/__init__.py +21 -0
  323. pysisyphus/stocastic/align.py +127 -0
  324. pysisyphus/testing.py +96 -0
  325. pysisyphus/thermo.py +156 -0
  326. pysisyphus/trj.py +824 -0
  327. pysisyphus/tsoptimizers/RSIRFOptimizer.py +56 -0
  328. pysisyphus/tsoptimizers/RSPRFOptimizer.py +182 -0
  329. pysisyphus/tsoptimizers/TRIM.py +59 -0
  330. pysisyphus/tsoptimizers/TSHessianOptimizer.py +463 -0
  331. pysisyphus/tsoptimizers/__init__.py +23 -0
  332. pysisyphus/wavefunction/Basis.py +239 -0
  333. pysisyphus/wavefunction/DIIS.py +76 -0
  334. pysisyphus/wavefunction/__init__.py +25 -0
  335. pysisyphus/wavefunction/build_ext.py +42 -0
  336. pysisyphus/wavefunction/cart2sph.py +190 -0
  337. pysisyphus/wavefunction/diabatization.py +304 -0
  338. pysisyphus/wavefunction/excited_states.py +435 -0
  339. pysisyphus/wavefunction/gen_ints.py +1811 -0
  340. pysisyphus/wavefunction/helpers.py +104 -0
  341. pysisyphus/wavefunction/ints/__init__.py +0 -0
  342. pysisyphus/wavefunction/ints/boys.py +193 -0
  343. pysisyphus/wavefunction/ints/boys_table_N_64_xasym_27.1_step_0.01.npy +0 -0
  344. pysisyphus/wavefunction/ints/cart_gto3d.py +176 -0
  345. pysisyphus/wavefunction/ints/coulomb3d.py +25928 -0
  346. pysisyphus/wavefunction/ints/diag_quadrupole3d.py +10036 -0
  347. pysisyphus/wavefunction/ints/dipole3d.py +8762 -0
  348. pysisyphus/wavefunction/ints/int2c2e3d.py +7198 -0
  349. pysisyphus/wavefunction/ints/int3c2e3d_sph.py +65040 -0
  350. pysisyphus/wavefunction/ints/kinetic3d.py +8240 -0
  351. pysisyphus/wavefunction/ints/ovlp3d.py +3777 -0
  352. pysisyphus/wavefunction/ints/quadrupole3d.py +15054 -0
  353. pysisyphus/wavefunction/ints/self_ovlp3d.py +198 -0
  354. pysisyphus/wavefunction/localization.py +458 -0
  355. pysisyphus/wavefunction/multipole.py +159 -0
  356. pysisyphus/wavefunction/normalization.py +36 -0
  357. pysisyphus/wavefunction/pop_analysis.py +134 -0
  358. pysisyphus/wavefunction/shells.py +1171 -0
  359. pysisyphus/wavefunction/wavefunction.py +504 -0
  360. pysisyphus/wrapper/__init__.py +11 -0
  361. pysisyphus/wrapper/exceptions.py +2 -0
  362. pysisyphus/wrapper/jmol.py +120 -0
  363. pysisyphus/wrapper/mwfn.py +169 -0
  364. pysisyphus/wrapper/packmol.py +71 -0
  365. pysisyphus/xyzloader.py +168 -0
  366. pysisyphus/yaml_mods.py +45 -0
  367. thermoanalysis/LICENSE +674 -0
  368. thermoanalysis/QCData.py +244 -0
  369. thermoanalysis/__init__.py +0 -0
  370. thermoanalysis/config.py +3 -0
  371. thermoanalysis/constants.py +20 -0
  372. 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()