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,1129 @@
1
+ # [1] https://www.sciencedirect.com/science/article/pii/S0166128098004758
2
+ # https://doi.org/10.1016/S0166-1280(98)00475-8
3
+ # Dapprich, Frisch, 1998
4
+ # [2] https://onlinelibrary.wiley.com/doi/abs/10.1002/9783527629213.ch2
5
+ # Clemente, Frisch, 2010
6
+ #
7
+ # Not implemented in pysisyphus
8
+ #
9
+ # [2] https://aip.scitation.org/doi/pdf/10.1063/1.2814164?class=pdf
10
+ # QM/QM ONIOM EE based on Mulliken charges
11
+ # Hratchian, Raghavachari, 2008
12
+ # [3] https://aip.scitation.org/doi/full/10.1063/1.3315417<Paste>
13
+ # QM/QM ONIOM EE based on Löwdin charges
14
+ # Mayhall, Hratchian, 2010
15
+ # [4] https://www.frontiersin.org/articles/10.3389/fchem.2018.00089/full
16
+ # Overview on hybrid methods
17
+ # [5] https://doi.org/10.1021/jp0446332
18
+ # Electronic embedding charge redistribution
19
+ # Lin, Truhlar 2004
20
+ # Excited state ONIOM
21
+ # [5] https://aip.scitation.org/doi/pdf/10.1063/1.4972000?class=pdf
22
+ # [6] https://pubs.rsc.org/en/content/articlehtml/2012/pc/c2pc90007f
23
+
24
+
25
+ import itertools as it
26
+ import logging
27
+ from collections import namedtuple
28
+
29
+ import numpy as np
30
+ import scipy.sparse as sparse
31
+
32
+ from pysisyphus.calculators import (
33
+ Composite,
34
+ Gaussian16,
35
+ MOPAC,
36
+ OBabel,
37
+ OpenMolcas,
38
+ ORCA,
39
+ ORCA5,
40
+ Psi4,
41
+ Turbomole,
42
+ XTB,
43
+ PyXTB,
44
+ )
45
+ from pysisyphus.calculators.Calculator import Calculator
46
+ from pysisyphus.elem_data import COVALENT_RADII as CR
47
+ from pysisyphus.Geometry import Geometry
48
+ from pysisyphus.helpers_pure import full_expand
49
+ from pysisyphus.intcoords.setup import get_bond_sets
50
+ from pysisyphus.intcoords.setup_fast import get_bond_vec_getter
51
+
52
+
53
+ CALC_DICT = {
54
+ "composite": Composite,
55
+ "g16": Gaussian16,
56
+ "openmolcas": OpenMolcas.OpenMolcas,
57
+ "mopac": MOPAC,
58
+ "obabel": OBabel,
59
+ "orca": ORCA.ORCA,
60
+ "orca5": ORCA5.ORCA5,
61
+ "psi4": Psi4,
62
+ "turbomole": Turbomole,
63
+ "xtb": XTB.XTB,
64
+ # "pypsi4": PyPsi4,
65
+ "pyxtb": PyXTB.PyXTB,
66
+ }
67
+ try:
68
+ from pysisyphus.calculators.PySCF import PySCF
69
+
70
+ CALC_DICT["pyscf"] = PySCF
71
+ except (ModuleNotFoundError, ImportError, OSError):
72
+ # print("Error importing PySCF in ONIOMv2")
73
+ pass
74
+
75
+
76
+ Link = namedtuple("Link", "ind parent_ind atom g")
77
+
78
+
79
+ def get_g_value(atom, parent_atom, link_atom):
80
+ cr, pcr, lcr = [CR[a.lower()] for a in (atom, parent_atom, link_atom)]
81
+
82
+ # Ratio between sum of CR_atom and CR_link with sum of CR_atom CR_parent_atom.
83
+ # See [1] Sect. 2.2 page 5.
84
+ g = (cr + lcr) / (cr + pcr)
85
+ return g
86
+
87
+
88
+ def cap_fragment(atoms, coords, fragment, link_atom="H", g=None):
89
+ coords3d = coords.reshape(-1, 3)
90
+
91
+ fragment_set = set(fragment)
92
+
93
+ # Determine bond(s) that connect fragment with the rest
94
+ bonds = get_bond_sets(atoms, coords3d)
95
+ bond_sets = [set(b) for b in bonds]
96
+
97
+ # Find all bonds that involve one atom of model. These bonds
98
+ # connect the model to the real geometry. We want to cap these
99
+ # bonds.
100
+ break_bonds = [b for b in bond_sets if len(b & fragment_set) == 1]
101
+
102
+ # Put capping atoms at every bond to break.
103
+ # The model fragment size corresponds to the length of the union of
104
+ # the model set and the atoms in break_bonds.
105
+ capped_frag = fragment_set.union(*break_bonds)
106
+ capped_inds = list(sorted(capped_frag))
107
+
108
+ # Index map between the new model geometry and the original indices
109
+ # in the real geometry.
110
+ atom_map = {
111
+ model_ind: real_ind
112
+ for real_ind, model_ind in zip(capped_inds, range(len(capped_inds)))
113
+ }
114
+
115
+ links = list()
116
+ for bb in break_bonds:
117
+ to_cap = bb - fragment_set
118
+ assert len(to_cap) == 1
119
+ ind = list(bb - to_cap)[0]
120
+ parent_ind = tuple(to_cap)[0]
121
+ if g is None:
122
+ g = get_g_value(atoms[ind], atoms[parent_ind], link_atom)
123
+ link = Link(ind=ind, parent_ind=parent_ind, atom=link_atom, g=g)
124
+ links.append(link)
125
+
126
+ return atom_map, links
127
+
128
+
129
+ def atom_inds_to_cart_inds(atom_inds):
130
+ stencil = np.array((0, 1, 2), dtype=int)
131
+ size_ = len(atom_inds)
132
+ cart_inds = np.tile(stencil, size_) + np.repeat(atom_inds, 3) * 3
133
+ return cart_inds
134
+
135
+
136
+ class ModelDummyCalc:
137
+ def __init__(self, model, cap=False): # , all_atoms, all_coords):
138
+ self.model = model
139
+ self.cap = cap
140
+
141
+ def get_energy(self, atoms, coords):
142
+ energy = self.model.get_energy(atoms, coords, cap=self.cap)
143
+ results = {"energy": energy}
144
+ return results
145
+
146
+ def get_forces(self, atoms, coords):
147
+ # if self.parent_name is not None:
148
+ # raise Exception("Currently, this does not work for Models with a parent!")
149
+ energy, forces = self.model.get_forces(atoms, coords, cap=self.cap)
150
+ forces_ = np.zeros((len(atoms), 3))
151
+ # The redistribution below only works withouth parent, as otherwise
152
+ # the numer of atoms on the RHS and LHS differ.
153
+ forces_[: len(atoms) - len(self.model.links)] = forces.reshape(-1, 3)[
154
+ self.model.atom_inds
155
+ ]
156
+ results = {"energy": energy, "forces": forces_.flatten()}
157
+ return results
158
+
159
+ # def get_hessian(self, atoms, coords):
160
+ # energy, hessian = self.model.get_hessian(atoms, coords, cap=False)
161
+ # results = {"energy": energy, "hessian": hessian}
162
+ # return results
163
+
164
+
165
+ class Model:
166
+ def __init__(
167
+ self,
168
+ name,
169
+ calc_level,
170
+ calc,
171
+ parent_name,
172
+ parent_calc_level,
173
+ parent_calc,
174
+ atom_inds,
175
+ parent_atom_inds,
176
+ use_link_atoms=True,
177
+ ):
178
+
179
+ self.name = name
180
+ self.calc_level = calc_level
181
+ self.calc = calc
182
+
183
+ self.parent_name = parent_name
184
+ self.parent_calc_level = parent_calc_level
185
+ self.parent_calc = parent_calc
186
+
187
+ self.atom_inds = list(atom_inds)
188
+ # parent_atom_inds may be None
189
+ try:
190
+ self.parent_atom_inds = list(parent_atom_inds)
191
+ except TypeError:
192
+ self.parent_atom_inds = None
193
+
194
+ self.use_link_atoms = use_link_atoms
195
+
196
+ self.links = list()
197
+ self.capped = False
198
+ self.J = None
199
+
200
+ self.log(f"Created model '{self}' with {len(self.atom_inds)} atoms.")
201
+
202
+ def log(self, message=""):
203
+ logger = logging.getLogger("calculator")
204
+ logger.debug(self.__str__() + " " + message)
205
+
206
+ def create_links(self, atoms, coords, debug=False):
207
+ self.capped = True
208
+
209
+ if self.use_link_atoms and self.parent_name is not None:
210
+ _, self.links = cap_fragment(atoms, coords, self.atom_inds)
211
+ self.capped_atom_num = len(self.atom_inds) + len(self.links)
212
+ for i, link in enumerate(self.links):
213
+ ind, parent_ind = link.ind, link.parent_ind
214
+ self.log(
215
+ f"\tCreated Link atom ({link.atom}) between {atoms[ind]}{ind} "
216
+ f"and {atoms[parent_ind]}{parent_ind} (g={link.g:.6f})"
217
+ )
218
+
219
+ if len(self.links) == 0:
220
+ self.log("Didn't create any link atoms!\n")
221
+
222
+ # self.J = self.get_jacobian()
223
+ self.J = self.get_sparse_jacobian()
224
+
225
+ if debug:
226
+ catoms, ccoords = self.capped_atoms_coords(atoms, coords)
227
+ geom = Geometry(catoms, ccoords)
228
+ geom.jmol()
229
+
230
+ def capped_atoms_coords(self, all_atoms, all_coords):
231
+ assert self.capped, "Did you forget to call create_links()?"
232
+
233
+ org_atom_num = len(self.atom_inds)
234
+ c3d = all_coords.reshape(-1, 3)
235
+
236
+ capped_atoms = [all_atoms[i] for i in self.atom_inds]
237
+ # Initialize empty coordinate array
238
+ capped_coords = np.zeros((self.capped_atom_num, 3))
239
+ # Copy non-capped coordinates
240
+ capped_coords[:org_atom_num] = c3d[self.atom_inds]
241
+
242
+ for i, link in enumerate(self.links):
243
+ capped_atoms.append(link.atom)
244
+
245
+ r1 = c3d[link.ind]
246
+ r3 = c3d[link.parent_ind]
247
+ r2 = r1 + link.g * (r3 - r1)
248
+ capped_coords[org_atom_num + i] = r2
249
+ return capped_atoms, capped_coords
250
+
251
+ def create_bond_vec_getters(self, atoms):
252
+ link_parent_inds = [link.parent_ind for link in self.links]
253
+ no_bonds_with = [
254
+ [
255
+ link.ind,
256
+ ]
257
+ for link in self.links
258
+ ]
259
+ self.log(
260
+ f"Model has {len(link_parent_inds)} link atom hosts: {link_parent_inds}"
261
+ )
262
+ covalent_radii = [CR[atom.lower()] for atom in atoms]
263
+ self.get_bond_vecs = get_bond_vec_getter(
264
+ atoms,
265
+ covalent_radii,
266
+ link_parent_inds,
267
+ no_bonds_with,
268
+ )
269
+
270
+ def get_jacobian(self):
271
+ try:
272
+ # Shape of Jacobian is (model + link, real). TypeError will be raised
273
+ # when self.parent_atom_inds is None.
274
+ jac_shape = (
275
+ len(self.atom_inds) * 3 + len(self.links) * 3,
276
+ len(self.parent_atom_inds) * 3,
277
+ )
278
+ except TypeError:
279
+ return None
280
+
281
+ J = np.zeros(jac_shape)
282
+ # Stencil for diagonal elements of 3x3 submatrix
283
+ stencil = np.array((0, 1, 2), dtype=int)
284
+ size_ = len(self.atom_inds)
285
+
286
+ model_rows = np.arange(size_ * 3)
287
+ # When more than two layers are present the inner layers aren't directly
288
+ # embedded in the outermost layer. This means parent_inds does not begin
289
+ # at 0, but with a higher index. So we need a map of the actual indices
290
+ # (not starting at 0) to the indices in the Jacobian which start at 0.
291
+ atom_inds = [self.parent_atom_inds.index(ind) for ind in self.atom_inds]
292
+ ind_map = {k: v for k, v in zip(self.atom_inds, atom_inds)}
293
+ model_cols = atom_inds_to_cart_inds(atom_inds)
294
+ J[model_rows, model_cols] = 1
295
+
296
+ # Link atoms
297
+ link_start = model_rows.max() + 1
298
+ for i, (ind, parent_ind, atom, g) in enumerate(self.links):
299
+ rows = link_start + i * 3 + stencil
300
+ cols = ind_map[ind] * 3 + stencil
301
+
302
+ J[rows, cols] = 1 - g
303
+ try:
304
+ parent_cols = self.parent_atom_inds.index(parent_ind) * 3 + stencil
305
+ J[rows, parent_cols] = g
306
+ # Raised when link atom is not coupled to layer above, but
307
+ # to a layer higher above.
308
+ except ValueError:
309
+ pass
310
+
311
+ return J
312
+
313
+ def get_sparse_jacobian(self):
314
+ try:
315
+ # Shape of Jacobian is (model + link, real). TypeError will be raised
316
+ # when self.parent_atom_inds is None.
317
+ jac_shape = (
318
+ len(self.atom_inds) * 3 + len(self.links) * 3,
319
+ len(self.parent_atom_inds) * 3,
320
+ )
321
+ except TypeError:
322
+ return None
323
+
324
+ # Stencil for diagonal elements of 3x3 submatrix
325
+ stencil = np.array((0, 1, 2), dtype=int)
326
+ ones = np.ones_like(stencil)
327
+ size_ = len(self.atom_inds)
328
+
329
+ model_rows = np.arange(size_ * 3)
330
+ # When more than two layers are present the inner layers aren't directly
331
+ # embedded in the outermost layer. This means parent_inds does not begin
332
+ # at 0, but with a higher index. So we need a map of the actual indices
333
+ # (not starting at 0) to the indices in the Jacobian which start at 0.
334
+ atom_inds = [self.parent_atom_inds.index(ind) for ind in self.atom_inds]
335
+ ind_map = {k: v for k, v in zip(self.atom_inds, atom_inds)}
336
+ model_cols = atom_inds_to_cart_inds(atom_inds)
337
+
338
+ jac_rows = model_rows.tolist()
339
+ jac_cols = model_cols.tolist()
340
+ jac_data = np.ones_like(jac_cols).tolist()
341
+
342
+ # Link atoms
343
+ link_start = model_rows.max() + 1
344
+ for i, (ind, parent_ind, atom, g) in enumerate(self.links):
345
+ rows = (link_start + i * 3 + stencil).tolist()
346
+ cols = (ind_map[ind] * 3 + stencil).tolist()
347
+ jac_rows += rows
348
+ jac_cols += cols
349
+ jac_data += (ones - g).tolist()
350
+
351
+ try:
352
+ parent_cols = (
353
+ self.parent_atom_inds.index(parent_ind) * 3 + stencil
354
+ ).tolist()
355
+ jac_rows += rows
356
+ jac_cols += parent_cols
357
+ jac_data += np.full_like(parent_cols, g, dtype=float).tolist()
358
+ # Raised when link atom is not coupled to layer above, but
359
+ # to a layer higher above.
360
+ except ValueError:
361
+ pass
362
+ J = sparse.csr_matrix((jac_data, (jac_rows, jac_cols)), shape=jac_shape)
363
+
364
+ return J
365
+
366
+ def get_energy(
367
+ self, atoms, coords, point_charges=None, parent_correction=True, cap=True
368
+ ):
369
+ self.log("Energy calculation")
370
+ if cap:
371
+ catoms, ccoords = self.capped_atoms_coords(atoms, coords)
372
+ else:
373
+ catoms = atoms
374
+ ccoords = coords
375
+
376
+ prepare_kwargs = {
377
+ "point_charges": point_charges,
378
+ }
379
+
380
+ self.log("Calculation at layer level")
381
+ results = self.calc.get_energy(catoms, ccoords, **prepare_kwargs)
382
+ energy = results["energy"]
383
+
384
+ # Calculate correction if parent layer is present and it is requested
385
+ if (self.parent_calc is not None) and parent_correction:
386
+ self.log("Calculation at parent layer level")
387
+ parent_results = self.parent_calc.get_energy(
388
+ catoms, ccoords, **prepare_kwargs
389
+ )
390
+ parent_energy = parent_results["energy"]
391
+ energy -= parent_energy
392
+ elif not parent_correction:
393
+ self.log("No parent correction!")
394
+
395
+ return energy
396
+
397
+ def get_forces(
398
+ self, atoms, coords, point_charges=None, parent_correction=True, cap=True
399
+ ):
400
+ self.log("Force calculation")
401
+ # catoms, ccoords = self.capped_atoms_coords(atoms, coords)
402
+ if cap:
403
+ catoms, ccoords = self.capped_atoms_coords(atoms, coords)
404
+ else:
405
+ catoms = atoms
406
+ ccoords = coords
407
+
408
+ prepare_kwargs = {
409
+ "point_charges": point_charges,
410
+ }
411
+
412
+ self.log("Calculation at layer level")
413
+ results = self.calc.get_forces(catoms, ccoords, **prepare_kwargs)
414
+ # These forces can contain contributions from link atoms.
415
+ forces = results["forces"]
416
+ energy = results["energy"]
417
+ # Redistribute link atom forces onto the two link atom hosts using the
418
+ # Jacobian J. Afterwards, the forces will have the shape of the parent-forces.
419
+ if self.J is not None:
420
+ # forces = forces.dot(self.J)
421
+ # f^T J = (J^T f)^T
422
+ # The transpose of the term in brackets can be ignored here, as numpy
423
+ # does not distinguish between f and f^T for a 1d-array.
424
+ forces = self.J.T @ forces
425
+
426
+ # Calculate correction if parent layer is present and it is requested
427
+ if (self.parent_calc is not None) and parent_correction:
428
+ self.log("Calculation at parent layer level")
429
+ parent_results = self.parent_calc.get_forces(
430
+ catoms, ccoords, **prepare_kwargs
431
+ )
432
+ parent_forces = parent_results["forces"]
433
+ parent_energy = parent_results["energy"]
434
+
435
+ # Correct energy and forces
436
+ energy -= parent_energy
437
+ forces -= self.J.T @ parent_forces
438
+ elif not parent_correction:
439
+ self.log("No parent correction!")
440
+
441
+ return energy, forces
442
+
443
+ def get_hessian(
444
+ self, atoms, coords, point_charges=None, parent_correction=True, cap=True
445
+ ):
446
+ self.log("Hessian calculation")
447
+ # catoms, ccoords = self.capped_atoms_coords(atoms, coords)
448
+ if cap:
449
+ catoms, ccoords = self.capped_atoms_coords(atoms, coords)
450
+ else:
451
+ catoms = atoms
452
+ ccoords = coords
453
+
454
+ prepare_kwargs = {
455
+ "point_charges": point_charges,
456
+ }
457
+
458
+ self.log("Calculation at layer level")
459
+ # results = self.calc.get_hessian(catoms, ccoords, prepare_kwargs)
460
+ results = self.calc.get_hessian(catoms, ccoords, **prepare_kwargs)
461
+ hessian = results["hessian"]
462
+ energy = results["energy"]
463
+ if self.J is not None:
464
+ # hessian = self.J.T.dot(hessian.dot(self.J))
465
+ hessian = (self.J.T @ hessian) @ self.J
466
+
467
+ # Calculate correction if parent layer is present and it is requested
468
+ if (self.parent_calc is not None) and parent_correction:
469
+ self.log("Calculation at parent layer level")
470
+ parent_results = self.parent_calc.get_hessian(
471
+ catoms, ccoords, **prepare_kwargs
472
+ )
473
+ parent_hessian = parent_results["hessian"]
474
+ parent_energy = parent_results["energy"]
475
+
476
+ # Correct energy and hessian
477
+ energy -= parent_energy
478
+ # hessian -= self.J.T.dot(parent_hessian.dot(self.J))
479
+ hessian -= (self.J.T @ parent_hessian) @ self.J
480
+ elif not parent_correction:
481
+ self.log("No parent correction!")
482
+
483
+ return energy, hessian
484
+
485
+ # def get_delta_S(self, atoms, coords):
486
+ # self.log("ΔS calculation")
487
+ # catoms, ccoords = self.capped_atoms_coords(atoms, coords)
488
+
489
+ # # Parent calculator
490
+ # E_parent_real = self.parent_calc.get_energy(atoms, coords)["energy"]
491
+ # self.parent_calc.reset()
492
+ # E_parent_model = self.parent_calc.get_energy(catoms, ccoords)["energy"]
493
+ # S_low = E_parent_real - E_parent_model
494
+ # self.log(f"S_low={S_low:.6f} au")
495
+ # print(f"S_low={S_low:.6f} au")
496
+ # # High level calculator
497
+ # E_high_real = self.calc.get_energy(atoms, coords)["energy"]
498
+ # self.calc.reset()
499
+ # E_high_model = self.calc.get_energy(catoms, ccoords)["energy"]
500
+ # S_high = E_high_real - E_high_model
501
+ # self.log(f"S_high={S_high:.6f} au")
502
+ # print(f"S_high={S_high:.6f} au")
503
+
504
+ # delta_S = S_low - S_high
505
+ # self.log(f"ΔS={delta_S:.6f} au")
506
+ # print(f"ΔS={delta_S:.6f} au")
507
+
508
+ # return delta_S
509
+
510
+ def parse_charges(self):
511
+ charges = self.calc.parse_charges()
512
+ try:
513
+ parent_charges = self.parent_calc.parse_charges()
514
+ except AttributeError:
515
+ parent_charges = None
516
+
517
+ return charges, parent_charges
518
+
519
+ def as_geom(self, all_atoms, all_coords):
520
+ capped_atoms, capped_coords3d = self.capped_atoms_coords(all_atoms, all_coords)
521
+ geom = Geometry(capped_atoms, capped_coords3d)
522
+ dummy_calc = self.as_calculator()
523
+ geom.set_calculator(dummy_calc)
524
+ return geom
525
+
526
+ def as_calculator(self, cap=False):
527
+ return ModelDummyCalc(self, cap=cap)
528
+
529
+ def __str__(self):
530
+ # return (
531
+ # f"Model({self.name}, {len(self.atom_inds)} atoms, "
532
+ # f"level={self.calc_level}, parent_level={self.parent_calc_level})"
533
+ # )
534
+ return f"{self.name}_{self.calc_level}"
535
+
536
+ def __repr__(self):
537
+ return self.__str__()
538
+
539
+
540
+ def get_embedding_charges(embedding, layer, parent_layer, coords3d):
541
+ # Only consider charges that belong to atoms in the parent
542
+ # layer. Otherwise this would result in additonal charges at
543
+ # the same positions as the atoms we would like to calculate.
544
+ if "electronic" in embedding:
545
+ assert (
546
+ len(parent_layer) == 1
547
+ ), "Multicenter ONIOM in intermediate layer is not supported!"
548
+ parent_model = parent_layer[0]
549
+ parent_inds = parent_model.atom_inds
550
+ point_charges, _ = parent_model.parse_charges()
551
+
552
+ layer_inds = set(*it.chain([model.atom_inds for model in layer]))
553
+ # Determine indices of atoms that are in the parent layer, but
554
+ # not in the current layer
555
+ only_parent_inds = list(set(parent_inds) - layer_inds)
556
+
557
+ del_charge_inds = list()
558
+ all_redist_coords_charges = list()
559
+ # Here, redistributed and scaled charges are calculated. In the EE-RC and EE-RCD
560
+ # schemes the link atom parent (LAP) charges are divided by the number of bonds
561
+ # connected to the LAP minus 1. They are put halfway along theses bonds.
562
+ # See [5] for a discussion.
563
+ # EE-RC and EE-RCD are very similar. The block below handles calculations that
564
+ # are common to both methods, e.g. calculation of the redistributed charges and
565
+ # their coordinates.
566
+ #
567
+ # This will be executed for 'electronic_rc' and 'electronic_rcd'
568
+ if "electronic_rc" in embedding:
569
+ # Collect charges for models in a layer, e.g., for multicenter ONIOM.
570
+ for model in layer:
571
+ redist_coords_charges = list()
572
+ single_redist_charges = list()
573
+ # Determine bonds, connected to link parent.
574
+ link_host_bond_vecs, bonded_inds = model.get_bond_vecs(
575
+ coords3d, return_bonded_inds=True
576
+ )
577
+ # Determine link atoms
578
+ links = model.links
579
+ for link, bond_vecs in zip(links, link_host_bond_vecs):
580
+ parent_ind = link.parent_ind
581
+ # Presence of a link atom implies a bond.
582
+ assert len(bond_vecs) > 0
583
+ # *parent_coords, parent_charge = point_charges[link.parent_ind]
584
+ # parent_charge = ee_charges[parent_ind]
585
+ parent_charge = point_charges[parent_ind]
586
+ parent_coords = coords3d[parent_ind]
587
+ bond_num = len(bond_vecs)
588
+ redist_charge = parent_charge / bond_num
589
+ single_redist_charges.append(redist_charge)
590
+ # Put modified charges halfway on the bonds
591
+ redist_coords = parent_coords + bond_vecs / 2
592
+ redist_coords_charges.extend(
593
+ [(*coords, redist_charge) for coords in redist_coords]
594
+ )
595
+ del_charge_inds.append(parent_ind)
596
+ redist_coords_charges = np.array(redist_coords_charges)
597
+
598
+ # Redistributed charges and dipoles to preserve the M1-M2 bond dipoles. See [5].
599
+ if embedding == "electronic_rcd":
600
+ # Multiply all redistributed charges by 2
601
+ redist_coords_charges[:, -1] *= 2
602
+ # Substract original redistributed charge from M2 charges
603
+ for binds, src in zip(bonded_inds, single_redist_charges):
604
+ point_charges[binds] -= src
605
+
606
+ # Gather redistributed charges of separate models (centers)
607
+ all_redist_coords_charges.extend(redist_coords_charges)
608
+
609
+ assert len(del_charge_inds) == len(set(del_charge_inds)), (
610
+ "It seems that one parent hosts multiple link atoms. I did not think about "
611
+ "cases like that yet!"
612
+ )
613
+ # Only keep charges that are not on link atom hosts/parents
614
+ keep_mask = [opi for opi in only_parent_inds if opi not in del_charge_inds]
615
+ kept_point_charges = point_charges[keep_mask]
616
+ kept_coords3d = coords3d[keep_mask]
617
+ kept_coords_point_charges = np.concatenate(
618
+ (kept_coords3d, kept_point_charges[:, None]), axis=1
619
+ )
620
+
621
+ # Join unmodified charges and redistributed charges
622
+ if len(all_redist_coords_charges) > 0:
623
+ kept_coords_point_charges = np.concatenate(
624
+ (kept_coords_point_charges, all_redist_coords_charges), axis=0
625
+ )
626
+ return kept_coords_point_charges
627
+
628
+
629
+ class LayerCalc:
630
+ def __init__(self, models, total_size, parent_layer_calc=None):
631
+ self.models = models
632
+ self.parent_layer_calc = parent_layer_calc
633
+ self.total_size = total_size # atoms in the total system
634
+
635
+ self.models_str = ", ".join([str(model) for model in self.models])
636
+
637
+ @property
638
+ def mult(self):
639
+ mults = [model.calc.mult for model in self.models]
640
+ mult0 = mults[0]
641
+ assert all([mult == mult0 for mult in mults])
642
+ return mult0
643
+
644
+ @mult.setter
645
+ def mult(self, mult):
646
+ for model in self.models:
647
+ model.calc.mult = mult
648
+
649
+ @property
650
+ def charge(self):
651
+ charges = [model.calc.mult for model in self.models]
652
+ charge0 = charges[0]
653
+ assert all([charge == charge0 for charge in charges])
654
+ return charge0
655
+
656
+ @charge.setter
657
+ def charge(self, charge):
658
+ for model in self.models:
659
+ model.calc.charge = charge
660
+
661
+ def run_calculations(self, atoms, coords, method):
662
+ results = [getattr(model, method)(atoms, coords) for model in self.models]
663
+ return results
664
+
665
+ @staticmethod
666
+ def energy_from_results(model_energies, parent_energy=None):
667
+ energy = sum(model_energies)
668
+ if parent_energy is not None:
669
+ energy += parent_energy
670
+ return energy
671
+
672
+ def do_parent(self, with_parent):
673
+ return (self.parent_layer_calc is not None) and with_parent
674
+
675
+ def get_energy(self, atoms, coords, with_parent=True):
676
+ model_energies = self.run_calculations(atoms, coords, "get_energy")
677
+ if self.do_parent(with_parent):
678
+ parent_result = self.parent_layer_calc.get_energy(
679
+ atoms, coords, with_parent=True
680
+ )
681
+ parent_energy = parent_result["energy"]
682
+ else:
683
+ parent_energy = None
684
+ energy = self.energy_from_results(model_energies, parent_energy)
685
+ return {"energy": energy}
686
+
687
+ def get_forces(self, atoms, coords, with_parent=True):
688
+ full_forces = np.zeros((self.total_size, 3))
689
+ model_energies = list()
690
+ for model in self.models:
691
+ model_energy, model_forces = model.get_forces(
692
+ atoms, coords, parent_correction=True
693
+ )
694
+ model_energies.append(model_energy)
695
+ full_forces[model.parent_atom_inds] = model_forces.reshape(-1, 3)
696
+ forces = full_forces.flatten()
697
+ if self.do_parent(with_parent):
698
+ parent_result = self.parent_layer_calc.get_forces(
699
+ atoms, coords, with_parent=True
700
+ )
701
+ parent_energy = parent_result["energy"]
702
+ parent_forces = parent_result["forces"]
703
+ forces += parent_forces
704
+ else:
705
+ parent_energy = None
706
+ energy = self.energy_from_results(model_energies, parent_energy)
707
+ results = {
708
+ "energy": energy,
709
+ "forces": forces,
710
+ }
711
+ return results
712
+
713
+ def get_hessian(self, atoms, coords, with_parent=True):
714
+ hessian = np.zeros((self.total_size*3, self.total_size*3))
715
+ model_energies = list()
716
+ for model in self.models:
717
+ model_energy, model_hessian = model.get_hessian(
718
+ atoms, coords, parent_correction=True
719
+ )
720
+ model_energies.append(model_energy)
721
+ try:
722
+ inds = 3*model.parent_atom_inds[:, None] + np.arange(3)
723
+ hessian[inds, inds] += model_hessian
724
+ except TypeError:
725
+ hessian += model_hessian
726
+ if self.do_parent(with_parent):
727
+ parent_result = self.parent_layer_calc.get_hessian(
728
+ atoms, coords, with_parent=True
729
+ )
730
+ parent_energy = parent_result["energy"]
731
+ parent_hessian = parent_result["hessian"]
732
+ hessian += parent_hessian
733
+ else:
734
+ parent_energy = None
735
+ energy = self.energy_from_results(model_energies, parent_energy)
736
+ result = {
737
+ "energy": energy,
738
+ "hessian": hessian,
739
+ }
740
+ return result
741
+
742
+ def __str__(self):
743
+ return f"LayerCalc({self.models_str})"
744
+
745
+ def __repr__(self):
746
+ return self.__str__()
747
+
748
+
749
+ class ONIOM(Calculator):
750
+ embeddings = {
751
+ "": "",
752
+ "electronic": "Electronic embedding",
753
+ "electronic_rc": "Electronic embedding with redistributed charges",
754
+ "electronic_rcd": "Electronic embedding with redistributed charges and dipoles",
755
+ }
756
+
757
+ def __init__(
758
+ self,
759
+ calcs,
760
+ models,
761
+ geom,
762
+ layers=None,
763
+ embedding="",
764
+ real_key="real",
765
+ use_link_atoms=True,
766
+ *args,
767
+ **kwargs,
768
+ ):
769
+ """
770
+ layer: list of models
771
+ len(layer) == 1: normal ONIOM, len(layer) >= 1: multicenter ONIOM.
772
+ model:
773
+ (sub)set of all atoms that resides in a certain layer and has
774
+ a certain calculator.
775
+ """
776
+
777
+ super().__init__(*args, **kwargs)
778
+
779
+ if embedding is None:
780
+ embedding = ""
781
+ assert (
782
+ embedding in self.embeddings.keys()
783
+ ), f"Valid embeddings are: {self.embeddings.keys()}"
784
+ self.embedding = embedding
785
+
786
+ assert real_key not in models, f'"{real_key}" must not be defined in "models"!'
787
+ assert real_key in calcs, f'"{real_key}" must be defined in "calcs"!'
788
+
789
+ self.use_link_atoms = use_link_atoms
790
+
791
+ # Expand index-lists in models
792
+ for model in models.values():
793
+ if ".." in model["inds"]:
794
+ model["inds"] = full_expand(model["inds"])
795
+
796
+ # When no ordering of layers is given we try to guess it from
797
+ # the size of the respective models. It's probably a better idea
798
+ # to always specify the layer ordering though ;)
799
+ if layers is None:
800
+ self.log(
801
+ "No explicit layer ordering specified! Determining layer "
802
+ "hierarchy from model sizes. This does not support multi-"
803
+ "center ONIOM!"
804
+ )
805
+ as_list = [(key, val) for key, val in models.items()]
806
+ # Determine hierarchy of models, from biggest to smallest model
807
+ layers = [
808
+ key
809
+ for key, val in sorted(
810
+ as_list, key=lambda model: -len(model[1]["inds"])
811
+ )
812
+ ]
813
+
814
+ assert real_key not in layers, f'"{real_key}" must not be defined in "layers"!'
815
+
816
+ ############
817
+ # #
818
+ # LAYERS #
819
+ # #
820
+ ############
821
+
822
+ # Add real model and layer as they are missing right now. The real
823
+ # layer is always the last layer. The real layer is always calculated
824
+ # by the 'realkey'-calculator.
825
+ layers = [real_key] + layers
826
+ models[real_key] = {
827
+ "calc": real_key,
828
+ "inds": list(range(len(geom.atoms))),
829
+ }
830
+ self.log(f"Layer-ordering from big to small: {layers}")
831
+
832
+ # Single-model layers will be given as strings. As we also support
833
+ # multicenter-ONIOM there may also be layers that are given as lists
834
+ # that contain multiple models per layer.
835
+ # Now we convert the single-model layers to lists of length 1, so
836
+ # every layer is a list.
837
+ layers = [
838
+ [
839
+ layer,
840
+ ]
841
+ if isinstance(layer, str)
842
+ else layer
843
+ for layer in layers
844
+ ]
845
+ self.layer_num = len(layers)
846
+ assert self.layer_num > 1, "ONIOM with only 1 layer requested. Aborting!"
847
+
848
+ ############
849
+ # #
850
+ # MODELS #
851
+ # #
852
+ ############
853
+
854
+ # Create mapping between model and its parent layer. Actually
855
+ # this is a bit hacky right now, as the mapping should not be between
856
+ # model and parent layer, but between model and parent model.
857
+ # This way we expect the parent layer to have the same calculator
858
+ # throughout, so multicenter ONIOM with different calculators
859
+ # in all but the smallest layer (highest level) is not well defined.
860
+ #
861
+ # If multicenter ONIOM in an intermediate layer is useful may
862
+ # be another question to be answered ;).
863
+ self.model_parent_layers = dict()
864
+ for i, layer in enumerate(layers[1:]):
865
+ self.model_parent_layers.update({model: i for model in layer})
866
+ model_keys = list(it.chain(*layers))
867
+
868
+ cur_calc_num = 0
869
+
870
+ def get_calc(calc_key, base_name=None):
871
+ """Helper function for easier generation of calculators
872
+ with incrementing calc_number."""
873
+ nonlocal cur_calc_num
874
+
875
+ kwargs = calcs[calc_key].copy()
876
+ type_ = kwargs.pop("type")
877
+
878
+ kwargs["calc_number"] = cur_calc_num
879
+ if base_name is not None:
880
+ kwargs["base_name"] = base_name
881
+
882
+ calc = CALC_DICT[type_](**kwargs)
883
+ cur_calc_num += 1
884
+ return calc
885
+
886
+ # Create models and required calculators.
887
+ self.models = list()
888
+ self.layers = [list() for _ in layers]
889
+ for model_key in model_keys:
890
+ model_calc_key = models[model_key]["calc"]
891
+ model_base_name = f"{model_key}_{model_calc_key}"
892
+ model_calc = get_calc(model_calc_key, base_name=model_base_name)
893
+ # Update parent information
894
+ try:
895
+ parent_layer_ind = self.model_parent_layers[model_key]
896
+ parent_layer = layers[parent_layer_ind]
897
+ parent_calc_keys = set(
898
+ [models[model_key]["calc"] for model_key in parent_layer]
899
+ )
900
+ assert len(parent_calc_keys) == 1, (
901
+ "It seems you are trying to run a multicenter ONIOM setup in "
902
+ "an intermediate layer with different calculators. This is "
903
+ "not supported right now."
904
+ )
905
+ parent_name = parent_layer[0]
906
+ parent_calc_key = models[parent_name]["calc"]
907
+ parent_base_name = f"{model}_parent"
908
+ parent_calc = get_calc(parent_calc_key, base_name=parent_base_name)
909
+ parent_atom_inds = models[parent_name]["inds"]
910
+ except KeyError:
911
+ parent_name = parent_calc_key = parent_calc = parent_atom_inds = None
912
+ parent_layer_ind = -1
913
+
914
+ model = Model(
915
+ name=model_key,
916
+ calc_level=model_calc_key,
917
+ calc=model_calc,
918
+ atom_inds=models[model_key]["inds"],
919
+ use_link_atoms=self.use_link_atoms,
920
+ #
921
+ parent_name=parent_name,
922
+ parent_calc_level=parent_calc_key,
923
+ parent_calc=parent_calc,
924
+ parent_atom_inds=parent_atom_inds,
925
+ )
926
+ self.models.append(model)
927
+ self.layers[parent_layer_ind + 1].append(model)
928
+
929
+ self.log("Created all ONIOM layers:")
930
+ for model in self.models:
931
+ self.log("\t" + str(model))
932
+
933
+ # Create link atoms
934
+ [model.create_links(geom.atoms, geom.cart_coords) for model in self.models]
935
+ # Create functions to calculate bond vectors with link atom hosts
936
+ [model.create_bond_vec_getters(geom.atoms) for model in self.models]
937
+
938
+ # And do a quick sanity check
939
+ assert (
940
+ len(self.models[0].links) == 0
941
+ ), "There must not be any links in the 'real' layer!"
942
+ # Look for link atoms that appear in two adjacent layers. In such situations
943
+ # the higher layer is coupled to a layer two levels below. This may be a bad
944
+ # idea.
945
+ for i, (lower_model, model) in enumerate(
946
+ zip(self.models[:-1], self.models[1:])
947
+ ):
948
+ lower_links = lower_model.links
949
+ links = model.links
950
+ same_links = [link for link in links if link in lower_links]
951
+ if same_links:
952
+ print(f"Found {len(same_links)} link(s) that appear(s) in two layers!")
953
+ for j, link in enumerate(same_links):
954
+ print(f"\t{j:02d}: {link}")
955
+ print(
956
+ f"Your current setup couples layer '{model.name}' to "
957
+ f"layer '{self.models[i-1].name}' two levels below! "
958
+ "This is probably a bad idea!"
959
+ )
960
+
961
+ self.log(
962
+ f"Created ONIOM calculator with {self.layer_num} layers and "
963
+ f"{len(self.models)} models."
964
+ )
965
+
966
+ self.layer_calcs = list()
967
+ for i, layer in enumerate(self.layers):
968
+ try:
969
+ parent_layer_calc = self.layer_calcs[-1]
970
+ except IndexError:
971
+ parent_layer_calc = None
972
+ layer_calc = LayerCalc(
973
+ models=layer,
974
+ total_size=len(geom.atoms),
975
+ parent_layer_calc=parent_layer_calc,
976
+ )
977
+ self.layer_calcs.append(layer_calc)
978
+
979
+ @property
980
+ def model_iter(self):
981
+ try:
982
+ # A layer may contain several models
983
+ model_iter = it.chain(*[layer for layer in self.layers])
984
+ except AttributeError:
985
+ model_iter = list()
986
+ return model_iter
987
+
988
+ @property
989
+ def mult(self):
990
+ mults = [model.calc.mult for model in self.model_iter]
991
+ mult0 = mults[0]
992
+ assert all([mult == mult0 for mult in mults])
993
+ return mult0
994
+
995
+ @mult.setter
996
+ def mult(self, mult):
997
+ for model in self.model_iter:
998
+ model.calc.mult = mult
999
+
1000
+ @property
1001
+ def charge(self):
1002
+ charges = [model.calc.charge for model in self.model_iter]
1003
+ charge0 = charges[0]
1004
+ return charge0
1005
+
1006
+ @charge.setter
1007
+ def charge(self, charge):
1008
+ for model in self.model_iter:
1009
+ model.calc.charge = charge
1010
+
1011
+ def run_calculations(self, atoms, coords, method):
1012
+ self.log(f"{self.embeddings[self.embedding]} ONIOM calculation")
1013
+
1014
+ all_results = list()
1015
+ for i, layer in enumerate(self.layers):
1016
+ point_charges = None
1017
+ # Calculate embedding charges, if required
1018
+ if self.embedding and (i > 0):
1019
+ parent_layer = self.layers[i - 1]
1020
+ coords3d = coords.reshape(-1, 3)
1021
+ point_charges = get_embedding_charges(
1022
+ self.embedding, layer, parent_layer, coords3d
1023
+ )
1024
+ self.log(
1025
+ f"Polarizing calculation in layer {i} ({layer}) by "
1026
+ f"charges from layer {i-1} ({self.layers[i-1]})."
1027
+ )
1028
+ ee_charge_sum = point_charges[:, -1].sum()
1029
+ self.log(f"sum(charges)={ee_charge_sum:.4f}")
1030
+
1031
+ # Enable for debugging
1032
+ # from pysisyphus.wrapper.jmol import render_geom_and_charges
1033
+ # if len(layer) == 1:
1034
+ # model = layer[0]
1035
+ # tmp_atoms, tmp_coords = model.capped_atoms_coords(atoms, coords)
1036
+ # render_geom_and_charges(
1037
+ # Geometry(tmp_atoms, tmp_coords), point_charges
1038
+ # )
1039
+
1040
+ results = [
1041
+ getattr(model, method)(atoms, coords, point_charges=point_charges)
1042
+ for model in layer
1043
+ ]
1044
+ all_results.extend(results)
1045
+
1046
+ self.calc_counter += 1
1047
+ return all_results
1048
+
1049
+ def run_calculation(self, atoms, coords):
1050
+ self.log("run_calculation() called. Doing simple energy calculation!")
1051
+ return self.get_energy(atoms, coords)
1052
+
1053
+ def get_energy(self, atoms, coords):
1054
+ all_energies = self.run_calculations(atoms, coords, "get_energy")
1055
+
1056
+ energy = sum(all_energies)
1057
+
1058
+ return {
1059
+ "energy": energy,
1060
+ }
1061
+
1062
+ def get_forces(self, atoms, coords):
1063
+ all_results = self.run_calculations(atoms, coords, "get_forces")
1064
+
1065
+ energies, forces_ = zip(*all_results)
1066
+ forces_ = [np.array(f).reshape(-1, 3) for f in forces_]
1067
+ energy = sum(energies)
1068
+
1069
+ forces = forces_[0] # first layer has shape of the total system
1070
+ for mdl, f in zip(self.models[1:], forces_[1:]):
1071
+ forces[mdl.parent_atom_inds] += f
1072
+
1073
+ return {
1074
+ "energy": energy,
1075
+ "forces": forces.flatten(),
1076
+ }
1077
+
1078
+ def get_hessian(self, atoms, coords):
1079
+ all_results = self.run_calculations(atoms, coords, "get_hessian")
1080
+
1081
+ energies, hessians = zip(*all_results)
1082
+ energy = sum(energies)
1083
+
1084
+ hessian = hessians[0] # first layer has shape of the total system
1085
+ for mdl, h in zip(self.models[1:], hessians[1:]):
1086
+ inds = atom_inds_to_cart_inds(mdl.parent_atom_inds)
1087
+ # Keep in mind that we modify hessians[0] in place
1088
+ hessian[inds[:, None], inds[None, :]] += h
1089
+
1090
+ return {
1091
+ "energy": energy,
1092
+ "hessian": hessian,
1093
+ }
1094
+
1095
+ def atom_inds_in_layer(self, index, exclude_inner=False):
1096
+ """Returns list of atom indices in layer at index.
1097
+
1098
+ Atoms that also appear in inner layer can be excluded on request.
1099
+
1100
+ Parameters
1101
+ ----------
1102
+ index : int
1103
+ pasd
1104
+ exclude_inner : bool, default=False, optional
1105
+ Whether to exclude atom indices that also appear in inner layers.
1106
+
1107
+ Returns
1108
+ -------
1109
+ atom_indices : list
1110
+ List containing the atom indices in the selected layer.
1111
+ """
1112
+
1113
+ layer = self.layers[index]
1114
+ atom_inds = list(it.chain(*[model.atom_inds for model in layer]))
1115
+ if exclude_inner and (index < len(self.layers) - 1):
1116
+ lower_inds = self.atom_inds_in_layer(index + 1)
1117
+ # Drop indices that appear in inner layers
1118
+ atom_inds = [i for i in atom_inds if i not in lower_inds]
1119
+ return atom_inds
1120
+
1121
+ def get_layer_calc(self, layer_ind):
1122
+ return self.layer_calcs[layer_ind]
1123
+
1124
+ def calc_layer(self, atoms, coords, index, parent_correction=True):
1125
+ layer = self.layers[index]
1126
+ assert len(layer) == 1, "Multicenter not yet supported!"
1127
+ (model,) = layer
1128
+ result = model.get_forces(atoms, coords, parent_correction=parent_correction)
1129
+ return result