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
@@ -0,0 +1,368 @@
1
+ import argparse
2
+ import sys
3
+ import warnings
4
+
5
+ import numpy as np
6
+
7
+ try:
8
+ from openbabel import pybel
9
+
10
+ HAS_OPENBABEL = True
11
+ except ModuleNotFoundError:
12
+ HAS_OPENBABEL = False
13
+
14
+
15
+ from pysisyphus.calculators import (
16
+ Composite,
17
+ HardSphere,
18
+ PWHardSphere,
19
+ TransTorque,
20
+ )
21
+ from pysisyphus.calculators.OBabel import OBabel
22
+ from pysisyphus.drivers.precon_pos_rot import (
23
+ center_fragments,
24
+ form_A,
25
+ get_steps_to_active_atom_mean,
26
+ get_which_frag,
27
+ rotate_inplace,
28
+ SteepestDescent,
29
+ )
30
+ from pysisyphus.Geometry import Geometry
31
+ from pysisyphus.helpers import align_coords, geom_loader
32
+ from pysisyphus.io import geom_to_crd_str
33
+ from pysisyphus.linalg import get_rot_mat_for_coords
34
+ from pysisyphus.optimizers.LBFGS import LBFGS
35
+ from pysisyphus.xyzloader import coords_to_trj
36
+
37
+
38
+ def merge_geoms(geom1, geom2, geom1_del=None, geom2_del=None, make_bonds=None):
39
+ """Merge geom1 and geom2 while keeping the original coordinates.
40
+
41
+ Supports deleting certain atoms.
42
+ """
43
+
44
+ if geom1_del is None:
45
+ geom1_del = list()
46
+ if geom2_del is None:
47
+ geom2_del = list()
48
+
49
+ geom1_del = np.array(geom1_del)
50
+ geom2_del = np.array(geom2_del)
51
+ # Update indices of atoms to be deleted in geom2 by the number of atoms in geom1,
52
+ # as geom1 and geom2 will be merged.
53
+ # If we want to delete atom 0 and atom 1 in geom2 and geom1 comprises 10 atoms,
54
+ # then the updated indices in geom2 will be 0 + 10 = 10 and 1 + 10 = 11 in the
55
+ # merged geometry.
56
+ atom_num1 = len(geom1.atoms)
57
+ geom2_del += atom_num1
58
+
59
+ ndel1 = len(geom1_del)
60
+
61
+ if make_bonds is not None:
62
+ make_bonds = np.array(make_bonds, dtype=int)
63
+ geom1_bond_inds, geom2_bond_inds = make_bonds.T
64
+ geom2_bond_inds += atom_num1
65
+ # Correct original bond indices given in make_bonds
66
+ #
67
+ # Determe how many atoms to be deleted lie below the atom(s) that are
68
+ # used to form new bonds.
69
+ corr1 = (geom1_del < geom1_bond_inds[:, None]).sum(axis=1)
70
+ geom1_bond_inds -= corr1
71
+ # Correct bond indices in geom2 by the number of deleted atoms in geom1.
72
+ geom2_bond_inds -= ndel1
73
+ corr2 = (geom2_del < geom2_bond_inds[:, None]).sum(axis=1)
74
+ geom2_bond_inds -= corr2
75
+ make_bonds_cor = np.stack((geom1_bond_inds, geom2_bond_inds), axis=1)
76
+ else:
77
+ make_bonds_cor = None
78
+
79
+ union = geom1 + geom2
80
+ union = union.del_atoms(list(geom1_del) + list(geom2_del))
81
+
82
+ # Set appropriate fragments
83
+ atom_num1 -= len(geom1_del)
84
+ frag1 = np.arange(atom_num1)
85
+ lig_atoms = geom2.atoms
86
+ frag2 = np.arange(atom_num1, atom_num1 + len(lig_atoms) - len(geom2_del))
87
+ union.fragments = {"geom1": frag1.tolist(), "geom2": frag2.tolist()}
88
+
89
+ return union, make_bonds_cor
90
+
91
+
92
+ def hardsphere_merge(geom1, geom2):
93
+ union = geom1 + geom2
94
+
95
+ atom_num = len(geom1.atoms)
96
+ # Set up lists containing the atom indices for the two fragments
97
+ frag_lists = [
98
+ [i for i, _ in enumerate(geom1.atoms)],
99
+ [atom_num + i for i, _ in enumerate(geom2.atoms)],
100
+ ]
101
+
102
+ def get_hs(kappa=1.0):
103
+ return HardSphere(
104
+ union,
105
+ frag_lists,
106
+ permutations=True,
107
+ kappa=kappa,
108
+ )
109
+
110
+ union.set_calculator(get_hs(1.0))
111
+ opt_kwargs = {
112
+ "max_cycles": 1000,
113
+ "max_step": 0.5,
114
+ "rms_force": 0.0005,
115
+ }
116
+ opt = SteepestDescent(union, **opt_kwargs)
117
+ opt.run()
118
+ return union
119
+
120
+
121
+ def prepare_merge(geom1, bond_diff, geom2=None, del1=None, del2=None, dump=False):
122
+ bond_diff = np.array(bond_diff, dtype=int)
123
+ if del1 is not None:
124
+ geom1 = geom1.del_atoms(del1)
125
+
126
+ if geom2:
127
+ if del2 is not None:
128
+ geom2 = geom2.del_atoms(del2)
129
+ union = geom1 + geom2
130
+ atom_num = len(geom1.atoms)
131
+ # Set up lists containing the atom indices for the two fragments
132
+ frag_lists = [
133
+ [i for i, _ in enumerate(geom1.atoms)],
134
+ [atom_num + i for i, _ in enumerate(geom2.atoms)],
135
+ ]
136
+ fragments = {"geom1": frag_lists[0], "geom2": frag_lists[1]}
137
+ union.fragments = fragments
138
+ else:
139
+ union = geom1
140
+ frag_lists = [union.fragments["geom1"], union.fragments["geom2"]]
141
+
142
+ backup = list()
143
+
144
+ def keep(comment):
145
+ backup.append((union.cart_coords.copy(), comment))
146
+
147
+ keep("Initial union")
148
+
149
+ which_frag = get_which_frag(frag_lists)
150
+ AR = form_A(frag_lists, which_frag, bond_diff)
151
+
152
+ # Center fragments at their geometric average
153
+ center_fragments(frag_lists, union)
154
+ keep("Centered fragments")
155
+
156
+ # Translate centroid of active atoms to origin
157
+ alphas = get_steps_to_active_atom_mean(frag_lists, frag_lists, AR, union.coords3d)
158
+ for frag, alpha in zip(frag_lists, alphas):
159
+ union.coords3d[frag] += alpha
160
+ keep("Shifted centroids to active atoms")
161
+
162
+ def get_hs(kappa=1.0, **kwargs):
163
+ return HardSphere(
164
+ union,
165
+ frag_lists,
166
+ permutations=True,
167
+ kappa=kappa,
168
+ **kwargs,
169
+ )
170
+
171
+ union.set_calculator(get_hs(1.0))
172
+ opt_kwargs = {
173
+ "max_cycles": 1000,
174
+ "max_step": 0.5,
175
+ "rms_force": 0.0005,
176
+ }
177
+ opt = SteepestDescent(union, **opt_kwargs)
178
+ opt.run()
179
+ keep("Initial hardsphere optimization")
180
+
181
+ # Corresponds to A.3 S_3 initial orientation of molecules in Habershon paper
182
+ rotate_inplace(frag_lists, union, bond_diff)
183
+ keep("Rotated after inital hardsphere optimization")
184
+
185
+ sub_frags = [bond_diff[:, 0].tolist(), bond_diff[:, 1].tolist()]
186
+ keys_calcs = {
187
+ "hs": get_hs(100.0, radii_offset=3.0),
188
+ "tt": TransTorque(frag_lists, frag_lists, AR, AR, kappa=2, do_trans=True),
189
+ "pwhs": PWHardSphere(union, frag_lists, sub_frags=sub_frags, kappa=10.0),
190
+ }
191
+ comp = Composite("hs + tt + pwhs", keys_calcs, remove_translation=True)
192
+ union.set_calculator(comp)
193
+
194
+ max_cycles = 15_000
195
+ max_dist = 50
196
+ max_step = max_dist / max_cycles
197
+ opt_kwargs = {
198
+ "max_cycles": max_cycles,
199
+ "max_step": max_step,
200
+ "rms_force": 0.1,
201
+ # "dump": True,
202
+ }
203
+ opt = SteepestDescent(union, **opt_kwargs)
204
+ opt.run()
205
+ keep("Hardsphere + TransTorque optimization")
206
+
207
+ if dump:
208
+ kept_coords, kept_comments = zip(*backup)
209
+ align_coords(kept_coords)
210
+ fn = "merge_dump_trj.xyz"
211
+ coords_to_trj(fn, union.atoms, kept_coords, comments=kept_comments)
212
+
213
+ return union
214
+
215
+
216
+ def merge_opt(union, bond_diff, ff="mmff94"):
217
+ """Fragment merging along given bond by forcefield optimization."""
218
+
219
+ geom1 = union.get_fragments("geom1")
220
+ freeze = list(range(len(geom1.atoms)))
221
+
222
+ # Create pybel.Molecule/OBMol to set the missing bonds
223
+ mol = pybel.readstring("xyz", union.as_xyz())
224
+ obmol = mol.OBMol
225
+ for from_, to_ in bond_diff:
226
+ obmol.AddBond(int(from_ + 1), int(to_ + 1), 1)
227
+
228
+ # Use modified pybel.Molecule
229
+ calc = OBabel(mol=mol, ff=ff)
230
+ funion = union.copy()
231
+ # Only releax second fragment
232
+ funion.freeze_atoms = freeze
233
+ funion.set_calculator(calc)
234
+
235
+ opt = LBFGS(funion, max_cycles=1000, max_step=0.5, dump=False, print_every=25)
236
+ opt.run()
237
+
238
+ return funion
239
+
240
+
241
+ def align_on_subset(geom1, union, del1=None):
242
+ """Align 'union' onto subset of 'geom1'"""
243
+
244
+ # Delete some coordinates (atoms) in geom1, that are not present in union
245
+ coords3d_1 = geom1.coords3d.copy()
246
+ atoms1 = geom1.atoms
247
+ if del1 is not None:
248
+ atoms1 = [atom for i, atom in enumerate(atoms1) if i not in del1]
249
+ coords3d_1 = np.delete(coords3d_1, del1, axis=0)
250
+ atoms1 = tuple(atoms1)
251
+ num1 = coords3d_1.shape[0]
252
+ # Restrict length of union to length of coords3d_1
253
+ coords3d_2 = union.coords3d.copy()
254
+ coords3d_2_subset = coords3d_2[:num1]
255
+ atoms2 = union.atoms
256
+ assert atoms1 == tuple(atoms2[:num1])
257
+
258
+ *_, rot_mat = get_rot_mat_for_coords(coords3d_1, coords3d_2_subset)
259
+
260
+ # Align merged system
261
+ coords3d_2_aligned = (coords3d_2 - coords3d_2.mean(axis=0)[None, :]).dot(rot_mat)
262
+
263
+ # Translate aligned system so that centroids of subsets match
264
+ centroid_1 = coords3d_1.mean(axis=0)
265
+ centroid_2 = coords3d_2_aligned[:num1].mean(axis=0)
266
+ coords3d_2_aligned += centroid_1 - centroid_2
267
+
268
+ aligned = Geometry(atoms2, coords3d_2_aligned)
269
+ subset = Geometry(atoms2[:num1], coords3d_2_aligned[:num1])
270
+ rest = Geometry(atoms2[num1:], coords3d_2_aligned[num1:])
271
+ return aligned, subset, rest
272
+
273
+
274
+ def merge_with_frozen_geom(
275
+ frozen_geom, lig_geom, make_bonds, frozen_del, lig_del, ff="mmff94"
276
+ ):
277
+ union, make_bonds_cor = merge_geoms(
278
+ frozen_geom, lig_geom, frozen_del, lig_del, make_bonds
279
+ )
280
+ atoms = union.atoms
281
+ print("Docking to form bonds:")
282
+ for i, (from_, to_) in enumerate(make_bonds_cor):
283
+ from_atom = atoms[from_]
284
+ to_atom = atoms[to_]
285
+ print(f"\t{i:02d} {from_atom}{from_}-{to_atom}{to_}")
286
+
287
+ union = prepare_merge(union, make_bonds_cor, dump=True)
288
+ if HAS_OPENBABEL:
289
+ opt_union = merge_opt(union, make_bonds_cor, ff=ff)
290
+ else:
291
+ warnings.warn(f"Could not import openbabel. Skipping '{ff}' optimization.")
292
+ opt_union = union
293
+ # aligned: whole system
294
+ # subset: frozen_geom - deleted atoms
295
+ # rest: aligned ligand
296
+ aligned, subset, rest = align_on_subset(frozen_geom, opt_union, frozen_del)
297
+ return aligned, subset, rest
298
+
299
+
300
+ def parse_args(args):
301
+ parser = argparse.ArgumentParser()
302
+
303
+ parser.add_argument("frozen_fn", help="Filename of the frozen geometry.")
304
+ parser.add_argument("lig_fn", help="Filename of the ligand to be merged.")
305
+ parser.add_argument(
306
+ "--frozen-del",
307
+ nargs="+",
308
+ type=int,
309
+ help="0-based atom indices to be deleted from frozen_fn.",
310
+ )
311
+ parser.add_argument(
312
+ "--lig-del",
313
+ nargs="+",
314
+ type=int,
315
+ help="0-based atom indices to be deleted from lig_fn.",
316
+ )
317
+ parser.add_argument(
318
+ "--make-bonds",
319
+ nargs="+",
320
+ type=int,
321
+ help="0-based indices of atom pairs (frozen, lig), forming bonds "
322
+ "in the merged geometry. lig indices should start at 0.",
323
+ )
324
+ parser.add_argument("--res", default="LIG")
325
+ parser.add_argument("--resno", type=int)
326
+
327
+ return parser.parse_args(args)
328
+
329
+
330
+ def run_merge():
331
+ args = parse_args(sys.argv[1:])
332
+
333
+ frozen_fn = args.frozen_fn
334
+ lig_fn = args.lig_fn
335
+ protein_pdb = frozen_fn
336
+ lig_pdb = lig_fn
337
+
338
+ frozen_geom = geom_loader(protein_pdb)
339
+ lig_geom = geom_loader(lig_pdb)
340
+
341
+ prot_del = args.frozen_del
342
+ lig_del = args.lig_del
343
+ make_bonds = args.make_bonds
344
+ make_bonds = np.array(make_bonds, dtype=int).reshape(-1, 2)
345
+ aligned, subset, rest = merge_with_frozen_geom(
346
+ frozen_geom, lig_geom, make_bonds, prot_del, lig_del
347
+ )
348
+
349
+ trj_fn = "merged_trj.xyz"
350
+ trj = "\n".join([geom.as_xyz() for geom in (aligned, rest)])
351
+ with open(trj_fn, "w") as handle:
352
+ handle.write(trj)
353
+ print(f"Dumped geometries to '{trj_fn}'.")
354
+
355
+ res = args.res
356
+ resno = args.resno
357
+ if (res is not None) and (resno is not None):
358
+ crd_str = geom_to_crd_str(
359
+ rest, res=res, resno=resno, ref_atoms=lig_geom.atoms, del_atoms=lig_del
360
+ )
361
+ crd_fn = "lig_aligned.crd"
362
+ with open(crd_fn, "w") as handle:
363
+ handle.write(crd_str)
364
+ print(f"Dumped ligand coordinates to to '{crd_fn}'.")
365
+
366
+
367
+ if __name__ == "__main__":
368
+ run_merge()
@@ -0,0 +1,322 @@
1
+ import argparse
2
+ import copy
3
+ import sys
4
+ from typing import Optional
5
+ import warnings
6
+
7
+ import numpy as np
8
+
9
+ from pysisyphus.constants import BOHR2ANG
10
+ from pysisyphus.drivers.merge import merge_with_frozen_geom
11
+ from pysisyphus.helpers import geom_loader
12
+ from pysisyphus.io.mol2 import parse_mol2, dict_to_mol2_string
13
+
14
+
15
+ def delete_atoms_bonds_inplace(
16
+ as_dict: dict, inds: list[int], atom_offset: int = 0, bond_offset: int = 0
17
+ ) -> dict[int, int]:
18
+ """Update atom_ids and bonds in mol2-dict inplace.
19
+
20
+ Parameter
21
+ ---------
22
+ as_dict
23
+ mol2-dict as returned from pysisyphus.io.mol2.parse_mol2.
24
+ inds
25
+ List of positive integer atom_ids to be deleted.
26
+ atom_offset
27
+ Integer >= 0. atom_ids will be shifted by this number.
28
+ bond_offset
29
+ Integer >= 0. bond_ids will be shifted by this number.
30
+
31
+ Returns
32
+ -------
33
+ atom_map
34
+ Dictionary w/ origin atom_ids as keys and updated atom_ids as values.
35
+ """
36
+ to_del = set(inds)
37
+
38
+ # Atoms
39
+ axs = as_dict["atoms_xyzs"]
40
+ axs_mod = list()
41
+ atoms_del = 0
42
+ atom_map = {}
43
+ for ax in axs:
44
+ if ax["atom_id"] in to_del:
45
+ warnings.warn("Charges were not updated after deleting atoms!")
46
+ print(f"Deleted atom {ax['atom_name']} with atom_id {ax['atom_id']}")
47
+ atoms_del += 1
48
+ continue
49
+ atom_id = ax["atom_id"]
50
+ mod_atom_id = atom_id - atoms_del + atom_offset
51
+ # Store updated atom_id in dict with original atom_id as key,
52
+ # so we can later acces them to update the bond atom_ids.
53
+ atom_map[atom_id] = mod_atom_id
54
+ ax["atom_id"] = mod_atom_id
55
+ axs_mod.append(ax)
56
+ as_dict["atoms_xyzs"] = axs_mod
57
+
58
+ # Bonds
59
+ bonds_mod = list()
60
+ bonds_del = 0
61
+ for bond in as_dict["bond"]:
62
+ bond_inds = set((bond["origin_atom_id"], bond["target_atom_id"]))
63
+ if bond_inds & to_del:
64
+ print(f"Deleted bond with bond_id {bond['bond_id']}")
65
+ bonds_del += 1
66
+ continue
67
+ bond["bond_id"] -= bonds_del
68
+ bond["bond_id"] += bond_offset
69
+ bond["origin_atom_id"] = atom_map[bond["origin_atom_id"]]
70
+ bond["target_atom_id"] = atom_map[bond["target_atom_id"]]
71
+ bonds_mod.append(bond)
72
+ as_dict["bond"] = bonds_mod
73
+ return atom_map
74
+
75
+
76
+ def update_coords_inplace(as_dict: dict, coords3d: np.ndarray):
77
+ """Update xyz coordinates in mol2 dict inplace."""
78
+ axs = as_dict["atoms_xyzs"]
79
+ assert len(axs) == len(coords3d)
80
+
81
+ for ax, nc3d in zip(axs, coords3d):
82
+ ax["xyz"] = (nc3d * BOHR2ANG).tolist()
83
+
84
+
85
+ def merge_mol2_dicts(as_dict1: dict, as_dict2: dict) -> dict:
86
+ """Merge two mol2 dict. Atoms, bonds and associated numbers will be updated.
87
+
88
+ Parameters
89
+ ----------
90
+ as_dict1
91
+ Mol2 dict.
92
+ as_dict2
93
+ Mo2 dict.
94
+
95
+ Returns
96
+ -------
97
+ merged
98
+ Merged mol2 dict. Most entries will be copied from as_dict1 but atoms,
99
+ bonds and associated counts will be updated with entries from as_dict2.
100
+ """
101
+ # Atoms
102
+ atoms_xyzs = as_dict1["atoms_xyzs"] + as_dict2["atoms_xyzs"]
103
+
104
+ # Bonds
105
+ bond = as_dict1["bond"] + as_dict2["bond"]
106
+
107
+ merged = copy.deepcopy(as_dict1)
108
+ merged.update(
109
+ {
110
+ "atoms_xyzs": atoms_xyzs,
111
+ "num_atoms": len(atoms_xyzs),
112
+ "bond": bond,
113
+ "num_bonds": len(bond),
114
+ "mol_name": "merged",
115
+ }
116
+ )
117
+ return merged
118
+
119
+
120
+ def get_substs(as_dict: dict) -> tuple[set[tuple[int, str]], int]:
121
+ keys = set()
122
+ for ax in as_dict["atoms_xyzs"]:
123
+ key = (ax["subst_id"], ax["subst_name"])
124
+ keys.add(key)
125
+ subst_ids, _ = zip(*keys)
126
+ max_subst_id = max(subst_ids)
127
+ return keys, max_subst_id
128
+
129
+
130
+ def update_subst_ids_inplace(as_dict: dict, offset: int):
131
+ for ax in as_dict["atoms_xyzs"]:
132
+ ax["subst_id"] += offset
133
+
134
+
135
+ def merge_mol2(
136
+ fn1: str,
137
+ fn2: str,
138
+ del1: Optional[list[int]] = None,
139
+ del2: Optional[list[int]] = None,
140
+ bonds: Optional[list[int]] = None,
141
+ new_coords: Optional[np.ndarray] = None,
142
+ ) -> dict:
143
+ if del1 is None:
144
+ del1 = list()
145
+ if del2 is None:
146
+ del2 = list()
147
+ if bonds is None:
148
+ bonds = list()
149
+ bonds = np.array(bonds, dtype=int).reshape(-1, 2)
150
+
151
+ dict1 = parse_mol2(fn1).as_dict()
152
+ # dict1 = m1.as_dict()
153
+ dict2 = parse_mol2(fn2).as_dict()
154
+ # dict2 = m2.as_dict()
155
+
156
+ # Delete atoms and assoicated bonds
157
+ print(f"Deleting atoms/bonds from '{fn1}'")
158
+ atom_map1 = delete_atoms_bonds_inplace(dict1, del1)
159
+ print()
160
+ natoms1 = len(dict1["atoms_xyzs"])
161
+ nbonds1 = len(dict1["bond"])
162
+
163
+ print(f"Deleting atoms/bonds from '{fn2}'")
164
+ atom_map2 = delete_atoms_bonds_inplace(
165
+ dict2, del2, atom_offset=natoms1, bond_offset=nbonds1
166
+ )
167
+
168
+ # Determine highest subst_id in dict1, to shift subst_ids in dict2
169
+ _, max_subst_id1 = get_substs(dict1)
170
+ print(f"Highest subst_id in '{fn1}' is {max_subst_id1}.")
171
+ update_subst_ids_inplace(dict2, max_subst_id1)
172
+
173
+ if new_coords is not None:
174
+ coords1 = new_coords[:natoms1]
175
+ coords2 = new_coords[natoms1:]
176
+ assert len(coords1) + len(coords2) == len(new_coords)
177
+ update_coords_inplace(dict1, coords1)
178
+ update_coords_inplace(dict2, coords2)
179
+ print("Updated coordinates")
180
+
181
+ # Add new bond(s) to dict2
182
+ bond2 = dict2["bond"]
183
+ nbonds2 = len(bond2)
184
+ for from_, to_ in bonds:
185
+ # Use updated atom_ids for the bond target/origin
186
+ origin = atom_map1[from_]
187
+ target = atom_map2[to_]
188
+ nbonds2 += 1
189
+ bond_type = 1
190
+ new_bond = {
191
+ "bond_id": nbonds1 + nbonds2,
192
+ "bond_type": bond_type,
193
+ "origin_atom_id": origin,
194
+ "target_atom_id": target,
195
+ }
196
+ print(f"Added new bond '{new_bond}'")
197
+ warnings.warn(f"Set bond_type to '{bond_type}'.")
198
+ bond2.append(new_bond)
199
+
200
+ merged = merge_mol2_dicts(dict1, dict2)
201
+ return merged
202
+
203
+
204
+ def merge_mol2_geoms(
205
+ fn1: str,
206
+ fn2: str,
207
+ bonds: list[list[int]],
208
+ del1: Optional[list[int]] = None,
209
+ del2: Optional[list[int]] = None,
210
+ out_fn: Optional[str] = None,
211
+ ) -> str:
212
+ """Merge geometries in mol2 files w/ atom deletion and bond formation.
213
+
214
+ Parameters
215
+ ----------
216
+ fn1
217
+ Filename of first mol2 file.
218
+ fn2
219
+ Filename of second mol2 file.
220
+ bonds
221
+ List of list of two integers. Each integer pair comprises an atom
222
+ of fn1 and an atom of fn2. Original indices/atom_ids must be used,
223
+ regardless of any atom deletion.
224
+ del1
225
+ Optional list of integers of atoms to be deleted. As for 'bonds',
226
+ atom_ids as appearing in the mol2 file must used.
227
+ del2
228
+ Same as 'del1' but deletes atoms in 'fn2'.
229
+ out_fn
230
+ Optional str. If given the resulting mol2-file will be written to
231
+ this filename.
232
+
233
+ Returns
234
+ -------
235
+ rendered
236
+ Merged mol2 w/ deleted atoms.
237
+ """
238
+
239
+ if del1 is None:
240
+ del1 = []
241
+ if del2 is None:
242
+ del2 = []
243
+
244
+ del1 = list(sorted(del1))
245
+ del2 = list(sorted(del2))
246
+ # Make 0-based indices from 1-based mol2 indices
247
+ del10 = [d - 1 for d in del1]
248
+ del20 = [d - 1 for d in del2]
249
+ bonds0 = np.array(bonds, dtype=int).reshape(-1, 2) - 1
250
+
251
+ geom1 = geom_loader(fn1)
252
+ geom2 = geom_loader(fn2)
253
+ # Get new coordinates by merging both fragments.
254
+ # Function takes 0-based indices
255
+ new_geom, *_ = merge_with_frozen_geom(geom1, geom2, bonds0, del10, del20)
256
+ new_coords3d = new_geom.coords3d
257
+
258
+ # Merge mol2 files w/ updated coordinates
259
+ # Takes 1-based indices, bad?!
260
+ merged = merge_mol2(fn1, fn2, del1, del2, bonds, new_coords3d)
261
+
262
+ # Render mol2 string from merged data
263
+ rendered = dict_to_mol2_string(merged)
264
+
265
+ # Dump merged mol2 dict to file
266
+ if out_fn is not None:
267
+ with open(out_fn, "w") as handle:
268
+ handle.write(rendered)
269
+ print(f"Dumped merged geometry to '{out_fn}'.")
270
+ return rendered
271
+
272
+
273
+ def parse_args(args):
274
+ parser = argparse.ArgumentParser(
275
+ description="Merge two mol2 files w/ atom deletion & bond formation."
276
+ )
277
+
278
+ parser.add_argument("fn1", help="Name of frozen mol2 file.")
279
+ parser.add_argument("fn2", help="Name of mobile mol2 file.")
280
+ parser.add_argument(
281
+ "--bonds",
282
+ nargs="+",
283
+ type=int,
284
+ help="1-based atom id pairs (id1, id2), between which bonds are formed."
285
+ "The original atom ids must be used, regardless of any atom deletions.",
286
+ )
287
+ parser.add_argument(
288
+ "--del1",
289
+ nargs="+",
290
+ type=int,
291
+ help="1-based atom ids to be deleted from fn1.",
292
+ )
293
+ parser.add_argument(
294
+ "--del2",
295
+ nargs="+",
296
+ type=int,
297
+ help="1-based atom ids to be deleted from fn2.",
298
+ )
299
+ parser.add_argument(
300
+ "--out", default="merged.mol2", help="Name of the final mol2 file."
301
+ )
302
+
303
+ return parser.parse_args(args)
304
+
305
+
306
+ def run() -> str:
307
+ """CLI frontend for merge_mol2_geoms()."""
308
+
309
+ args = parse_args(sys.argv[1:])
310
+
311
+ fn1 = args.fn1
312
+ fn2 = args.fn2
313
+ bonds = args.bonds
314
+ del1 = args.del1
315
+ del2 = args.del2
316
+ out = args.out
317
+
318
+ return merge_mol2_geoms(fn1, fn2, bonds, del1, del2, out)
319
+
320
+
321
+ if __name__ == "__main__":
322
+ run()