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/irc/IRC.py
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
# https://verahill.blogspot.de/2013/06/439-calculate-frequencies-from-hessian.html
|
|
2
|
+
# https://chemistry.stackexchange.com/questions/74639
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from math import ceil, log
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import h5py
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from pysisyphus.constants import BOHR2ANG, AU2KJPERMOL
|
|
14
|
+
from pysisyphus.Geometry import Geometry
|
|
15
|
+
from pysisyphus.helpers import check_for_end_sign
|
|
16
|
+
from pysisyphus.helpers_pure import (
|
|
17
|
+
highlight_text,
|
|
18
|
+
eigval_to_wavenumber,
|
|
19
|
+
report_isotopes,
|
|
20
|
+
rms,
|
|
21
|
+
)
|
|
22
|
+
from pysisyphus.irc.Instanton import T_crossover_from_eigval
|
|
23
|
+
from pysisyphus.io import save_third_deriv
|
|
24
|
+
from pysisyphus.optimizers.guess_hessians import get_guess_hessian
|
|
25
|
+
from pysisyphus.TablePrinter import TablePrinter
|
|
26
|
+
from pysisyphus.xyzloader import make_trj_str, make_xyz_str
|
|
27
|
+
|
|
28
|
+
import torch
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IRC:
|
|
32
|
+
valid_displs = ("energy", "length")
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
geometry,
|
|
37
|
+
step_length=0.1,
|
|
38
|
+
max_cycles=125,
|
|
39
|
+
downhill=False,
|
|
40
|
+
forward=True,
|
|
41
|
+
backward=True,
|
|
42
|
+
root=0,
|
|
43
|
+
hessian_init=None,
|
|
44
|
+
displ="energy",
|
|
45
|
+
displ_energy=1e-3,
|
|
46
|
+
displ_length=0.1,
|
|
47
|
+
rms_grad_thresh=1e-3,
|
|
48
|
+
hard_rms_grad_thresh=None,
|
|
49
|
+
energy_thresh=1e-6,
|
|
50
|
+
imag_below=0.0,
|
|
51
|
+
force_inflection=True,
|
|
52
|
+
check_bonds=False,
|
|
53
|
+
out_dir=".",
|
|
54
|
+
prefix="",
|
|
55
|
+
dump_fn="irc_data.h5",
|
|
56
|
+
dump_every=5,
|
|
57
|
+
):
|
|
58
|
+
"""Base class for IRC calculations.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
geometry : Geometry
|
|
63
|
+
Transtion state geometry, or initial geometry for downhill run.
|
|
64
|
+
step_length : float, optional
|
|
65
|
+
Step length in unweighted coordinates.
|
|
66
|
+
max_cycles : int, optional
|
|
67
|
+
Positive integer, controlloing the maximum number of IRC steps
|
|
68
|
+
taken in a direction (forward/backward/downhill).
|
|
69
|
+
downhill : bool, default=False
|
|
70
|
+
Downhill run from a non-stationary point with non-vanishing
|
|
71
|
+
gradient. Disables forward and backward runs.
|
|
72
|
+
forward : bool, default=True
|
|
73
|
+
Integrate IRC in positive s direction.
|
|
74
|
+
backward : bool, default=True
|
|
75
|
+
Integrate IRC in negative s direction.
|
|
76
|
+
root : int, default=0
|
|
77
|
+
Use n-th root for initial displacement from TS.
|
|
78
|
+
hessian_init : str, default=None
|
|
79
|
+
Path to Hessian HDF5 file, e.g., from a previous TS calculation.
|
|
80
|
+
displ: str, one of ("energy", "length")
|
|
81
|
+
Controlls initial displacement from the TS. 'energy' assumes a
|
|
82
|
+
quadratic model, from which a step length for a given energy
|
|
83
|
+
lowering (see 'displ_energy') is determined. 'length' corresponds
|
|
84
|
+
to a displacement along the transition vector.
|
|
85
|
+
displ_energy : float, default=1e-3
|
|
86
|
+
Required energy lowering from the TS in au (Hartree). Used with
|
|
87
|
+
'displ: energy'.
|
|
88
|
+
displ_length : float, default=0.1
|
|
89
|
+
Step length along the transition vector. Used only with
|
|
90
|
+
'displ: length'.
|
|
91
|
+
rms_grad_thresh : float, default=1e-3,
|
|
92
|
+
Convergence is signalled when to root mean square of the unweighted
|
|
93
|
+
gradient is less than or equal to this value
|
|
94
|
+
energy_thresh : float, default=1e-6,
|
|
95
|
+
Signal convergence when the energy difference between two points
|
|
96
|
+
is equal to or less than 'energy_thresh'.
|
|
97
|
+
imag_below : float, default=0.0
|
|
98
|
+
Require the wavenumber of the imaginary mode to be below the
|
|
99
|
+
given threshold. If given, it should be a negative number.
|
|
100
|
+
force_inflection : bool, optional
|
|
101
|
+
Don't indicate convergence before passing an inflection point.
|
|
102
|
+
check_bonds : bool, optional, default=True
|
|
103
|
+
Report whether bonds are formed/broken along the IRC, w.r.t the TS.
|
|
104
|
+
out_dir : str, optional
|
|
105
|
+
Dump everything into 'out_dir' directory instead of the CWD.
|
|
106
|
+
prefix : str, optional
|
|
107
|
+
Short string that is prepended to all files that are created
|
|
108
|
+
by this class, e.g., trajectories and HDF5 dumps.
|
|
109
|
+
dump_fn : str, optional
|
|
110
|
+
Base name for the HDF5 files.
|
|
111
|
+
dump_every : int, optional
|
|
112
|
+
Dump to HDF5 every n-th cycle.
|
|
113
|
+
"""
|
|
114
|
+
assert step_length > 0, "step_length must be positive"
|
|
115
|
+
assert max_cycles > 0, "max_cycles must be positive"
|
|
116
|
+
|
|
117
|
+
self.logger = logging.getLogger("irc")
|
|
118
|
+
|
|
119
|
+
self.geometry = geometry
|
|
120
|
+
self.atoms = self.geometry.atoms
|
|
121
|
+
assert self.geometry.coord_type == "cart"
|
|
122
|
+
|
|
123
|
+
report_isotopes(self.geometry, "the IRC")
|
|
124
|
+
|
|
125
|
+
self.step_length = step_length
|
|
126
|
+
self.max_cycles = max_cycles
|
|
127
|
+
self.downhill = downhill
|
|
128
|
+
# Disable forward/backward when downhill is set
|
|
129
|
+
self.forward = not self.downhill and forward
|
|
130
|
+
self.backward = not self.downhill and backward
|
|
131
|
+
self.root = root
|
|
132
|
+
if hessian_init is None:
|
|
133
|
+
hessian_init = "calc" if not self.downhill else "unit"
|
|
134
|
+
self.hessian_init = hessian_init
|
|
135
|
+
self.displ = displ
|
|
136
|
+
assert (
|
|
137
|
+
self.displ in self.valid_displs
|
|
138
|
+
), f"'displ: {self.displ}' not in {self.valid_displs}"
|
|
139
|
+
self.displ_energy = float(displ_energy)
|
|
140
|
+
self.displ_length = float(displ_length)
|
|
141
|
+
self.rms_grad_thresh = float(rms_grad_thresh)
|
|
142
|
+
self.hard_rms_grad_thresh = hard_rms_grad_thresh
|
|
143
|
+
self.energy_thresh = float(energy_thresh)
|
|
144
|
+
assert imag_below <= 0.0
|
|
145
|
+
self.imag_below = imag_below
|
|
146
|
+
self.force_inflection = force_inflection
|
|
147
|
+
self.check_bonds = check_bonds
|
|
148
|
+
self.out_dir = out_dir
|
|
149
|
+
self.out_dir = Path(self.out_dir)
|
|
150
|
+
if not self.out_dir.exists():
|
|
151
|
+
os.mkdir(self.out_dir)
|
|
152
|
+
self.prefix = f"{prefix}_" if prefix else prefix
|
|
153
|
+
self.dump_fn = dump_fn
|
|
154
|
+
self.dump_every = int(dump_every)
|
|
155
|
+
|
|
156
|
+
# Determine bonds at TS
|
|
157
|
+
self.ts_bond_sets = self.geometry.bond_sets
|
|
158
|
+
self.ref_bond_sets = {}
|
|
159
|
+
|
|
160
|
+
self._m_sqrt = np.sqrt(self.geometry.masses_rep)
|
|
161
|
+
|
|
162
|
+
# cache frequently‑used active indices
|
|
163
|
+
if getattr(self.geometry, "within_partial_hessian", None) is not None:
|
|
164
|
+
self._act_atoms = self.geometry.hess_active_atom_indices
|
|
165
|
+
self._act_dofs = self.geometry.hess_active_dof_indices
|
|
166
|
+
else:
|
|
167
|
+
self._act_atoms = self.geometry.active_atom_indices
|
|
168
|
+
self._act_dofs = self.geometry.active_dof_indices
|
|
169
|
+
|
|
170
|
+
self.all_energies = list()
|
|
171
|
+
self.all_coords = list()
|
|
172
|
+
self.all_gradients = list()
|
|
173
|
+
self.all_mw_coords = list()
|
|
174
|
+
self.all_mw_gradients = list()
|
|
175
|
+
|
|
176
|
+
# step length dE max(|grad|) rms(grad)
|
|
177
|
+
col_fmts = "int float float float float".split()
|
|
178
|
+
header = ("Step", "IRC length", "dE / au", "max(|grad|)", "rms(grad)")
|
|
179
|
+
self.table = TablePrinter(header, col_fmts)
|
|
180
|
+
|
|
181
|
+
self.cycle_places = ceil(log(self.max_cycles, 10))
|
|
182
|
+
|
|
183
|
+
self.mm_inv2 = self.geometry.mm_sqrt_inv[np.ix_(self._act_dofs, self._act_dofs)]
|
|
184
|
+
|
|
185
|
+
def get_path_for_fn(self, fn):
|
|
186
|
+
return self.out_dir / f"{self.prefix}{fn}"
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def coords(self):
|
|
190
|
+
return self.geometry.coords
|
|
191
|
+
|
|
192
|
+
@coords.setter
|
|
193
|
+
def coords(self, coords):
|
|
194
|
+
self.geometry.coords = coords
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def mw_coords(self):
|
|
198
|
+
return self.geometry.mw_coords
|
|
199
|
+
|
|
200
|
+
@mw_coords.setter
|
|
201
|
+
def mw_coords(self, mw_coords):
|
|
202
|
+
self.geometry.mw_coords = mw_coords
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def energy(self):
|
|
206
|
+
return self.geometry.energy
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def gradient(self):
|
|
210
|
+
return self.geometry.gradient
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def mw_gradient(self):
|
|
214
|
+
return self.geometry.mw_gradient
|
|
215
|
+
|
|
216
|
+
# @property
|
|
217
|
+
# def mw_hessian(self):
|
|
218
|
+
# # TODO: This can be removed when the mw_hessian property is updated
|
|
219
|
+
# # in Geometry.py.
|
|
220
|
+
# return self.geometry.mw_hessian
|
|
221
|
+
|
|
222
|
+
def log(self, msg):
|
|
223
|
+
# self.logger.debug(f"step {self.cur_cycle:03d}, {msg}")
|
|
224
|
+
self.logger.debug(msg)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def m_sqrt(self):
|
|
228
|
+
return self._m_sqrt
|
|
229
|
+
|
|
230
|
+
def unweight_vec(self, vec):
|
|
231
|
+
return self.m_sqrt * vec
|
|
232
|
+
|
|
233
|
+
def mass_weigh_hessian(self, hessian):
|
|
234
|
+
return self.geometry.mass_weigh_hessian(hessian)
|
|
235
|
+
|
|
236
|
+
# mass‑weight only the sub‑Hessian that belongs to the moving atoms
|
|
237
|
+
def _mw_hessian_active(self, H_act):
|
|
238
|
+
if isinstance(H_act, torch.Tensor):
|
|
239
|
+
if not isinstance(self.mm_inv2, torch.Tensor):
|
|
240
|
+
self.mm_inv2 = torch.as_tensor(self.mm_inv2, dtype=H_act.dtype, device=H_act.device)
|
|
241
|
+
return self.mm_inv2 @ H_act @ self.mm_inv2 # in‑place not possible → tiny matrix
|
|
242
|
+
return self.mm_inv2.dot(H_act).dot(self.mm_inv2)
|
|
243
|
+
|
|
244
|
+
# Eckart projector that *ignores* frozen atoms
|
|
245
|
+
def _project_active(self, mw_H_act, *, return_P=False):
|
|
246
|
+
if self.geometry.is_analytical_2d or mw_H_act.shape[0] <= 6:
|
|
247
|
+
return (mw_H_act, None) if return_P else mw_H_act
|
|
248
|
+
|
|
249
|
+
from pysisyphus.Geometry import get_trans_rot_projector
|
|
250
|
+
|
|
251
|
+
coords_act = self.geometry.coords3d[self._act_atoms].flatten()
|
|
252
|
+
masses_act = self.geometry.masses[self._act_atoms]
|
|
253
|
+
P = get_trans_rot_projector(coords_act, masses=masses_act, full=False)
|
|
254
|
+
|
|
255
|
+
if isinstance(mw_H_act, torch.Tensor):
|
|
256
|
+
P = torch.as_tensor(P, dtype=mw_H_act.dtype, device=mw_H_act.device)
|
|
257
|
+
proj = (P @ mw_H_act @ P.T)
|
|
258
|
+
proj = 0.5 * (proj + proj.T) # restore symmetry
|
|
259
|
+
else:
|
|
260
|
+
proj = P.dot(mw_H_act).dot(P.T)
|
|
261
|
+
proj = 0.5 * (proj + proj.T)
|
|
262
|
+
return (proj, P) if return_P else proj
|
|
263
|
+
|
|
264
|
+
# Expand an active‑vector (or ‑step) to 3N
|
|
265
|
+
def _full(self, vec_act):
|
|
266
|
+
inds = self._act_dofs
|
|
267
|
+
if isinstance(vec_act, torch.Tensor):
|
|
268
|
+
idx = torch.as_tensor(inds, dtype=torch.long, device=vec_act.device)
|
|
269
|
+
full = torch.zeros(self.coords.size, dtype=vec_act.dtype, device=vec_act.device)
|
|
270
|
+
full.index_copy_(0, idx, vec_act)
|
|
271
|
+
return full
|
|
272
|
+
full = np.zeros(self.coords.size, dtype=vec_act.dtype if hasattr(vec_act, "dtype") else float)
|
|
273
|
+
full[inds] = vec_act
|
|
274
|
+
return full
|
|
275
|
+
|
|
276
|
+
def prepare(self, direction):
|
|
277
|
+
self.direction = direction
|
|
278
|
+
self.converged = False
|
|
279
|
+
self.energy_increased = False
|
|
280
|
+
self.energy_converged = False
|
|
281
|
+
self.past_inflection = not self.force_inflection
|
|
282
|
+
|
|
283
|
+
self.irc_energies = list()
|
|
284
|
+
# Not mass-weighted
|
|
285
|
+
self.irc_coords = list()
|
|
286
|
+
self.irc_gradients = list()
|
|
287
|
+
# Mass-weighted
|
|
288
|
+
self.irc_mw_coords = list()
|
|
289
|
+
self.irc_mw_gradients = list()
|
|
290
|
+
|
|
291
|
+
self.ref_bond_sets = self.ts_bond_sets.copy()
|
|
292
|
+
|
|
293
|
+
# Over the course of the IRC the hessian may get updated.
|
|
294
|
+
# Copying the initial hessian here ensures a clean start in combined
|
|
295
|
+
# forward and backward runs. Otherwise we may accidentally use
|
|
296
|
+
# the updated hessian from the end of the first run for the second
|
|
297
|
+
# run.
|
|
298
|
+
|
|
299
|
+
self.mw_hessian = self._mw_hessian_active(self.init_hessian)
|
|
300
|
+
|
|
301
|
+
trj_fn = self.get_path_for_fn(f"{direction}_irc_trj.xyz")
|
|
302
|
+
self.trj_handle = open(trj_fn, "w")
|
|
303
|
+
|
|
304
|
+
# We don't need an initial displacement when going downhill
|
|
305
|
+
if self.downhill:
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# Do inital displacement from the TS
|
|
309
|
+
if direction == "forward":
|
|
310
|
+
initial_step = self.init_displ_plus
|
|
311
|
+
elif direction == "backward":
|
|
312
|
+
initial_step = self.init_displ_minus
|
|
313
|
+
else:
|
|
314
|
+
raise Exception("Invalid direction='{direction}'!")
|
|
315
|
+
self.coords = self.ts_coords + initial_step
|
|
316
|
+
|
|
317
|
+
if self.displ in ("energy"):
|
|
318
|
+
actual_energy = self.energy
|
|
319
|
+
actual_lowering = self.ts_energy - actual_energy
|
|
320
|
+
diff = self.displ_energy - actual_lowering
|
|
321
|
+
|
|
322
|
+
def en_str(en):
|
|
323
|
+
return f"{en: .4f} au ({en*AU2KJPERMOL: .2f} kJ mol⁻¹)"
|
|
324
|
+
|
|
325
|
+
print(
|
|
326
|
+
f"Requested energy lowering: {en_str(self.displ_energy)}\n"
|
|
327
|
+
f" Actual energy lowering: {en_str(actual_lowering)}\n"
|
|
328
|
+
f" Δ: {en_str(diff)}"
|
|
329
|
+
)
|
|
330
|
+
if actual_lowering < 0.0:
|
|
331
|
+
print("Displaced geometry is higher in energy compared to TS!")
|
|
332
|
+
print("\n")
|
|
333
|
+
sys.stdout.flush()
|
|
334
|
+
initial_step_length = np.linalg.norm(initial_step)
|
|
335
|
+
self.logger.info(
|
|
336
|
+
f"Did inital step of length {initial_step_length:.4f} " "from the TS."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def initial_displacement(self):
|
|
340
|
+
"""Returns non-mass-weighted steps in +s and -s direction
|
|
341
|
+
for initial displacement from the TS. Earlier version only
|
|
342
|
+
returned one step, that was later multiplied by either 1 or -1,
|
|
343
|
+
depending on the desired IRC direction (forward/backward).
|
|
344
|
+
The current implementation directly returns two steps for forward
|
|
345
|
+
and backward direction. Whereas for plus and minus steps for
|
|
346
|
+
displ 'length' and displ 'energy'
|
|
347
|
+
step_plus = -step_minus
|
|
348
|
+
is valid. The latter step is formed as
|
|
349
|
+
x(ds) = ds * v0 + ds**2 * v1
|
|
350
|
+
so
|
|
351
|
+
x(ds) != -x(ds)
|
|
352
|
+
as
|
|
353
|
+
ds * v0 + ds**2 * v1 != -ds * v0 - ds**2 * v1 .
|
|
354
|
+
|
|
355
|
+
So, all required step are formed directly and later used as appropriate.
|
|
356
|
+
|
|
357
|
+
See
|
|
358
|
+
https://aip.scitation.org/doi/pdf/10.1063/1.454172
|
|
359
|
+
https://pubs.acs.org/doi/10.1021/j100338a027
|
|
360
|
+
https://aip.scitation.org/doi/pdf/10.1063/1.459634
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
mw_hessian = self._mw_hessian_active(self.init_hessian)
|
|
364
|
+
if self.coords.size > 3:
|
|
365
|
+
proj_hessian, P = self._project_active(mw_hessian, return_P=True)
|
|
366
|
+
# Don't project single atom species and analytical potentials
|
|
367
|
+
else:
|
|
368
|
+
proj_hessian = mw_hessian
|
|
369
|
+
if isinstance(proj_hessian, torch.Tensor):
|
|
370
|
+
P = torch.eye(self.coords.size, device=proj_hessian.device, dtype=proj_hessian.dtype)
|
|
371
|
+
else:
|
|
372
|
+
P = np.eye(self.coords.size)
|
|
373
|
+
|
|
374
|
+
del mw_hessian
|
|
375
|
+
|
|
376
|
+
if isinstance(proj_hessian, torch.Tensor):
|
|
377
|
+
eigvals, eigvecs = torch.linalg.eigh(proj_hessian)
|
|
378
|
+
eigvals = eigvals.to(torch.double).cpu().numpy()
|
|
379
|
+
mw_cart_displs = P.T @ eigvecs if P is not None else eigvecs
|
|
380
|
+
if not isinstance(self.mm_inv2, torch.Tensor):
|
|
381
|
+
self.mm_inv2 = torch.as_tensor(self.mm_inv2, dtype=proj_hessian.dtype, device=proj_hessian.device)
|
|
382
|
+
cart_displs = self.mm_inv2 @ mw_cart_displs
|
|
383
|
+
else:
|
|
384
|
+
eigvals, eigvecs = np.linalg.eigh(proj_hessian)
|
|
385
|
+
mw_cart_displs = P.T.dot(eigvecs)
|
|
386
|
+
cart_displs = self.mm_inv2.dot(mw_cart_displs)
|
|
387
|
+
|
|
388
|
+
nus = eigval_to_wavenumber(eigvals)
|
|
389
|
+
nu_root = nus[self.root]
|
|
390
|
+
assert nu_root <= self.imag_below, (
|
|
391
|
+
f"Wavenumber {nu_root:.2f} cm⁻¹ of imaginary mode {self.root} is above "
|
|
392
|
+
f"the threshold of {self.imag_below:.2f} cm⁻¹."
|
|
393
|
+
)
|
|
394
|
+
neg_inds = eigvals < -1e-8
|
|
395
|
+
assert sum(neg_inds) > 0, "The hessian does not have any negative eigenvalues!"
|
|
396
|
+
|
|
397
|
+
min_eigval = eigvals[self.root]
|
|
398
|
+
min_nu = nus[self.root]
|
|
399
|
+
T_c = T_crossover_from_eigval(min_eigval)
|
|
400
|
+
min_msg = (
|
|
401
|
+
f"Transition vector is mode {self.root} with wavenumber {min_nu:.2f} cm⁻¹.\n"
|
|
402
|
+
f"Crossover temperature T_c: {T_c:.2f} K"
|
|
403
|
+
)
|
|
404
|
+
# Doing it this way hurts ... I'll have to improve my logging game...
|
|
405
|
+
self.log(min_msg)
|
|
406
|
+
print(min_msg)
|
|
407
|
+
|
|
408
|
+
# Mass-weighted
|
|
409
|
+
mw_trans_vec = mw_cart_displs[:, self.root]
|
|
410
|
+
if isinstance(mw_trans_vec, torch.Tensor):
|
|
411
|
+
mw_trans_vec = mw_trans_vec.cpu().numpy()
|
|
412
|
+
|
|
413
|
+
# Not mass-weighted
|
|
414
|
+
trans_vec = cart_displs[:, self.root]
|
|
415
|
+
if isinstance(trans_vec, torch.Tensor):
|
|
416
|
+
trans_vec = trans_vec.cpu().numpy()
|
|
417
|
+
|
|
418
|
+
mw_trans_vec = self._full(mw_trans_vec)
|
|
419
|
+
trans_vec = self._full(trans_vec)
|
|
420
|
+
|
|
421
|
+
self.mw_transition_vector = mw_trans_vec
|
|
422
|
+
self.transition_vector = trans_vec / np.linalg.norm(trans_vec)
|
|
423
|
+
|
|
424
|
+
if self.downhill:
|
|
425
|
+
mw_step_plus = mw_step_minus = np.zeros_like(self.transition_vector)
|
|
426
|
+
msg = "Downhill run. No initial displacement from the TS."
|
|
427
|
+
elif self.displ == "length":
|
|
428
|
+
msg = "Using length-based initial displacement from the TS."
|
|
429
|
+
mw_step_plus = self.displ_length * mw_trans_vec
|
|
430
|
+
mw_step_minus = -mw_step_plus
|
|
431
|
+
elif self.displ == "energy":
|
|
432
|
+
# Calculate the length of the initial step away from the TS to initiate
|
|
433
|
+
# the IRC/MEP. We assume a quadratic potential and calculate the
|
|
434
|
+
# displacement for a given energy lowering.
|
|
435
|
+
# dE = (k*dq**2)/2 (dE = energy lowering, k = eigenvalue corresponding
|
|
436
|
+
# to the transition vector/imaginary mode, dq = step length)
|
|
437
|
+
# dq = sqrt(dE*2/k)
|
|
438
|
+
# See 10.1021/ja00295a002 and 10.1063/1.462674
|
|
439
|
+
# 10.1002/jcc.540080808 proposes 3 kcal/mol as initial energy lowering
|
|
440
|
+
msg = (
|
|
441
|
+
f"Energy-based (ΔE={self.displ_energy} au) initial displacement from "
|
|
442
|
+
"the TS using 2rd derivatives."
|
|
443
|
+
)
|
|
444
|
+
step_length = np.sqrt(self.displ_energy * 2 / np.abs(min_eigval))
|
|
445
|
+
# Guard against near-zero eigenvalue producing an excessively
|
|
446
|
+
# large initial displacement.
|
|
447
|
+
max_displ = 0.5 # au in mass-weighted coordinates
|
|
448
|
+
if step_length > max_displ:
|
|
449
|
+
print(
|
|
450
|
+
f"Warning: energy-based initial displacement {step_length:.4f} au "
|
|
451
|
+
f"exceeds {max_displ} au (|eigval|={np.abs(min_eigval):.6e}). "
|
|
452
|
+
f"Clamping to {max_displ} au."
|
|
453
|
+
)
|
|
454
|
+
self.log(
|
|
455
|
+
f"Clamped initial displacement from {step_length:.4f} to "
|
|
456
|
+
f"{max_displ} au."
|
|
457
|
+
)
|
|
458
|
+
step_length = max_displ
|
|
459
|
+
# This calculation is derived from the mass-weighted hessian, so we
|
|
460
|
+
# have to multiply this step length with the mass-weighted
|
|
461
|
+
# mode and un-weigh it.
|
|
462
|
+
mw_step_plus = step_length * mw_trans_vec
|
|
463
|
+
mw_step_minus = -mw_step_plus
|
|
464
|
+
else:
|
|
465
|
+
raise Exception(f"self.displ={self.displ} is invalid!")
|
|
466
|
+
|
|
467
|
+
step_plus = mw_step_plus / self.m_sqrt
|
|
468
|
+
step_minus = mw_step_minus / self.m_sqrt
|
|
469
|
+
self.log(msg)
|
|
470
|
+
print(msg)
|
|
471
|
+
print(
|
|
472
|
+
"Initial step lengths (not mass-weighted):\n"
|
|
473
|
+
f"\t Forward: {np.linalg.norm(step_plus):.4f} au\n"
|
|
474
|
+
f"\tBackward: {np.linalg.norm(step_minus):.4f} au"
|
|
475
|
+
)
|
|
476
|
+
return step_plus, step_minus
|
|
477
|
+
|
|
478
|
+
def get_conv_fact(self, mw_grad, min_fact=2.0):
|
|
479
|
+
# Numerical integration of differential equations requires a step length and/or
|
|
480
|
+
# we have to terminate the integration at some point, e.g. when the desired
|
|
481
|
+
# step length is reached. IRCs are integrated in mass-weighted coordinates,
|
|
482
|
+
# but self.step_length is given in unweighted coordinates. Unweighting a step
|
|
483
|
+
# in mass-weighted coordinates will reduce its norm as we divide by sqrt(m).
|
|
484
|
+
#
|
|
485
|
+
# If we want to do an Euler-integration we have to decide on a step size
|
|
486
|
+
# when a desired integration length is to be reached in a given number of steps.
|
|
487
|
+
# [3] proposes using Δs/250 with a maximum of 500 steps, so something like
|
|
488
|
+
# Δs/(max_steps / 2). It seems we can't use this because (at
|
|
489
|
+
# least for the systems I tested) this will lead to a step length that is too
|
|
490
|
+
# small, so the predictor Euler-integration will fail to converge in the
|
|
491
|
+
# prescribed number of cycles. It fails because simply dividing the desired
|
|
492
|
+
# step length in unweighted coordinates does not take into account the mass
|
|
493
|
+
# dependence. Such a step size is appropriate for integrations in unweighted
|
|
494
|
+
# coordinates, but not when using mass-weighted coordinates.
|
|
495
|
+
#
|
|
496
|
+
# We determine a conversion factor from comparing the magnitudes (norms) of
|
|
497
|
+
# the mass-weighted and un-mass-weighted gradients. This takes into account
|
|
498
|
+
# which atoms are actually moving, so it should be a good guess.
|
|
499
|
+
norm_mw_grad = np.linalg.norm(mw_grad)
|
|
500
|
+
if mw_grad.shape[0] == self._m_sqrt.shape[0]:
|
|
501
|
+
norm_grad = np.linalg.norm(self.unweight_vec(mw_grad))
|
|
502
|
+
else:
|
|
503
|
+
m_sqrt_vec = self._m_sqrt[self._act_dofs]
|
|
504
|
+
norm_grad = np.linalg.norm(mw_grad * m_sqrt_vec)
|
|
505
|
+
|
|
506
|
+
if not np.isfinite(norm_mw_grad) or norm_mw_grad == 0.0:
|
|
507
|
+
conv_fact = min_fact
|
|
508
|
+
self.log("mw_grad norm is zero/NaN; using minimum conversion factor.")
|
|
509
|
+
else:
|
|
510
|
+
conv_fact = norm_grad / norm_mw_grad
|
|
511
|
+
# Cap conversion factor when using an active subspace to avoid huge steps.
|
|
512
|
+
if mw_grad.shape[0] != self._m_sqrt.shape[0]:
|
|
513
|
+
max_fact = 50.0
|
|
514
|
+
if conv_fact > max_fact:
|
|
515
|
+
self.log(f"Clamping conversion factor {conv_fact:.4f} -> {max_fact:.1f}.")
|
|
516
|
+
conv_fact = max_fact
|
|
517
|
+
conv_fact = max(min_fact, conv_fact)
|
|
518
|
+
self.log(f"Un-weighted / mass-weighted conversion factor {conv_fact:.4f}")
|
|
519
|
+
return conv_fact
|
|
520
|
+
|
|
521
|
+
def report_bonds(self, prefix, bonds):
|
|
522
|
+
if len(bonds) == 0:
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
plural = "s" if len(bonds) > 1 else ""
|
|
526
|
+
bond_strs = list()
|
|
527
|
+
for from_, to_ in bonds:
|
|
528
|
+
from_atom = self.atoms[from_]
|
|
529
|
+
to_atom = self.atoms[to_]
|
|
530
|
+
bond_strs.append(f"[{from_atom}{from_}-{to_atom}{to_}]")
|
|
531
|
+
bonds_str = ", ".join(bond_strs)
|
|
532
|
+
self.table.print(f"Bond{plural} {prefix}: {bonds_str}")
|
|
533
|
+
|
|
534
|
+
def irc(self, direction):
|
|
535
|
+
self.log(highlight_text(f"IRC {direction}", level=1))
|
|
536
|
+
self.cur_direction = direction
|
|
537
|
+
self.prepare(direction)
|
|
538
|
+
# Calculate gradient
|
|
539
|
+
self.gradient
|
|
540
|
+
self.irc_energies.append(self.energy)
|
|
541
|
+
# Non mass-weighted
|
|
542
|
+
self.irc_coords.append(self.coords)
|
|
543
|
+
self.irc_gradients.append(self.gradient)
|
|
544
|
+
# Mass-weighted
|
|
545
|
+
self.irc_mw_coords.append(self.mw_coords)
|
|
546
|
+
self.irc_mw_gradients.append(self.mw_gradient)
|
|
547
|
+
|
|
548
|
+
self.table.print_header()
|
|
549
|
+
for self.cur_cycle in range(self.max_cycles):
|
|
550
|
+
self.log(highlight_text(f"IRC step {self.cur_cycle:03d}") + "\n")
|
|
551
|
+
|
|
552
|
+
# Dump current coordinates to trj
|
|
553
|
+
comment = f"{direction} IRC, step {self.cur_cycle}"
|
|
554
|
+
coords_str = make_xyz_str(
|
|
555
|
+
self.atoms, BOHR2ANG * self.coords.reshape((-1, 3)), comment
|
|
556
|
+
)
|
|
557
|
+
self.trj_handle.write(coords_str + "\n")
|
|
558
|
+
self.trj_handle.flush()
|
|
559
|
+
|
|
560
|
+
self.log(f"Current energy: {self.energy:.6f} au")
|
|
561
|
+
#
|
|
562
|
+
# Take IRC step.
|
|
563
|
+
#
|
|
564
|
+
self.step()
|
|
565
|
+
|
|
566
|
+
# Calculate gradient and energy on the new geometry
|
|
567
|
+
# Non mass-weighted
|
|
568
|
+
self.log("Calculating energy and gradient at new geometry.")
|
|
569
|
+
self.irc_coords.append(self.coords)
|
|
570
|
+
self.irc_gradients.append(self.gradient)
|
|
571
|
+
self.irc_energies.append(self.energy)
|
|
572
|
+
# Mass-weighted
|
|
573
|
+
self.irc_mw_coords.append(self.mw_coords)
|
|
574
|
+
self.irc_mw_gradients.append(self.mw_gradient)
|
|
575
|
+
|
|
576
|
+
rms_grad = rms(self.gradient)
|
|
577
|
+
|
|
578
|
+
# Only update once
|
|
579
|
+
if not self.past_inflection:
|
|
580
|
+
self.past_inflection = rms_grad >= self.rms_grad_thresh
|
|
581
|
+
_ = "" if self.past_inflection else "not yet"
|
|
582
|
+
self.log(f"(rms(grad) > threshold) {_} fullfilled!")
|
|
583
|
+
|
|
584
|
+
irc_length = np.linalg.norm(self.irc_mw_coords[0] - self.irc_mw_coords[-1])
|
|
585
|
+
dE = self.irc_energies[-1] - self.irc_energies[-2]
|
|
586
|
+
max_grad = np.abs(self.gradient).max()
|
|
587
|
+
|
|
588
|
+
row_args = (self.cur_cycle, irc_length, dE, max_grad, rms_grad)
|
|
589
|
+
self.table.print_row(row_args)
|
|
590
|
+
try:
|
|
591
|
+
# The derived IRC classes may want to do some printing
|
|
592
|
+
add_info = self.get_additional_print()
|
|
593
|
+
self.table.print(add_info)
|
|
594
|
+
except AttributeError:
|
|
595
|
+
pass
|
|
596
|
+
|
|
597
|
+
if self.check_bonds:
|
|
598
|
+
cur_bond_sets = self.geometry.bond_sets
|
|
599
|
+
formed = cur_bond_sets - self.ref_bond_sets
|
|
600
|
+
broken = self.ref_bond_sets - cur_bond_sets
|
|
601
|
+
self.report_bonds("formed", formed)
|
|
602
|
+
self.report_bonds("broken", broken)
|
|
603
|
+
# Update bond sets to avoid repeated reporting of bond topology changes
|
|
604
|
+
self.ref_bond_sets -= broken
|
|
605
|
+
self.ref_bond_sets |= formed # union
|
|
606
|
+
|
|
607
|
+
last_energy = self.irc_energies[-2]
|
|
608
|
+
this_energy = self.irc_energies[-1]
|
|
609
|
+
|
|
610
|
+
break_msg = ""
|
|
611
|
+
self.energy_increased = this_energy > last_energy
|
|
612
|
+
self.energy_converged = abs(last_energy - this_energy) <= self.energy_thresh
|
|
613
|
+
if self.converged:
|
|
614
|
+
break_msg = "Integrator indicated convergence!"
|
|
615
|
+
elif self.past_inflection and (rms_grad <= self.rms_grad_thresh):
|
|
616
|
+
break_msg = "rms(grad) converged!"
|
|
617
|
+
self.converged = True
|
|
618
|
+
elif (
|
|
619
|
+
self.hard_rms_grad_thresh
|
|
620
|
+
and (not self.past_inflection)
|
|
621
|
+
and (rms_grad <= self.hard_rms_grad_thresh)
|
|
622
|
+
):
|
|
623
|
+
break_msg = "rms(grad) below hard threshold."
|
|
624
|
+
# TODO: Allow some threshold?
|
|
625
|
+
elif self.energy_increased:
|
|
626
|
+
break_msg = "Energy increased!"
|
|
627
|
+
elif self.energy_converged:
|
|
628
|
+
break_msg = "Energy converged!"
|
|
629
|
+
self.converged = True
|
|
630
|
+
|
|
631
|
+
# dumped = (self.cur_cycle % self.dump_every) == 0
|
|
632
|
+
# if dumped:
|
|
633
|
+
# dump_fn = self.get_path_for_fn(f"{direction}_{self.dump_fn}")
|
|
634
|
+
# self.dump_data(dump_fn)
|
|
635
|
+
|
|
636
|
+
if break_msg:
|
|
637
|
+
self.table.print(break_msg)
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
if check_for_end_sign():
|
|
641
|
+
break
|
|
642
|
+
self.log("")
|
|
643
|
+
sys.stdout.flush()
|
|
644
|
+
else:
|
|
645
|
+
print("IRC steps exceeded. Stopping.")
|
|
646
|
+
print()
|
|
647
|
+
|
|
648
|
+
if direction == "forward":
|
|
649
|
+
self.irc_energies.reverse()
|
|
650
|
+
self.irc_coords.reverse()
|
|
651
|
+
self.irc_gradients.reverse()
|
|
652
|
+
self.irc_mw_coords.reverse()
|
|
653
|
+
self.irc_mw_gradients.reverse()
|
|
654
|
+
|
|
655
|
+
# if not dumped:
|
|
656
|
+
# self.dump_data(dump_fn)
|
|
657
|
+
|
|
658
|
+
self.cur_direction = None
|
|
659
|
+
self.trj_handle.close()
|
|
660
|
+
|
|
661
|
+
def set_data(self, prefix):
|
|
662
|
+
energies_name = f"{prefix}_energies"
|
|
663
|
+
coords_name = f"{prefix}_coords"
|
|
664
|
+
grad_name = f"{prefix}_gradients"
|
|
665
|
+
mw_coords_name = f"{prefix}_mw_coords"
|
|
666
|
+
mw_grad_name = f"{prefix}_mw_gradients"
|
|
667
|
+
|
|
668
|
+
setattr(self, coords_name, self.irc_coords)
|
|
669
|
+
setattr(self, grad_name, self.irc_gradients)
|
|
670
|
+
setattr(self, mw_coords_name, self.irc_mw_coords)
|
|
671
|
+
setattr(self, mw_grad_name, self.irc_mw_gradients)
|
|
672
|
+
setattr(self, energies_name, self.irc_energies)
|
|
673
|
+
|
|
674
|
+
self.all_energies.extend(getattr(self, energies_name))
|
|
675
|
+
self.all_coords.extend(getattr(self, coords_name))
|
|
676
|
+
self.all_gradients.extend(getattr(self, grad_name))
|
|
677
|
+
self.all_mw_coords.extend(getattr(self, mw_coords_name))
|
|
678
|
+
self.all_mw_gradients.extend(getattr(self, mw_grad_name))
|
|
679
|
+
|
|
680
|
+
# Free per-direction lists to reduce memory usage
|
|
681
|
+
del self.irc_coords
|
|
682
|
+
del self.irc_gradients
|
|
683
|
+
del self.irc_mw_coords
|
|
684
|
+
del self.irc_mw_gradients
|
|
685
|
+
del self.irc_energies
|
|
686
|
+
|
|
687
|
+
setattr(self, f"{prefix}_is_converged", self.converged)
|
|
688
|
+
setattr(self, f"{prefix}_energy_increased", self.energy_increased)
|
|
689
|
+
setattr(self, f"{prefix}_energy_converged", self.energy_converged)
|
|
690
|
+
setattr(self, f"{prefix}_cycle", self.cur_cycle)
|
|
691
|
+
self.dump_ends(".", prefix, getattr(self, mw_coords_name))
|
|
692
|
+
|
|
693
|
+
def report_conv_thresholds(self):
|
|
694
|
+
threshs = [
|
|
695
|
+
f"\t rms(|gradient|) <= {self.rms_grad_thresh:.6f} E_h a_0⁻¹",
|
|
696
|
+
f"\t Δenergy <= {self.energy_thresh:.6f} E_h",
|
|
697
|
+
]
|
|
698
|
+
# Drop hard rms grad item
|
|
699
|
+
if self.hard_rms_grad_thresh is not None:
|
|
700
|
+
threshs.insert(
|
|
701
|
+
1,
|
|
702
|
+
f"\thard rms(|gradient|) <= {self.hard_rms_grad_thresh:.6f} E_h a_0⁻¹",
|
|
703
|
+
)
|
|
704
|
+
print(
|
|
705
|
+
"Convergence thresholds (non mass-weighted gradient):\n"
|
|
706
|
+
+ "\n".join(threshs)
|
|
707
|
+
+ "\n"
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
def run(self):
|
|
711
|
+
self.report_conv_thresholds()
|
|
712
|
+
# Calculate data at TS and create backup
|
|
713
|
+
self.ts_coords = self.coords.copy()
|
|
714
|
+
self.ts_mw_coords = self.mw_coords.copy()
|
|
715
|
+
print("Calculating energy and gradient at TS.")
|
|
716
|
+
self.ts_gradient = self.gradient.copy()
|
|
717
|
+
self.ts_mw_gradient = self.mw_gradient.copy()
|
|
718
|
+
self.ts_energy = self.energy
|
|
719
|
+
|
|
720
|
+
ts_grad_norm = np.linalg.norm(self.ts_gradient)
|
|
721
|
+
ts_grad_max = np.abs(self.ts_gradient).max()
|
|
722
|
+
ts_grad_rms = rms(self.ts_gradient)
|
|
723
|
+
|
|
724
|
+
self.log(
|
|
725
|
+
"Transition state (TS):\n"
|
|
726
|
+
f"\t energy={self.ts_energy:.6f} au\n"
|
|
727
|
+
f"\tnorm(grad)={ts_grad_norm:.6f}\n"
|
|
728
|
+
f"\t max(grad)={ts_grad_max:.6f}\n"
|
|
729
|
+
f"\t rms(grad)={ts_grad_rms:.6f}"
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
self.init_hessian = self.geometry.hessian
|
|
733
|
+
has_partial = getattr(self.geometry, "within_partial_hessian", None) is not None
|
|
734
|
+
act_n_dof = (
|
|
735
|
+
int(self.geometry.within_partial_hessian.get("active_n_dof", 0))
|
|
736
|
+
if has_partial else 0
|
|
737
|
+
)
|
|
738
|
+
if has_partial:
|
|
739
|
+
self._act_atoms = self.geometry.hess_active_atom_indices
|
|
740
|
+
self._act_dofs = self.geometry.hess_active_dof_indices
|
|
741
|
+
self.mm_inv2 = self.geometry.mm_sqrt_inv[np.ix_(self._act_dofs, self._act_dofs)]
|
|
742
|
+
self.geometry.clear()
|
|
743
|
+
# convert to active doFs (skip if already partial)
|
|
744
|
+
if has_partial:
|
|
745
|
+
if self.init_hessian.shape != (act_n_dof, act_n_dof):
|
|
746
|
+
self.init_hessian = self.init_hessian[self._act_dofs][:, self._act_dofs]
|
|
747
|
+
else:
|
|
748
|
+
self.init_hessian = self.init_hessian[self._act_dofs][:, self._act_dofs]
|
|
749
|
+
|
|
750
|
+
# For forward/backward runs from a TS we need an intial displacement,
|
|
751
|
+
# calculated from the transition vector (imaginary mode) of the TS
|
|
752
|
+
# hessian. If we need/want a Hessian for a downhill run from a
|
|
753
|
+
# non-stationary point (with non-vanishing gradient) depends on the
|
|
754
|
+
# actual IRC integrator (e.g. EulerPC and LQA need a Hessian).
|
|
755
|
+
if not self.downhill:
|
|
756
|
+
self.init_displ_plus, self.init_displ_minus = self.initial_displacement()
|
|
757
|
+
|
|
758
|
+
print(
|
|
759
|
+
"IRC length in mw. coords, max(|grad|) and rms(grad) in "
|
|
760
|
+
"unweighted coordinates."
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
if self.forward:
|
|
764
|
+
print("\n" + highlight_text("IRC - Forward") + "\n")
|
|
765
|
+
self.irc("forward")
|
|
766
|
+
self.set_data("forward")
|
|
767
|
+
if isinstance(self.mw_hessian, torch.Tensor):
|
|
768
|
+
self.forward_mw_hessian = self.mw_hessian.detach().clone()
|
|
769
|
+
else:
|
|
770
|
+
self.forward_mw_hessian = self.mw_hessian.copy()
|
|
771
|
+
|
|
772
|
+
# Add TS/starting data
|
|
773
|
+
self.all_energies.append(self.ts_energy)
|
|
774
|
+
self.all_coords.append(self.ts_coords)
|
|
775
|
+
self.all_gradients.append(self.ts_gradient)
|
|
776
|
+
self.all_mw_coords.append(self.ts_mw_coords)
|
|
777
|
+
self.all_mw_gradients.append(self.ts_mw_gradient)
|
|
778
|
+
self.ts_index = len(self.all_energies) - 1
|
|
779
|
+
|
|
780
|
+
if self.backward:
|
|
781
|
+
print("\n" + highlight_text("IRC - Backward") + "\n")
|
|
782
|
+
self.irc("backward")
|
|
783
|
+
self.set_data("backward")
|
|
784
|
+
|
|
785
|
+
if self.downhill:
|
|
786
|
+
print("\n" + highlight_text("IRC - Downhill") + "\n")
|
|
787
|
+
self.irc("downhill")
|
|
788
|
+
self.set_data("downhill")
|
|
789
|
+
|
|
790
|
+
self.all_mw_coords = np.array(self.all_mw_coords)
|
|
791
|
+
self.all_energies = np.array(self.all_energies)
|
|
792
|
+
self.postprocess()
|
|
793
|
+
if not self.downhill:
|
|
794
|
+
self.dump_ends(".", "finished", trj=True)
|
|
795
|
+
|
|
796
|
+
# # Dump the whole IRC to HDF5
|
|
797
|
+
# dump_fn = self.get_path_for_fn("finished_" + self.dump_fn)
|
|
798
|
+
# self.dump_data(dump_fn, full=True)
|
|
799
|
+
|
|
800
|
+
# Convert to arrays and free original Python lists
|
|
801
|
+
for name in "all_energies all_coords all_gradients all_mw_coords all_mw_gradients".split():
|
|
802
|
+
setattr(self, name, np.array(getattr(self, name)))
|
|
803
|
+
|
|
804
|
+
# Right now self.all_mw_coords is still in mass-weighted coordinates.
|
|
805
|
+
# Convert them to un-mass-weighted coordinates.
|
|
806
|
+
self.all_mw_coords_umw = self.all_mw_coords / self.m_sqrt
|
|
807
|
+
|
|
808
|
+
def postprocess(self):
|
|
809
|
+
pass
|
|
810
|
+
|
|
811
|
+
def dump_ends(self, path, prefix, coords=None, trj=False):
|
|
812
|
+
if coords is None:
|
|
813
|
+
coords = self.all_mw_coords
|
|
814
|
+
coords = coords.copy()
|
|
815
|
+
coords /= self.m_sqrt
|
|
816
|
+
coords = coords.reshape(-1, len(self.atoms), 3) * BOHR2ANG
|
|
817
|
+
if trj:
|
|
818
|
+
trj_string = make_trj_str(self.atoms, coords, comments=self.all_energies)
|
|
819
|
+
trj_fn = self.get_path_for_fn(f"{prefix}_irc_trj.xyz")
|
|
820
|
+
with open(trj_fn, "w") as handle:
|
|
821
|
+
handle.write(trj_string)
|
|
822
|
+
|
|
823
|
+
first_coords = coords[0]
|
|
824
|
+
first_fn = self.get_path_for_fn(f"{prefix}_first.xyz")
|
|
825
|
+
with open(first_fn, "w") as handle:
|
|
826
|
+
handle.write(make_xyz_str(self.atoms, first_coords))
|
|
827
|
+
|
|
828
|
+
last_coords = coords[-1]
|
|
829
|
+
first_fn = self.get_path_for_fn(f"{prefix}_last.xyz")
|
|
830
|
+
with open(first_fn, "w") as handle:
|
|
831
|
+
handle.write(make_xyz_str(self.atoms, last_coords))
|
|
832
|
+
|
|
833
|
+
def get_irc_data(self):
|
|
834
|
+
data_dict = {
|
|
835
|
+
"energies": np.array(self.irc_energies, dtype=float),
|
|
836
|
+
"coords": np.array(self.irc_coords, dtype=float),
|
|
837
|
+
"gradients": np.array(self.irc_gradients, dtype=float),
|
|
838
|
+
"mw_coords": np.array(self.irc_mw_coords, dtype=float),
|
|
839
|
+
"mw_gradients": np.array(self.irc_mw_gradients, dtype=float),
|
|
840
|
+
}
|
|
841
|
+
return data_dict
|
|
842
|
+
|
|
843
|
+
def get_full_irc_data(self):
|
|
844
|
+
data_dict = {
|
|
845
|
+
"energies": np.array(self.all_energies, dtype=float),
|
|
846
|
+
"coords": np.array(self.all_coords, dtype=float),
|
|
847
|
+
"gradients": np.array(self.all_gradients, dtype=float),
|
|
848
|
+
"mw_coords": np.array(self.all_mw_coords, dtype=float),
|
|
849
|
+
"mw_gradients": np.array(self.all_mw_gradients, dtype=float),
|
|
850
|
+
"ts_index": np.array(self.ts_index, dtype=int),
|
|
851
|
+
}
|
|
852
|
+
return data_dict
|
|
853
|
+
|
|
854
|
+
def dump_data(self, dump_fn=None, full=False):
|
|
855
|
+
get_data = self.get_full_irc_data if full else self.get_irc_data
|
|
856
|
+
data_dict = get_data()
|
|
857
|
+
|
|
858
|
+
data_dict.update(
|
|
859
|
+
{
|
|
860
|
+
"atoms": np.array(self.atoms, dtype="S"),
|
|
861
|
+
"rms_grad_thresh": np.array(self.rms_grad_thresh),
|
|
862
|
+
}
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
if dump_fn is None:
|
|
866
|
+
dump_fn = self.get_path_for_fn(self.dump_fn)
|
|
867
|
+
|
|
868
|
+
with h5py.File(dump_fn, "w") as handle:
|
|
869
|
+
for key, val in data_dict.items():
|
|
870
|
+
handle.create_dataset(name=key, dtype=val.dtype, data=val)
|
|
871
|
+
|
|
872
|
+
def get_endpoint_and_ts_geoms(self):
|
|
873
|
+
assert not self.downhill, "Downhill is not yet handled"
|
|
874
|
+
first = self.all_coords[0]
|
|
875
|
+
last = self.all_coords[-1]
|
|
876
|
+
ts = self.ts_coords.copy()
|
|
877
|
+
geoms = [Geometry(self.atoms, coords) for coords in (first, ts, last)]
|
|
878
|
+
return geoms
|