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,962 @@
1
+ # [1] http://link.aip.org/link/doi/10.1063/1.1515483
2
+ # The efficient optimization of molecular geometries using redundant internal
3
+ # coordinates
4
+ # Bakken, Helgaker, 2002
5
+
6
+ from collections import namedtuple
7
+ import itertools as it
8
+ from typing import Optional
9
+
10
+ import numpy as np
11
+ from scipy.spatial.distance import pdist, squareform
12
+ from sklearn.cluster import KMeans
13
+
14
+ from pysisyphus.constants import BOHR2ANG
15
+ from pysisyphus.config import BEND_MIN_DEG, DIHED_MAX_DEG
16
+ from pysisyphus.helpers_pure import log, sort_by_central, merge_sets
17
+ from pysisyphus.elem_data import VDW_RADII, COVALENT_RADII as CR
18
+ from pysisyphus.intcoords import Stretch, Bend, LinearBend, Torsion
19
+ from pysisyphus.intcoords.setup_fast import find_bonds as find_bonds_fast
20
+ from pysisyphus.intcoords.PrimTypes import PrimTypes, PrimMap, Rotations
21
+ from pysisyphus.intcoords.valid import bend_valid, dihedral_valid
22
+
23
+
24
+ BOND_FACTOR = 1.3
25
+
26
+
27
+ def get_pair_covalent_radii(atoms):
28
+ atoms = [a.lower() for a in atoms]
29
+ cov_radii = np.array([CR[a.lower()] for a in atoms])
30
+ pair_cov_radii = np.array([r1 + r2 for r1, r2 in it.combinations(cov_radii, 2)])
31
+ return pair_cov_radii
32
+
33
+
34
+ def get_bond_mat(geom, bond_factor=BOND_FACTOR):
35
+ cdm = pdist(geom.coords3d)
36
+ pair_cov_radii = get_pair_covalent_radii(geom.atoms)
37
+ bond_mat = squareform(cdm <= (pair_cov_radii * bond_factor))
38
+ return bond_mat
39
+
40
+
41
+ def get_bond_sets(
42
+ atoms, coords3d, bond_factor=BOND_FACTOR, return_cdm=False, return_cbm=False
43
+ ):
44
+ """I'm sorry, but this function does not return sets, but an int ndarray."""
45
+ cdm = pdist(coords3d)
46
+ # Generate indices corresponding to the atom pairs in the
47
+ # condensed distance matrix cdm.
48
+ atom_inds = list(it.combinations(range(len(coords3d)), 2))
49
+ atom_inds = np.array(atom_inds, dtype=int)
50
+ scaled_cr_sums = bond_factor * get_pair_covalent_radii(atoms)
51
+ # condensed bond matrix
52
+ cbm = cdm <= scaled_cr_sums
53
+ bond_inds = atom_inds[cbm]
54
+ if not return_cbm and not return_cdm:
55
+ return bond_inds
56
+ add_returns = tuple(
57
+ [mat for flag, mat in ((return_cdm, cdm), (return_cbm, cbm)) if flag]
58
+ )
59
+ return (bond_inds,) + add_returns
60
+
61
+
62
+ def get_fragments(atoms, coords, bond_inds=None, bond_factor=BOND_FACTOR):
63
+ """This misses unconnected single atoms!"""
64
+ coords3d = coords.reshape(-1, 3)
65
+ if bond_inds is None:
66
+ # Bond indices without interfragment bonds and/or hydrogen bonds
67
+ bond_inds = get_bond_sets(atoms, coords3d, bond_factor=bond_factor)
68
+
69
+ bond_ind_sets = [frozenset(bi) for bi in bond_inds]
70
+ fragments = merge_sets(bond_ind_sets)
71
+
72
+ return fragments
73
+
74
+
75
+ def connect_fragments(
76
+ cdm, fragments, max_aux=3.78, aux_factor=BOND_FACTOR, logger=None
77
+ ):
78
+ """Determine the smallest interfragment bond for a list
79
+ of fragments and a condensed distance matrix. For more than a few fragments
80
+ this function performs poorly, as each fragment is connected to all reamaining
81
+ fragments, leading to an explosion of bonds, bends and dihedrals."""
82
+ if len(fragments) > 1:
83
+ log(
84
+ logger,
85
+ f"Detected {len(fragments)} fragments. Generating interfragment bonds.",
86
+ )
87
+ dist_mat = squareform(cdm)
88
+ interfrag_inds = list()
89
+ aux_interfrag_inds = list()
90
+ for frag1, frag2 in it.combinations(fragments, 2):
91
+ log(logger, f"\tConnecting {len(frag1)} atom and {len(frag2)} atom fragment")
92
+ inds = [(i1, i2) for i1, i2 in it.product(frag1, frag2)]
93
+ distances = np.array([dist_mat[ind] for ind in inds])
94
+
95
+ # Determine minimum distance bond
96
+ min_ind = distances.argmin()
97
+ min_dist = distances[min_ind]
98
+ interfrag_bond = tuple(inds[min_ind])
99
+ interfrag_inds.append(interfrag_bond)
100
+ log(logger, f"\tMinimum distance bond: {interfrag_bond}, {min_dist:.4f} au")
101
+
102
+ # Determine auxiliary interfragment bonds that are either below max_aux
103
+ # (default 2 Å, ≈ 3.78 au), or less than aux_factor (default 1.3) times the
104
+ # minimum interfragment distance.
105
+ below_max_aux = [
106
+ ind for ind in inds if (dist_mat[ind] < max_aux) and (ind != interfrag_bond)
107
+ ]
108
+ if below_max_aux:
109
+ log(
110
+ logger,
111
+ f"\tAux. interfrag bonds below {max_aux*BOHR2ANG:.2f} Å:\n"
112
+ + "\n".join(
113
+ [f"\t\t{ind}: {dist_mat[ind]:.4f} au" for ind in below_max_aux]
114
+ ),
115
+ )
116
+ scaled_min_dist = aux_factor * min_dist
117
+ above_min_dist = [
118
+ ind
119
+ for ind in inds
120
+ if (dist_mat[ind] < scaled_min_dist)
121
+ and (ind != interfrag_bond)
122
+ and (ind not in below_max_aux)
123
+ ]
124
+ if above_min_dist:
125
+ log(
126
+ logger,
127
+ f"\tAux. interfrag bonds below {aux_factor:.2f} * min_dist:\n"
128
+ + "\n".join(
129
+ [f"\t\t{ind}: {dist_mat[ind]:.4f} au" for ind in above_min_dist]
130
+ ),
131
+ )
132
+ aux_interfrag_inds.extend(below_max_aux)
133
+ aux_interfrag_inds.extend(above_min_dist)
134
+ # Or as Philipp proposed: two loops over the fragments and only
135
+ # generate interfragment distances. So we get a full matrix with
136
+ # the original indices but only the required distances.
137
+ return interfrag_inds, aux_interfrag_inds
138
+
139
+
140
+ def connect_fragments_kmeans(
141
+ cdm,
142
+ fragments,
143
+ atoms,
144
+ aux_below_thresh=3.7807, # 2 Å
145
+ aux_add_dist=2.8356, # 1.5 Å
146
+ aux_keep=5,
147
+ aux_no_hh=True,
148
+ min_dist_thresh=5.0,
149
+ min_scale=1.2,
150
+ logger=None,
151
+ ):
152
+ """Generate (auxiliary) interfragment bonds.
153
+
154
+ In the a first step, minimum distance interfragment bonds (IFBs) are determined between
155
+ all possible fragment pairs. Similarly, possible auxiliary IFBs are determined.
156
+ Candidates for auxiliary IFBs are:
157
+ IFB <= aux_below_thresh, default 2 Å
158
+ IFB <= (minimum distance IFB + aux_add_dist), default 1.5 Å
159
+ By default, only the first aux_keep (default = 5) auxiliary IFBs are kept.
160
+
161
+ Connecting all fragments can lead to bonds between very distant atoms. If more than
162
+ two fragments are present we cluster the minimum distance IFB distances using KMeans,
163
+ to determine a reasonable length for valid IFBs. We start out with two clusters and
164
+ increase the number of cluster until the center of one cluster is around the scaled
165
+ global minimum distance between the fragments. The center of this cluster is then used
166
+ as a cutoff vor valid IFBs.
167
+
168
+ After pruning all possible IFBs we can determine the fragment pairs, that are actually
169
+ connected. This information is then used to also prune possible interfragment bonds.
170
+ Only auxiliary IFBs between fragments that are actually connected via IFBs are kept.
171
+ """
172
+ # atoms = [atom.lower() for atom in atoms]
173
+ if len(fragments) > 1:
174
+ log(
175
+ logger,
176
+ f"Detected {len(fragments)} fragments. Generating interfragment bonds.",
177
+ )
178
+ dist_mat = squareform(cdm)
179
+
180
+ frag_pairs = list()
181
+ interfrag_inds = list()
182
+ interfrag_dists = list()
183
+ aux_dict = dict()
184
+ for i, j in it.combinations(range(len(fragments)), 2):
185
+ frag1 = fragments[i]
186
+ frag2 = fragments[j]
187
+ log(logger, f"\tConnecting {len(frag1)}-atom and {len(frag2)}-atom fragments.")
188
+ # Pairs of possible interfragment bonds
189
+ inds = np.array([(i1, i2) for i1, i2 in it.product(frag1, frag2)], dtype=int)
190
+ distances = np.array([dist_mat[k, l] for k, l in inds])
191
+
192
+ frag_pairs.append((i, j))
193
+ # Determine minimum distance bond
194
+ min_ind = distances.argmin()
195
+ min_dist = distances[min_ind]
196
+ interfrag_inds.append(inds[min_ind])
197
+ interfrag_dists.append(min_dist)
198
+ """
199
+ But also consider other bonds with reasonable lengths as auxiliary interfrag bonds.
200
+ Contrary to [1] we don't use a scaled minimum interfragment distances (MID),
201
+ but just add a fixed value to the MID. Scaling a possibly big MID would
202
+ probably include too many coordinates.
203
+ """
204
+ # if aux_no_hh:
205
+ # is_hh = np.array(
206
+ # [(atoms[k] == "h") and (atoms[k] == atoms[l]) for k, l in inds]
207
+ # )
208
+ # else:
209
+ # is_hh = np.zeros(len(inds))
210
+
211
+ aux_mask = np.logical_or(
212
+ distances <= aux_below_thresh,
213
+ distances <= (min_dist + aux_add_dist),
214
+ )
215
+ # aux_mask = distances <= 1.1 * min_dist
216
+ # aux_mask = np.logical_and(aux_mask, ~is_hh)
217
+ # Don't include interfragment bond
218
+ aux_mask[min_ind] = False
219
+ aux_inds = inds[aux_mask]
220
+
221
+ if aux_keep >= 0:
222
+ aux_dists = distances[aux_mask]
223
+ aux_keep_inds = aux_dists.argsort()[:aux_keep]
224
+ aux_inds = aux_inds[aux_keep_inds]
225
+
226
+ aux_dict[(i, j)] = aux_inds
227
+ frag_pairs = np.array(frag_pairs, dtype=int)
228
+
229
+ """
230
+ The code below tries to determine a reasonable distance, to define
231
+ interfragment bonds. We don't want to use all interfragment bonds defined
232
+ above, as this would connect all fragments to each other, resulting in an
233
+ explosion of the number of internal coordinates.
234
+
235
+ Here we try to cluster the interfragment distances, until one cluster center
236
+ comes close to the scaled minimum interfragment distance. We then use this
237
+ distance to filter all interfragment bonds.
238
+ """
239
+ if len(fragments) > 2:
240
+ dists = np.reshape(interfrag_dists, (-1, 1))
241
+ min_dist = dists.min()
242
+
243
+ for n_clusters in range(2, 10):
244
+ kmeans = KMeans(n_clusters=n_clusters)
245
+ _ = kmeans.fit_predict(dists)
246
+ min_center = kmeans.cluster_centers_.min()
247
+
248
+ if min_center <= (min_scale * min_dist):
249
+ break
250
+ else:
251
+ raise Exception("Not handled!")
252
+
253
+ interfrag_inds = np.array(interfrag_inds)
254
+ mask = dists <= min_scale * min_center
255
+ # We also use interfragment bonds that still have a reasonable length.
256
+ if (min_dist_thresh is not None) and (min_dist_thresh > 0.0):
257
+ mask |= dists <= min_dist_thresh
258
+ mask = mask.flatten()
259
+ interfrag_inds = interfrag_inds[mask]
260
+ conn_frags_mask = mask
261
+ else:
262
+ conn_frags_mask = np.ones(len(frag_pairs), dtype=bool)
263
+
264
+ # Only keep auxiliary interfragment bonds between actually connected fragments
265
+ actually_connected_frags = frag_pairs[conn_frags_mask]
266
+ aux_interfrag_inds = list(
267
+ it.chain(*[aux_dict[(i, j)] for i, j in actually_connected_frags])
268
+ )
269
+ aux_interfrag_inds = np.array(aux_interfrag_inds, dtype=int)
270
+ return interfrag_inds, aux_interfrag_inds
271
+
272
+
273
+ def connect_fragments_ahlrichs(
274
+ cdm,
275
+ fragments,
276
+ atoms,
277
+ min_dist_scale=1.1,
278
+ scale=1.2,
279
+ avoid_h=False,
280
+ logger=None,
281
+ ):
282
+ atoms = [atom.lower() for atom in atoms]
283
+ if len(fragments) > 1:
284
+ log(
285
+ logger,
286
+ f"Detected {len(fragments)} fragments. Generating interfragment bonds.",
287
+ )
288
+ dist_mat = squareform(cdm)
289
+
290
+ frag_pairs = list()
291
+ interfrag_inds = list()
292
+ max_dist = 3.0 / BOHR2ANG
293
+ all_fragment_inds = [i for i, _ in enumerate(fragments)]
294
+ # Leave out the first fragment, so we can also handle only 1 fragment here.
295
+ unconnected_fragment_inds = all_fragment_inds.copy()[1:]
296
+ h_inds = set([i for i, atom in enumerate(atoms) if atom.lower() == "h"])
297
+ while True:
298
+ for i, j in it.product(all_fragment_inds, unconnected_fragment_inds):
299
+ # Don't try to connect a fragment to itself and don't try to connect
300
+ # fragments two times, e.g., (0 and 2) as well as (2 and 0).
301
+ if i >= j:
302
+ continue
303
+
304
+ frag1 = fragments[i]
305
+ frag2 = fragments[j]
306
+ log(
307
+ logger,
308
+ f"\tConnecting {len(frag1)}-atom and {len(frag2)}-atom fragments.",
309
+ )
310
+ # Pairs of possible interfragment bonds
311
+ inds = np.array(
312
+ [(i1, i2) for i1, i2 in it.product(frag1, frag2)], dtype=int
313
+ )
314
+ distances = np.array([dist_mat[k, l] for k, l in inds])
315
+
316
+ frag_pairs.append((i, j))
317
+ # Determine minimum distance bond
318
+ min_ind = distances.argmin()
319
+ min_dist = distances[min_ind]
320
+
321
+ if avoid_h:
322
+ sort_inds = np.argsort(distances, kind="stable")
323
+ # Try to avoid interfragment bonds involving hydrogen.
324
+ for (k, dist) in zip(sort_inds, distances[sort_inds]):
325
+ if set(inds[k]) & h_inds:
326
+ continue
327
+ if dist >= 1.5 * min_dist:
328
+ break
329
+ min_ind = k
330
+ min_dist = distances[k]
331
+ break
332
+
333
+ if min_dist <= max_dist:
334
+ # Scaling of bigger 'min_dist' values by a fixed factor can lead
335
+ # to definition of many additional bonds. We restrict the offset
336
+ # to at most 1 Å.
337
+ offset = min((min_dist_scale - 1.0) * min_dist, 1.0 / BOHR2ANG)
338
+ mask = distances <= (min_dist + offset)
339
+ interfrag_inds.extend(inds[mask])
340
+ # Indicate that the just connected fragments don't have to be
341
+ # connected anymore. In the current cycle of the while loop additional
342
+ # bonds to the just connected fragments can still be defined.
343
+ unconnected_fragment_inds = [
344
+ k for k in unconnected_fragment_inds if k not in (i, j)
345
+ ]
346
+ # Leave the outer while loop when all fragments are connected
347
+ if len(unconnected_fragment_inds) == 0:
348
+ break
349
+ # If there are still unconnected fragments present we allow longer
350
+ # interfragment bonds.
351
+ max_dist *= scale
352
+
353
+ interfrag_inds = np.array(interfrag_inds, dtype=int)
354
+ return interfrag_inds, list()
355
+
356
+
357
+ def get_hydrogen_bond_inds(atoms, coords3d, bond_inds, logger=None):
358
+ tmp_sets = [frozenset(bi) for bi in bond_inds]
359
+ hydrogen_inds = [i for i, a in enumerate(atoms) if a.lower() == "h"]
360
+ x_inds = [i for i, a in enumerate(atoms) if a.lower() in "n o f p s cl".split()]
361
+ hydrogen_bond_inds = list()
362
+ for h_ind, x_ind in it.product(hydrogen_inds, x_inds):
363
+ as_set = set((h_ind, x_ind))
364
+ if as_set not in tmp_sets:
365
+ continue
366
+ # Check if distance of H to another electronegative atom Y is
367
+ # greater than (1.1 * sum of their covalent radii) but smaller than
368
+ # (0.9 * sum of their van der Waals radii). If the
369
+ # angle X-H-Y is greater than 90° a hydrogen bond is asigned.
370
+ y_inds = set(x_inds) - set((x_ind,))
371
+ for y_ind in y_inds:
372
+ y_atom = atoms[y_ind].lower()
373
+ cov_rad_sum = CR["h"] + CR[y_atom]
374
+ distance = Stretch._calculate(coords3d, (h_ind, y_ind))
375
+ vdw_rad_sum = VDW_RADII["h"] + VDW_RADII[y_atom]
376
+ angle = Bend._calculate(coords3d, (x_ind, h_ind, y_ind))
377
+ if (1.1 * cov_rad_sum < distance < 0.9 * vdw_rad_sum) and (
378
+ angle > np.pi / 2
379
+ ):
380
+ hydrogen_bond_inds.append((h_ind, y_ind))
381
+ log(
382
+ logger,
383
+ f"Detected hydrogen bond between atoms {h_ind} "
384
+ f"({atoms[h_ind]}) and {y_ind} ({atoms[y_ind]})",
385
+ )
386
+
387
+ return hydrogen_bond_inds
388
+
389
+
390
+ def get_hydrogen_bond_inds_v2(atoms, coords3d, bond_inds, logger=None):
391
+ def to_set(iterable):
392
+ return {frozenset(_) for _ in iterable}
393
+
394
+ atoms_lower = [atom.lower() for atom in atoms]
395
+ # Determine Hydrogen indices
396
+ org_h_inds = {i for i, atom in enumerate(atoms_lower) if atom == "h"}
397
+
398
+ org_bond_sets = to_set(bond_inds)
399
+ # Determine Hydrogen bonding partners in original bond set
400
+ org_h_partners = dict()
401
+ for org_bond in org_bond_sets:
402
+ if h_set := org_bond & org_h_inds:
403
+ (x_ind,) = org_bond - h_set
404
+ (h_ind,) = h_set
405
+ org_h_partners.setdefault(h_ind, list()).append(x_ind)
406
+
407
+ hx = {"h", "n", "o", "f", "p", "s", "cl"}
408
+ # Determine indices of potential hydrogen bond acceptors and hydrogen to
409
+ # carry out a search for bonds with a bigger bond factor.
410
+ hx_inds = [i for i, atom in enumerate(atoms) if atom.lower() in hx]
411
+ hx_atoms = [atoms[i] for i in hx_inds]
412
+ hx_map = {j: i for j, i in enumerate(hx_inds)}
413
+ # See pysisyphus.elemdata.HBOND_FACTORS
414
+ hx_coords3d = coords3d[hx_inds]
415
+ # Search bonds with KDTree
416
+ h_bonds = find_bonds_fast(hx_atoms, hx_coords3d, bond_factor=2.3)
417
+ h_bonds = to_set(h_bonds)
418
+ # Map back to original indices. Until now the indices were only in the basis
419
+ # of the reduced number of atoms.
420
+ h_bonds = {frozenset((hx_map[i], hx_map[j])) for i, j in h_bonds}
421
+ # Drop h_bonds that were already defined as "normal" bonds
422
+ h_bonds = h_bonds - org_bond_sets
423
+
424
+ hydrogen_bond_inds = list()
425
+ # h_bonds can still contain XX and HH bonds
426
+ for h_bond in h_bonds:
427
+ hi = h_bond & org_h_inds
428
+ # Skip XX and HH. We are only interest in 'h_bond' with one hydrogen.
429
+ if len(hi) in (0, 2):
430
+ continue
431
+ (h_ind,) = hi
432
+ (y_partner,) = h_bond - hi
433
+ v = coords3d[y_partner] - coords3d[h_ind]
434
+ for x_partner in org_h_partners[h_ind]:
435
+ u = coords3d[x_partner] - coords3d[h_ind]
436
+ # > 90°
437
+ if u.dot(v) < 0.0:
438
+ hydrogen_bond_inds.append((h_ind, y_partner))
439
+ return hydrogen_bond_inds
440
+
441
+
442
+ def get_bend_inds(coords3d, bond_inds, min_deg, max_deg, logger=None):
443
+ bond_sets = {frozenset(bi) for bi in bond_inds}
444
+
445
+ bend_inds = list()
446
+ for bond_set1, bond_set2 in it.combinations(bond_sets, 2):
447
+ union = bond_set1 | bond_set2
448
+ if len(union) == 3:
449
+ indices, _ = sort_by_central(bond_set1, bond_set2)
450
+ if not bend_valid(coords3d, indices, min_deg, max_deg):
451
+ log(logger, f"Bend {indices} is not valid!")
452
+ continue
453
+ bend_inds.append(indices)
454
+
455
+ return bend_inds
456
+
457
+
458
+ def get_linear_bend_inds(coords3d, cbm, bends, min_deg=175, max_bonds=4, logger=None):
459
+ linear_bends = list()
460
+ complements = list()
461
+
462
+ if min_deg is None:
463
+ return linear_bends, complements
464
+
465
+ bm = squareform(cbm)
466
+ for bend in bends:
467
+ deg = np.rad2deg(Bend._calculate(coords3d, bend))
468
+ bonds = sum(bm[bend[1]])
469
+ if (deg >= min_deg) and (bonds <= max_bonds):
470
+ log(
471
+ logger,
472
+ f"Bend {bend}={deg:.1f}° is (close to) linear. "
473
+ "Creating linear bend & complement.",
474
+ )
475
+ linear_bends.append(bend)
476
+ complements.append(bend)
477
+ return linear_bends, complements
478
+
479
+
480
+ def get_dihedral_inds(coords3d, bond_inds, bend_inds, max_deg, logger=None):
481
+ max_rad = np.deg2rad(max_deg)
482
+ bond_dict = dict()
483
+ for from_, to_ in bond_inds:
484
+ bond_dict.setdefault(from_, list()).append(to_)
485
+ bond_dict.setdefault(to_, list()).append(from_)
486
+ proper_dihedral_inds = list()
487
+ improper_candidates = list()
488
+ improper_dihedral_inds = list()
489
+
490
+ def log_dihed_skip(inds):
491
+ log(
492
+ logger,
493
+ f"Skipping generation of dihedral {inds} "
494
+ "as some of the the atoms are (close too) linear.",
495
+ )
496
+
497
+ def set_dihedral_index(dihedral_ind, proper=True):
498
+ dihed = tuple(dihedral_ind)
499
+ check_in = proper_dihedral_inds if proper else improper_dihedral_inds
500
+ # Check if this dihedral is already present
501
+ if (dihed in check_in) or (dihed[::-1] in check_in):
502
+ return
503
+ # Assure that the angles are below 175° (3.054326 rad)
504
+ if not dihedral_valid(coords3d, dihedral_ind, deg_thresh=max_deg):
505
+ log_dihed_skip(dihedral_ind)
506
+ return
507
+ if proper:
508
+ proper_dihedral_inds.append(dihed)
509
+ else:
510
+ improper_dihedral_inds.append(dihed)
511
+
512
+ for bond, bend in it.product(bond_inds, bend_inds):
513
+ central = bend[1]
514
+ bend_set = set(bend)
515
+ bond_set = set(bond)
516
+ # Check if the two sets share one common atom. If not continue.
517
+ intersect = bend_set & bond_set
518
+ if len(intersect) != 1:
519
+ continue
520
+
521
+ # TODO: check collinearity of bond and bend.
522
+
523
+ # When the common atom between bond and bend is a terminal, and not a central atom
524
+ # in the bend we create a proper dihedral. Improper dihedrals are only created
525
+ # when no proper dihedrals have been found.
526
+ if central not in bond_set:
527
+ # The new terminal atom in the dihedral is the one, that doesn' intersect.
528
+ terminal = tuple(bond_set - intersect)[0]
529
+ intersecting_atom = tuple(intersect)[0]
530
+ bend_terminal = tuple(bend_set - {central} - intersect)[0]
531
+
532
+ bend_rad = Bend._calculate(coords3d, bend)
533
+ # Bend atoms are nearly collinear. Check if we can skip the central bend atom
534
+ # and use an atom that is conneced to the terminal atom of the bend or bond.
535
+ if bend_rad >= max_rad:
536
+ bend_terminal_bonds = set(bond_dict[bend_terminal]) - bend_set
537
+ bond_terminal_bonds = set(bond_dict[terminal]) - bond_set
538
+ set_dihedrals = [
539
+ (terminal, intersecting_atom, bend_terminal, betb)
540
+ for betb in bend_terminal_bonds
541
+ ] + [
542
+ (bend_terminal, intersecting_atom, terminal, botb)
543
+ for botb in bond_terminal_bonds
544
+ ]
545
+ # Hardcoded for now ... look ahead to next shell of atoms
546
+ if not any(
547
+ [
548
+ dihedral_valid(coords3d, inds, deg_thresh=max_deg)
549
+ for inds in set_dihedrals
550
+ ]
551
+ ):
552
+ set_dihedrals = []
553
+ for betb in bend_terminal_bonds:
554
+ bend_terminal_bonds_v2 = (
555
+ set(bond_dict[betb]) - bend_set - bond_set
556
+ )
557
+ set_dihedrals = [
558
+ (terminal, intersecting_atom, betb, betb_v2)
559
+ for betb_v2 in bend_terminal_bonds_v2
560
+ ]
561
+ for botb in bond_terminal_bonds:
562
+ bond_terminal_bonds_v2 = (
563
+ set(bond_dict[botb]) - bend_set - bond_set
564
+ )
565
+ set_dihedrals = [
566
+ (bend_terminal, intersecting_atom, botb, botb_v2)
567
+ for botb_v2 in bond_terminal_bonds_v2
568
+ ]
569
+ elif intersecting_atom == bend[0]:
570
+ set_dihedrals = [[terminal] + list(bend)]
571
+ else:
572
+ set_dihedrals = [list(bend) + [terminal]]
573
+ [set_dihedral_index(dihed) for dihed in set_dihedrals]
574
+ # If the common atom is the central atom we try to form an out
575
+ # of plane bend / improper torsion. They may be created later on.
576
+ else:
577
+ fourth_atom = list(bond_set - intersect)
578
+ dihedral_ind = list(bend) + fourth_atom
579
+ # This way dihedrals may be generated that contain linear
580
+ # atoms and these would be undefinied. So we check for this.
581
+ if dihedral_valid(coords3d, dihedral_ind, deg_thresh=max_deg):
582
+ improper_candidates.append(dihedral_ind)
583
+ else:
584
+ log_dihed_skip(dihedral_ind)
585
+
586
+ # Now try to create the remaining improper dihedrals.
587
+ if (len(coords3d) >= 4) and (len(proper_dihedral_inds) == 0):
588
+ log(
589
+ logger,
590
+ "Could not define any proper dihedrals! Generating improper dihedrals!",
591
+ )
592
+ for improp in improper_candidates:
593
+ set_dihedral_index(improp, proper=False)
594
+ log(
595
+ logger,
596
+ "Permutational symmetry not considerd in generation of "
597
+ "improper dihedrals.",
598
+ )
599
+
600
+ return proper_dihedral_inds, improper_dihedral_inds
601
+
602
+
603
+ def sort_by_prim_type(to_sort=None):
604
+ if to_sort is None:
605
+ to_sort = list()
606
+
607
+ by_prim_type = [[], [], []]
608
+ for item in to_sort:
609
+ len_ = len(item)
610
+ # len -> index
611
+ # 2 -> 0 (bond)
612
+ # 3 -> 1 (bend)
613
+ # 4 -> 2 (torsion)
614
+ by_prim_type[len_ - 2].append(tuple(item))
615
+ return by_prim_type
616
+
617
+
618
+ CoordInfo = namedtuple(
619
+ "CoordInfo",
620
+ "bonds hydrogen_bonds interfrag_bonds aux_interfrag_bonds "
621
+ "bends linear_bends linear_bend_complements "
622
+ # "dihedrals typed_prims fragments cdm cbm".split(),
623
+ "proper_dihedrals improper_dihedrals "
624
+ "translation_inds rotation_inds cartesian_inds "
625
+ "typed_prims fragments".split(),
626
+ )
627
+
628
+
629
+ def setup_redundant(
630
+ atoms,
631
+ coords3d,
632
+ factor=BOND_FACTOR,
633
+ define_prims=None,
634
+ min_deg=BEND_MIN_DEG,
635
+ dihed_max_deg=DIHED_MAX_DEG,
636
+ lb_min_deg=None,
637
+ lb_max_bonds=4,
638
+ min_weight=None,
639
+ tric=False,
640
+ hybrid=False,
641
+ interfrag_hbonds=True,
642
+ hbond_angles=False,
643
+ freeze_atoms=None,
644
+ define_for=None,
645
+ internals_with_frozen=False,
646
+ rm_for_frag: Optional[set] = None,
647
+ logger=None,
648
+ ):
649
+ if define_prims is None:
650
+ define_prims = list()
651
+ if freeze_atoms is None:
652
+ freeze_atoms = list()
653
+ if define_for is None:
654
+ define_for = list()
655
+ if rm_for_frag is None:
656
+ rm_for_frag = set()
657
+
658
+ log(
659
+ logger,
660
+ f"Detecting primitive internals for {len(atoms)} atoms.\n"
661
+ f"Excluding {len(freeze_atoms)} frozen atoms from the internal coordinate setup.",
662
+ )
663
+
664
+ # Mask array. By default all atomes are used to generate internal coordinates.
665
+ use_atoms = np.ones_like(atoms, dtype=bool)
666
+ # Only use atoms in 'define_for' to generate internal coordinates
667
+ if define_for:
668
+ use_atoms[:] = False # Disable/mask all others
669
+ use_atoms[define_for] = True
670
+ # If not explicitly enabled, don't form internal coordinates containing frozen atoms.
671
+ # With 'internals_with_frozen', the bonds will be filtered for bonds, containing
672
+ # at most one frozen atom.
673
+ elif not internals_with_frozen:
674
+ use_atoms[freeze_atoms] = False
675
+ freeze_atom_set = set(freeze_atoms)
676
+ atoms = [atom for mobile, atom in zip(use_atoms, atoms) if mobile]
677
+ coords3d = coords3d[use_atoms]
678
+
679
+ # Maps (different) indices of mobile atoms back to their original indices
680
+ freeze_map = {
681
+ sub_ind: org_ind for sub_ind, org_ind in enumerate(np.where(use_atoms)[0])
682
+ }
683
+ mobile_org_inds = set(freeze_map.values())
684
+
685
+ def keep_coord(prim_cls, prim_inds):
686
+ return (
687
+ True
688
+ if (min_weight is None)
689
+ else (prim_cls._weight(atoms, coords3d, prim_inds, 0.12) >= min_weight)
690
+ )
691
+
692
+ def keep_coords(prims, prim_cls):
693
+ return [prim for prim in prims if keep_coord(prim_cls, prim)]
694
+
695
+ # Bonds
696
+ bonds, cdm, cbm = get_bond_sets(
697
+ atoms,
698
+ coords3d,
699
+ bond_factor=factor,
700
+ return_cdm=True,
701
+ return_cbm=True,
702
+ )
703
+ if internals_with_frozen:
704
+ bonds = [bond for bond in bonds if len(set(bond) & freeze_atom_set) <= 1]
705
+ bonds = [tuple(bond) for bond in bonds]
706
+ bonds = keep_coords(bonds, Stretch)
707
+ bonds = [bond for bond in bonds if rm_for_frag.isdisjoint(set(bond))]
708
+
709
+ # Fragments
710
+ fragments = merge_sets(bonds) + [
711
+ frozenset((rmed_atom,)) for rmed_atom in rm_for_frag
712
+ ]
713
+ # Check for unbonded single atoms and create fragments for them.
714
+ bonded_set = set(tuple(np.ravel(bonds)))
715
+ unbonded_set = set(range(len(atoms))) - bonded_set - freeze_atom_set
716
+ log(
717
+ logger,
718
+ f"Merging bonded atoms yielded {len(fragments)} fragment(s) and "
719
+ f"{len(unbonded_set)} atoms.",
720
+ )
721
+ fragments.extend([frozenset((atom,)) for atom in unbonded_set])
722
+
723
+ interfrag_bonds = list()
724
+ aux_interfrag_bonds = list()
725
+ translation_inds = list()
726
+ rotation_inds = list()
727
+ # With translational & rotational internal coordinates (TRIC) we don't need
728
+ # interfragment coordinates.
729
+ if tric:
730
+ translation_inds = [list(fragment) for fragment in fragments]
731
+ # Exclude rotational coordinates for atomic species (1 atom)
732
+ rotation_inds = [list(fragment) for fragment in fragments if len(fragment) > 1]
733
+ # Without TRIC we have to somehow connect all fragments.
734
+ else:
735
+ # interfrag_bonds, aux_interfrag_bonds = connect_fragments_kmeans(
736
+ interfrag_bonds, aux_interfrag_bonds = connect_fragments_ahlrichs(
737
+ cdm, fragments, atoms, logger=logger
738
+ )
739
+
740
+ # Hydrogen bonds
741
+ assert interfrag_hbonds, "Disabling interfrag_hbonds is not yet supported!"
742
+ hydrogen_bonds = get_hydrogen_bond_inds(atoms, coords3d, bonds, logger=logger)
743
+ hydrogen_set = [frozenset(bond) for bond in hydrogen_bonds]
744
+
745
+ def remove_h_bonds(bond_list):
746
+ return [bond for bond in bond_list if set(bond) not in hydrogen_set]
747
+
748
+ # Remove newly obtained hydrogen bonds from other lists
749
+ interfrag_bonds = remove_h_bonds(interfrag_bonds)
750
+ aux_interfrag_bonds = remove_h_bonds(aux_interfrag_bonds)
751
+ bonds = remove_h_bonds(bonds)
752
+ aux_bonds = list() # Not defined by default
753
+ # Don't use auxilary interfragment bonds for bend detection
754
+ bonds_for_bends = [
755
+ bonds,
756
+ ]
757
+ # If we use regular redundant internals (not TRIC) we define interfragment
758
+ # bends.
759
+ if not tric:
760
+ bonds_for_bends += [interfrag_bonds]
761
+ if hbond_angles:
762
+ bonds_for_bends += [hydrogen_bonds]
763
+ bonds_for_bends = set([frozenset(bond) for bond in it.chain(*bonds_for_bends)])
764
+
765
+ # Bends
766
+ bends = get_bend_inds(
767
+ coords3d,
768
+ bonds_for_bends,
769
+ min_deg=min_deg,
770
+ # All bends will be checked, for being (close to) linear and will be removed from
771
+ # bend_inds, if needed. Thats why we keep 180° here.
772
+ max_deg=180.0,
773
+ logger=logger,
774
+ )
775
+ bends = keep_coords(bends, Bend)
776
+
777
+ # Linear Bends and orthogonal complements
778
+ linear_bends, linear_bend_complements = get_linear_bend_inds(
779
+ coords3d,
780
+ cbm,
781
+ bends,
782
+ min_deg=lb_min_deg,
783
+ max_bonds=lb_max_bonds,
784
+ logger=logger,
785
+ )
786
+ # Remove linear bends from bends
787
+ bends = [bend for bend in bends if bend not in linear_bends]
788
+ linear_bends = keep_coords(linear_bends, LinearBend)
789
+ linear_bend_complements = keep_coords(linear_bend_complements, LinearBend)
790
+
791
+ # Dihedrals
792
+ bends_for_dihedrals = bends + linear_bends
793
+ proper_dihedrals, improper_dihedrals = get_dihedral_inds(
794
+ coords3d,
795
+ bonds_for_bends,
796
+ bends_for_dihedrals,
797
+ max_deg=dihed_max_deg,
798
+ logger=logger,
799
+ )
800
+ proper_dihedrals = keep_coords(proper_dihedrals, Torsion)
801
+ improper_dihedrals = keep_coords(improper_dihedrals, Torsion)
802
+ # Improper dihedrals are disabled for now in TRIC
803
+ if tric:
804
+ improper_dihedrals = []
805
+
806
+ cartesian_inds = []
807
+ if hybrid:
808
+ cartesian_inds = [i for i, _ in enumerate(atoms)]
809
+
810
+ """
811
+ When additional primitives are given in 'define_prims', we want to append
812
+ them to the correct lists, that may contain already some primitive internals
813
+ detected by our algorithms. Here we define a map between the PrimTypes and the
814
+ present lists.
815
+ """
816
+ defined_cartesians = list()
817
+ defined_translations = list()
818
+ defined_rotations = list()
819
+
820
+ define_map = {
821
+ PrimTypes.BOND: "bonds",
822
+ PrimTypes.AUX_BOND: "aux_bonds",
823
+ PrimTypes.HYDROGEN_BOND: "hydrogen_bonds",
824
+ PrimTypes.INTERFRAG_BOND: "interfrag_bonds",
825
+ PrimTypes.AUX_INTERFRAG_BOND: "aux_interfrag_bonds",
826
+ PrimTypes.BEND: "bends",
827
+ PrimTypes.LINEAR_BEND: "linear_bends",
828
+ PrimTypes.LINEAR_BEND_COMPLEMENT: "linear_bend_complements",
829
+ PrimTypes.PROPER_DIHEDRAL: "proper_dihedrals",
830
+ PrimTypes.IMPROPER_DIHEDRAL: "improper_dihedrals",
831
+ }
832
+ unmapped_typed_prims = list()
833
+ for type_, *indices in define_prims:
834
+ try:
835
+ key = define_map[type_]
836
+ except KeyError:
837
+ try:
838
+ key = define_map[PrimTypes(type_)]
839
+ except KeyError:
840
+ """
841
+ With the current approach, some primitives in 'define_prims' can't
842
+ be mapped to their respective lists, e.g., given CARTESIAN_X/Y/Z.
843
+ Currently, every item in 'cartesian_inds' is expanded to three
844
+ Cartesians (X/Y/Z). When only the X-component is to be defined, adding
845
+ only the atom index to the list would result in all three components
846
+ to be generated.
847
+
848
+ So instead of adding them to their respective lists we keep them in
849
+ 'unmapped_typed_prims' and use them later as is.
850
+ """
851
+ unmapped_typed_prims.append((type_, *indices))
852
+ continue
853
+ locals()[key].append(tuple(indices))
854
+
855
+ def make_tp(prim_type, *indices):
856
+ """Map possibly modified indices to their original indices.
857
+
858
+ With frozen atoms, the indices used to set up internal coordinates do
859
+ not correspond to the actual indices. Here we map them back.
860
+ """
861
+ try:
862
+ org_indices = [freeze_map[ind] for ind in indices]
863
+ """The given 'indices' may not be present in freeze_map. This can happen,
864
+ when coordinates are rebuilt, frozen atoms are excluded from using them
865
+ in the coordinate definition and a previous set of coordinates ALREADY
866
+ using the original indices is supplied."""
867
+ except KeyError as error:
868
+ """In such a case we check if the given coordinates are already fully
869
+ defined in terms of original indices. If so, we use them as is."""
870
+ if set(indices) < mobile_org_inds:
871
+ org_indices = indices
872
+ else:
873
+ raise error
874
+ return (prim_type, *org_indices)
875
+
876
+ # Shortcut for PrimTypes Enum
877
+ pt = PrimTypes
878
+ # Create actual typed prims with the desired indices
879
+ typed_prims = (
880
+ # Bonds, two indices
881
+ [make_tp(pt.BOND, *bond) for bond in bonds]
882
+ + [make_tp(pt.AUX_BOND, *abond) for abond in aux_bonds]
883
+ + [make_tp(pt.HYDROGEN_BOND, *hbond) for hbond in hydrogen_bonds]
884
+ + [make_tp(pt.INTERFRAG_BOND, *ifbond) for ifbond in interfrag_bonds]
885
+ + [make_tp(pt.AUX_INTERFRAG_BOND, *aifbond) for aifbond in aux_interfrag_bonds]
886
+ # Bends, three indices
887
+ + [make_tp(pt.BEND, *bend) for bend in bends]
888
+ + [make_tp(pt.LINEAR_BEND, *lbend) for lbend in linear_bends]
889
+ + [
890
+ make_tp(pt.LINEAR_BEND_COMPLEMENT, *lbendc)
891
+ for lbendc in linear_bend_complements
892
+ ]
893
+ # Dihedral, four indices
894
+ + [make_tp(pt.PROPER_DIHEDRAL, *pdihedral) for pdihedral in proper_dihedrals]
895
+ + [
896
+ make_tp(pt.IMPROPER_DIHEDRAL, *idihedral)
897
+ for idihedral in improper_dihedrals
898
+ ]
899
+ + [make_tp(pt.CARTESIAN_X, cind) for cind in cartesian_inds]
900
+ + [make_tp(pt.CARTESIAN_Y, cind) for cind in cartesian_inds]
901
+ + [make_tp(pt.CARTESIAN_Z, cind) for cind in cartesian_inds]
902
+ + [make_tp(pt.CARTESIAN_Z, cind) for cind in cartesian_inds]
903
+ )
904
+
905
+ # Translational and rotational coordinates result in 3 different coordinates each
906
+ for frag in translation_inds:
907
+ typed_prims += [
908
+ make_tp(pt.TRANSLATION_X, *frag),
909
+ make_tp(pt.TRANSLATION_Y, *frag),
910
+ make_tp(pt.TRANSLATION_Z, *frag),
911
+ ]
912
+ for frag in rotation_inds:
913
+ typed_prims += [
914
+ make_tp(pt.ROTATION_A, *frag),
915
+ make_tp(pt.ROTATION_B, *frag),
916
+ make_tp(pt.ROTATION_C, *frag),
917
+ ]
918
+ typed_prims += unmapped_typed_prims
919
+ # Drop duplicated typed_prims
920
+ typed_prims = tuple(dict.fromkeys(typed_prims))
921
+
922
+ coord_info = CoordInfo(
923
+ bonds=bonds,
924
+ hydrogen_bonds=hydrogen_bonds,
925
+ interfrag_bonds=interfrag_bonds,
926
+ aux_interfrag_bonds=aux_interfrag_bonds,
927
+ bends=bends,
928
+ linear_bends=linear_bends,
929
+ linear_bend_complements=linear_bend_complements,
930
+ proper_dihedrals=proper_dihedrals,
931
+ improper_dihedrals=improper_dihedrals,
932
+ translation_inds=translation_inds,
933
+ rotation_inds=rotation_inds,
934
+ cartesian_inds=cartesian_inds,
935
+ typed_prims=typed_prims,
936
+ fragments=fragments,
937
+ )
938
+ return coord_info
939
+
940
+
941
+ def setup_redundant_from_geom(geom, *args, **kwargs):
942
+ return setup_redundant(geom.atoms, geom.coords3d, *args, **kwargs)
943
+
944
+
945
+ def get_primitives(coords3d, typed_prims, logger=None):
946
+ primitives = list()
947
+ for type_, *indices in typed_prims:
948
+ cls = PrimMap[type_]
949
+ cls_kwargs = {"indices": indices}
950
+ if type_ in Rotations:
951
+ cls_kwargs["ref_coords3d"] = coords3d
952
+ primitives.append(cls(**cls_kwargs))
953
+
954
+ msg = (
955
+ "Defined primitives\n"
956
+ + "\n".join(
957
+ [f"\t{i:03d}: {str(p.indices): >14}" for i, p in enumerate(primitives)]
958
+ )
959
+ + "\n"
960
+ )
961
+ log(logger, msg)
962
+ return primitives