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
pysisyphus/Geometry.py ADDED
@@ -0,0 +1,1667 @@
1
+ from collections import Counter, namedtuple
2
+ import copy
3
+ import itertools as it
4
+ import re
5
+ import shlex
6
+ import subprocess
7
+ import tempfile
8
+ import sys
9
+ import torch
10
+
11
+ import h5py
12
+ import numpy as np
13
+ from scipy.spatial.distance import pdist
14
+ from scipy.spatial.transform import Rotation
15
+ import rmsd
16
+
17
+ try:
18
+ from thermoanalysis.QCData import QCData
19
+ from thermoanalysis.thermo import thermochemistry
20
+ except ModuleNotFoundError:
21
+ pass
22
+
23
+ from pysisyphus import logger
24
+ from pysisyphus.config import p_DEFAULT, T_DEFAULT
25
+ from pysisyphus.constants import BOHR2ANG
26
+ from pysisyphus.elem_data import (
27
+ MASS_DICT,
28
+ ISOTOPE_DICT,
29
+ ATOMIC_NUMBERS,
30
+ COVALENT_RADII as CR,
31
+ VDW_RADII as VDWR,
32
+ )
33
+ from pysisyphus.helpers_pure import (
34
+ eigval_to_wavenumber,
35
+ full_expand,
36
+ molecular_volume,
37
+ to_subscript_num,
38
+ )
39
+ from pysisyphus.intcoords import (
40
+ DLC,
41
+ HDLC,
42
+ RedundantCoords,
43
+ TRIC,
44
+ TMTRIC,
45
+ HybridRedundantCoords,
46
+ CartesianCoords,
47
+ MWCartesianCoords,
48
+ )
49
+ from pysisyphus.intcoords.exceptions import (
50
+ NeedNewInternalsException,
51
+ RebuiltInternalsException,
52
+ DifferentCoordLengthsException,
53
+ )
54
+ from pysisyphus.intcoords.helpers import get_tangent
55
+ from pysisyphus.intcoords.setup import BOND_FACTOR
56
+ from pysisyphus.intcoords.setup_fast import find_bonds
57
+ from pysisyphus.xyzloader import make_xyz_str
58
+
59
+
60
+ def inertia_tensor(coords3d, masses):
61
+ """Inertita tensor.
62
+
63
+ | x² xy xz |
64
+ (x y z)^T . (x y z) = | xy y² yz |
65
+ | xz yz z² |
66
+ """
67
+ x, y, z = coords3d.T
68
+ squares = np.sum(coords3d**2 * masses[:, None], axis=0)
69
+ I_xx = squares[1] + squares[2]
70
+ I_yy = squares[0] + squares[2]
71
+ I_zz = squares[0] + squares[1]
72
+ I_xy = -np.sum(masses * x * y)
73
+ I_xz = -np.sum(masses * x * z)
74
+ I_yz = -np.sum(masses * y * z)
75
+ I = np.array(((I_xx, I_xy, I_xz), (I_xy, I_yy, I_yz), (I_xz, I_yz, I_zz)))
76
+ return I
77
+
78
+
79
+ def get_trans_rot_vectors(cart_coords, masses, rot_thresh=1e-6):
80
+ """Vectors describing translation and rotation.
81
+
82
+ These vectors are used for the Eckart projection by constructing
83
+ a projector from them.
84
+
85
+ See Martin J. Field - A Pratcial Introduction to the simulation
86
+ of Molecular Systems, 2007, Cambridge University Press, Eq. (8.23),
87
+ (8.24) and (8.26) for the actual projection.
88
+
89
+ See also https://chemistry.stackexchange.com/a/74923.
90
+
91
+ Parameters
92
+ ----------
93
+ cart_coords : np.array, 1d, shape (3 * atoms.size, )
94
+ Atomic masses in amu.
95
+ masses : iterable, 1d, shape (atoms.size, )
96
+ Atomic masses in amu.
97
+
98
+ Returns
99
+ -------
100
+ ortho_vecs : np.array(6, 3*atoms.size)
101
+ 2d array containing row vectors describing translations
102
+ and rotations.
103
+ """
104
+
105
+ coords3d = np.reshape(cart_coords, (-1, 3))
106
+ total_mass = masses.sum()
107
+ com = 1 / total_mass * np.sum(coords3d * masses[:, None], axis=0)
108
+ coords3d_centered = coords3d - com[None, :]
109
+
110
+ I = inertia_tensor(coords3d, masses)
111
+ _, Iv = np.linalg.eigh(I)
112
+ Iv = Iv.T
113
+
114
+ masses_rep = np.repeat(masses, 3)
115
+ sqrt_masses = np.sqrt(masses_rep)
116
+ num = len(masses)
117
+
118
+ def get_trans_vecs():
119
+ """Mass-weighted unit vectors of the three cartesian axes."""
120
+
121
+ for vec in ((1, 0, 0), (0, 1, 0), (0, 0, 1)):
122
+ _ = sqrt_masses * np.tile(vec, num)
123
+ yield _ / np.linalg.norm(_)
124
+
125
+ def get_rot_vecs():
126
+ """As done in geomeTRIC."""
127
+
128
+ rot_vecs = np.zeros((3, cart_coords.size))
129
+ # p_vecs = Iv.dot(coords3d_centered.T).T
130
+ for i in range(masses.size):
131
+ p_vec = Iv.dot(coords3d_centered[i])
132
+ for ix in range(3):
133
+ rot_vecs[0, 3 * i + ix] = Iv[2, ix] * p_vec[1] - Iv[1, ix] * p_vec[2]
134
+ rot_vecs[1, 3 * i + ix] = Iv[2, ix] * p_vec[0] - Iv[0, ix] * p_vec[2]
135
+ rot_vecs[2, 3 * i + ix] = Iv[0, ix] * p_vec[1] - Iv[1, ix] * p_vec[0]
136
+ rot_vecs *= sqrt_masses[None, :]
137
+ return rot_vecs
138
+
139
+ trans_vecs = list(get_trans_vecs())
140
+ rot_vecs = np.array(get_rot_vecs())
141
+ # Drop vectors with vanishing norms
142
+ rot_vecs = rot_vecs[np.linalg.norm(rot_vecs, axis=1) > rot_thresh]
143
+ tr_vecs = np.concatenate((trans_vecs, rot_vecs), axis=0)
144
+ tr_vecs = np.linalg.qr(tr_vecs.T)[0].T
145
+ return tr_vecs
146
+
147
+
148
+ def get_trans_rot_projector(cart_coords, masses, full=False):
149
+ tr_vecs = get_trans_rot_vectors(cart_coords, masses=masses)
150
+ U, s, _ = np.linalg.svd(tr_vecs.T)
151
+ if full:
152
+ P = np.eye(cart_coords.size)
153
+ for tr_vec in tr_vecs:
154
+ P -= np.outer(tr_vec, tr_vec)
155
+ else:
156
+ P = U[:, s.size :].T
157
+ return P
158
+
159
+
160
+ class Geometry:
161
+ coord_types = {
162
+ "cart": None,
163
+ "redund": RedundantCoords,
164
+ "hredund": HybridRedundantCoords,
165
+ "dlc": DLC,
166
+ "hdlc": HDLC,
167
+ "tric": TRIC,
168
+ "tmtric": TMTRIC,
169
+ "cartesian": CartesianCoords,
170
+ "mwcartesian": MWCartesianCoords,
171
+ }
172
+
173
+ def __init__(
174
+ self,
175
+ atoms,
176
+ coords,
177
+ fragments=None,
178
+ coord_type="cart",
179
+ coord_kwargs=None,
180
+ isotopes=None,
181
+ freeze_atoms=None,
182
+ comment="",
183
+ name="",
184
+ ):
185
+ """Object representing atoms in a coordinate system.
186
+
187
+ The Geometry represents atoms and their positions in coordinate
188
+ system. By default cartesian coordinates are used, but internal
189
+ coordinates are also possible.
190
+
191
+ Parameters
192
+ ----------
193
+ atoms : iterable
194
+ Iterable of length N, containing element symbols.
195
+ coords : 1d iterable
196
+ 1d iterable of length 3N, containing the cartesian coordinates
197
+ of N atoms.
198
+ fragments : dict, optional
199
+ Dict with different keys denoting different fragments. The values
200
+ contain lists of atom indices.
201
+ coord_type : {"cart", "redund"}, optional
202
+ Type of coordinate system to use. Right now cartesian (cart)
203
+ and redundand (redund) are supported.
204
+ coord_kwargs : dict, optional
205
+ Dictionary containing additional arguments that get passed
206
+ to the constructor of the internal coordinate class.
207
+ isotopes : iterable of pairs, optional
208
+ Iterable of pairs consisting of 0-based atom index and either an integer
209
+ or a float. If an integer is given the closest isotope mass will be selected.
210
+ Given a float, this float will be directly used as mass.
211
+ freeze_atoms : iterable of integers
212
+ Specifies which atoms should remain fixed at their initial positions.
213
+ comment : str, optional
214
+ Comment string.
215
+ name : str, optional
216
+ Verbose name of the geometry, e.g. methanal or water. Used for printing
217
+ """
218
+ self.atoms = tuple([atom.capitalize() for atom in atoms])
219
+ # self._coords always holds cartesian coordinates.
220
+ self._coords = np.array(coords, dtype=float).flatten()
221
+ assert self._coords.size == (3 * len(self.atoms)), (
222
+ f"Expected 3N={3*len(self.atoms)} cartesian coordinates but got "
223
+ f"{self._coords.size}. Did you accidentally supply internal "
224
+ "coordinates?"
225
+ )
226
+ if fragments is None:
227
+ fragments = dict()
228
+ self.fragments = fragments
229
+ self.coord_type = coord_type
230
+ if coord_kwargs is None:
231
+ coord_kwargs = dict()
232
+ self.coord_kwargs = coord_kwargs
233
+ if isotopes is None:
234
+ isotopes = list()
235
+ self.isotopes = isotopes
236
+ if freeze_atoms is None:
237
+ freeze_atoms = list()
238
+ elif type(freeze_atoms) is str:
239
+ freeze_atoms = full_expand(freeze_atoms)
240
+ self.freeze_atoms = np.array(freeze_atoms, dtype=int)
241
+ self.comment = comment
242
+ self.name = name
243
+
244
+ self._masses = None
245
+ self._energy = None
246
+ self._forces = None
247
+ self._hessian = None
248
+ self.within_partial_hessian = None
249
+ self._all_energies = None
250
+ self.calculator = None
251
+
252
+ assert (
253
+ # Negative atom indices are not allowed.
254
+ all(self.freeze_atoms >= 0)
255
+ and (
256
+ # Allow an empty array, no frozen atoms.
257
+ (self.freeze_atoms.size == 0)
258
+ # Or check that the biggest index is still in the valid range
259
+ or (self.freeze_atoms.max() < len(self.atoms))
260
+ )
261
+ ), f"'freeze_atoms' must all be >= 0 and < {len(self.atoms)}!"
262
+
263
+ # Disallow any coord_kwargs with coord_type == 'cart'
264
+ if (coord_type == "cart") and not (coord_kwargs is None or coord_kwargs == {}):
265
+ print(
266
+ "coord_type is set to 'cart' but coord_kwargs were given. "
267
+ "This is probably not intended. Exiting!"
268
+ )
269
+ sys.exit()
270
+
271
+ # Coordinate systems are handled below
272
+ coord_class = self.coord_types[self.coord_type]
273
+ if coord_class:
274
+ if (len(self.freeze_atoms) > 0) and ("freeze_atoms" not in coord_kwargs):
275
+ coord_kwargs["freeze_atoms"] = freeze_atoms
276
+ self.internal = coord_class(
277
+ atoms,
278
+ self.coords3d.copy(),
279
+ masses=self.masses,
280
+ **coord_kwargs,
281
+ )
282
+ else:
283
+ self.internal = None
284
+
285
+ @property
286
+ def moving_atoms(self):
287
+ return [atom for i, atom in enumerate(self.atoms) if i not in self.freeze_atoms]
288
+
289
+ def moving_atoms_jmol(self):
290
+ atoms = list()
291
+ freeze_atoms = self.freeze_atoms
292
+ for i, atom in enumerate(self.atoms):
293
+ atom = atom if i not in freeze_atoms else "X"
294
+ atoms.append(atom)
295
+ self.jmol(atoms=atoms)
296
+
297
+ @property
298
+ def sum_formula(self):
299
+ unique_atoms = sorted(set(self.atoms))
300
+ counter = Counter(self.atoms)
301
+ atoms = list()
302
+ num_strs = list()
303
+
304
+ def set_atom(atom):
305
+ atoms.append(atom)
306
+ num = counter[atom]
307
+ if num == 1:
308
+ num_str = ""
309
+ else:
310
+ num_str = to_subscript_num(num)
311
+ num_strs.append(num_str)
312
+
313
+ # Hill-System
314
+ for atom in ("C", "H"):
315
+ try:
316
+ unique_atoms.remove(atom)
317
+ set_atom(atom)
318
+ except ValueError:
319
+ pass
320
+ for atom in unique_atoms:
321
+ set_atom(atom)
322
+
323
+ return "".join([f"{atom}{num_str}" for atom, num_str in zip(atoms, num_strs)])
324
+
325
+ def assert_compatibility(self, other):
326
+ """Assert that two Geometries can be substracted from each other.
327
+
328
+ Parameters
329
+ ----------
330
+ other : Geometry
331
+ Geometry for comparison.
332
+ """
333
+ same_atoms = self.atoms == other.atoms
334
+ same_coord_type = self.coord_type == other.coord_type
335
+ same_coord_length = len(self.coords) == len(other.coords)
336
+ assert same_atoms, "Atom number/ordering is incompatible!"
337
+ assert same_coord_type, "coord_types are incompatible!"
338
+ try:
339
+ assert same_coord_length, "Different length of coordinate vectors!"
340
+ except AssertionError:
341
+ raise DifferentCoordLengthsException
342
+
343
+ def __eq__(self, other):
344
+ return (self.atoms == other.atoms) and np.allclose(
345
+ self.coords, other.coords, atol=1e-8
346
+ )
347
+
348
+ def __sub__(self, other):
349
+ self.assert_compatibility(other)
350
+ if self.coord_type in ("cart", "cartesian"):
351
+ diff = self.coords - other.coords
352
+ elif self.coord_type in ("redund", "dlc"):
353
+ # Take periodicity of dihedrals into account by calling
354
+ # get_tangent(). Care has to be taken regarding the orientation
355
+ # of the returned tangent vector. It points from self to other.
356
+ #
357
+ # As we want to return the difference between two vectors we
358
+ # have to reverse the direction of the tangent by multiplying it
359
+ # with -1 to be consistent with basic subtraction laws ...
360
+ # A - B = C, where C is a vector pointing from B to A (B + C = A)
361
+ # In our case get_tangent returns B - A, that is a vector pointing
362
+ # from A to B.
363
+ diff = -get_tangent(
364
+ self.internal.prim_coords,
365
+ other.internal.prim_coords,
366
+ self.internal.dihedral_indices,
367
+ )
368
+ else:
369
+ raise Exception("Invalid coord_type!")
370
+
371
+ # Convert to DLC
372
+ if self.coord_type == "dlc":
373
+ diff = self.internal.U.T.dot(diff)
374
+ return diff
375
+
376
+ def __add__(self, other):
377
+ atoms = tuple(self.atoms) + tuple(other.atoms)
378
+ coords = np.concatenate((self.cart_coords, other.cart_coords))
379
+ return Geometry(atoms, coords)
380
+
381
+ def atom_xyz_iter(self):
382
+ return iter(zip(self.atoms, self.coords3d))
383
+
384
+ def copy(self, coord_type=None, coord_kwargs=None):
385
+ """Returns a new Geometry object with same atoms and coordinates.
386
+
387
+ Parameters
388
+ ----------
389
+ coord_type : str
390
+ Desired coord_type, defaults to current coord_type.
391
+
392
+ coord_kwargs : dict, optional
393
+ Any desired coord_kwargs that will be passed to the RedundantCoords
394
+ object.
395
+ Returns
396
+ -------
397
+ geom : Geometry
398
+ New Geometry object with the same atoms and coordinates.
399
+ """
400
+ if coord_type is None:
401
+ coord_type = self.coord_type
402
+
403
+ if coord_kwargs is None:
404
+ coord_kwargs = dict()
405
+
406
+ # Geometry constructor will exit when coord_kwargs are given
407
+ # with coord_type == 'cart'. So we only supply it when we are
408
+ # NOT using cartesian coordinates.
409
+ _coord_kwargs = None
410
+ if coord_type != "cart":
411
+ try:
412
+ typed_prims = self.internal.typed_prims
413
+ # Will be raised if the current coord_type is 'cart'
414
+ except AttributeError:
415
+ typed_prims = None
416
+ _coord_kwargs = {
417
+ "typed_prims": typed_prims,
418
+ "check_bends": True,
419
+ }
420
+ _coord_kwargs.update(coord_kwargs)
421
+ return Geometry(
422
+ self.atoms,
423
+ self._coords.copy(),
424
+ coord_type=coord_type,
425
+ coord_kwargs=_coord_kwargs,
426
+ isotopes=copy.deepcopy(self.isotopes),
427
+ freeze_atoms=self.freeze_atoms.copy(),
428
+ )
429
+
430
+ def copy_all(self, coord_type=None, coord_kwargs=None):
431
+ new_geom = self.copy(coord_type, coord_kwargs)
432
+ new_geom.set_calculator(self.calculator)
433
+ new_geom.energy = self._energy
434
+ if self._forces is not None:
435
+ new_geom.cart_forces = self._forces
436
+ if self._hessian is not None:
437
+ new_geom.cart_hessian = self._hessian
438
+ return new_geom
439
+
440
+ def atom_indices(self):
441
+ """Dict with atom types as key and corresponding indices as values.
442
+
443
+ Returns
444
+ -------
445
+ inds_dict : dict
446
+ Unique atom types as keys, corresponding indices as values.
447
+ """
448
+ inds_dict = {}
449
+ for atom_type in set(self.atoms):
450
+ inds_dict[atom_type] = [
451
+ i for i, atom in enumerate(self.atoms) if atom == atom_type
452
+ ]
453
+ return inds_dict
454
+
455
+ @property
456
+ def atom_types(self):
457
+ return set(self.atoms)
458
+
459
+ @property
460
+ def atomic_numbers(self):
461
+ return [ATOMIC_NUMBERS[a.lower()] for a in self.atoms]
462
+
463
+ def get_fragments(self, regex):
464
+ regex = re.compile(regex)
465
+ frags = [frag for frag in self.fragments.keys() if regex.search(frag)]
466
+ org_indices = list(it.chain(*[self.fragments[frag] for frag in frags]))
467
+
468
+ new_atoms = [self.atoms[ind] for ind in org_indices]
469
+ new_coords = self.coords3d[org_indices].copy()
470
+ new_fragments = dict()
471
+ i = 0
472
+ for frag in frags:
473
+ frag_atoms = len(self.fragments[frag])
474
+ new_fragments[frag] = list(range(i, i + frag_atoms))
475
+ i += frag_atoms
476
+ return Geometry(new_atoms, new_coords, fragments=new_fragments)
477
+
478
+ @property
479
+ def layers(self):
480
+ try:
481
+ layers = self.calculator.layers
482
+ except AttributeError:
483
+ layers = (None,)
484
+ return layers
485
+
486
+ def del_atoms(self, inds, **kwargs):
487
+ atoms = [atom for i, atom in enumerate(self.atoms) if not (i in inds)]
488
+ c3d = self.coords3d
489
+ coords3d = np.array(
490
+ [c3d[i] for i, _ in enumerate(self.atoms) if not (i in inds)]
491
+ )
492
+ return Geometry(atoms, coords3d.flatten(), **kwargs)
493
+
494
+ def set_calculator(self, calculator, clear=True):
495
+ """Reset the object and set a calculator."""
496
+ if clear:
497
+ self.clear()
498
+ self.calculator = calculator
499
+
500
+ if hasattr(self.calculator, "freeze_atoms"):
501
+ self.calculator.freeze_atoms = self.freeze_atoms.copy()
502
+
503
+ @property
504
+ def is_analytical_2d(self):
505
+ try:
506
+ return self.calculator.analytical_2d
507
+ except AttributeError:
508
+ return False
509
+
510
+ @property
511
+ def mm_inv(self):
512
+ """Inverted mass matrix.
513
+
514
+ Returns a diagonal matrix containing the inverted atomic
515
+ masses.
516
+ """
517
+ return np.diag(1 / self.masses_rep)
518
+
519
+ @property
520
+ def mm_sqrt_inv(self):
521
+ """Inverted square root of the mass matrix."""
522
+ return np.diag(1 / (self.masses_rep**0.5))
523
+
524
+ @property
525
+ def coords(self):
526
+ """1d vector of atomic coordinates.
527
+
528
+ Returns
529
+ -------
530
+ coords : np.array
531
+ 1d array holding the current coordinates.
532
+ """
533
+ if self.internal:
534
+ coords = self.internal.coords
535
+ else:
536
+ # self._coords will always hold Cartesian coordinates.
537
+ coords = self._coords
538
+ return coords
539
+
540
+ def set_coord(self, ind, coord):
541
+ """Set a coordinate by index.
542
+
543
+ Parameters
544
+ ----------
545
+ ind : int
546
+ Index in of the coordinate to set in the self.coords array.
547
+ coord : float
548
+ Coordinate value.
549
+ """
550
+ assert (
551
+ self.coord_type == "cart" and len(self.freeze_atoms) == 0
552
+ ), "set_coord was not yet tested with coord_type != 'cart' and frozen atoms!"
553
+ self.coords[ind] = coord
554
+ self.clear()
555
+
556
+ def set_coords(self, coords, cartesian=False, update_constraints=False):
557
+ coords = np.array(coords).flatten()
558
+
559
+ # Do Internal->Cartesian backtransformation if internal coordinates are used.
560
+ if self.internal:
561
+ # When internal coordinates are employed it may happen, that the underlying
562
+ # Cartesian coordinates are updated, e.g. from the IPIServer calculator, which
563
+ # may yield different internal coordinates.
564
+ #
565
+ # Here we update the Cartesians of the internal coordinate object to the new
566
+ # values and calculate new internal coordinates, from which we can derive a step
567
+ # in internals.
568
+ if cartesian:
569
+ self.assert_cart_coords(coords)
570
+ cart_coords = coords.copy()
571
+ # Update Cartesians of internal coordinate object and calculate
572
+ # new internals.
573
+ self.internal.coords3d = coords
574
+ # Determine new internal coordinates, so we can later calculate a
575
+ # step in internal coordinates.
576
+ coords = self.internal.coords
577
+ # Finally we also update the Cartesian coordinates of the Geometry object,
578
+ # so the subsequent sanity check does not fail. This also allows updating
579
+ # the coordiantes of atoms that are frozen. We set Geometry._coords directly,
580
+ # instead of Geometry.cart_coords or Geometry.coords3d, to avoid an infinite
581
+ # recursion.
582
+ self._coords = cart_coords
583
+
584
+ # Sanity check, asserting that the cartesian coordinates of the
585
+ # Geometry object and the internal coordinate object are the same.
586
+ np.testing.assert_allclose(self.coords3d, self.internal.coords3d)
587
+
588
+ try:
589
+ int_step = coords - self.internal.coords
590
+ cart_step = self.internal.transform_int_step(
591
+ int_step, update_constraints=update_constraints
592
+ )
593
+ # From now on coords will always hold Cartesian coordinates!
594
+ coords = self._coords + cart_step
595
+ except NeedNewInternalsException as exception:
596
+ invalid_inds = exception.invalid_inds
597
+ # Check if the remaining internal coordinates are valid
598
+ valid_typed_prims = [
599
+ typed_prim
600
+ for i, typed_prim in enumerate(self.internal.typed_prims)
601
+ if i not in invalid_inds
602
+ ]
603
+ coords3d = exception.coords3d.copy()
604
+ coord_class = self.coord_types[self.coord_type]
605
+ coord_kwargs = self.coord_kwargs.copy()
606
+ """Instead of using only the remaining, valid typed_prims
607
+ we could look for an entirely new set of typed_prims.
608
+
609
+ But when we do this and we end up with more coordinates
610
+ than before, this will lead to problems with the HDF5 dump.
611
+
612
+ No problems arise when fewer coordinates are used
613
+ (valid_typed_prims <= self.internal.typed_prims).
614
+ With typed prims, only the remaining, valid typed_prims
615
+ will be defined for the new geometry.
616
+
617
+ coord_kwargs["typed_prims"] = valid_typed_prims # Currently disabled
618
+
619
+ With 'define_prims' the remaining, valid typed_prims
620
+ will be used, together with newly determined internal
621
+ coordinates. This supports, e.g., the switch from a simple
622
+ bend to a linear bend and its complement.
623
+
624
+ Currently the default."""
625
+ coord_kwargs["define_prims"] = valid_typed_prims
626
+
627
+ self.internal = coord_class(self.atoms, coords3d, **coord_kwargs)
628
+ self._coords = coords3d.flatten()
629
+ raise RebuiltInternalsException(
630
+ typed_prims=self.internal.typed_prims.copy()
631
+ )
632
+
633
+ # Restore original coordinates of frozen atoms. Right now this should
634
+ # be redundant, as the Cartesian step is also constrainted in the
635
+ # Internal->Cartesian backtransformation. But we keep it for now.
636
+ coords.reshape(-1, 3)[self.freeze_atoms] = self.coords3d[self.freeze_atoms]
637
+ # Set new Cartesian coordinates
638
+ self._coords = coords
639
+ # Reset all values because no calculations with the new coords
640
+ # have been performed yet.
641
+ self.clear()
642
+
643
+ def reset_coords(self, new_typed_prims=None):
644
+ if self.coord_type == "cart":
645
+ return
646
+
647
+ coord_class = self.coord_types[self.coord_type]
648
+ self.internal = coord_class(
649
+ self.atoms, self.coords3d, typed_prims=new_typed_prims
650
+ )
651
+
652
+ @coords.setter
653
+ def coords(self, coords):
654
+ """Wrapper for saving coordinates internally.
655
+
656
+ Parameters
657
+ ----------
658
+ coords : np.array
659
+ 1d array containing atomic coordiantes. It's length
660
+ depends on the coordinate system.
661
+ """
662
+ self.set_coords(coords)
663
+
664
+ @property
665
+ def coords3d(self):
666
+ """Coordinates in 3d.
667
+
668
+ Returns
669
+ -------
670
+ coords3d : np.array
671
+ Coordinates of the Geometry as 2D array.
672
+ """
673
+ return self._coords.reshape(-1, 3)
674
+
675
+ @coords3d.setter
676
+ def coords3d(self, coords3d):
677
+ self.set_coords(coords3d, cartesian=True)
678
+
679
+ @property
680
+ def cart_coords(self):
681
+ return self._coords
682
+
683
+ @cart_coords.setter
684
+ def cart_coords(self, coords):
685
+ self.set_coords(coords, cartesian=True)
686
+
687
+ @property
688
+ def coords_by_type(self):
689
+ """Coordinates in 3d by atom type and their corresponding indices.
690
+
691
+ Returns
692
+ -------
693
+ cbt : dict
694
+ Dictionary with the unique atom types of the Geometry as keys.
695
+ It's values are the 3d coordinates of the corresponding atom type.
696
+ inds : dict
697
+ Dictionary with the unique atom types of the Geometry as keys.
698
+ It's values are the original indices of the 3d coordinates in the
699
+ whole coords3d array.
700
+ """
701
+ cbt = dict()
702
+ inds = dict()
703
+ # for i, (atom, c3d) in enumerate(zip(self.atoms, self.coords3d)):
704
+ # cbt.setdefault(atom, list()).append((i, c3d.tolist()))
705
+ for i, (atom, c3d) in enumerate(zip(self.atoms, self.coords3d)):
706
+ cbt.setdefault(atom, list()).append((c3d))
707
+ inds.setdefault(atom, list()).append(i)
708
+ for atom, c3d in cbt.items():
709
+ cbt[atom] = np.array(c3d)
710
+ inds[atom] = np.array(inds[atom])
711
+ return cbt, inds
712
+
713
+ @property
714
+ def comment(self):
715
+ en_width = 20
716
+ # Check if we have to drop an (old) energy entry
717
+ try:
718
+ _ = float(self._comment[:en_width])
719
+ # Drop old energy entry
720
+ self._comment = self._comment[en_width + 2 :]
721
+ except (ValueError, IndexError):
722
+ pass
723
+
724
+ # Prepend (new) energy, if present
725
+ if self._energy:
726
+ en_str = f"{self._energy: >{en_width}.8f} , "
727
+ else:
728
+ en_str = ""
729
+ return f"{en_str}{self._comment}"
730
+
731
+ @comment.setter
732
+ def comment(self, new_comment):
733
+ self._comment = new_comment
734
+
735
+ @property
736
+ def masses(self):
737
+ if self._masses is None:
738
+ # Lookup tabuled masses in internal database
739
+ masses = np.array([MASS_DICT[atom.lower()] for atom in self.atoms])
740
+ # Use (different) isotope masses if requested
741
+ for atom_index, iso_mass in self.isotopes:
742
+ if "." not in str(iso_mass):
743
+ atom = self.atoms[atom_index].lower()
744
+ key = (atom, iso_mass)
745
+ try:
746
+ iso_mass = ISOTOPE_DICT[key]
747
+ except KeyError as err:
748
+ print(
749
+ f"Found no suitable mass for '{atom.capitalize()}' with approx. "
750
+ f"mass of ~{iso_mass} au!"
751
+ )
752
+ raise err
753
+ masses[atom_index] = float(iso_mass)
754
+ self.masses = masses
755
+ return self._masses
756
+
757
+ @masses.setter
758
+ def masses(self, masses):
759
+ assert len(masses) == len(self.atoms)
760
+ masses = np.array(masses, dtype=float)
761
+ self._masses = masses
762
+ # Also try to propagate updated masses to the internal coordiante object
763
+ try:
764
+ self.internal.masses = masses
765
+ except AttributeError:
766
+ pass
767
+
768
+ @property
769
+ def masses_rep(self):
770
+ # Some of the analytical potentials are only 2D
771
+ repeat_masses = 2 if (self._coords.size == 2) else 3
772
+ return np.repeat(self.masses, repeat_masses)
773
+
774
+ @property
775
+ def total_mass(self):
776
+ return sum(self.masses)
777
+
778
+ def center_of_mass_at(self, coords3d):
779
+ """Returns the center of mass at given coords3d.
780
+
781
+ Parameters
782
+ ----------
783
+ coords3d : np.array, shape(N, 3)
784
+ Cartesian coordiantes.
785
+
786
+ Returns
787
+ -------
788
+ R : np.array, shape(3, )
789
+ Center of mass.
790
+ """
791
+ return 1 / self.total_mass * np.sum(coords3d * self.masses[:, None], axis=0)
792
+
793
+ @property
794
+ def center_of_mass(self):
795
+ """Returns the center of mass.
796
+
797
+ Returns
798
+ -------
799
+ R : np.array, shape(3, )
800
+ Center of mass.
801
+ """
802
+ return self.center_of_mass_at(self.coords3d)
803
+
804
+ @property
805
+ def centroid(self):
806
+ """Geometric center of the Geometry.
807
+
808
+ Returns
809
+ -------
810
+ R : np.array, shape(3, )
811
+ Geometric center of the Geometry.
812
+ """
813
+ return self.coords3d.mean(axis=0)
814
+
815
+ def center(self):
816
+ self.coords3d -= self.centroid[None, :]
817
+
818
+ @property
819
+ def mw_coords(self):
820
+ """Mass-weighted coordinates.
821
+
822
+ Returns
823
+ -------
824
+ mw_coords : np.array
825
+ 1d array containing the mass-weighted cartesian coordiantes.
826
+ """
827
+ return np.sqrt(self.masses_rep) * self._coords
828
+
829
+ @mw_coords.setter
830
+ def mw_coords(self, mw_coords):
831
+ """Set mass-weighted coordinates."""
832
+ self.coords = mw_coords / np.sqrt(self.masses_rep)
833
+
834
+ def fd_coords3d_gen(self, step_size=1e-3):
835
+ """Iterator returning 3d Cartesians for finite-differences."""
836
+ coords3d = self.coords3d.copy()
837
+ zeros = np.zeros_like(coords3d)
838
+ for i, _ in enumerate(self.coords3d):
839
+ for j in (0, 1, 2):
840
+ step = zeros.copy()
841
+ step[i, j] = step_size
842
+ yield i, j, coords3d + step, coords3d - step
843
+
844
+ @property
845
+ def covalent_radii(self):
846
+ return np.array([CR[a.lower()] for a in self.atoms])
847
+
848
+ @property
849
+ def vdw_radii(self):
850
+ return np.array([VDWR[a.lower()] for a in self.atoms])
851
+
852
+ def vdw_volume(self, **kwargs):
853
+ V_au, *_ = molecular_volume(self.coords3d, self.vdw_radii, **kwargs)
854
+ return V_au
855
+
856
+ @property
857
+ def inertia_tensor(self):
858
+ return inertia_tensor(self.coords3d, self.masses)
859
+
860
+ def principal_axes_are_aligned(self):
861
+ """Check if the principal axes are aligned with the cartesian axes.
862
+
863
+ Returns
864
+ -------
865
+ aligned : bool
866
+ Wether the principal axes are aligned or not.
867
+ """
868
+ w, v = np.linalg.eigh(self.inertia_tensor)
869
+ return np.allclose(v, np.eye(3)), v
870
+
871
+ def align_principal_axes(self):
872
+ """Align the principal axes to the cartesian axes.
873
+
874
+ https://math.stackexchange.com/questions/145023
875
+ """
876
+ I = self.inertia_tensor
877
+ w, v = np.linalg.eigh(I)
878
+ # rot = np.linalg.solve(v, np.eye(3))
879
+ # self.coords3d = rot.dot(self.coords3d.T).T
880
+ self.coords3d = v.T.dot(self.coords3d.T).T
881
+
882
+ def standard_orientation(self):
883
+ # Translate center of mass to cartesian origin
884
+ self.coords3d -= self.center_of_mass
885
+ # Try to rotate the principal axes onto the cartesian axes
886
+ for i in range(5):
887
+ self.align_principal_axes()
888
+ aligned, vecs = self.principal_axes_are_aligned()
889
+ if aligned:
890
+ break
891
+
892
+ def reparametrize(self):
893
+ if not hasattr(self.calculator, 'get_coords'):
894
+ return False
895
+ try:
896
+ results = self.calculator.get_coords(self.atoms, self.cart_coords)
897
+ self.set_coords(results["coords"], cartesian=True)
898
+ return True
899
+ except Exception:
900
+ return False
901
+
902
+ @property
903
+ def energy(self):
904
+ """Energy of the current atomic configuration.
905
+
906
+ Returns
907
+ -------
908
+ energy : float
909
+ Energy of the current atomic configuration.
910
+ """
911
+ if self._energy is None:
912
+ results = self.calculator.get_energy(self.atoms, self._coords)
913
+ self.set_results(results)
914
+ return self._energy
915
+
916
+ @energy.setter
917
+ def energy(self, energy):
918
+ """Internal wrapper for setting the energy.
919
+
920
+ Parameters
921
+ ----------
922
+ energy : float
923
+ """
924
+ self._energy = energy
925
+
926
+ @property
927
+ def all_energies(self):
928
+ """Return energies of all states that were calculated.
929
+
930
+ This will also set self.energy, which may NOT be the ground state,
931
+ but the state correspondig to the 'root' attribute of the calculator."""
932
+ if self._all_energies is None:
933
+ results = self.calculator.get_energy(self.atoms, self._coords)
934
+ self.set_results(results)
935
+ return self._all_energies
936
+
937
+ @all_energies.setter
938
+ def all_energies(self, all_energies):
939
+ """Internal wrapper for setting all energies.
940
+
941
+ Parameters
942
+ ----------
943
+ all_energies : np.array
944
+ """
945
+ self._all_energies = all_energies
946
+
947
+ @property
948
+ def cart_forces(self):
949
+ if self._forces is None:
950
+ results = self.calculator.get_forces(self.atoms, self._coords)
951
+ self.set_results(results)
952
+ return self._forces
953
+
954
+ @cart_forces.setter
955
+ def cart_forces(self, cart_forces):
956
+ cart_forces = np.array(cart_forces)
957
+ assert cart_forces.shape == self.cart_coords.shape
958
+ self._forces = cart_forces
959
+
960
+ @property
961
+ def forces(self):
962
+ """Energy of the current atomic configuration.
963
+
964
+ Returns
965
+ -------
966
+ force : np.array
967
+ 1d array containing the forces acting on the atoms. Negative
968
+ of the gradient.
969
+ """
970
+ forces = self.cart_forces
971
+ if self.internal:
972
+ forces = self.internal.transform_forces(forces)
973
+ return forces
974
+
975
+ @forces.setter
976
+ def forces(self, forces):
977
+ """Internal wrapper for setting the forces.
978
+
979
+ Parameters
980
+ ----------
981
+ forces : np.array
982
+ """
983
+ forces = np.array(forces)
984
+ assert forces.shape == self.cart_coords.shape
985
+ self._forces = forces
986
+
987
+ @property
988
+ def cart_gradient(self):
989
+ return -self.cart_forces
990
+
991
+ @cart_gradient.setter
992
+ def cart_gradient(self, cart_gradient):
993
+ self.cart_forces = -cart_gradient
994
+
995
+ @property
996
+ def gradient(self):
997
+ """Negative of the force.
998
+
999
+ Returns
1000
+ -------
1001
+ gradient : np.array
1002
+ 1d array containing the negative of the current forces.
1003
+ """
1004
+ return -self.forces
1005
+
1006
+ # @gradient.setter
1007
+ # def gradient(self, gradient):
1008
+ # """Internal wrapper for setting the gradient."""
1009
+ # # No check here as this is handled by in the forces.setter.
1010
+ # self.forces = -gradient
1011
+
1012
+ @property
1013
+ def mw_gradient(self):
1014
+ """Mass-weighted gradient.
1015
+
1016
+ Returns
1017
+ -------
1018
+ mw_gradient : np.array
1019
+ Returns the mass-weighted gradient.
1020
+ """
1021
+ return -self.forces / np.sqrt(self.masses_rep)
1022
+
1023
+ @property
1024
+ def cart_hessian(self):
1025
+ if self._hessian is None:
1026
+ results = self.calculator.get_hessian(self.atoms, self._coords)
1027
+ self.set_results(results)
1028
+ return self._hessian
1029
+
1030
+ @cart_hessian.setter
1031
+ def cart_hessian(self, cart_hessian):
1032
+ if cart_hessian is not None:
1033
+ # cart_hessian = np.array(cart_hessian)
1034
+ if self.within_partial_hessian is not None:
1035
+ active_n_dof = int(self.within_partial_hessian.get("active_n_dof", 0))
1036
+ full_n_dof = int(
1037
+ self.within_partial_hessian.get("full_n_dof", self.cart_coords.size)
1038
+ )
1039
+ if cart_hessian.shape != (full_n_dof, full_n_dof):
1040
+ assert cart_hessian.shape == (active_n_dof, active_n_dof)
1041
+ else:
1042
+ assert cart_hessian.shape == (self.cart_coords.size, self.cart_coords.size)
1043
+ self._hessian = cart_hessian
1044
+
1045
+ @property
1046
+ def hessian(self):
1047
+ """Matrix of second derivatives of the energy in respect to atomic
1048
+ displacements.
1049
+
1050
+ Returns
1051
+ -------
1052
+ hessian : np.array
1053
+ 2d array containing the second derivatives of the energy with respect
1054
+ to atomic/coordinate displacements depending on the type of
1055
+ coordiante system.
1056
+ """
1057
+ hessian = self.cart_hessian
1058
+ if self.internal:
1059
+ int_gradient = self.gradient
1060
+ return self.internal.transform_hessian(hessian, int_gradient)
1061
+ return hessian
1062
+
1063
+ # @hessian.setter
1064
+ # def hessian(self, hessian):
1065
+ # """Internal wrapper for setting the hessian."""
1066
+ # assert hessian.shape == (self.coords.size, self.coords.size)
1067
+ # self._hessian = hessian
1068
+
1069
+ def mass_weigh_hessian(self, hessian):
1070
+ if (
1071
+ self.within_partial_hessian is not None
1072
+ and hessian is not None
1073
+ and hessian.shape == (int(self.within_partial_hessian.get("active_n_dof", 0)),
1074
+ int(self.within_partial_hessian.get("active_n_dof", 0)))
1075
+ ):
1076
+ act_atoms = self.hess_active_atom_indices
1077
+ masses_act = self.masses[act_atoms]
1078
+ m3 = np.repeat(masses_act, 3)
1079
+ inv_sqrt_m = 1.0 / np.sqrt(m3)
1080
+ if isinstance(hessian, torch.Tensor):
1081
+ inv = torch.as_tensor(inv_sqrt_m, dtype=hessian.dtype, device=hessian.device)
1082
+ hessian.mul_(inv.view(-1, 1))
1083
+ hessian.mul_(inv.view(1, -1))
1084
+ return hessian
1085
+ hessian *= inv_sqrt_m[:, None]
1086
+ hessian *= inv_sqrt_m[None, :]
1087
+ return hessian
1088
+
1089
+ inv_sqrt_m = 1.0 / (self.masses_rep ** 0.5)
1090
+ if isinstance(hessian, torch.Tensor):
1091
+ s = torch.tensor(inv_sqrt_m, dtype=hessian.dtype, device=hessian.device)
1092
+ return hessian * s[:, None] * s[None, :]
1093
+ else:
1094
+ return hessian * inv_sqrt_m[:, None] * inv_sqrt_m[None, :]
1095
+
1096
+ @property
1097
+ def mw_hessian(self):
1098
+ """Mass-weighted hessian.
1099
+
1100
+ Returns
1101
+ -------
1102
+ mw_hessian : np.array
1103
+ 2d array containing the mass-weighted hessian M^(-1/2) H M^(-1/2).
1104
+ """
1105
+ # M^(-1/2) H M^(-1/2)
1106
+ # TODO: Do the right thing here when the hessian is not yet calculated.
1107
+ # this would probably involve figuring out how to mass-weigh and
1108
+ # internal coordinat hessian... I think this is described in one
1109
+ # of the Gonzalez-Schlegel-papers about the GS2 algorithm.
1110
+ return self.mass_weigh_hessian(self.cart_hessian)
1111
+
1112
+ def unweight_mw_hessian(self, mw_hessian):
1113
+ """Unweight a mass-weighted hessian.
1114
+
1115
+ Parameters
1116
+ ----------
1117
+ mw_hessian : np.array
1118
+ Mass-weighted hessian to be unweighted.
1119
+
1120
+ Returns
1121
+ -------
1122
+ hessian : np.array
1123
+ 2d array containing the hessian.
1124
+ """
1125
+ sqrt_m = self.masses_rep ** 0.5
1126
+ if isinstance(mw_hessian, torch.Tensor):
1127
+ s = torch.tensor(sqrt_m, dtype=mw_hessian.dtype, device=mw_hessian.device)
1128
+ return mw_hessian * s[:, None] * s[None, :]
1129
+ else:
1130
+ return mw_hessian * sqrt_m[:, None] * sqrt_m[None, :]
1131
+
1132
+ # indices (0 … N‑1) of atoms that are *not* frozen
1133
+ @property
1134
+ def active_atom_indices(self):
1135
+ if not hasattr(self, "_active_atom_indices"):
1136
+ self._active_atom_indices = np.array(
1137
+ [i for i in range(len(self.atoms)) if i not in self.freeze_atoms],
1138
+ dtype=int,
1139
+ )
1140
+ return self._active_atom_indices
1141
+
1142
+ # 3N‑dimensional indices of DOFs that are not frozen (x,y,z per atom)
1143
+ @property
1144
+ def active_dof_indices(self):
1145
+ if not hasattr(self, "_active_dof_indices"):
1146
+ act = []
1147
+ for a in self.active_atom_indices:
1148
+ act.extend([3 * a, 3 * a + 1, 3 * a + 2])
1149
+ self._active_dof_indices = np.asarray(act, dtype=int)
1150
+ return self._active_dof_indices
1151
+
1152
+ @property
1153
+ def hess_active_atom_indices(self):
1154
+ if self.within_partial_hessian is None:
1155
+ return self.active_atom_indices
1156
+ return self.within_partial_hessian["active_atoms"]
1157
+
1158
+ @property
1159
+ def hess_active_dof_indices(self):
1160
+ if self.within_partial_hessian is None:
1161
+ return self.active_dof_indices
1162
+ return self.within_partial_hessian["active_dofs"]
1163
+
1164
+ # Convenience: extract / insert an active slice
1165
+ def full_from_active(self, active_vec):
1166
+ """Expand a vector defined on active DOFs to 3N, keeping frozen data."""
1167
+ if isinstance(active_vec, torch.Tensor):
1168
+ full = torch.zeros_like(self.cart_coords)
1169
+ full[self.active_dof_indices] = active_vec
1170
+ return full
1171
+ full = np.zeros_like(self.cart_coords)
1172
+ full[self.active_dof_indices] = active_vec
1173
+ return full
1174
+
1175
+ def active_from_full(self, full_vec):
1176
+ """Return the part of a 3N vector that belongs to active DOFs."""
1177
+ return full_vec[self.active_dof_indices]
1178
+
1179
+ def full_from_hess_active(self, active_vec):
1180
+ """Expand a vector defined on Hessian-active DOFs to full 3N."""
1181
+ inds = self.hess_active_dof_indices
1182
+ if isinstance(active_vec, torch.Tensor):
1183
+ idx = torch.as_tensor(inds, dtype=torch.long, device=active_vec.device)
1184
+ full = torch.zeros(self.cart_coords.size, dtype=active_vec.dtype, device=active_vec.device)
1185
+ full.index_copy_(0, idx, active_vec)
1186
+ return full
1187
+ full = np.zeros_like(self.cart_coords)
1188
+ full[inds] = active_vec
1189
+ return full
1190
+
1191
+ def hess_active_from_full(self, full_vec):
1192
+ """Return the part of a 3N vector that belongs to Hessian-active DOFs."""
1193
+ inds = self.hess_active_dof_indices
1194
+ if isinstance(full_vec, torch.Tensor):
1195
+ idx = torch.as_tensor(inds, dtype=torch.long, device=full_vec.device)
1196
+ return full_vec.index_select(0, idx)
1197
+ return full_vec[inds]
1198
+
1199
+ def set_h5_hessian(self, fn):
1200
+ with h5py.File(fn, "r") as handle:
1201
+ atoms = handle.attrs["atoms"]
1202
+ hessian = handle["hessian"][:]
1203
+
1204
+ # Also check lengths, as zip would lead to trunction for
1205
+ # different lenghts of self.atoms and atoms.
1206
+ valid = (len(atoms) == len(self.atoms)) and all(
1207
+ [ga.lower() == a.lower() for ga, a in zip(self.atoms, atoms)]
1208
+ )
1209
+ if valid:
1210
+ self.cart_hessian = hessian
1211
+
1212
+ def get_normal_modes(self, cart_hessian=None, full=False):
1213
+ """Normal mode wavenumbers, eigenvalues and Cartesian displacements Hessian."""
1214
+ if cart_hessian is None:
1215
+ cart_hessian = self.cart_hessian
1216
+
1217
+ mw_hessian = self.mass_weigh_hessian(cart_hessian)
1218
+ proj_hessian, P = self.eckart_projection(mw_hessian, return_P=True, full=full)
1219
+
1220
+ is_partial = (
1221
+ self.within_partial_hessian is not None
1222
+ and proj_hessian is not None
1223
+ and proj_hessian.shape == (int(self.within_partial_hessian.get("active_n_dof", 0)),
1224
+ int(self.within_partial_hessian.get("active_n_dof", 0)))
1225
+ )
1226
+
1227
+ if isinstance(proj_hessian, torch.Tensor):
1228
+ eigvals, eigvecs = torch.linalg.eigh(proj_hessian)
1229
+ mw_cart_displs = P.T @ eigvecs
1230
+ if is_partial:
1231
+ masses_act = self.masses[self.hess_active_atom_indices]
1232
+ m3 = torch.repeat_interleave(
1233
+ torch.as_tensor(masses_act, dtype=proj_hessian.dtype, device=proj_hessian.device), 3
1234
+ )
1235
+ cart_displs_act = mw_cart_displs / torch.sqrt(m3.view(-1, 1))
1236
+ cart_displs_act /= torch.linalg.norm(cart_displs_act, dim=0)
1237
+ cart_displs = torch.zeros(
1238
+ (self.cart_coords.size, cart_displs_act.shape[1]),
1239
+ dtype=cart_displs_act.dtype,
1240
+ device=cart_displs_act.device,
1241
+ )
1242
+ idx = torch.as_tensor(self.hess_active_dof_indices, dtype=torch.long, device=cart_displs.device)
1243
+ cart_displs.index_copy_(0, idx, cart_displs_act)
1244
+ else:
1245
+ inv_sqrt_m = torch.tensor(1.0 / (self.masses_rep ** 0.5), dtype=proj_hessian.dtype, device=proj_hessian.device)
1246
+ cart_displs = mw_cart_displs * inv_sqrt_m[:, None]
1247
+ cart_displs /= torch.linalg.norm(cart_displs, dim=0)
1248
+ eigvals = eigvals.cpu().numpy()
1249
+ else:
1250
+ eigvals, eigvecs = np.linalg.eigh(proj_hessian)
1251
+ mw_cart_displs = P.T.dot(eigvecs)
1252
+ if is_partial:
1253
+ masses_act = self.masses[self.hess_active_atom_indices]
1254
+ m3 = np.repeat(masses_act, 3)
1255
+ cart_displs_act = mw_cart_displs / np.sqrt(m3)[:, None]
1256
+ cart_displs_act /= np.linalg.norm(cart_displs_act, axis=0)
1257
+ cart_displs = np.zeros((self.cart_coords.size, cart_displs_act.shape[1]))
1258
+ cart_displs[self.hess_active_dof_indices, :] = cart_displs_act
1259
+ else:
1260
+ inv_sqrt_m = 1.0 / (self.masses_rep ** 0.5)
1261
+ cart_displs = mw_cart_displs * inv_sqrt_m[:, None]
1262
+ cart_displs /= np.linalg.norm(cart_displs, axis=0)
1263
+
1264
+ nus = eigval_to_wavenumber(eigvals)
1265
+ return nus, eigvals, mw_cart_displs, cart_displs
1266
+
1267
+ def get_imag_frequencies(self, hessian=None, thresh=1e-6):
1268
+ vibfreqs, eigvals, *_ = self.get_normal_modes(hessian)
1269
+ return vibfreqs[eigvals < thresh]
1270
+
1271
+ def get_thermoanalysis(
1272
+ self, energy=None, cart_hessian=None, T=T_DEFAULT, p=p_DEFAULT, point_group="c1"
1273
+ ):
1274
+ if cart_hessian is None:
1275
+ cart_hessian = self.cart_hessian
1276
+ # Delte any supplied energy value when a Hessian calculation is carried out
1277
+ energy = None
1278
+
1279
+ if energy is None:
1280
+ energy = self.energy
1281
+
1282
+ vibfreqs, *_ = self.get_normal_modes(cart_hessian)
1283
+ try:
1284
+ mult = self.calculator.mult
1285
+ except AttributeError:
1286
+ mult = 1
1287
+ logger.debug(
1288
+ "Multiplicity for electronic entropy could not be determined! "
1289
+ f"Using 2S+1 = {mult}."
1290
+ )
1291
+
1292
+ thermo_dict = {
1293
+ "masses": self.masses,
1294
+ "wavenumbers": vibfreqs,
1295
+ "coords3d": self.coords3d,
1296
+ "scf_energy": energy,
1297
+ "mult": mult,
1298
+ }
1299
+
1300
+ qcd = QCData(thermo_dict, point_group=point_group)
1301
+ thermo = thermochemistry(
1302
+ qcd, temperature=T, pressure=p, invert_imags=-15.0, cutoff=25.0
1303
+ )
1304
+
1305
+ return thermo
1306
+
1307
+ def get_trans_rot_projector(self, full=False):
1308
+ return get_trans_rot_projector(self.cart_coords, masses=self.masses, full=full)
1309
+
1310
+ def eckart_projection(self, mw_hessian, return_P=False, full=False):
1311
+ # Must not project analytical 2d potentials.
1312
+ if self.is_analytical_2d:
1313
+ return mw_hessian
1314
+
1315
+ if (
1316
+ self.within_partial_hessian is not None
1317
+ and mw_hessian is not None
1318
+ and mw_hessian.shape == (int(self.within_partial_hessian.get("active_n_dof", 0)),
1319
+ int(self.within_partial_hessian.get("active_n_dof", 0)))
1320
+ ):
1321
+ coords_act = self.coords3d[self.hess_active_atom_indices].flatten()
1322
+ masses_act = self.masses[self.hess_active_atom_indices]
1323
+ P = get_trans_rot_projector(coords_act, masses=masses_act, full=full)
1324
+ else:
1325
+ P = self.get_trans_rot_projector(full=full)
1326
+ if isinstance(mw_hessian, torch.Tensor):
1327
+ P = torch.tensor(P, device=mw_hessian.device, dtype=mw_hessian.dtype)
1328
+ proj_hessian = P @ mw_hessian @ P.T
1329
+ # Projection seems to slightly break symmetry (sometimes?). Resymmetrize.
1330
+ proj_hessian = (proj_hessian + proj_hessian.T) / 2
1331
+ else:
1332
+ proj_hessian = P.dot(mw_hessian).dot(P.T)
1333
+ # Projection seems to slightly break symmetry (sometimes?). Resymmetrize.
1334
+ proj_hessian = (proj_hessian + proj_hessian.T) / 2
1335
+ if return_P:
1336
+ return proj_hessian, P
1337
+ else:
1338
+ return proj_hessian
1339
+
1340
+ def calc_energy_and_forces(self):
1341
+ """Force a calculation of the current energy and forces."""
1342
+ results = self.calculator.get_forces(self.atoms, self.cart_coords)
1343
+ self.set_results(results)
1344
+
1345
+ def assert_cart_coords(self, coords):
1346
+ assert coords.size == self.cart_coords.size, (
1347
+ "This method only works with cartesian coordinate input. "
1348
+ "Did you accidentally provide internal coordinates?"
1349
+ )
1350
+
1351
+ def get_temporary_coords(self, coords):
1352
+ if self.coord_type != "cart":
1353
+ int_step = coords - self.internal.coords
1354
+ cart_step = self.internal.transform_int_step(int_step, pure=True)
1355
+ coords = self.cart_coords + cart_step
1356
+ self.assert_cart_coords(coords)
1357
+ return coords
1358
+
1359
+ def get_energy_at(self, coords):
1360
+ coords = self.get_temporary_coords(coords)
1361
+ return self.calculator.get_energy(self.atoms, coords)["energy"]
1362
+
1363
+ def get_energy_at_cart_coords(self, cart_coords):
1364
+ self.assert_cart_coords(cart_coords)
1365
+ return self.calculator.get_energy(self.atoms, cart_coords)["energy"]
1366
+
1367
+ def get_energy_and_forces_at(self, coords):
1368
+ """Calculate forces and energies at the given coordinates.
1369
+
1370
+ The results are not saved in the Geometry object."""
1371
+ coords = self.get_temporary_coords(coords)
1372
+ results = self.calculator.get_forces(self.atoms, coords)
1373
+ self.zero_frozen_forces(results["forces"])
1374
+
1375
+ if self.coord_type != "cart":
1376
+ results["forces"] = self.internal.transform_forces(results["forces"])
1377
+
1378
+ return results
1379
+
1380
+ def get_energy_and_cart_forces_at(self, cart_coords):
1381
+ self.assert_cart_coords(cart_coords)
1382
+ results = self.calculator.get_forces(self.atoms, cart_coords)
1383
+ self.zero_frozen_forces(results["forces"])
1384
+ return results
1385
+
1386
+ def get_energy_and_cart_hessian_at(self, cart_coords):
1387
+ self.assert_cart_coords(cart_coords)
1388
+ results = self.calculator.get_hessian(self.atoms, cart_coords)
1389
+ return results
1390
+
1391
+ def calc_double_ao_overlap(self, geom2):
1392
+ return self.calculator.run_double_mol_calculation(
1393
+ self.atoms, self.coords, geom2.coords
1394
+ )
1395
+
1396
+ def zero_frozen_forces(self, cart_forces):
1397
+ cart_forces.reshape(-1, 3)[self.freeze_atoms] = 0.0
1398
+
1399
+ def clear(self):
1400
+ """Reset the object state."""
1401
+
1402
+ self._energy = None
1403
+ self._forces = None
1404
+ self._hessian = None
1405
+ self.within_partial_hessian = None
1406
+ self.true_energy = None
1407
+ self.true_forces = None
1408
+ self.true_hessian = None
1409
+ self._all_energies = None
1410
+ self.results = {}
1411
+
1412
+ def set_results(self, results):
1413
+ """Save the results from a dictionary.
1414
+
1415
+ Parameters
1416
+ ----------
1417
+ results : dict
1418
+ The keys in this dict will be set as attributes in the current
1419
+ object, with the corresponding item as value.
1420
+ """
1421
+
1422
+ if "within_partial_hessian" in results:
1423
+ self.within_partial_hessian = results["within_partial_hessian"]
1424
+ elif "hessian" in results:
1425
+ self.within_partial_hessian = None
1426
+
1427
+ trans = {
1428
+ "energy": "energy",
1429
+ "forces": "cart_forces",
1430
+ "hessian": "cart_hessian",
1431
+ "within_partial_hessian": "within_partial_hessian",
1432
+ # True properties in AFIR calculations
1433
+ "true_energy": "true_energy",
1434
+ "true_forces": "true_forces",
1435
+ "true_hessian": "true_hessian",
1436
+ # Overlap calculator; includes excited states
1437
+ "all_energies": "all_energies",
1438
+ }
1439
+
1440
+ for key in results:
1441
+ if key == "within_partial_hessian":
1442
+ continue
1443
+ # Zero forces of frozen atoms
1444
+ if key == "forces":
1445
+ self.zero_frozen_forces(results[key])
1446
+
1447
+ setattr(self, trans[key], results[key])
1448
+ self.results = results
1449
+
1450
+ def as_xyz(self, comment="", atoms=None, cart_coords=None):
1451
+ """Current geometry as a string in XYZ-format.
1452
+
1453
+ Parameters
1454
+ ----------
1455
+ comment : str, optional
1456
+ Will be written in the second line (comment line) of the
1457
+ XYZ-string.
1458
+ cart_coords : np.array, 1d, shape (3 * atoms.size, )
1459
+ Cartesians for dumping instead of self._coords.
1460
+
1461
+ Returns
1462
+ -------
1463
+ xyz_str : str
1464
+ Current geometry as string in XYZ-format.
1465
+ """
1466
+ if atoms is None:
1467
+ atoms = self.atoms
1468
+ if cart_coords is None:
1469
+ cart_coords = self._coords
1470
+ cart_coords = cart_coords.copy()
1471
+ cart_coords *= BOHR2ANG
1472
+ if comment == "":
1473
+ comment = self.comment
1474
+ return make_xyz_str(atoms, cart_coords.reshape((-1, 3)), comment)
1475
+
1476
+ def dump_xyz(self, fn, cart_coords=None, **kwargs):
1477
+ fn = str(fn)
1478
+ if not fn.lower().endswith(".xyz"):
1479
+ fn = fn + ".xyz"
1480
+ with open(fn, "w") as handle:
1481
+ handle.write(self.as_xyz(cart_coords=cart_coords, **kwargs))
1482
+
1483
+ def get_subgeom(self, indices, coord_type="cart", sort=False):
1484
+ """Return a Geometry containing a subset of the current Geometry.
1485
+
1486
+ Parameters
1487
+ ----------
1488
+ indices : iterable of ints
1489
+ Atomic indices that the define the subset of the current Geometry.
1490
+ coord_type : str, ("cart", "redund"), optional
1491
+ Coordinate system of the new Geometry.
1492
+
1493
+ Returns
1494
+ -------
1495
+ sub_geom : Geometry
1496
+ Subset of the current Geometry.
1497
+ """
1498
+ if sort:
1499
+ indices = sorted(indices)
1500
+ ind_list = list(indices)
1501
+ sub_atoms = [self.atoms[i] for i in ind_list]
1502
+ sub_coords = self.coords3d[ind_list]
1503
+ sub_geom = Geometry(sub_atoms, sub_coords.flatten(), coord_type=coord_type)
1504
+ return sub_geom
1505
+
1506
+ def get_subgeom_without(self, indices, **kwargs):
1507
+ with_indices = [ind for ind, _ in enumerate(self.atoms) if ind not in indices]
1508
+ return self.get_subgeom(with_indices, **kwargs)
1509
+
1510
+ def rmsd(self, geom):
1511
+ return rmsd.kabsch_rmsd(
1512
+ self.coords3d - self.centroid, geom.coords3d - geom.centroid
1513
+ )
1514
+
1515
+ def as_g98_list(self):
1516
+ """Returns data for fake Gaussian98 standard orientation output.
1517
+
1518
+ Returns
1519
+ -------
1520
+ g98_list : list
1521
+ List with one row per atom. Every row contains [center number,
1522
+ atomic number, atomic type (always 0 for now), X Y Z coordinates
1523
+ in Angstrom.
1524
+ """
1525
+ Atom = namedtuple("Atom", "center_num atom_num atom_type x y z")
1526
+ atoms = list()
1527
+ for i, (a, c) in enumerate(zip(self.atoms, self.coords3d), 1):
1528
+ x, y, z = c * BOHR2ANG
1529
+ atom = Atom(i, ATOMIC_NUMBERS[a.lower()], 0, x, y, z)
1530
+ atoms.append(atom)
1531
+ return atoms
1532
+
1533
+ def tmp_xyz_handle(self, atoms=None, cart_coords=None):
1534
+ tmp_xyz = tempfile.NamedTemporaryFile(suffix=".xyz")
1535
+ tmp_xyz.write(self.as_xyz(atoms=atoms, cart_coords=cart_coords).encode("utf-8"))
1536
+ tmp_xyz.flush()
1537
+ return tmp_xyz
1538
+
1539
+ def jmol(self, atoms=None, cart_coords=None):
1540
+ """Show geometry in jmol."""
1541
+ tmp_xyz = self.tmp_xyz_handle(atoms, cart_coords)
1542
+ jmol_cmd = "jmol"
1543
+ try:
1544
+ subprocess.run([jmol_cmd, tmp_xyz.name])
1545
+ except FileNotFoundError:
1546
+ print(f"'{jmol_cmd}' seems not to be on your path!")
1547
+ tmp_xyz.close()
1548
+
1549
+ def modes3d(self):
1550
+ try:
1551
+ bonds = self.internal.bond_atom_indices
1552
+ bonds_str = " --bonds " + " ".join(map(str, it.chain(*bonds)))
1553
+ except AttributeError:
1554
+ bonds_str = ""
1555
+
1556
+ tmp_xyz = self.tmp_xyz_handle()
1557
+ cmd = ["modes3d.py", tmp_xyz.name]
1558
+ if bonds_str:
1559
+ cmd.extend(shlex.split(bonds_str))
1560
+ subprocess.run(cmd)
1561
+ tmp_xyz.close()
1562
+
1563
+ def as_ase_atoms(self):
1564
+ try:
1565
+ import ase
1566
+ except ImportError:
1567
+ print("Please install the 'ase' package!")
1568
+ return None
1569
+
1570
+ # ASE coordinates are in Angstrom
1571
+ atoms = ase.Atoms(symbols=self.atoms, positions=self.coords3d * BOHR2ANG)
1572
+
1573
+ if self.calculator is not None:
1574
+ from pysisyphus.calculators import FakeASE
1575
+
1576
+ ase_calc = FakeASE(self.calculator)
1577
+ atoms.set_calculator(ase_calc)
1578
+ return atoms
1579
+
1580
+ def get_restart_info(self):
1581
+ # Geometry restart information
1582
+ restart_info = {
1583
+ "atoms": self.atoms,
1584
+ "cart_coords": self.cart_coords.tolist(),
1585
+ "coord_type": self.coord_type,
1586
+ "comment": self.comment,
1587
+ }
1588
+ try:
1589
+ typed_prims = self.internal.typed_prims
1590
+ except AttributeError:
1591
+ typed_prims = None
1592
+ restart_info["typed_prims"] = typed_prims
1593
+
1594
+ # Calculator restart information
1595
+ try:
1596
+ calc_restart_info = self.calculator.get_restart_info()
1597
+ except AttributeError:
1598
+ calc_restart_info = dict()
1599
+ restart_info["calc_info"] = calc_restart_info
1600
+
1601
+ return restart_info
1602
+
1603
+ def set_restart_info(self, restart_info):
1604
+ assert self.atoms == restart_info["atoms"]
1605
+ self.cart_coords = np.array(restart_info["cart_coords"], dtype=float)
1606
+
1607
+ try:
1608
+ self.calculator.set_restart_info(restart_info["calc_info"])
1609
+ except KeyError:
1610
+ print("No calculator restart information found!")
1611
+ except AttributeError:
1612
+ print("Could not restart calculator, as no calculator is set!")
1613
+
1614
+ def get_sphere_radius(self, offset=4):
1615
+ distances = pdist(self.coords3d)
1616
+
1617
+ radius = (distances.max() / 2) + offset
1618
+ return radius
1619
+
1620
+ def without_hydrogens(self):
1621
+ atoms_no_h, coords3d_no_h = zip(
1622
+ *[
1623
+ (atom, coords)
1624
+ for atom, coords in zip(self.atoms, self.coords3d)
1625
+ if atom.lower() != "h"
1626
+ ]
1627
+ )
1628
+ return Geometry(atoms_no_h, np.array(coords3d_no_h).flatten())
1629
+
1630
+ def describe(self):
1631
+ return f"Geometry({self.sum_formula}, {len(self.atoms)} atoms)"
1632
+
1633
+ def approximate_radius(self):
1634
+ """Approximate molecule radius from the biggest atom distance along an axis."""
1635
+ coords3d = self.coords3d - self.centroid[None, :]
1636
+ mins = coords3d.min(axis=0)
1637
+ maxs = coords3d.max(axis=0)
1638
+ dists = maxs - mins
1639
+ max_dist = dists.max()
1640
+ return max_dist
1641
+
1642
+ def rotate(self, copy=False, rng=None):
1643
+ if copy:
1644
+ geom = self.copy()
1645
+ else:
1646
+ geom = self
1647
+
1648
+ rot = Rotation.random(random_state=rng)
1649
+ geom.coords3d = rot.apply(geom.coords3d)
1650
+ return geom
1651
+
1652
+ @property
1653
+ def bond_sets(self, bond_factor=BOND_FACTOR):
1654
+ bonds = find_bonds(
1655
+ self.atoms, self.coords3d, self.covalent_radii, bond_factor=bond_factor
1656
+ )
1657
+ bond_sets = set([frozenset(b) for b in bonds])
1658
+ return bond_sets
1659
+
1660
+ def __str__(self):
1661
+ name = ""
1662
+ if self.name:
1663
+ name = f"{self.name}, "
1664
+ return f"Geometry({name}{self.sum_formula})"
1665
+
1666
+ def __repr__(self):
1667
+ return self.__str__()