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.
- hessian_ff/__init__.py +50 -0
- hessian_ff/analytical_hessian.py +609 -0
- hessian_ff/constants.py +46 -0
- hessian_ff/forcefield.py +339 -0
- hessian_ff/loaders.py +608 -0
- hessian_ff/native/Makefile +8 -0
- hessian_ff/native/__init__.py +28 -0
- hessian_ff/native/analytical_hessian.py +88 -0
- hessian_ff/native/analytical_hessian_ext.cpp +258 -0
- hessian_ff/native/bonded.py +82 -0
- hessian_ff/native/bonded_ext.cpp +640 -0
- hessian_ff/native/loader.py +349 -0
- hessian_ff/native/nonbonded.py +118 -0
- hessian_ff/native/nonbonded_ext.cpp +1150 -0
- hessian_ff/prmtop_parmed.py +23 -0
- hessian_ff/system.py +107 -0
- hessian_ff/terms/__init__.py +14 -0
- hessian_ff/terms/angle.py +73 -0
- hessian_ff/terms/bond.py +44 -0
- hessian_ff/terms/cmap.py +406 -0
- hessian_ff/terms/dihedral.py +141 -0
- hessian_ff/terms/nonbonded.py +209 -0
- hessian_ff/tests/__init__.py +0 -0
- hessian_ff/tests/conftest.py +75 -0
- hessian_ff/tests/data/small/complex.parm7 +1346 -0
- hessian_ff/tests/data/small/complex.pdb +125 -0
- hessian_ff/tests/data/small/complex.rst7 +63 -0
- hessian_ff/tests/test_coords_input.py +44 -0
- hessian_ff/tests/test_energy_force.py +49 -0
- hessian_ff/tests/test_hessian.py +137 -0
- hessian_ff/tests/test_smoke.py +18 -0
- hessian_ff/tests/test_validation.py +40 -0
- hessian_ff/workflows.py +889 -0
- mlmm/__init__.py +36 -0
- mlmm/__main__.py +7 -0
- mlmm/_version.py +34 -0
- mlmm/add_elem_info.py +374 -0
- mlmm/advanced_help.py +91 -0
- mlmm/align_freeze_atoms.py +601 -0
- mlmm/all.py +3535 -0
- mlmm/bond_changes.py +231 -0
- mlmm/bool_compat.py +223 -0
- mlmm/cli.py +574 -0
- mlmm/cli_utils.py +166 -0
- mlmm/default_group.py +337 -0
- mlmm/defaults.py +467 -0
- mlmm/define_layer.py +526 -0
- mlmm/dft.py +1041 -0
- mlmm/energy_diagram.py +253 -0
- mlmm/extract.py +2213 -0
- mlmm/fix_altloc.py +464 -0
- mlmm/freq.py +1406 -0
- mlmm/harmonic_constraints.py +140 -0
- mlmm/hessian_cache.py +44 -0
- mlmm/hessian_calc.py +174 -0
- mlmm/irc.py +638 -0
- mlmm/mlmm_calc.py +2262 -0
- mlmm/mm_parm.py +945 -0
- mlmm/oniom_export.py +1983 -0
- mlmm/oniom_import.py +457 -0
- mlmm/opt.py +1742 -0
- mlmm/path_opt.py +1353 -0
- mlmm/path_search.py +2299 -0
- mlmm/preflight.py +88 -0
- mlmm/py.typed +1 -0
- mlmm/pysis_runner.py +45 -0
- mlmm/scan.py +1047 -0
- mlmm/scan2d.py +1226 -0
- mlmm/scan3d.py +1265 -0
- mlmm/scan_common.py +184 -0
- mlmm/summary_log.py +736 -0
- mlmm/trj2fig.py +448 -0
- mlmm/tsopt.py +2871 -0
- mlmm/utils.py +2309 -0
- mlmm/xtb_embedcharge_correction.py +475 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/METADATA +1159 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/RECORD +372 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/WHEEL +5 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/entry_points.txt +2 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/licenses/LICENSE +674 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/top_level.txt +4 -0
- pysisyphus/Geometry.py +1667 -0
- pysisyphus/LICENSE +674 -0
- pysisyphus/TableFormatter.py +63 -0
- pysisyphus/TablePrinter.py +74 -0
- pysisyphus/__init__.py +12 -0
- pysisyphus/calculators/AFIR.py +452 -0
- pysisyphus/calculators/AnaPot.py +20 -0
- pysisyphus/calculators/AnaPot2.py +48 -0
- pysisyphus/calculators/AnaPot3.py +12 -0
- pysisyphus/calculators/AnaPot4.py +20 -0
- pysisyphus/calculators/AnaPotBase.py +337 -0
- pysisyphus/calculators/AnaPotCBM.py +25 -0
- pysisyphus/calculators/AtomAtomTransTorque.py +154 -0
- pysisyphus/calculators/CFOUR.py +250 -0
- pysisyphus/calculators/Calculator.py +844 -0
- pysisyphus/calculators/CerjanMiller.py +24 -0
- pysisyphus/calculators/Composite.py +123 -0
- pysisyphus/calculators/ConicalIntersection.py +171 -0
- pysisyphus/calculators/DFTBp.py +430 -0
- pysisyphus/calculators/DFTD3.py +66 -0
- pysisyphus/calculators/DFTD4.py +84 -0
- pysisyphus/calculators/Dalton.py +61 -0
- pysisyphus/calculators/Dimer.py +681 -0
- pysisyphus/calculators/Dummy.py +20 -0
- pysisyphus/calculators/EGO.py +76 -0
- pysisyphus/calculators/EnergyMin.py +224 -0
- pysisyphus/calculators/ExternalPotential.py +264 -0
- pysisyphus/calculators/FakeASE.py +35 -0
- pysisyphus/calculators/FourWellAnaPot.py +28 -0
- pysisyphus/calculators/FreeEndNEBPot.py +39 -0
- pysisyphus/calculators/Gaussian09.py +18 -0
- pysisyphus/calculators/Gaussian16.py +726 -0
- pysisyphus/calculators/HardSphere.py +159 -0
- pysisyphus/calculators/IDPPCalculator.py +49 -0
- pysisyphus/calculators/IPIClient.py +133 -0
- pysisyphus/calculators/IPIServer.py +234 -0
- pysisyphus/calculators/LEPSBase.py +24 -0
- pysisyphus/calculators/LEPSExpr.py +139 -0
- pysisyphus/calculators/LennardJones.py +80 -0
- pysisyphus/calculators/MOPAC.py +219 -0
- pysisyphus/calculators/MullerBrownSympyPot.py +51 -0
- pysisyphus/calculators/MultiCalc.py +85 -0
- pysisyphus/calculators/NFK.py +45 -0
- pysisyphus/calculators/OBabel.py +87 -0
- pysisyphus/calculators/ONIOMv2.py +1129 -0
- pysisyphus/calculators/ORCA.py +893 -0
- pysisyphus/calculators/ORCA5.py +6 -0
- pysisyphus/calculators/OpenMM.py +88 -0
- pysisyphus/calculators/OpenMolcas.py +281 -0
- pysisyphus/calculators/OverlapCalculator.py +908 -0
- pysisyphus/calculators/Psi4.py +218 -0
- pysisyphus/calculators/PyPsi4.py +37 -0
- pysisyphus/calculators/PySCF.py +341 -0
- pysisyphus/calculators/PyXTB.py +73 -0
- pysisyphus/calculators/QCEngine.py +106 -0
- pysisyphus/calculators/Rastrigin.py +22 -0
- pysisyphus/calculators/Remote.py +76 -0
- pysisyphus/calculators/Rosenbrock.py +15 -0
- pysisyphus/calculators/SocketCalc.py +97 -0
- pysisyphus/calculators/TIP3P.py +111 -0
- pysisyphus/calculators/TransTorque.py +161 -0
- pysisyphus/calculators/Turbomole.py +965 -0
- pysisyphus/calculators/VRIPot.py +37 -0
- pysisyphus/calculators/WFOWrapper.py +333 -0
- pysisyphus/calculators/WFOWrapper2.py +341 -0
- pysisyphus/calculators/XTB.py +418 -0
- pysisyphus/calculators/__init__.py +81 -0
- pysisyphus/calculators/cosmo_data.py +139 -0
- pysisyphus/calculators/parser.py +150 -0
- pysisyphus/color.py +19 -0
- pysisyphus/config.py +133 -0
- pysisyphus/constants.py +65 -0
- pysisyphus/cos/AdaptiveNEB.py +230 -0
- pysisyphus/cos/ChainOfStates.py +725 -0
- pysisyphus/cos/FreeEndNEB.py +25 -0
- pysisyphus/cos/FreezingString.py +103 -0
- pysisyphus/cos/GrowingChainOfStates.py +71 -0
- pysisyphus/cos/GrowingNT.py +309 -0
- pysisyphus/cos/GrowingString.py +508 -0
- pysisyphus/cos/NEB.py +189 -0
- pysisyphus/cos/SimpleZTS.py +64 -0
- pysisyphus/cos/__init__.py +22 -0
- pysisyphus/cos/stiffness.py +199 -0
- pysisyphus/drivers/__init__.py +17 -0
- pysisyphus/drivers/afir.py +855 -0
- pysisyphus/drivers/barriers.py +271 -0
- pysisyphus/drivers/birkholz.py +138 -0
- pysisyphus/drivers/cluster.py +318 -0
- pysisyphus/drivers/diabatization.py +133 -0
- pysisyphus/drivers/merge.py +368 -0
- pysisyphus/drivers/merge_mol2.py +322 -0
- pysisyphus/drivers/opt.py +375 -0
- pysisyphus/drivers/perf.py +91 -0
- pysisyphus/drivers/pka.py +52 -0
- pysisyphus/drivers/precon_pos_rot.py +669 -0
- pysisyphus/drivers/rates.py +480 -0
- pysisyphus/drivers/replace.py +219 -0
- pysisyphus/drivers/scan.py +212 -0
- pysisyphus/drivers/spectrum.py +166 -0
- pysisyphus/drivers/thermo.py +31 -0
- pysisyphus/dynamics/Gaussian.py +103 -0
- pysisyphus/dynamics/__init__.py +20 -0
- pysisyphus/dynamics/colvars.py +136 -0
- pysisyphus/dynamics/driver.py +297 -0
- pysisyphus/dynamics/helpers.py +256 -0
- pysisyphus/dynamics/lincs.py +105 -0
- pysisyphus/dynamics/mdp.py +364 -0
- pysisyphus/dynamics/rattle.py +121 -0
- pysisyphus/dynamics/thermostats.py +128 -0
- pysisyphus/dynamics/wigner.py +266 -0
- pysisyphus/elem_data.py +3473 -0
- pysisyphus/exceptions.py +2 -0
- pysisyphus/filtertrj.py +69 -0
- pysisyphus/helpers.py +623 -0
- pysisyphus/helpers_pure.py +649 -0
- pysisyphus/init_logging.py +50 -0
- pysisyphus/intcoords/Bend.py +69 -0
- pysisyphus/intcoords/Bend2.py +25 -0
- pysisyphus/intcoords/BondedFragment.py +32 -0
- pysisyphus/intcoords/Cartesian.py +41 -0
- pysisyphus/intcoords/CartesianCoords.py +140 -0
- pysisyphus/intcoords/Coords.py +56 -0
- pysisyphus/intcoords/DLC.py +197 -0
- pysisyphus/intcoords/DistanceFunction.py +34 -0
- pysisyphus/intcoords/DummyImproper.py +70 -0
- pysisyphus/intcoords/DummyTorsion.py +72 -0
- pysisyphus/intcoords/LinearBend.py +105 -0
- pysisyphus/intcoords/LinearDisplacement.py +80 -0
- pysisyphus/intcoords/OutOfPlane.py +59 -0
- pysisyphus/intcoords/PrimTypes.py +286 -0
- pysisyphus/intcoords/Primitive.py +137 -0
- pysisyphus/intcoords/RedundantCoords.py +659 -0
- pysisyphus/intcoords/RobustTorsion.py +59 -0
- pysisyphus/intcoords/Rotation.py +147 -0
- pysisyphus/intcoords/Stretch.py +31 -0
- pysisyphus/intcoords/Torsion.py +101 -0
- pysisyphus/intcoords/Torsion2.py +25 -0
- pysisyphus/intcoords/Translation.py +45 -0
- pysisyphus/intcoords/__init__.py +61 -0
- pysisyphus/intcoords/augment_bonds.py +126 -0
- pysisyphus/intcoords/derivatives.py +10512 -0
- pysisyphus/intcoords/eval.py +80 -0
- pysisyphus/intcoords/exceptions.py +37 -0
- pysisyphus/intcoords/findiffs.py +48 -0
- pysisyphus/intcoords/generate_derivatives.py +414 -0
- pysisyphus/intcoords/helpers.py +235 -0
- pysisyphus/intcoords/logging_conf.py +10 -0
- pysisyphus/intcoords/mp_derivatives.py +10836 -0
- pysisyphus/intcoords/setup.py +962 -0
- pysisyphus/intcoords/setup_fast.py +176 -0
- pysisyphus/intcoords/update.py +272 -0
- pysisyphus/intcoords/valid.py +89 -0
- pysisyphus/interpolate/Geodesic.py +93 -0
- pysisyphus/interpolate/IDPP.py +55 -0
- pysisyphus/interpolate/Interpolator.py +116 -0
- pysisyphus/interpolate/LST.py +70 -0
- pysisyphus/interpolate/Redund.py +152 -0
- pysisyphus/interpolate/__init__.py +9 -0
- pysisyphus/interpolate/helpers.py +34 -0
- pysisyphus/io/__init__.py +22 -0
- pysisyphus/io/aomix.py +178 -0
- pysisyphus/io/cjson.py +24 -0
- pysisyphus/io/crd.py +101 -0
- pysisyphus/io/cube.py +220 -0
- pysisyphus/io/fchk.py +184 -0
- pysisyphus/io/hdf5.py +49 -0
- pysisyphus/io/hessian.py +72 -0
- pysisyphus/io/mol2.py +146 -0
- pysisyphus/io/molden.py +293 -0
- pysisyphus/io/orca.py +189 -0
- pysisyphus/io/pdb.py +269 -0
- pysisyphus/io/psf.py +79 -0
- pysisyphus/io/pubchem.py +31 -0
- pysisyphus/io/qcschema.py +34 -0
- pysisyphus/io/sdf.py +29 -0
- pysisyphus/io/xyz.py +61 -0
- pysisyphus/io/zmat.py +175 -0
- pysisyphus/irc/DWI.py +108 -0
- pysisyphus/irc/DampedVelocityVerlet.py +134 -0
- pysisyphus/irc/Euler.py +22 -0
- pysisyphus/irc/EulerPC.py +345 -0
- pysisyphus/irc/GonzalezSchlegel.py +187 -0
- pysisyphus/irc/IMKMod.py +164 -0
- pysisyphus/irc/IRC.py +878 -0
- pysisyphus/irc/IRCDummy.py +10 -0
- pysisyphus/irc/Instanton.py +307 -0
- pysisyphus/irc/LQA.py +53 -0
- pysisyphus/irc/ModeKill.py +136 -0
- pysisyphus/irc/ParamPlot.py +53 -0
- pysisyphus/irc/RK4.py +36 -0
- pysisyphus/irc/__init__.py +31 -0
- pysisyphus/irc/initial_displ.py +219 -0
- pysisyphus/linalg.py +411 -0
- pysisyphus/line_searches/Backtracking.py +88 -0
- pysisyphus/line_searches/HagerZhang.py +184 -0
- pysisyphus/line_searches/LineSearch.py +232 -0
- pysisyphus/line_searches/StrongWolfe.py +108 -0
- pysisyphus/line_searches/__init__.py +9 -0
- pysisyphus/line_searches/interpol.py +15 -0
- pysisyphus/modefollow/NormalMode.py +40 -0
- pysisyphus/modefollow/__init__.py +10 -0
- pysisyphus/modefollow/davidson.py +199 -0
- pysisyphus/modefollow/lanczos.py +95 -0
- pysisyphus/optimizers/BFGS.py +99 -0
- pysisyphus/optimizers/BacktrackingOptimizer.py +113 -0
- pysisyphus/optimizers/ConjugateGradient.py +98 -0
- pysisyphus/optimizers/CubicNewton.py +75 -0
- pysisyphus/optimizers/FIRE.py +113 -0
- pysisyphus/optimizers/HessianOptimizer.py +1176 -0
- pysisyphus/optimizers/LBFGS.py +228 -0
- pysisyphus/optimizers/LayerOpt.py +411 -0
- pysisyphus/optimizers/MicroOptimizer.py +169 -0
- pysisyphus/optimizers/NCOptimizer.py +90 -0
- pysisyphus/optimizers/Optimizer.py +1084 -0
- pysisyphus/optimizers/PreconLBFGS.py +260 -0
- pysisyphus/optimizers/PreconSteepestDescent.py +7 -0
- pysisyphus/optimizers/QuickMin.py +74 -0
- pysisyphus/optimizers/RFOptimizer.py +181 -0
- pysisyphus/optimizers/RSA.py +99 -0
- pysisyphus/optimizers/StabilizedQNMethod.py +248 -0
- pysisyphus/optimizers/SteepestDescent.py +23 -0
- pysisyphus/optimizers/StringOptimizer.py +173 -0
- pysisyphus/optimizers/__init__.py +41 -0
- pysisyphus/optimizers/closures.py +301 -0
- pysisyphus/optimizers/cls_map.py +58 -0
- pysisyphus/optimizers/exceptions.py +6 -0
- pysisyphus/optimizers/gdiis.py +280 -0
- pysisyphus/optimizers/guess_hessians.py +311 -0
- pysisyphus/optimizers/hessian_updates.py +355 -0
- pysisyphus/optimizers/poly_fit.py +285 -0
- pysisyphus/optimizers/precon.py +153 -0
- pysisyphus/optimizers/restrict_step.py +24 -0
- pysisyphus/pack.py +172 -0
- pysisyphus/peakdetect.py +948 -0
- pysisyphus/plot.py +1031 -0
- pysisyphus/run.py +2106 -0
- pysisyphus/socket_helper.py +74 -0
- pysisyphus/stocastic/FragmentKick.py +132 -0
- pysisyphus/stocastic/Kick.py +81 -0
- pysisyphus/stocastic/Pipeline.py +303 -0
- pysisyphus/stocastic/__init__.py +21 -0
- pysisyphus/stocastic/align.py +127 -0
- pysisyphus/testing.py +96 -0
- pysisyphus/thermo.py +156 -0
- pysisyphus/trj.py +824 -0
- pysisyphus/tsoptimizers/RSIRFOptimizer.py +56 -0
- pysisyphus/tsoptimizers/RSPRFOptimizer.py +182 -0
- pysisyphus/tsoptimizers/TRIM.py +59 -0
- pysisyphus/tsoptimizers/TSHessianOptimizer.py +463 -0
- pysisyphus/tsoptimizers/__init__.py +23 -0
- pysisyphus/wavefunction/Basis.py +239 -0
- pysisyphus/wavefunction/DIIS.py +76 -0
- pysisyphus/wavefunction/__init__.py +25 -0
- pysisyphus/wavefunction/build_ext.py +42 -0
- pysisyphus/wavefunction/cart2sph.py +190 -0
- pysisyphus/wavefunction/diabatization.py +304 -0
- pysisyphus/wavefunction/excited_states.py +435 -0
- pysisyphus/wavefunction/gen_ints.py +1811 -0
- pysisyphus/wavefunction/helpers.py +104 -0
- pysisyphus/wavefunction/ints/__init__.py +0 -0
- pysisyphus/wavefunction/ints/boys.py +193 -0
- pysisyphus/wavefunction/ints/boys_table_N_64_xasym_27.1_step_0.01.npy +0 -0
- pysisyphus/wavefunction/ints/cart_gto3d.py +176 -0
- pysisyphus/wavefunction/ints/coulomb3d.py +25928 -0
- pysisyphus/wavefunction/ints/diag_quadrupole3d.py +10036 -0
- pysisyphus/wavefunction/ints/dipole3d.py +8762 -0
- pysisyphus/wavefunction/ints/int2c2e3d.py +7198 -0
- pysisyphus/wavefunction/ints/int3c2e3d_sph.py +65040 -0
- pysisyphus/wavefunction/ints/kinetic3d.py +8240 -0
- pysisyphus/wavefunction/ints/ovlp3d.py +3777 -0
- pysisyphus/wavefunction/ints/quadrupole3d.py +15054 -0
- pysisyphus/wavefunction/ints/self_ovlp3d.py +198 -0
- pysisyphus/wavefunction/localization.py +458 -0
- pysisyphus/wavefunction/multipole.py +159 -0
- pysisyphus/wavefunction/normalization.py +36 -0
- pysisyphus/wavefunction/pop_analysis.py +134 -0
- pysisyphus/wavefunction/shells.py +1171 -0
- pysisyphus/wavefunction/wavefunction.py +504 -0
- pysisyphus/wrapper/__init__.py +11 -0
- pysisyphus/wrapper/exceptions.py +2 -0
- pysisyphus/wrapper/jmol.py +120 -0
- pysisyphus/wrapper/mwfn.py +169 -0
- pysisyphus/wrapper/packmol.py +71 -0
- pysisyphus/xyzloader.py +168 -0
- pysisyphus/yaml_mods.py +45 -0
- thermoanalysis/LICENSE +674 -0
- thermoanalysis/QCData.py +244 -0
- thermoanalysis/__init__.py +0 -0
- thermoanalysis/config.py +3 -0
- thermoanalysis/constants.py +20 -0
- 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__()
|