Loading...
Searching...
No Matches
eonc::tunneling Namespace Reference

Classes

struct  Instanton
struct  InstantonOptions
class  Profile
 Energy along the path, interpolated as a monotone cubic (Fritsch and Carlson), flat at both ends because the ends of a band are minima. More...
struct  RateInstanton
struct  RateInstantonOptions
struct  RingSpectrum
 Spectrum of a closed ring's Hessian without forming it. More...
struct  Splitting

Typedefs

using BatchPotential
 V (eV) and dV/dq (eV / (amu^0.5 Angstrom)) at every point of q, all in one call so a potential can spread the beads over its calculators.
using BeadHessian = std::function<MatrixXd(long j, const VectorXd &q)>
 The mass-weighted Hessian d2V/dq2 at interior bead j (1..P-1).
using RingBeadHessian = std::function<MatrixXd(long j, const VectorXd &q)>
 The mass-weighted Hessian d2V/dq2 at ring bead j (0..N-1).

Functions

double massWeightedDistance (const Matter &a, const Matter &b)
 sqrt(sum_i m_i |b_i - a_i|^2) under the minimum image of a's cell.
std::vector< double > massWeightedPath (const std::vector< std::shared_ptr< Matter > > &band)
 Cumulative mass-weighted arc length at each image of a band.
double wellCurvature (const Profile &p, bool leftEnd)
 d2V/ds2 at one end of the path, in eV / (amu Angstrom^2), from a least squares fit of a s^2 + b s^3 to the points within half the barrier above that end.
double hbarOmega (double curvature)
 hbar omega in eV for a mass-weighted curvature.
double wkbAction (const Profile &p, double energy, int points=4001)
 (1/hbar) integral sqrt(2 (V(s) - E)) ds over the path where V > E.
Splitting wkbSplitting (const Profile &p, double hwReactant, double hwProduct)
 delta0 = (hbar omega / pi) exp(-S) with the Landau and Lifshitz prefactor.
std::vector< VectorXd > ringFromPath (const std::vector< VectorXd > &path, const std::vector< double > &energies, double betaHbar, long beads)
 Closed ring of beads samples of path whose imaginary-time period is betaHbar.
double wkbLogRateAlongPath (const Profile &profile, double beta, double hwReactant)
 ln(k), k in 1/time, for the one-dimensional thermal rate along profile.
Splitting bandSplitting (const std::vector< std::shared_ptr< Matter > > &band, double referenceEnergy)
 The splitting of a converged band, with the well frequencies from the band's curvature at each end.
double pathOmega (const MatrixXd &hessStart, const MatrixXd &hessEnd, const VectorXd &start, const VectorXd &end)
 omega along the straight line between the minima from the curvature of each well there, the larger of the two; in 1 / time.
Instanton optimizeInstanton (const VectorXd &start, const VectorXd &end, double betaHbar, std::vector< VectorXd > guess, const BatchPotential &potential, const InstantonOptions &options)
 Minimises the action from guess (P + 1 beads, ends at the minima, or empty for a tanh kink along the straight line).
void instantonSplitting (Instanton &inst, const BeadHessian &hessian, const MatrixXd &hessStart, const MatrixXd &hessEnd)
 Fills delta0, zeroMode and modeSeparation from the bead Hessians and the Hessians of the two minima.
double crossoverTemperature (const MatrixXd &hessSaddle)
 T_c = hbar omega_b / (2 pi kB) from the mass-weighted Hessian at the saddle, in K; throws when the Hessian has no negative eigenvalue.
RingSpectrum ringSpectrum (const std::vector< MatrixXd > &beadHessians, double c, const std::vector< VectorXd > &tau)
 The ring Hessian of bead Hessians beadHessians (d2V/dq2 at each of the N beads) and spring constant c, with the normalised direction tau (N beads) projected out through the determinant lemma det(J + tau tau^T) = det' J when J tau = 0.
double cyclicRingLogAbsDet (double c, const std::vector< MatrixXd > &diag)
 log|det| of the cyclic block-tridiagonal ring Hessian.
std::vector< VectorXd > cyclicRingSolve (double c, const std::vector< MatrixXd > &diag, const std::vector< VectorXd > &rhs)
 Solves that same cyclic ring Hessian.
RateInstanton optimizeRateInstanton (const VectorXd &saddle, const MatrixXd &hessSaddle, double beta, std::vector< VectorXd > guess, const BatchPotential &potential, const RateInstantonOptions &options)
 Finds the rate instanton at inverse temperature beta (1 / eV).
void instantonRate (RateInstanton &inst, const RingBeadHessian &hessian, const MatrixXd &hessReactant, double vReactant, const MatrixXd &hessSaddle=MatrixXd(), double vSaddle=0.0, long rigidModes=0, long denseLimit=4096)
 Fills the rate from the bead Hessians, the reactant minimum's Hessian and energy, and optionally the saddle's Hessian and energy for the classical comparison (pass an empty matrix to skip it).
double parabolicFactor (double temperature, double crossover)
 (pi T_c / T) / sin(pi T_c / T).
double harmonicTstLogRate (const MatrixXd &hessReactant, const MatrixXd &hessSaddle, double beta, double barrier, long rigidModes)
 ln(k) for classical harmonic transition-state theory, k in 1/time.
double quantumHarmonicTstLogRate (const MatrixXd &hessReactant, const MatrixXd &hessSaddle, double beta, double barrier, long rigidModes)
 ln(k) for quantum harmonic transition-state theory, k in 1/time: (1 / (2 pi beta hbar)) prod_r 2 sinh(beta hbar omega_r / 2) / prod'_s 2 sinh(beta hbar omega_s / 2) exp(-beta barrier), the zero-point and quantised partition functions of every bound mode; the saddle's unstable mode leaves the product.

Variables

constexpr double kHbar = 0.06465415129579072
 hbar in eV^0.5 amu^0.5 Angstrom, correctly rounded from the exact SI values (h = 6.62607015e-34 J s, e = 1.602176634e-19 C) and the CODATA 2022 dalton, 1.66053906892e-27 kg.
constexpr double kBoltzmann = 8.617333262145177e-5
 Boltzmann constant in eV / K, correctly rounded from the exact 1.380649e-23 J / K.
constexpr double kTimeUnitSeconds = 1.0180505717871193e-14
 One unit of time, sqrt(amu Angstrom^2 / eV), in seconds.

Typedef Documentation

◆ BatchPotential

Initial value:
std::function<void(const std::vector<VectorXd> &q, std::vector<double> &v,
std::vector<VectorXd> &grad)>

V (eV) and dV/dq (eV / (amu^0.5 Angstrom)) at every point of q, all in one call so a potential can spread the beads over its calculators.

Definition at line 152 of file Tunneling.h.

◆ BeadHessian

using eonc::tunneling::BeadHessian = std::function<MatrixXd(long j, const VectorXd &q)>

The mass-weighted Hessian d2V/dq2 at interior bead j (1..P-1).

Definition at line 157 of file Tunneling.h.

◆ RingBeadHessian

using eonc::tunneling::RingBeadHessian = std::function<MatrixXd(long j, const VectorXd &q)>

The mass-weighted Hessian d2V/dq2 at ring bead j (0..N-1).

Definition at line 359 of file Tunneling.h.

Function Documentation

◆ bandSplitting()

Splitting eonc::tunneling::bandSplitting ( const std::vector< std::shared_ptr< Matter > > & band,
double referenceEnergy )

The splitting of a converged band, with the well frequencies from the band's curvature at each end.

Definition at line 458 of file Tunneling.cpp.

459 {
460 std::vector<double> v;
461 v.reserve(band.size());
462 for (const auto &image : band) {
463 v.push_back(image->getPotentialEnergy() - referenceEnergy);
464 }
465 const Profile p(massWeightedPath(band), std::move(v));
466 return wkbSplitting(p, hbarOmega(wellCurvature(p, true)),
467 hbarOmega(wellCurvature(p, false)));
468}
Splitting wkbSplitting(const Profile &p, double hwReactant, double hwProduct)
delta0 = (hbar omega / pi) exp(-S) with the Landau and Lifshitz prefactor.
double wellCurvature(const Profile &p, bool leftEnd)
d2V/ds2 at one end of the path, in eV / (amu Angstrom^2), from a least squares fit of a s^2 + b s^3 t...
double hbarOmega(double curvature)
hbar omega in eV for a mass-weighted curvature.
std::vector< double > massWeightedPath(const std::vector< std::shared_ptr< Matter > > &band)
Cumulative mass-weighted arc length at each image of a band.
Definition Tunneling.cpp:52

◆ crossoverTemperature()

double eonc::tunneling::crossoverTemperature ( const MatrixXd & hessSaddle)

T_c = hbar omega_b / (2 pi kB) from the mass-weighted Hessian at the saddle, in K; throws when the Hessian has no negative eigenvalue.

Definition at line 860 of file Tunneling.cpp.

860 {
861 const Eigen::SelfAdjointEigenSolver<MatrixXd> es(
862 0.5 * (hessSaddle + hessSaddle.transpose()));
863 const double lambda = es.eigenvalues()(0);
864 if (!(lambda < 0.0)) {
865 throw std::invalid_argument(
866 "crossoverTemperature: the saddle Hessian has no negative eigenvalue");
867 }
868 return kHbar * std::sqrt(-lambda) / (2.0 * std::numbers::pi * kBoltzmann);
869}
constexpr double kHbar
hbar in eV^0.5 amu^0.5 Angstrom, correctly rounded from the exact SI values (h = 6....
Definition Tunneling.h:37
constexpr double kBoltzmann
Boltzmann constant in eV / K, correctly rounded from the exact 1.380649e-23 J / K.
Definition Tunneling.h:41

◆ cyclicRingLogAbsDet()

double eonc::tunneling::cyclicRingLogAbsDet ( double c,
const std::vector< MatrixXd > & diag )

log|det| of the cyclic block-tridiagonal ring Hessian.

Each diag[j] already contains the bead Hessian plus 2 c I, and the neighbour coupling is -c I, including the corner that closes the ring. A singular ring returns -infinity. Throws when the open chain is singular.

Definition at line 1580 of file Tunneling.cpp.

1580 {
1581 requireCyclicBlocks(c, diag, "cyclicRingLogAbsDet");
1582 return CyclicFactor(c, diag).logAbs;
1583}

◆ cyclicRingSolve()

std::vector< VectorXd > eonc::tunneling::cyclicRingSolve ( double c,
const std::vector< MatrixXd > & diag,
const std::vector< VectorXd > & rhs )

Solves that same cyclic ring Hessian.

Throws when the ring is singular or the right-hand side does not match the blocks.

Definition at line 1585 of file Tunneling.cpp.

1587 {
1588 requireCyclicBlocks(c, diag, "cyclicRingSolve");
1589 if (static_cast<long>(rhs.size()) != static_cast<long>(diag.size())) {
1590 throw std::invalid_argument(
1591 "cyclicRingSolve: one right-hand side per bead");
1592 }
1593 for (const auto &row : rhs) {
1594 if (row.size() != diag.front().rows()) {
1595 throw std::invalid_argument(
1596 "cyclicRingSolve: the right-hand side does not match the blocks");
1597 }
1598 }
1599 return CyclicFactor(c, diag).solve(rhs);
1600}

◆ harmonicTstLogRate()

double eonc::tunneling::harmonicTstLogRate ( const MatrixXd & hessReactant,
const MatrixXd & hessSaddle,
double beta,
double barrier,
long rigidModes )

ln(k) for classical harmonic transition-state theory, k in 1/time.

rigidModes eigenvalues nearest zero are omitted at each Hessian. The saddle's most negative eigenvalue is the barrier mode and leaves the product.

Definition at line 3151 of file Tunneling.cpp.

3153 {
3154 if (!(beta > 0.0) || hessReactant.size() == 0 || hessSaddle.size() == 0 ||
3155 hessReactant.rows() != hessReactant.cols() ||
3156 hessSaddle.rows() != hessSaddle.cols() ||
3157 hessReactant.rows() != hessSaddle.rows() || rigidModes < 0) {
3158 throw std::invalid_argument(
3159 "harmonicTstLogRate: need beta > 0, matching square Hessians and a "
3160 "non-negative rigid-mode count");
3161 }
3162 const Eigen::SelfAdjointEigenSolver<MatrixXd> er(
3163 0.5 * (hessReactant + hessReactant.transpose()), Eigen::EigenvaluesOnly);
3164 const Eigen::SelfAdjointEigenSolver<MatrixXd> es(
3165 0.5 * (hessSaddle + hessSaddle.transpose()), Eigen::EigenvaluesOnly);
3166 const VectorXd &lr = er.eigenvalues();
3167 const VectorXd &ls = es.eigenvalues();
3168 if (!(ls(0) < 0.0)) {
3169 throw std::invalid_argument(
3170 "harmonicTstLogRate: the saddle Hessian has no negative eigenvalue");
3171 }
3172 const std::vector<bool> rigidR = nearestZero(lr, rigidModes);
3173 const std::vector<bool> rigidS = nearestZero(ls, rigidModes);
3174 double logRatio = 0.0;
3175 for (long m = 0; m < lr.size(); ++m) {
3176 if (!rigidR[static_cast<size_t>(m)]) {
3177 logRatio += 0.5 * std::log(lr(m));
3178 }
3179 }
3180 // Eigenvalue 0 is the unstable mode, the most negative.
3181 for (long m = 1; m < ls.size(); ++m) {
3182 if (!rigidS[static_cast<size_t>(m)]) {
3183 logRatio -= 0.5 * std::log(std::abs(ls(m)));
3184 }
3185 }
3186 return logRatio - std::log(2.0 * std::numbers::pi) - beta * barrier;
3187}

◆ hbarOmega()

double eonc::tunneling::hbarOmega ( double curvature)

hbar omega in eV for a mass-weighted curvature.

Definition at line 144 of file Tunneling.cpp.

144 {
145 if (!(curvature > 0.0)) {
146 throw std::invalid_argument("a well needs a positive curvature");
147 }
148 return kHbar * std::sqrt(curvature);
149}

◆ instantonRate()

void eonc::tunneling::instantonRate ( RateInstanton & inst,
const RingBeadHessian & hessian,
const MatrixXd & hessReactant,
double vReactant,
const MatrixXd & hessSaddle = MatrixXd(),
double vSaddle = 0.0,
long rigidModes = 0,
long denseLimit = 4096 )

Fills the rate from the bead Hessians, the reactant minimum's Hessian and energy, and optionally the saddle's Hessian and energy for the classical comparison (pass an empty matrix to skip it).

rigidModes is the count of rigid-body zero modes to omit: the translations, plus a rotation only when the reactant Hessian leaves it null (a free cluster has them, a crystal does not, and an atom held fixed has none). They leave the centroid factors, so the rotational and translational partition functions of reactant and instanton cancel. Up to denseLimit ring degrees of freedom the product is the dense eigenproduct, checked against the cyclic block determinant. Beyond that, and for a limit of 0, the block determinant is used and the eigenvalues nearest zero come from inverse iteration on that factorisation. A negative limit forces the dense product.

Definition at line 2963 of file Tunneling.cpp.

2966 {
2967 const long N = static_cast<long>(inst.beads.size());
2968 if (N < 4 || !(inst.betaN > 0.0)) {
2969 throw std::invalid_argument("instantonRate: no optimised ring");
2970 }
2971 const long f = inst.beads.front().size();
2972 if (rigidModes < 0 || rigidModes > f) {
2973 throw std::invalid_argument(
2974 "instantonRate: rigidModes exceeds the degrees of freedom");
2975 }
2976 const double bnh = inst.betaN * kHbar;
2977 const double c = 1.0 / (bnh * bnh);
2978 const MatrixXd eye = MatrixXd::Identity(f, f);
2979
2980 std::vector<MatrixXd> hBead(static_cast<size_t>(N));
2981 std::vector<MatrixXd> diag(static_cast<size_t>(N));
2982 for (long j = 0; j < N; ++j) {
2983 const MatrixXd h = hessian(j, inst.beads[static_cast<size_t>(j)]);
2984 if (h.rows() != f || h.cols() != f) {
2985 throw std::runtime_error("instantonRate: bead Hessian size");
2986 }
2987 hBead[static_cast<size_t>(j)] = 0.5 * (h + h.transpose());
2988 diag[static_cast<size_t>(j)] =
2989 hBead[static_cast<size_t>(j)] + 2.0 * c * eye;
2990 }
2991
2992 const Eigen::SelfAdjointEigenSolver<MatrixXd> er(
2993 0.5 * (hessReactant + hessReactant.transpose()));
2994 const VectorXd &lr = er.eigenvalues();
2995 const std::vector<bool> rigidR = nearestZero(lr, rigidModes);
2996 MatrixXd nullBasis(f, 0);
2997 // The rigid vectors leave the product whether or not every bead Hessian
2998 // annihilates them exactly; a finite-difference Hessian never does, and
2999 // the reactant side drops the same modes at k = 0.
3000 for (long m = 0; m < lr.size(); ++m) {
3001 if (!rigidR[static_cast<size_t>(m)]) {
3002 continue;
3003 }
3004 nullBasis.conservativeResize(f, nullBasis.cols() + 1);
3005 nullBasis.col(nullBasis.cols() - 1) = er.eigenvectors().col(m);
3006 }
3007 if (rigidModes >= f) {
3008 throw std::runtime_error("instantonRate: every direction is a rigid mode");
3009 }
3010 // det' through the block chain: the cyclic zero mode and the rigid null
3011 // vectors leave the product by the determinant lemma, the inertia comes
3012 // from the Schur complements, and the N f by N f matrix is never formed.
3013 std::vector<VectorXd> cycle(static_cast<size_t>(N));
3014 double cycleNorm = 0.0;
3015 for (long j = 0; j < N; ++j) {
3016 cycle[static_cast<size_t>(j)] =
3017 0.5 * (inst.beads[static_cast<size_t>((j + 1) % N)] -
3018 inst.beads[static_cast<size_t>((j + N - 1) % N)]);
3019 cycleNorm += cycle[static_cast<size_t>(j)].squaredNorm();
3020 }
3021 if (!(cycleNorm > 0.0)) {
3022 throw std::runtime_error(
3023 "instantonRate: the beads coincide, so the ring has collapsed");
3024 }
3025 scale(cycle, 1.0 / std::sqrt(cycleNorm));
3026 // Each omitted direction is lifted by a spring-sized curvature c, far
3027 // above any physical near-zero eigenvalue, and the lift comes off the
3028 // log-determinant again: det(J + c u u^T) = c det' J when J u = 0.
3029 std::vector<std::vector<VectorXd>> dropped{cycle};
3030 std::vector<double> kappas{c};
3031 for (long r = 0; r < nullBasis.cols(); ++r) {
3032 dropped.emplace_back(
3033 static_cast<size_t>(N),
3034 (nullBasis.col(r) / std::sqrt(static_cast<double>(N))).eval());
3035 kappas.push_back(c);
3036 }
3037 const WoodburyRing ring(c, diag, true, dropped, kappas, true);
3038 if (!ring.ok() || !std::isfinite(ring.logAbsDet())) {
3039 throw std::runtime_error(
3040 "instantonRate: the ring Hessian is singular and the zero mode was "
3041 "not removed with the rigid modes");
3042 }
3043 inst.zeroEigenvalue = dot(cycle, applyDiagonal(diag, c, true, cycle));
3044 inst.negativeModes = ring.negative();
3045 // The lowest ring eigenvalue, for the report, from products alone.
3046 {
3047 auto applyFull = [&](const std::vector<VectorXd> &vec) {
3048 return applyDiagonal(diag, c, true, vec);
3049 };
3050 const long dim = N * f;
3051 const long steps = std::min(dim, static_cast<long>(60));
3052 std::vector<VectorXd> start = cycle;
3053 std::uint64_t h = 0x9E3779B97F4A7C15ULL;
3054 for (auto &bead : start) {
3055 for (long a2 = 0; a2 < bead.size(); ++a2) {
3056 h ^= h << 13;
3057 h ^= h >> 7;
3058 h ^= h << 17;
3059 bead(a2) += 0.1 * (static_cast<double>(h >> 11) * 0x1.0p-53 - 0.5);
3060 }
3061 }
3062 const std::vector<RingMode> modes =
3063 lowestRingModes(applyFull, std::move(start), steps);
3064 inst.negativeEigenvalue = 0.0;
3065 for (const auto &mode : modes) {
3066 if (mode.theta < inst.negativeEigenvalue &&
3067 std::abs(dot(mode.vector, cycle)) < 0.5) {
3068 inst.negativeEigenvalue = mode.theta;
3069 }
3070 }
3071 // A numerical null eigenvalue can sit just below zero off the cycle,
3072 // where the lift along tau does not reach it; it is not a second
3073 // unstable mode when it is tiny next to the barrier curvature.
3074 if (inst.negativeModes > 1 && inst.negativeEigenvalue < 0.0) {
3075 long tiny = 0;
3076 for (const auto &mode : modes) {
3077 if (mode.theta < 0.0 && mode.theta > 1e-3 * inst.negativeEigenvalue &&
3078 std::abs(dot(mode.vector, cycle)) < 0.5) {
3079 ++tiny;
3080 }
3081 }
3082 inst.negativeModes = std::max(1L, inst.negativeModes - tiny);
3083 }
3084 }
3085 const long nDrop = 1 + nullBasis.cols();
3086 const double logDetPrime =
3087 ring.logAbsDet() - static_cast<double>(nDrop) * std::log(c);
3088 const double logProd =
3089 static_cast<double>(N * f - nDrop) * std::log(bnh) + 0.5 * logDetPrime;
3090
3091 inst.logRateTimesZr = -std::log(bnh) +
3092 0.5 * std::log(inst.bN / (2.0 * std::numbers::pi *
3093 inst.betaN * kHbar * kHbar)) -
3094 logProd - inst.betaN * inst.ringPotential;
3095
3096 for (long m = 0; m < lr.size(); ++m) {
3097 if (!rigidR[static_cast<size_t>(m)] && !(lr(m) > 0.0)) {
3098 throw std::runtime_error(
3099 "instantonRate: the reactant Hessian is not positive definite");
3100 }
3101 }
3102 // The rigid modes leave the centroid (k = 0) factor, as the ring's own
3103 // rigid modes leave its product; both carry them as free particles for
3104 // k > 0.
3105 double logZr = -inst.beta * vReactant;
3106 for (long k = 0; k < N; ++k) {
3107 const double sk = std::sin(std::numbers::pi * static_cast<double>(k) /
3108 static_cast<double>(N));
3109 for (long m = 0; m < lr.size(); ++m) {
3110 if (k == 0 && rigidR[static_cast<size_t>(m)]) {
3111 continue;
3112 }
3113 const double l = rigidR[static_cast<size_t>(m)] ? 0.0 : lr(m);
3114 logZr -= std::log(bnh) + 0.5 * std::log(l + 4.0 * c * sk * sk);
3115 }
3116 }
3117 inst.logZr = logZr;
3118 inst.logRate = inst.logRateTimesZr - logZr;
3119 inst.rate = std::exp(inst.logRate) / kTimeUnitSeconds;
3120 inst.effectiveBarrier =
3121 -std::log(2.0 * std::numbers::pi * kHbar * inst.beta) / inst.beta -
3122 inst.logRate / inst.beta;
3123
3124 if (hessSaddle.size() > 0) {
3126 hessReactant, hessSaddle, inst.beta, vSaddle - vReactant, rigidModes);
3127 inst.classicalRate = std::exp(inst.classicalLogRate) / kTimeUnitSeconds;
3128 }
3129}
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
constexpr double kTimeUnitSeconds
One unit of time, sqrt(amu Angstrom^2 / eV), in seconds.
Definition Tunneling.h:224
double harmonicTstLogRate(const MatrixXd &hessReactant, const MatrixXd &hessSaddle, double beta, double barrier, long rigidModes)
ln(k) for classical harmonic transition-state theory, k in 1/time.
long negativeModes
eigenvalues below the zero mode
Definition Tunneling.h:327
double zeroEigenvalue
the eigenvalue left out
Definition Tunneling.h:326
double beta
1 / (kB T), 1 / eV
Definition Tunneling.h:319
double bN
sum_j |q_{j+1} - q_j|^2, amu Angstrom^2
Definition Tunneling.h:324
double logRate
ln k, k in 1 / time
Definition Tunneling.h:332
std::vector< VectorXd > beads
N beads, q_N = q_0 implied.
Definition Tunneling.h:317
double classicalRate
Classical harmonic transition-state theory at the same T, 1 / s, when the saddle Hessian was given; i...
Definition Tunneling.h:339
double logRateTimesZr
ln(k Z_r), k in 1 / time
Definition Tunneling.h:330
double negativeEigenvalue
of the ring Hessian, 1 / time^2
Definition Tunneling.h:325
double effectiveBarrier
-kB T ln(2 pi hbar beta k): the barrier an Eyring rate would need, eV.
Definition Tunneling.h:335

◆ instantonSplitting()

void eonc::tunneling::instantonSplitting ( Instanton & inst,
const BeadHessian & hessian,
const MatrixXd & hessStart,
const MatrixXd & hessEnd )

Fills delta0, zeroMode and modeSeparation from the bead Hessians and the Hessians of the two minima.

Definition at line 781 of file Tunneling.cpp.

782 {
783 const long P = static_cast<long>(inst.path.size()) - 1;
784 if (P < 4 || !(inst.dtau > 0.0)) {
785 throw std::invalid_argument("instantonSplitting: no optimised path");
786 }
787 const double dtau = inst.dtau;
788 const double c = 1.0 / dtau;
789 const long n = inst.path.front().size();
790 const MatrixXd spring = 2.0 * c * MatrixXd::Identity(n, n);
791
792 std::vector<MatrixXd> diag;
793 diag.reserve(static_cast<size_t>(P - 1));
794 for (long j = 1; j < P; ++j) {
795 const MatrixXd h = hessian(j, inst.path[static_cast<size_t>(j)]);
796 if (h.rows() != n || h.cols() != n) {
797 throw std::runtime_error("instantonSplitting: bead Hessian size");
798 }
799 diag.push_back(spring + dtau * 0.5 * (h + h.transpose()));
800 }
801 const BlockChain chain(c, diag);
802
803 auto wellLogDet = [&](const MatrixXd &h) {
804 const std::vector<MatrixXd> d(static_cast<size_t>(P - 1),
805 spring + dtau * 0.5 * (h + h.transpose()));
806 const BlockChain well(c, d);
807 if (well.sign() < 0) {
808 throw std::runtime_error(
809 "instantonSplitting: a well Hessian is not positive definite");
810 }
811 return well.logAbsDet();
812 };
813 const double logDetWell = 0.5 * (wellLogDet(hessStart) + wellLogDet(hessEnd));
814
815 // The zero mode is the kink's translation in imaginary time, along the
816 // discrete velocity v; det' J = det J (v^T J^-1 v) for v its eigenvector.
817 std::vector<VectorXd> v(static_cast<size_t>(P - 1));
818 for (long j = 1; j < P; ++j) {
819 v[static_cast<size_t>(j - 1)] = inst.path[static_cast<size_t>(j + 1)] -
820 inst.path[static_cast<size_t>(j - 1)];
821 }
822 scale(v, 1.0 / std::sqrt(dot(v, v)));
823 const double vJv = dot(v, chain.solve(v));
824 const int signPrime = chain.sign() * (vJv < 0.0 ? -1 : 1);
825 if (signPrime < 0) {
826 throw std::runtime_error(
827 "instantonSplitting: the path is not a minimum of the action "
828 "(a negative mode besides the kink's translation)");
829 }
830 inst.zeroMode = 1.0 / vJv;
831 const double logDetPrime = chain.logAbsDet() + std::log(std::abs(vJv));
832
833 // Next eigenvalue: inverse iteration orthogonal to v.
834 std::vector<VectorXd> w(v.size());
835 for (size_t k = 0; k < w.size(); ++k) {
836 w[k].resize(n);
837 for (long i = 0; i < n; ++i) {
838 w[k](i) = std::sin(0.7 * static_cast<double>(k) +
839 1.3 * static_cast<double>(i) + 0.1);
840 }
841 }
842 double lambda1 = 0.0;
843 for (int it = 0; it < 40; ++it) {
844 const double proj = dot(v, w);
845 for (size_t k = 0; k < w.size(); ++k) {
846 w[k] -= proj * v[k];
847 }
848 scale(w, 1.0 / std::sqrt(dot(w, w)));
849 std::vector<VectorXd> z = chain.solve(w);
850 lambda1 = 1.0 / dot(w, z);
851 w = std::move(z);
852 }
853 inst.modeSeparation = std::abs(lambda1 / inst.zeroMode);
854
855 inst.delta0 = 2.0 * kHbar *
856 std::sqrt(inst.s0 / (2.0 * std::numbers::pi * kHbar * dtau)) *
857 std::exp(0.5 * (logDetWell - logDetPrime) - inst.action);
858}
double zeroMode
the eigenvalue det' leaves out
Definition Tunneling.h:166
double action
(S - S_well) / hbar
Definition Tunneling.h:164
double s0
integral of |dq/dtau|^2 dtau
Definition Tunneling.h:165
double modeSeparation
The second smallest eigenvalue of J over the zero mode's: small means the kink is not isolated in ima...
Definition Tunneling.h:176
double dtau
betaHbar / P
Definition Tunneling.h:163
double delta0
tunnelling splitting, eV
Definition Tunneling.h:167
std::vector< VectorXd > path
P + 1 beads, ends at the minima.
Definition Tunneling.h:160

◆ massWeightedDistance()

double eonc::tunneling::massWeightedDistance ( const Matter & a,
const Matter & b )

sqrt(sum_i m_i |b_i - a_i|^2) under the minimum image of a's cell.

Definition at line 34 of file Tunneling.cpp.

34 {
35 if (a.numberOfAtoms() != b.numberOfAtoms()) {
36 throw std::invalid_argument("the structures hold different atom counts");
37 }
38 const AtomMatrix dr = a.pbc(b.getPositions() - a.getPositions());
39 const auto mass = a.getMasses();
40 double sum = 0.0;
41 for (long i = 0; i < a.numberOfAtoms(); ++i) {
42 if (mass(i) <= 0.0) {
43 throw std::invalid_argument(
44 "every atom needs a positive mass for a mass-weighted path");
45 }
46 sum += mass(i) * dr.row(i).squaredNorm();
47 }
48 return std::sqrt(sum);
49}
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
const AtomMatrix & getPositions() const
Definition Matter.cpp:308
long int numberOfAtoms() const
Definition Matter.cpp:273
AtomMatrix pbc(const AtomMatrix &diff) const
Definition Matter.cpp:810
Eigen::Matrix< double, Eigen::Dynamic, 1 > getMasses() const
Definition Matter.cpp:750

◆ massWeightedPath()

std::vector< double > eonc::tunneling::massWeightedPath ( const std::vector< std::shared_ptr< Matter > > & band)

Cumulative mass-weighted arc length at each image of a band.

Definition at line 52 of file Tunneling.cpp.

52 {
53 std::vector<double> s{0.0};
54 s.reserve(band.size());
55 for (size_t i = 1; i < band.size(); ++i) {
56 s.push_back(s.back() + massWeightedDistance(*band[i - 1], *band[i]));
57 }
58 return s;
59}
double massWeightedDistance(const Matter &a, const Matter &b)
sqrt(sum_i m_i |b_i - a_i|^2) under the minimum image of a's cell.
Definition Tunneling.cpp:34

◆ optimizeInstanton()

Instanton eonc::tunneling::optimizeInstanton ( const VectorXd & start,
const VectorXd & end,
double betaHbar,
std::vector< VectorXd > guess,
const BatchPotential & potential,
const InstantonOptions & options )

Minimises the action from guess (P + 1 beads, ends at the minima, or empty for a tanh kink along the straight line).

Evaluates the interior beads once per iteration and line-search step.

Definition at line 633 of file Tunneling.cpp.

636 {
637 const long P = options.beads;
638 if (P < 4 || !(betaHbar > 0.0) || start.size() != end.size()) {
639 throw std::invalid_argument("optimizeInstanton: need P >= 4, beta hbar > 0 "
640 "and ends of one dimension");
641 }
642 Instanton inst;
643 inst.betaHbar = betaHbar;
644 inst.dtau = betaHbar / static_cast<double>(P);
645 const double dtau = inst.dtau;
646
647 std::vector<double> vEnds;
648 std::vector<VectorXd> gEnds;
649 potential({start, end}, vEnds, gEnds);
650 if (vEnds.size() != 2) {
651 throw std::runtime_error("instanton: potential returned the wrong count");
652 }
653 inst.asymmetry = vEnds[1] - vEnds[0];
654
655 // Beads along the guess (or the straight line) on a tanh kink centred at
656 // beta hbar / 2 whose width follows the harmonic decay of a well.
657 if (guess.size() < 2) {
658 guess = {start, end};
659 }
660 std::vector<double> cum(guess.size(), 0.0);
661 for (size_t k = 1; k < guess.size(); ++k) {
662 cum[k] = cum[k - 1] + (guess[k] - guess[k - 1]).norm();
663 }
664 if (!(cum.back() > 0.0)) {
665 throw std::invalid_argument("optimizeInstanton: the two minima coincide");
666 }
667 const double width = betaHbar / (2.0 * options.betaHbarOmega);
668 std::vector<VectorXd> x(static_cast<size_t>(P - 1));
669 for (long j = 1; j < P; ++j) {
670 const double tau = static_cast<double>(j) * dtau - 0.5 * betaHbar;
671 const double f = 0.5 * (1.0 + std::tanh(tau / width));
672 x[static_cast<size_t>(j - 1)] = alongPolyline(guess, cum, f);
673 }
674
675 // L-BFGS with a backtracking Armijo line search.
676 ActionEval cur =
677 evaluateAction(x, start, end, vEnds[0], vEnds[1], dtau, potential);
678 std::deque<std::pair<std::vector<VectorXd>, std::vector<VectorXd>>> pairs;
679 for (long it = 0; it < options.maxIterations; ++it) {
680 inst.iterations = it;
681 if (largestBeadNorm(cur.grad) / dtau < options.forceTolerance) {
682 inst.converged = true;
683 break;
684 }
685 std::vector<VectorXd> q = cur.grad;
686 std::vector<double> alpha(pairs.size());
687 for (size_t i = pairs.size(); i-- > 0;) {
688 const double rho = 1.0 / dot(pairs[i].second, pairs[i].first);
689 alpha[i] = rho * dot(pairs[i].first, q);
690 for (size_t k = 0; k < q.size(); ++k) {
691 q[k] -= alpha[i] * pairs[i].second[k];
692 }
693 }
694 // Without history, half the inverse spring stiffness 2 / dtau.
695 double gamma = dtau / 4.0;
696 if (!pairs.empty()) {
697 gamma = dot(pairs.back().first, pairs.back().second) /
698 dot(pairs.back().second, pairs.back().second);
699 }
700 scale(q, gamma);
701 for (size_t i = 0; i < pairs.size(); ++i) {
702 const double rho = 1.0 / dot(pairs[i].second, pairs[i].first);
703 const double beta = rho * dot(pairs[i].second, q);
704 for (size_t k = 0; k < q.size(); ++k) {
705 q[k] += (alpha[i] - beta) * pairs[i].first[k];
706 }
707 }
708 // q is now the inverse-Hessian estimate times the gradient; step -q.
709 double slope = -dot(cur.grad, q);
710 if (!(slope < 0.0)) {
711 pairs.clear();
712 q = cur.grad;
713 scale(q, dtau / 4.0);
714 slope = -dot(cur.grad, q);
715 }
716 double step = 1.0;
717 ActionEval next;
718 std::vector<VectorXd> trial(x.size());
719 bool accepted = false;
720 for (int ls = 0; ls < 30; ++ls) {
721 for (size_t k = 0; k < x.size(); ++k) {
722 trial[k] = x[k] - step * q[k];
723 }
724 next = evaluateAction(trial, start, end, vEnds[0], vEnds[1], dtau,
725 potential);
726 // Near the minimum the action changes by less than its round-off;
727 // there a step that shrinks the gradient is progress too.
728 const bool armijo = next.action <= cur.action + 1e-4 * step * slope;
729 const bool flat = std::abs(next.action - cur.action) <=
730 1e-13 * std::max(1.0, std::abs(cur.action));
731 if (armijo ||
732 (flat && largestBeadNorm(next.grad) < largestBeadNorm(cur.grad))) {
733 accepted = true;
734 break;
735 }
736 step *= 0.5;
737 }
738 if (!accepted) {
739 break;
740 }
741 std::vector<VectorXd> sk(x.size()), yk(x.size());
742 for (size_t k = 0; k < x.size(); ++k) {
743 sk[k] = trial[k] - x[k];
744 yk[k] = next.grad[k] - cur.grad[k];
745 }
746 if (dot(sk, yk) > 0.0) {
747 pairs.emplace_back(std::move(sk), std::move(yk));
748 if (static_cast<long>(pairs.size()) > options.memory) {
749 pairs.pop_front();
750 }
751 }
752 x = std::move(trial);
753 cur = std::move(next);
754 }
755 if (!inst.converged &&
756 largestBeadNorm(cur.grad) / dtau < options.forceTolerance) {
757 inst.converged = true;
758 }
759
760 inst.path.reserve(static_cast<size_t>(P + 1));
761 inst.path.push_back(start);
762 inst.path.insert(inst.path.end(), x.begin(), x.end());
763 inst.path.push_back(end);
764 inst.energies.reserve(static_cast<size_t>(P + 1));
765 inst.energies.push_back(vEnds[0]);
766 inst.energies.insert(inst.energies.end(), cur.v.begin(), cur.v.end());
767 inst.energies.push_back(vEnds[1]);
768 const double sWell = betaHbar * 0.5 * (vEnds[0] + vEnds[1]);
769 inst.action = (cur.action - sWell) / kHbar;
770 double s0 = 0.0;
771 for (long j = 0; j < P; ++j) {
772 s0 += (inst.path[static_cast<size_t>(j + 1)] -
773 inst.path[static_cast<size_t>(j)])
774 .squaredNorm();
775 }
776 inst.s0 = s0 / dtau;
777 inst.symmetricEnough = std::abs(inst.asymmetry) * betaHbar / kHbar < 0.1;
778 return inst;
779}
long maxIterations
L-BFGS iterations.
Definition Tunneling.h:144
double forceTolerance
largest per-bead |dS/dq| / dtau, eV / (amu^0.5 Angstrom)
Definition Tunneling.h:145
long beads
P: segments from one minimum to the other.
Definition Tunneling.h:141
long memory
L-BFGS correction pairs.
Definition Tunneling.h:147
double betaHbarOmega
beta hbar omega of the stiffer end along the path; sets the imaginary time
Definition Tunneling.h:142

◆ optimizeRateInstanton()

RateInstanton eonc::tunneling::optimizeRateInstanton ( const VectorXd & saddle,
const MatrixXd & hessSaddle,
double beta,
std::vector< VectorXd > guess,
const BatchPotential & potential,
const RateInstantonOptions & options )

Finds the rate instanton at inverse temperature beta (1 / eV).

guess holds N beads, or is empty for a ring stretched along the saddle's unstable mode to where V has dropped by (1 - T / T_c) of the lower of the two barriers. At or below newtonLimit active coordinates the step is an index-1 Newton step on a Bofill Hessian, and below 0.75 T_c an empty guess cools from 0.85 T_c. Beyond that limit the step is minimum-mode following. An even ring whose beads match under j -> N - j is evaluated from one turning point to the other and mirrored. saddle and hessSaddle are mass-weighted.

Definition at line 2674 of file Tunneling.cpp.

2678 {
2679 const long N = options.beads;
2680 if (N < 4 || !(beta > 0.0) || hessSaddle.rows() != saddle.size()) {
2681 throw std::invalid_argument(
2682 "optimizeRateInstanton: need N >= 4, beta > 0 and a saddle Hessian "
2683 "of the saddle's dimension");
2684 }
2685 RateInstanton inst;
2686 inst.beta = beta;
2687 inst.betaN = beta / static_cast<double>(N);
2688 inst.temperature = 1.0 / (kBoltzmann * beta);
2689 inst.crossover = crossoverTemperature(hessSaddle);
2690 if (!(inst.temperature < inst.crossover)) {
2691 throw std::invalid_argument(
2692 "optimizeRateInstanton: T is at or above the crossover temperature; "
2693 "the ring collapses onto the saddle and steepest descent needs the "
2694 "parabolic barrier correction, of which classical transition-state "
2695 "theory is only the one-bead limit");
2696 }
2697 bool mirror = options.halfRing && N % 2 == 0;
2698 if (mirror && static_cast<long>(guess.size()) == N) {
2699 const long mid = N / 2;
2700 for (long j = 1; j < mid && mirror; ++j) {
2701 if ((guess[static_cast<size_t>(j)] - guess[static_cast<size_t>(N - j)])
2702 .norm() > 1e-8) {
2703 mirror = false;
2704 }
2705 }
2706 }
2707 const long active = mirror ? (N / 2 + 1) : N;
2708 if (options.newtonLimit > 0 &&
2709 active * saddle.size() <= options.newtonLimit) {
2710 return optimizeRateByNewton(saddle, hessSaddle, beta, std::move(guess),
2711 potential, options);
2712 }
2713 const double bnh = inst.betaN * kHbar;
2714 const double c = 1.0 / (bnh * bnh);
2715
2716 const ColMajorXd saddleCurvature =
2717 0.5 * (hessSaddle + hessSaddle.transpose());
2718 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es(saddleCurvature);
2719 const VectorXd dir = es.eigenvectors().col(0);
2720
2721 if (static_cast<long>(guess.size()) != N) {
2722 std::vector<double> v0;
2723 std::vector<VectorXd> g0;
2724 potential({saddle}, v0, g0);
2725 const double vS = v0.at(0);
2726 // Scan in steps of a quarter of the length over which the barrier's
2727 // curvature drops V by kB T_c.
2728 const double h = 0.25 * std::sqrt(2.0 * kBoltzmann * inst.crossover /
2729 -es.eigenvalues()(0));
2730 const long pts = 200;
2731 const double dPlus = sideDrop(saddle, dir, vS, 1.0, h, pts, potential);
2732 const double dMinus = sideDrop(saddle, dir, vS, -1.0, h, pts, potential);
2733 const double dMin = std::min(dPlus, dMinus);
2734 const double drop =
2735 (1.0 - inst.temperature / inst.crossover) *
2736 (std::isfinite(dMin) ? dMin : kBoltzmann * inst.crossover);
2737 const double sPlus =
2738 turningDistance(saddle, dir, vS, drop, 1.0, h, pts, potential);
2739 const double sMinus =
2740 turningDistance(saddle, dir, vS, drop, -1.0, h, pts, potential);
2741 guess.resize(static_cast<size_t>(N));
2742 for (long j = 0; j < N; ++j) {
2743 const double ct =
2744 std::cos(2.0 * std::numbers::pi * static_cast<double>(j) /
2745 static_cast<double>(N));
2746 guess[static_cast<size_t>(j)] =
2747 saddle + dir * (ct >= 0.0 ? sPlus * ct : sMinus * ct);
2748 }
2749 }
2750
2751 // An even count whose beads already match under j -> N-j is the
2752 // out-and-back instanton. The images are assigned equal, so the
2753 // potential on one half is copied, and the step stays on the closed ring.
2754 const bool wantMirror = options.halfRing && N % 2 == 0;
2755 std::vector<VectorXd> x = std::move(guess);
2756 bool fold = false;
2757 if (wantMirror) {
2758 fold = true;
2759 const long m = N / 2;
2760 for (long j = 1; j < m; ++j) {
2761 if ((x[static_cast<size_t>(j)] - x[static_cast<size_t>(N - j)]).norm() >
2762 1e-8) {
2763 fold = false;
2764 break;
2765 }
2766 }
2767 }
2768 BatchPotential evalPot = potential;
2769 if (fold) {
2770 evalPot = [&](const std::vector<VectorXd> &q, std::vector<double> &v,
2771 std::vector<VectorXd> &g) {
2772 const long m = N / 2;
2773 bool sym = true;
2774 for (long j = 1; j < m; ++j) {
2775 if ((q[static_cast<size_t>(j)] - q[static_cast<size_t>(N - j)])
2776 .squaredNorm() != 0.0) {
2777 sym = false;
2778 break;
2779 }
2780 }
2781 if (!sym) {
2782 potential(q, v, g);
2783 return;
2784 }
2785 std::vector<VectorXd> uniq(static_cast<size_t>(m + 1));
2786 for (long j = 0; j <= m; ++j) {
2787 uniq[static_cast<size_t>(j)] = q[static_cast<size_t>(j)];
2788 }
2789 std::vector<double> vu;
2790 std::vector<VectorXd> gu;
2791 potential(uniq, vu, gu);
2792 v.assign(static_cast<size_t>(N), 0.0);
2793 g.assign(static_cast<size_t>(N), VectorXd());
2794 for (long j = 0; j <= m; ++j) {
2795 v[static_cast<size_t>(j)] = vu[static_cast<size_t>(j)];
2796 g[static_cast<size_t>(j)] = gu[static_cast<size_t>(j)];
2797 }
2798 for (long j = 1; j < m; ++j) {
2799 v[static_cast<size_t>(N - j)] = vu[static_cast<size_t>(j)];
2800 g[static_cast<size_t>(N - j)] = gu[static_cast<size_t>(j)];
2801 }
2802 };
2803 }
2804 auto symmetrize = [&](std::vector<VectorXd> &q) {
2805 if (!fold) {
2806 return;
2807 }
2808 const long m = N / 2;
2809 for (long j = 1; j < m; ++j) {
2810 const size_t a = static_cast<size_t>(j);
2811 const size_t b = static_cast<size_t>(N - j);
2812 const VectorXd mid = 0.5 * (q[a] + q[b]);
2813 q[a] = mid;
2814 q[b] = mid;
2815 }
2816 };
2817 symmetrize(x);
2818
2819 RingEval cur = evaluateRing(x, c, evalPot, options.energyShift);
2820 // The unstable mode of the ring starts as every bead moving along the
2821 // saddle's unstable direction.
2822 std::vector<VectorXd> mode(x.size(), dir);
2823 double curvature = lowestMode(x, cur, c, evalPot, mode, options.lanczosFirst,
2824 options.lanczosStep, fold);
2825 std::deque<std::pair<std::vector<VectorXd>, std::vector<VectorXd>>> pairs;
2826 auto effective = [&](const std::vector<VectorXd> &g) {
2827 const double par = dot(g, mode);
2828 std::vector<VectorXd> e = g;
2829 const double f = curvature < 0.0 ? 2.0 : 1.0;
2830 for (size_t j = 0; j < e.size(); ++j) {
2831 e[j] -= f * par * mode[j];
2832 if (!(curvature < 0.0)) {
2833 e[j] = -par * mode[j]; // climb along the mode only
2834 }
2835 }
2836 return e;
2837 };
2838 std::vector<VectorXd> geff = effective(cur.grad);
2839 long limit = options.maxIterations;
2840 for (long it = 0; it < limit; ++it) {
2841 inst.iterations = it;
2842 if (curvature < 0.0 && largestBeadNorm(cur.grad) < options.forceTolerance) {
2843 std::vector<VectorXd> oddMode;
2844 const double oddCurv =
2845 fold ? lowestOddMode(x, cur, c, potential, oddMode,
2846 options.lanczosFirst, options.lanczosStep)
2847 : 0.0;
2848 if (!(oddCurv < -1e-3 * std::abs(curvature))) {
2849 inst.converged = true;
2850 break;
2851 }
2852 // A mirror-symmetric stationary point with a second unstable mode
2853 // odd under the mirror, such as two copies of the instanton on one
2854 // ring. The rest of the search runs on the whole ring from a kick
2855 // along that mode which lowers beta_N U_N by one in the quadratic
2856 // model.
2857 fold = false;
2858 evalPot = potential;
2859 const double kick = std::sqrt(2.0 / (inst.betaN * -oddCurv));
2860 for (size_t k = 0; k < x.size(); ++k) {
2861 x[k] += kick * oddMode[k];
2862 }
2863 cur = evaluateRing(x, c, evalPot, options.energyShift);
2864 curvature = lowestMode(x, cur, c, evalPot, mode, options.lanczosFirst,
2865 options.lanczosStep, fold);
2866 pairs.clear();
2867 geff = effective(cur.grad);
2868 limit = it + options.maxIterations;
2869 }
2870 std::vector<VectorXd> trial(x.size());
2871 std::vector<VectorXd> d = geff;
2872 std::vector<double> alpha(pairs.size());
2873 for (size_t i = pairs.size(); i-- > 0;) {
2874 const double rho = 1.0 / dot(pairs[i].second, pairs[i].first);
2875 alpha[i] = rho * dot(pairs[i].first, d);
2876 for (size_t k = 0; k < d.size(); ++k) {
2877 d[k] -= alpha[i] * pairs[i].second[k];
2878 }
2879 }
2880 double gamma = 1.0 / (4.0 * c); // spring stiffness sets the first scale
2881 if (!pairs.empty()) {
2882 gamma = dot(pairs.back().first, pairs.back().second) /
2883 dot(pairs.back().second, pairs.back().second);
2884 }
2885 scale(d, gamma);
2886 for (size_t i = 0; i < pairs.size(); ++i) {
2887 const double rho = 1.0 / dot(pairs[i].second, pairs[i].first);
2888 const double b = rho * dot(pairs[i].second, d);
2889 for (size_t k = 0; k < d.size(); ++k) {
2890 d[k] += (alpha[i] - b) * pairs[i].first[k];
2891 }
2892 }
2893 if (!(dot(d, geff) > 0.0)) { // not a descent direction on geff
2894 pairs.clear();
2895 d = geff;
2896 scale(d, 1.0 / (4.0 * c));
2897 }
2898 const double big = largestBeadNorm(d);
2899 if (big > options.maxStep) {
2900 scale(d, options.maxStep / big);
2901 }
2902 for (size_t k = 0; k < x.size(); ++k) {
2903 trial[k] = x[k] - d[k];
2904 }
2905 symmetrize(trial);
2906 RingEval next = evaluateRing(trial, c, evalPot, options.energyShift);
2907 const double prevCurv = curvature;
2908 const long restart = options.lanczosRestart;
2909 curvature = lowestMode(trial, next, c, evalPot, mode, restart,
2910 options.lanczosStep, fold);
2911 std::vector<VectorXd> geffNext = effective(next.grad);
2912 if ((prevCurv < 0.0) != (curvature < 0.0)) {
2913 pairs.clear();
2914 } else {
2915 std::vector<VectorXd> sk(x.size()), yk(x.size());
2916 for (size_t k = 0; k < x.size(); ++k) {
2917 sk[k] = trial[k] - x[k];
2918 yk[k] = geffNext[k] - geff[k];
2919 }
2920 if (dot(sk, yk) > 0.0) {
2921 pairs.emplace_back(std::move(sk), std::move(yk));
2922 if (static_cast<long>(pairs.size()) > options.memory) {
2923 pairs.pop_front();
2924 }
2925 }
2926 }
2927 x = std::move(trial);
2928 cur = std::move(next);
2929 geff = std::move(geffNext);
2930 }
2931 if (!inst.converged && curvature < 0.0 &&
2932 largestBeadNorm(cur.grad) < options.forceTolerance) {
2933 inst.converged = true;
2934 }
2935 inst.beads = x;
2936 inst.energies = cur.v;
2937 inst.ringPotential = cur.u;
2938 inst.bN = 0.0;
2939 for (size_t j = 0; j < x.size(); ++j) {
2940 inst.bN += (x[(j + 1) % x.size()] - x[j]).squaredNorm();
2941 }
2942 return inst;
2943}
void * sym(Handle h, const char *name) noexcept
Definition DynLib.h:75
std::function< void(const std::vector< VectorXd > &q, std::vector< double > &v, std::vector< VectorXd > &grad)> BatchPotential
V (eV) and dV/dq (eV / (amu^0.5 Angstrom)) at every point of q, all in one call so a potential can sp...
Definition Tunneling.h:152
double crossoverTemperature(const MatrixXd &hessSaddle)
T_c = hbar omega_b / (2 pi kB) from the mass-weighted Hessian at the saddle, in K; throws when the He...
long memory
L-BFGS correction pairs.
Definition Tunneling.h:264
double maxStep
largest bead move per step, amu^0.5 Angstrom
Definition Tunneling.h:262
double energyShift
subtracted from every bead potential, eV
Definition Tunneling.h:275
long beads
N, beads on the ring.
Definition Tunneling.h:255
long lanczosRestart
Lanczos steps from the previous mode.
Definition Tunneling.h:260
double forceTolerance
largest per-bead |dU_N/dq|, eV / (amu^0.5 Angstrom)
Definition Tunneling.h:257
bool halfRing
Even N, when the guess already matches under j -> N - j: evaluate the potential from one turning poin...
Definition Tunneling.h:268
double lanczosStep
finite-difference step, amu^0.5 Angstrom
Definition Tunneling.h:261
long newtonLimit
Active coordinates at or below this take the Newton step.
Definition Tunneling.h:279
long lanczosFirst
Lanczos steps for the first minimum mode.
Definition Tunneling.h:259
long maxIterations
translation steps
Definition Tunneling.h:256

◆ parabolicFactor()

double eonc::tunneling::parabolicFactor ( double temperature,
double crossover )

(pi T_c / T) / sin(pi T_c / T).

Defined only above T_c. The factor diverges as T approaches T_c from above and tends to 1 at high T.

Definition at line 3131 of file Tunneling.cpp.

3131 {
3132 if (!(temperature > 0.0) || !(crossover > 0.0)) {
3133 throw std::invalid_argument(
3134 "parabolicFactor: temperature and crossover must be positive");
3135 }
3136 if (!(temperature > crossover)) {
3137 throw std::invalid_argument(
3138 "parabolicFactor: T is at or below the crossover; the factor "
3139 "diverges there");
3140 }
3141 // beta hbar omega_b / 2 = pi T_c / T, since T_c = hbar omega_b / (2 pi kB).
3142 const double phase = std::numbers::pi * crossover / temperature;
3143 const double s = std::sin(phase);
3144 if (!(s > 0.0)) {
3145 throw std::invalid_argument(
3146 "parabolicFactor: the sine of the barrier phase is not positive");
3147 }
3148 return phase / s;
3149}

◆ pathOmega()

double eonc::tunneling::pathOmega ( const MatrixXd & hessStart,
const MatrixXd & hessEnd,
const VectorXd & start,
const VectorXd & end )

omega along the straight line between the minima from the curvature of each well there, the larger of the two; in 1 / time.

Definition at line 622 of file Tunneling.cpp.

623 {
624 const VectorXd d = (end - start).normalized();
625 const double k = std::max(d.dot(hessStart * d), d.dot(hessEnd * d));
626 if (!(k > 0.0)) {
627 throw std::invalid_argument(
628 "pathOmega: no positive curvature along the path at either minimum");
629 }
630 return std::sqrt(k);
631}

◆ quantumHarmonicTstLogRate()

double eonc::tunneling::quantumHarmonicTstLogRate ( const MatrixXd & hessReactant,
const MatrixXd & hessSaddle,
double beta,
double barrier,
long rigidModes )

ln(k) for quantum harmonic transition-state theory, k in 1/time: (1 / (2 pi beta hbar)) prod_r 2 sinh(beta hbar omega_r / 2) / prod'_s 2 sinh(beta hbar omega_s / 2) exp(-beta barrier), the zero-point and quantised partition functions of every bound mode; the saddle's unstable mode leaves the product.

Times the parabolic factor it is the N -> infinity ring-polymer rate above T_c, so it joins the instanton rate at the crossover; its high-temperature limit is harmonicTstLogRate.

Definition at line 3189 of file Tunneling.cpp.

3191 {
3192 if (!(beta > 0.0) || hessReactant.size() == 0 || hessSaddle.size() == 0 ||
3193 hessReactant.rows() != hessReactant.cols() ||
3194 hessSaddle.rows() != hessSaddle.cols() ||
3195 hessReactant.rows() != hessSaddle.rows() || rigidModes < 0) {
3196 throw std::invalid_argument(
3197 "quantumHarmonicTstLogRate: need beta > 0, matching square Hessians "
3198 "and a non-negative rigid-mode count");
3199 }
3200 const ColMajorXd hr = 0.5 * (hessReactant + hessReactant.transpose());
3201 const ColMajorXd hsd = 0.5 * (hessSaddle + hessSaddle.transpose());
3202 const Eigen::SelfAdjointEigenSolver<ColMajorXd> er(hr,
3203 Eigen::EigenvaluesOnly);
3204 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es(hsd,
3205 Eigen::EigenvaluesOnly);
3206 const VectorXd lr = er.eigenvalues();
3207 const VectorXd ls = es.eigenvalues();
3208 if (!(ls(0) < 0.0)) {
3209 throw std::invalid_argument("quantumHarmonicTstLogRate: the saddle "
3210 "Hessian has no negative eigenvalue");
3211 }
3212 const std::vector<bool> rigidR = nearestZero(lr, rigidModes);
3213 const std::vector<bool> rigidS = nearestZero(ls, rigidModes);
3214 const double bh = beta * kHbar;
3215 // ln(2 sinh(x / 2)) without overflow for large x.
3216 auto logTwoSinhHalf = [](double x) {
3217 return 0.5 * x + std::log1p(-std::exp(-x));
3218 };
3219 double logRatio = 0.0;
3220 for (long m = 0; m < lr.size(); ++m) {
3221 if (!rigidR[static_cast<size_t>(m)]) {
3222 logRatio += logTwoSinhHalf(bh * std::sqrt(lr(m)));
3223 }
3224 }
3225 for (long m = 1; m < ls.size(); ++m) {
3226 if (!rigidS[static_cast<size_t>(m)]) {
3227 logRatio -= logTwoSinhHalf(bh * std::sqrt(std::abs(ls(m))));
3228 }
3229 }
3230 return logRatio - std::log(2.0 * std::numbers::pi * bh) - beta * barrier;
3231}

◆ ringFromPath()

std::vector< VectorXd > eonc::tunneling::ringFromPath ( const std::vector< VectorXd > & path,
const std::vector< double > & energies,
double betaHbar,
long beads )

Closed ring of beads samples of path whose imaginary-time period is betaHbar.

Bead 0 is the reactant-side turning point and bead N/2 the other; bead N - j repeats bead j. Throws when the path has no barrier, or when the period at the barrier top already exceeds betaHbar.

Definition at line 270 of file Tunneling.cpp.

272 {
273 if (path.size() < 3 || path.size() != energies.size() || beads < 4 ||
274 !(betaHbar > 0.0)) {
275 throw std::invalid_argument(
276 "ringFromPath: a path of at least three points with energies, "
277 "N >= 4 and beta hbar > 0");
278 }
279 const long width = path.front().size();
280 for (const auto &q : path) {
281 if (q.size() != width) {
282 throw std::invalid_argument("ringFromPath: the path changes dimension");
283 }
284 }
285 const std::vector<double> s = arcLengths(path);
286 const Profile profile(s, energies);
287 double sTop = s.front();
288 double vTop = -std::numeric_limits<double>::infinity();
289 const int grid = 4000;
290 for (int k = 0; k <= grid; ++k) {
291 const double sk =
292 s.front() + (s.back() - s.front()) * static_cast<double>(k) / grid;
293 if (profile(sk) > vTop) {
294 vTop = profile(sk);
295 sTop = sk;
296 }
297 }
298 const double vLow = std::max(energies.front(), energies.back());
299 if (!(vTop > vLow)) {
300 throw std::invalid_argument("ringFromPath: the path has no barrier");
301 }
302 auto period = [&](double energy) {
303 const auto [sMinus, sPlus] = turningPoints(profile, sTop, energy);
304 return 2.0 * halfPeriod(profile, sMinus, sPlus, energy);
305 };
306 // The crossover along the path from a parabola through the three input
307 // points around the barrier top, not from the interpolant, whose slope is
308 // clamped to zero at the top node.
309 {
310 size_t top = 0;
311 for (size_t k = 1; k < energies.size(); ++k) {
312 if (energies[k] > energies[top]) {
313 top = k;
314 }
315 }
316 if (top == 0 || top + 1 == energies.size()) {
317 throw std::invalid_argument(
318 "ringFromPath: the barrier top is an end of the path");
319 }
320 const double h1 = s[top] - s[top - 1], h2 = s[top + 1] - s[top];
321 const double curvature =
322 2.0 *
323 (h1 * energies[top + 1] - (h1 + h2) * energies[top] +
324 h2 * energies[top - 1]) /
325 (h1 * h2 * (h1 + h2));
326 if (!(curvature < 0.0)) {
327 throw std::invalid_argument("ringFromPath: no curvature at the top");
328 }
329 const double tc = kHbar * std::sqrt(-curvature) / (2.0 * std::numbers::pi);
330 if (!(kHbar / betaHbar < tc)) {
331 throw std::invalid_argument(
332 "ringFromPath: the temperature is at or above the crossover along "
333 "this path");
334 }
335 }
336 // Bracket the orbit energy geometrically above the lower end: the period
337 // grows only logarithmically as E approaches a well bottom.
338 const double span = vTop - vLow;
339 double eHi = vTop - 1e-9 * span;
340 double eLo = vLow + 1e-14 * span;
341 if (period(eLo) < betaHbar) {
342 // The path does not reach a long enough orbit. The lowest one it
343 // holds is the start.
344 eHi = eLo;
345 }
346 for (int k = 0; k < 200 && eHi > eLo; ++k) {
347 const double e = vLow + std::sqrt((eLo - vLow) * (eHi - vLow));
348 if (period(e) > betaHbar) {
349 eLo = e;
350 } else {
351 eHi = e;
352 }
353 if (eHi - eLo < 1e-15 * span) {
354 break;
355 }
356 }
357 const double energy = 0.5 * (eLo + eHi);
358 const auto [sMinus, sPlus] = turningPoints(profile, sTop, energy);
359 std::vector<double> tau;
360 std::vector<double> pos;
361 const double half = halfPeriod(profile, sMinus, sPlus, energy, &tau, &pos);
362 if (tau.size() < 2 || tau.size() != pos.size()) {
363 throw std::invalid_argument("ringFromPath: the orbit has no length");
364 }
365 // Bead j sits at imaginary time j * beta hbar / N on the way from the
366 // reactant-side turning point to the other side. The return repeats it.
367 std::vector<VectorXd> ring(static_cast<size_t>(beads), VectorXd::Zero(width));
368 for (long j = 0; j <= beads / 2; ++j) {
369 const double t =
370 std::min(half, half * 2.0 * static_cast<double>(j) / beads);
371 const auto it = std::upper_bound(tau.begin(), tau.end(), t);
372 const size_t k = std::clamp<size_t>(
373 static_cast<size_t>(std::distance(tau.begin(), it)), 1, tau.size() - 1);
374 const double seg = tau[k] - tau[k - 1];
375 const double w = seg > 0.0 ? (t - tau[k - 1]) / seg : 0.0;
376 const double sj =
377 pos[k - 1] + std::clamp(w, 0.0, 1.0) * (pos[k] - pos[k - 1]);
378 ring[static_cast<size_t>(j)] = atArcLength(path, s, sj);
379 if (j > 0 && j < beads - j) {
380 ring[static_cast<size_t>(beads - j)] = ring[static_cast<size_t>(j)];
381 }
382 }
383 return ring;
384}
Energy along the path, interpolated as a monotone cubic (Fritsch and Carlson), flat at both ends beca...
Definition Tunneling.h:54

◆ ringSpectrum()

RingSpectrum eonc::tunneling::ringSpectrum ( const std::vector< MatrixXd > & beadHessians,
double c,
const std::vector< VectorXd > & tau )

The ring Hessian of bead Hessians beadHessians (d2V/dq2 at each of the N beads) and spring constant c, with the normalised direction tau (N beads) projected out through the determinant lemma det(J + tau tau^T) = det' J when J tau = 0.

Block LU of the open chain plus a low-rank correction for the closure and tau, O(N f^3).

Definition at line 1562 of file Tunneling.cpp.

1563 {
1564 if (beadHessians.empty() || beadHessians.size() != tau.size()) {
1565 throw std::invalid_argument(
1566 "ringSpectrum: N bead Hessians and N tau blocks");
1567 }
1568 const std::vector<MatrixXd> diag = ringDiagonal(beadHessians, c, false);
1569 const WoodburyRing ring(c, diag, true, {tau}, {1.0}, true);
1570 if (!ring.ok()) {
1571 throw std::runtime_error("ringSpectrum: singular ring");
1572 }
1573 RingSpectrum out;
1574 out.logDetPrime = ring.logAbsDet();
1575 out.negativeModes = ring.negative();
1576 out.zeroEigenvalue = dot(tau, applyDiagonal(diag, c, true, tau));
1577 return out;
1578}
Spectrum of a closed ring's Hessian without forming it.
Definition Tunneling.h:298
double logDetPrime
ln |det' J|: the product over every eigenvalue but the one along tau.
Definition Tunneling.h:300

◆ wellCurvature()

double eonc::tunneling::wellCurvature ( const Profile & p,
bool leftEnd )

d2V/ds2 at one end of the path, in eV / (amu Angstrom^2), from a least squares fit of a s^2 + b s^3 to the points within half the barrier above that end.

The cubic term takes the anharmonic part a parabola alone would fold into the curvature.

Definition at line 104 of file Tunneling.cpp.

104 {
105 const auto &s = p.s();
106 const auto &v = p.v();
107 const size_t n = s.size();
108 const double top = *std::max_element(v.begin(), v.end());
109 const double floor = leftEnd ? v.front() : v.back();
110 const double half = 0.5 * (top - floor);
111 // Sums for the normal equations of y = a x^2 + b x^3.
112 double s44 = 0, s45 = 0, s55 = 0, sy2 = 0, sy3 = 0;
113 size_t used = 0;
114 for (size_t j = 1; j < n; ++j) {
115 const size_t i = leftEnd ? j : n - 1 - j;
116 const double x = leftEnd ? s[i] - s.front() : s.back() - s[i];
117 const double y = v[i] - floor;
118 if (y > half) {
119 break;
120 }
121 const double x2 = x * x;
122 s44 += x2 * x2;
123 s45 += x2 * x2 * x;
124 s55 += x2 * x2 * x2;
125 sy2 += y * x2;
126 sy3 += y * x2 * x;
127 ++used;
128 }
129 if (used == 0) {
130 // The next image already stands above half the barrier: the parabola
131 // through it is all the band says about this well.
132 const size_t i = leftEnd ? 1 : n - 2;
133 const double x = leftEnd ? s[i] - s.front() : s.back() - s[i];
134 return 2.0 * (v[i] - floor) / (x * x);
135 }
136 if (used == 1) {
137 return 2.0 * sy2 / s44;
138 }
139 const double det = s44 * s55 - s45 * s45;
140 const double a = (sy2 * s55 - sy3 * s45) / det;
141 return 2.0 * a;
142}
const std::vector< double > & v() const
Definition Tunneling.h:59
const std::vector< double > & s() const
Definition Tunneling.h:58

◆ wkbAction()

double eonc::tunneling::wkbAction ( const Profile & p,
double energy,
int points )

(1/hbar) integral sqrt(2 (V(s) - E)) ds over the path where V > E.

Definition at line 151 of file Tunneling.cpp.

151 {
152 const double a = p.s().front();
153 const double b = p.s().back();
154 const double h = (b - a) / (points - 1);
155 double sum = 0.0;
156 for (int i = 0; i < points; ++i) {
157 const double gap = p(a + i * h) - energy;
158 const double f = gap > 0.0 ? std::sqrt(2.0 * gap) : 0.0;
159 sum += (i == 0 || i == points - 1) ? 0.5 * f : f;
160 }
161 return sum * h / kHbar;
162}

◆ wkbLogRateAlongPath()

double eonc::tunneling::wkbLogRateAlongPath ( const Profile & profile,
double beta,
double hwReactant )

ln(k), k in 1/time, for the one-dimensional thermal rate along profile.

hwReactant is hbar omega of the reactant well, in eV. Below the barrier the transmission is the WKB factor; above it, the parabolic continuation.

Definition at line 386 of file Tunneling.cpp.

387 {
388 if (!(beta > 0.0) || !(hwReactant > 0.0)) {
389 throw std::invalid_argument(
390 "wkbLogRateAlongPath: beta and hbar omega must be positive");
391 }
392 const double vReactant = profile.v().front();
393 const double s0 = profile.s().front();
394 const double s1 = profile.s().back();
395 double vTop = vReactant;
396 double sTop = s0;
397 const int grid = 2000;
398 for (int k = 0; k <= grid; ++k) {
399 const double sk = s0 + (s1 - s0) * static_cast<double>(k) / grid;
400 const double vk = profile(sk);
401 if (vk >= vTop) {
402 vTop = vk;
403 sTop = sk;
404 }
405 }
406 const double barrier = vTop - vReactant;
407 if (!(barrier > 0.0)) {
408 throw std::invalid_argument(
409 "wkbLogRateAlongPath: no barrier above the reactant");
410 }
411 double ds = 1e-3 * (s1 - s0);
412 ds = std::min(ds, std::min(sTop - s0, s1 - sTop));
413 if (!(ds > 0.0)) {
414 throw std::invalid_argument(
415 "wkbLogRateAlongPath: the barrier top is at an end of the path");
416 }
417 const double curvature =
418 std::max(1e-12, -(profile(sTop + ds) - 2.0 * vTop + profile(sTop - ds)) /
419 (ds * ds));
420 const double hwBarrier = kHbar * std::sqrt(curvature);
421 // int P(E) exp(-beta E) dE from the reactant up to where the Boltzmann
422 // factor has died. E is measured from the reactant.
423 const double eMax = barrier + 40.0 / beta;
424 const int points = 600;
425 auto logAdd = [](double a, double b) {
426 if (a == -std::numeric_limits<double>::infinity()) {
427 return b;
428 }
429 const double m = std::max(a, b);
430 return m + std::log(std::exp(a - m) + std::exp(b - m));
431 };
432 double logTerms = -std::numeric_limits<double>::infinity();
433 double prevLog = -std::numeric_limits<double>::infinity();
434 double prevE = 0.0;
435 for (int k = 0; k <= points; ++k) {
436 const double energy = eMax * static_cast<double>(k) / points;
437 double theta = 0.0;
438 if (energy < barrier) {
439 theta = wkbAction(profile, vReactant + energy);
440 } else {
441 theta = -std::numbers::pi * (energy - barrier) / hwBarrier;
442 }
443 const double logP =
444 theta > 20.0 ? -2.0 * theta : -std::log1p(std::exp(2.0 * theta));
445 const double logF = logP - beta * energy;
446 if (k > 0) {
447 const double segment =
448 std::log(0.5 * (energy - prevE)) + logAdd(prevLog, logF);
449 logTerms = logAdd(logTerms, segment);
450 }
451 prevLog = logF;
452 prevE = energy;
453 }
454 const double logFlux = logTerms - std::log(2.0 * std::numbers::pi * kHbar);
455 return logFlux + std::log(2.0 * std::sinh(0.5 * beta * hwReactant));
456}
double wkbAction(const Profile &p, double energy, int points)
(1/hbar) integral sqrt(2 (V(s) - E)) ds over the path where V > E.

◆ wkbSplitting()

Splitting eonc::tunneling::wkbSplitting ( const Profile & p,
double hwReactant,
double hwProduct )

delta0 = (hbar omega / pi) exp(-S) with the Landau and Lifshitz prefactor.

The level is the higher of the two harmonic ground states, V_i + hbar omega_i / 2, and omega is the geometric mean of the wells; for a symmetric double well both reduce to the textbook formula.

Definition at line 166 of file Tunneling.cpp.

166 {
167 const auto &v = p.v();
168 Splitting out;
169 const double top = *std::max_element(v.begin(), v.end());
170 out.delta = v.back() - v.front();
171 out.barrier = top - v.front();
172 out.hwReactant = hwReactant;
173 out.hwProduct = hwProduct;
174 out.referenceEnergy =
175 std::max(v.front() + 0.5 * hwReactant, v.back() + 0.5 * hwProduct);
176 out.action = wkbAction(p, out.referenceEnergy);
177 const double hw = std::sqrt(hwReactant * hwProduct);
178 out.delta0 = hw / std::numbers::pi * std::exp(-out.action);
179 out.deepWells =
180 (top - v.front()) > hwReactant && (top - v.back()) > hwProduct;
181 return out;
182}
bool deepWells
Both barriers stand above hbar omega; below that WKB is not the right tool and the number is reported...
Definition Tunneling.h:87
double hwReactant
hbar omega of the reactant well, eV
Definition Tunneling.h:80
double delta0
tunnelling splitting, eV
Definition Tunneling.h:84
double barrier
path maximum above the reactant, eV
Definition Tunneling.h:79
double referenceEnergy
the level tunnelling happens at, eV
Definition Tunneling.h:82
double action
the WKB exponent, dimensionless
Definition Tunneling.h:83
double hwProduct
hbar omega of the product well, eV
Definition Tunneling.h:81
double delta
product minus reactant, eV
Definition Tunneling.h:78

Variable Documentation

◆ kBoltzmann

double eonc::tunneling::kBoltzmann = 8.617333262145177e-5
inlineconstexpr

Boltzmann constant in eV / K, correctly rounded from the exact 1.380649e-23 J / K.

Definition at line 41 of file Tunneling.h.

◆ kHbar

double eonc::tunneling::kHbar = 0.06465415129579072
inlineconstexpr

hbar in eV^0.5 amu^0.5 Angstrom, correctly rounded from the exact SI values (h = 6.62607015e-34 J s, e = 1.602176634e-19 C) and the CODATA 2022 dalton, 1.66053906892e-27 kg.

Definition at line 37 of file Tunneling.h.

◆ kTimeUnitSeconds

double eonc::tunneling::kTimeUnitSeconds = 1.0180505717871193e-14
inlineconstexpr

One unit of time, sqrt(amu Angstrom^2 / eV), in seconds.

Definition at line 224 of file Tunneling.h.