36 throw std::invalid_argument(
"the structures hold different atom counts");
43 throw std::invalid_argument(
44 "every atom needs a positive mass for a mass-weighted path");
46 sum += mass(i) * dr.row(i).squaredNorm();
48 return std::sqrt(sum);
53 std::vector<double> s{0.0};
54 s.reserve(band.size());
55 for (
size_t i = 1; i < band.size(); ++i) {
65 const size_t n =
s_.size();
66 if (n < 2 ||
v_.size() != n) {
67 throw std::invalid_argument(
68 "a profile needs matching s and V with two points or more");
70 for (
size_t k = 1; k < n; ++k) {
71 if (!(
s_[k] >
s_[k - 1])) {
72 throw std::invalid_argument(
73 "the path coordinate must increase along the band");
76 for (
size_t k = 1; k + 1 < n; ++k) {
77 const double h0 =
s_[k] -
s_[k - 1];
78 const double h1 =
s_[k + 1] -
s_[k];
79 const double d0 = (
v_[k] -
v_[k - 1]) / h0;
80 const double d1 = (
v_[k + 1] -
v_[k]) / h1;
84 const double w1 = 2.0 * h1 + h0;
85 const double w2 = h1 + 2.0 * h0;
86 m_[k] = (w1 + w2) / (w1 / d0 + w2 / d1);
92 x = std::clamp(x,
s_.front(),
s_.back());
93 auto it = std::upper_bound(
s_.begin(),
s_.end(), x);
94 size_t k =
static_cast<size_t>(std::distance(
s_.begin(), it));
95 k = std::clamp<size_t>(k == 0 ? 0 : k - 1, 0,
s_.size() - 2);
96 const double h =
s_[k + 1] -
s_[k];
97 const double t = (x -
s_[k]) / h;
98 const double t2 = t * t;
99 const double t3 = t2 * t;
100 return (2 * t3 - 3 * t2 + 1) *
v_[k] + (t3 - 2 * t2 + t) * h *
m_[k] +
101 (-2 * t3 + 3 * t2) *
v_[k + 1] + (t3 - t2) * h *
m_[k + 1];
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);
112 double s44 = 0, s45 = 0, s55 = 0, sy2 = 0, sy3 = 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;
121 const double x2 = x * x;
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);
137 return 2.0 * sy2 / s44;
139 const double det = s44 * s55 - s45 * s45;
140 const double a = (sy2 * s55 - sy3 * s45) / det;
145 if (!(curvature > 0.0)) {
146 throw std::invalid_argument(
"a well needs a positive curvature");
148 return kHbar * std::sqrt(curvature);
152 const double a = p.
s().front();
153 const double b = p.
s().back();
154 const double h = (b - a) / (points - 1);
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;
161 return sum * h /
kHbar;
167 const auto &v = p.
v();
169 const double top = *std::max_element(v.begin(), v.end());
170 out.
delta = v.back() - v.front();
175 std::max(v.front() + 0.5 * hwReactant, v.back() + 0.5 * hwProduct);
177 const double hw = std::sqrt(hwReactant * hwProduct);
178 out.
delta0 = hw / std::numbers::pi * std::exp(-out.
action);
180 (top - v.front()) > hwReactant && (top - v.back()) > hwProduct;
186std::vector<double> arcLengths(
const std::vector<VectorXd> &path) {
187 std::vector<double> s(path.size(), 0.0);
188 for (
size_t k = 1; k < path.size(); ++k) {
189 s[k] = s[k - 1] + (path[k] - path[k - 1]).norm();
194VectorXd atArcLength(
const std::vector<VectorXd> &path,
195 const std::vector<double> &s,
double target) {
196 const auto it = std::upper_bound(s.begin(), s.end(), target);
197 const size_t k = std::clamp<size_t>(
198 static_cast<size_t>(std::distance(s.begin(), it)), 1, path.size() - 1);
199 const double seg = s[k] - s[k - 1];
200 const double t = seg > 0.0 ? (target - s[k - 1]) / seg : 0.0;
201 return path[k - 1] + std::clamp(t, 0.0, 1.0) * (path[k] - path[k - 1]);
205std::pair<double, double> turningPoints(
const Profile &p,
double sTop,
207 const double s0 = p.s().front();
208 const double s1 = p.s().back();
209 auto cross = [&](
double from,
double to) {
210 const int steps = 2000;
213 for (
int k = 1; k <= steps; ++k) {
214 const double sk = from + (to - from) *
static_cast<double>(k) / steps;
215 if (p(sk) <= energy) {
216 a = from + (to - from) *
static_cast<double>(k - 1) / steps;
224 for (
int k = 0; k < 60; ++k) {
225 const double m = 0.5 * (a + b);
232 return 0.5 * (a + b);
234 return {cross(sTop, s0), cross(sTop, s1)};
240double halfPeriod(
const Profile &p,
double sMinus,
double sPlus,
double energy,
241 std::vector<double> *cumulative =
nullptr,
242 std::vector<double> *positions =
nullptr) {
243 const int panels = 4000;
244 const double mid = 0.5 * (sMinus + sPlus);
245 const double half = 0.5 * (sPlus - sMinus);
247 if (cumulative !=
nullptr) {
248 cumulative->assign(1, 0.0);
249 positions->assign(1, sMinus);
251 for (
int k = 0; k < panels; ++k) {
253 std::numbers::pi * (
static_cast<double>(k) + 0.5) / panels;
254 const double s = mid - half * std::cos(phi);
255 const double under = 2.0 * (p(s) - energy);
256 const double integrand =
257 half * std::sin(phi) / std::sqrt(std::max(under, 1e-300));
258 total += integrand * std::numbers::pi / panels;
259 if (cumulative !=
nullptr) {
260 cumulative->push_back(total);
261 positions->push_back(
262 mid - half * std::cos(std::numbers::pi * (k + 1.0) / panels));
271 const std::vector<double> &energies,
272 double betaHbar,
long beads) {
273 if (path.size() < 3 || path.size() != energies.size() || beads < 4 ||
275 throw std::invalid_argument(
276 "ringFromPath: a path of at least three points with energies, "
277 "N >= 4 and beta hbar > 0");
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");
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) {
292 s.front() + (s.back() - s.front()) *
static_cast<double>(k) / grid;
293 if (profile(sk) > vTop) {
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");
302 auto period = [&](
double energy) {
303 const auto [sMinus, sPlus] = turningPoints(profile, sTop, energy);
304 return 2.0 * halfPeriod(profile, sMinus, sPlus, energy);
311 for (
size_t k = 1; k < energies.size(); ++k) {
312 if (energies[k] > energies[top]) {
316 if (top == 0 || top + 1 == energies.size()) {
317 throw std::invalid_argument(
318 "ringFromPath: the barrier top is an end of the path");
320 const double h1 = s[top] - s[top - 1], h2 = s[top + 1] - s[top];
321 const double curvature =
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");
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 "
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) {
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) {
353 if (eHi - eLo < 1e-15 * span) {
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");
367 std::vector<VectorXd> ring(
static_cast<size_t>(beads), VectorXd::Zero(width));
368 for (
long j = 0; j <= beads / 2; ++j) {
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;
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)];
388 if (!(beta > 0.0) || !(hwReactant > 0.0)) {
389 throw std::invalid_argument(
390 "wkbLogRateAlongPath: beta and hbar omega must be positive");
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;
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);
406 const double barrier = vTop - vReactant;
407 if (!(barrier > 0.0)) {
408 throw std::invalid_argument(
409 "wkbLogRateAlongPath: no barrier above the reactant");
411 double ds = 1e-3 * (s1 - s0);
412 ds = std::min(ds, std::min(sTop - s0, s1 - sTop));
414 throw std::invalid_argument(
415 "wkbLogRateAlongPath: the barrier top is at an end of the path");
417 const double curvature =
418 std::max(1e-12, -(profile(sTop + ds) - 2.0 * vTop + profile(sTop - ds)) /
420 const double hwBarrier =
kHbar * std::sqrt(curvature);
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()) {
429 const double m = std::max(a, b);
430 return m + std::log(std::exp(a - m) + std::exp(b - m));
432 double logTerms = -std::numeric_limits<double>::infinity();
433 double prevLog = -std::numeric_limits<double>::infinity();
435 for (
int k = 0; k <= points; ++k) {
436 const double energy = eMax *
static_cast<double>(k) / points;
438 if (energy < barrier) {
439 theta =
wkbAction(profile, vReactant + energy);
441 theta = -std::numbers::pi * (energy - barrier) / hwBarrier;
444 theta > 20.0 ? -2.0 * theta : -std::log1p(std::exp(2.0 * theta));
445 const double logF = logP - beta * energy;
447 const double segment =
448 std::log(0.5 * (energy - prevE)) + logAdd(prevLog, logF);
449 logTerms = logAdd(logTerms, segment);
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));
459 double referenceEnergy) {
460 std::vector<double> v;
461 v.reserve(band.size());
462 for (
const auto &image : band) {
463 v.push_back(image->getPotentialEnergy() - referenceEnergy);
474constexpr long kDenseRing = 4096;
479constexpr double kTrackOverlap = 0.3;
480constexpr long kRitzCap = 400;
483 Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor>;
490 BlockChain(
double c,
const std::vector<MatrixXd> &diag)
492 lu_.reserve(diag.size());
493 for (
size_t k = 0; k < diag.size(); ++k) {
494 ColMajorXd d = diag[k];
496 d -= c_ * c_ * lu_.back().inverse();
498 lu_.emplace_back(std::move(d));
499 const auto &f = lu_.back();
500 sign_ *=
static_cast<int>(std::lround(f.permutationP().determinant()));
501 for (
long i = 0; i < d.rows(); ++i) {
502 const double u = f.matrixLU()(i, i);
504 throw std::runtime_error(
"instanton: singular chain Hessian block");
506 sign_ *= u < 0.0 ? -1 : 1;
507 logAbsDet_ += std::log(std::abs(u));
511 double logAbsDet()
const {
return logAbsDet_; }
512 int sign()
const {
return sign_; }
514 std::vector<VectorXd> solve(
const std::vector<VectorXd> &b)
const {
515 const size_t m = b.size();
516 std::vector<VectorXd> y(m), x(m);
518 for (
size_t k = 1; k < m; ++k) {
519 y[k] = b[k] + c_ * lu_[k - 1].solve(y[k - 1]);
521 x[m - 1] = lu_[m - 1].solve(y[m - 1]);
522 for (
size_t k = m - 1; k-- > 0;) {
523 x[k] = lu_[k].solve(y[k] + c_ * x[k + 1]);
528 std::vector<MatrixXd> solve(
const std::vector<MatrixXd> &b)
const {
529 const size_t m = b.size();
530 std::vector<MatrixXd> y(m), x(m);
532 for (
size_t k = 1; k < m; ++k) {
533 y[k] = b[k] + c_ * lu_[k - 1].solve(y[k - 1]);
535 x[m - 1] = lu_[m - 1].solve(y[m - 1]);
536 for (
size_t k = m - 1; k-- > 0;) {
537 x[k] = lu_[k].solve(y[k] + c_ * x[k + 1]);
544 std::vector<Eigen::PartialPivLU<ColMajorXd>> lu_;
545 double logAbsDet_ = 0.0;
549double dot(
const std::vector<VectorXd> &a,
const std::vector<VectorXd> &b) {
551 for (
size_t k = 0; k < a.size(); ++k) {
557void scale(std::vector<VectorXd> &a,
double f) {
564VectorXd alongPolyline(
const std::vector<VectorXd> &pts,
565 const std::vector<double> &cum,
double f) {
566 const double target = f * cum.back();
567 const auto it = std::upper_bound(cum.begin(), cum.end(), target);
568 const size_t k = std::clamp<size_t>(
569 static_cast<size_t>(std::distance(cum.begin(), it)), 1, pts.size() - 1);
570 const double seg = cum[k] - cum[k - 1];
571 const double t = seg > 0.0 ? (target - cum[k - 1]) / seg : 0.0;
572 return pts[k - 1] + std::clamp(t, 0.0, 1.0) * (pts[k] - pts[k - 1]);
577 std::vector<double> v;
578 std::vector<VectorXd> grad;
579 std::vector<VectorXd> potGrad;
582ActionEval evaluateAction(
const std::vector<VectorXd> &interior,
583 const VectorXd &start,
const VectorXd &end,
584 double vStart,
double vEnd,
double dtau,
587 potential(interior, out.v, out.potGrad);
588 const size_t m = interior.size();
589 if (out.v.size() != m || out.potGrad.size() != m) {
590 throw std::runtime_error(
"instanton: potential returned the wrong count");
592 auto bead = [&](
size_t j) ->
const VectorXd & {
593 return j == 0 ? start : (j == m + 1 ? end : interior[j - 1]);
595 double kinetic = 0.0;
596 for (
size_t j = 0; j <= m; ++j) {
597 kinetic += (bead(j + 1) - bead(j)).squaredNorm();
599 double pot = 0.5 * (vStart + vEnd);
600 for (
double vj : out.v) {
603 out.action = 0.5 * kinetic / dtau + dtau * pot;
605 for (
size_t j = 1; j <= m; ++j) {
606 out.grad[j - 1] = (2.0 * bead(j) - bead(j - 1) - bead(j + 1)) / dtau +
607 dtau * out.potGrad[j - 1];
612double largestBeadNorm(
const std::vector<VectorXd> &g) {
614 for (
const auto &v : g) {
615 m = std::max(m, v.norm());
623 const VectorXd &start,
const VectorXd &end) {
624 const VectorXd d = (end - start).normalized();
625 const double k = std::max(d.dot(hessStart * d), d.dot(hessEnd * d));
627 throw std::invalid_argument(
628 "pathOmega: no positive curvature along the path at either minimum");
634 double betaHbar, std::vector<VectorXd> guess,
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");
644 inst.
dtau = betaHbar /
static_cast<double>(P);
645 const double dtau = inst.
dtau;
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");
657 if (guess.size() < 2) {
658 guess = {start, end};
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();
664 if (!(cum.back() > 0.0)) {
665 throw std::invalid_argument(
"optimizeInstanton: the two minima coincide");
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);
677 evaluateAction(x, start, end, vEnds[0], vEnds[1], dtau, potential);
678 std::deque<std::pair<std::vector<VectorXd>, std::vector<VectorXd>>> pairs;
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];
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);
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];
709 double slope = -dot(cur.grad, q);
710 if (!(slope < 0.0)) {
713 scale(q, dtau / 4.0);
714 slope = -dot(cur.grad, q);
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];
724 next = evaluateAction(trial, start, end, vEnds[0], vEnds[1], dtau,
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));
732 (flat && largestBeadNorm(next.grad) < largestBeadNorm(cur.grad))) {
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];
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) {
752 x = std::move(trial);
753 cur = std::move(next);
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));
768 const double sWell = betaHbar * 0.5 * (vEnds[0] + vEnds[1]);
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)])
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");
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);
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");
799 diag.push_back(spring + dtau * 0.5 * (h + h.transpose()));
801 const BlockChain chain(c, diag);
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");
811 return well.logAbsDet();
813 const double logDetWell = 0.5 * (wellLogDet(hessStart) + wellLogDet(hessEnd));
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)];
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);
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)");
831 const double logDetPrime = chain.logAbsDet() + std::log(std::abs(vJv));
834 std::vector<VectorXd> w(v.size());
835 for (
size_t k = 0; k < w.size(); ++k) {
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);
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) {
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);
856 std::sqrt(inst.
s0 / (2.0 * std::numbers::pi *
kHbar * dtau)) *
857 std::exp(0.5 * (logDetWell - logDetPrime) - inst.
action);
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");
868 return kHbar * std::sqrt(-lambda) / (2.0 * std::numbers::pi *
kBoltzmann);
875 std::vector<double> v;
876 std::vector<VectorXd> grad;
879RingEval evaluateRing(
const std::vector<VectorXd> &x,
double c,
881 double energyShift = 0.0) {
883 std::vector<VectorXd> gv;
884 potential(x, out.v, gv);
885 for (
double &v : out.v) {
888 const size_t n = x.size();
889 if (out.v.size() != n || gv.size() != n) {
890 throw std::runtime_error(
891 "rate instanton: potential returned the wrong count");
895 for (
size_t j = 0; j < n; ++j) {
896 const VectorXd &prev = x[(j + n - 1) % n];
897 const VectorXd &next = x[(j + 1) % n];
898 out.grad[j] = gv[j] + c * (2.0 * x[j] - prev - next);
899 spring += (next - x[j]).squaredNorm();
902 out.u += 0.5 * c * spring;
908double lowestMode(
const std::vector<VectorXd> &x,
const RingEval &here,
910 std::vector<VectorXd> &mode,
long steps,
double eps,
911 bool mirror =
false) {
912 const size_t n = x.size();
915 const bool reflect = mirror && n % 2 == 0;
916 auto snap = [&](std::vector<VectorXd> &q) {
920 const long m =
static_cast<long>(n) / 2;
921 for (
long j = 1; j < m; ++j) {
922 const size_t a =
static_cast<size_t>(j);
923 const size_t b = n - a;
924 const VectorXd mid = 0.5 * (q[a] + q[b]);
929 auto hv = [&](
const std::vector<VectorXd> &u) {
930 std::vector<VectorXd> xp(n);
931 for (
size_t j = 0; j < n; ++j) {
932 xp[j] = x[j] +
eps * u[j];
935 const RingEval e = evaluateRing(xp, c, potential);
936 std::vector<VectorXd> out(n);
937 for (
size_t j = 0; j < n; ++j) {
938 out[j] = (e.grad[j] - here.grad[j]) / eps;
943 std::vector<std::vector<VectorXd>> basis;
944 std::vector<double> alpha, beta;
945 std::vector<VectorXd> q = mode;
947 scale(q, 1.0 / std::sqrt(dot(q, q)));
948 for (
long k = 0; k < steps; ++k) {
950 std::vector<VectorXd> w = hv(q);
951 const double a = dot(w, q);
953 for (
const auto &b : basis) {
954 const double p = dot(w, b);
955 for (
size_t j = 0; j < n; ++j) {
960 const double bnorm = std::sqrt(dot(w, w));
961 if (!(bnorm > 1e-12) || k + 1 == steps) {
964 beta.push_back(bnorm);
965 scale(w, 1.0 / bnorm);
968 const long m =
static_cast<long>(alpha.size());
970 for (
long i = 0; i < m; ++i) {
971 t(i, i) = alpha[
static_cast<size_t>(i)];
973 t(i, i + 1) = t(i + 1, i) = beta[
static_cast<size_t>(i)];
976 const Eigen::SelfAdjointEigenSolver<MatrixXd> es(t);
977 const VectorXd y = es.eigenvectors().col(0);
978 std::vector<VectorXd> ritz(n);
979 for (
size_t j = 0; j < n; ++j) {
980 ritz[j] = VectorXd::Zero(x[j].size());
982 for (
long i = 0; i < m; ++i) {
983 for (
size_t j = 0; j < n; ++j) {
984 ritz[j] += y(i) * basis[
static_cast<size_t>(i)][j];
987 scale(ritz, 1.0 / std::sqrt(dot(ritz, ritz)));
989 const double rnorm = std::sqrt(dot(ritz, ritz));
991 scale(ritz, 1.0 / rnorm);
993 mode = std::move(ritz);
994 return es.eigenvalues()(0);
1002double lowestOddMode(
const std::vector<VectorXd> &x,
const RingEval &here,
1004 std::vector<VectorXd> &mode,
long steps,
double eps) {
1005 const size_t n = x.size();
1006 const size_t m = n / 2;
1007 std::vector<VectorXd> cycle(n);
1008 for (
size_t j = 0; j < n; ++j) {
1009 cycle[j] = x[(j + 1) % n] - x[(j + n - 1) % n];
1011 auto odd = [&](std::vector<VectorXd> &q) {
1014 for (
size_t a = 1; a < m; ++a) {
1015 const VectorXd half = 0.5 * (q[a] - q[n - a]);
1021 const double cnorm = std::sqrt(dot(cycle, cycle));
1023 scale(cycle, 1.0 / cnorm);
1025 auto project = [&](std::vector<VectorXd> &q) {
1028 const double p = dot(q, cycle);
1029 for (
size_t j = 0; j < n; ++j) {
1030 q[j] -= p * cycle[j];
1034 auto hv = [&](
const std::vector<VectorXd> &u) {
1035 std::vector<VectorXd> xp(n);
1036 for (
size_t j = 0; j < n; ++j) {
1037 xp[j] = x[j] +
eps * u[j];
1039 const RingEval e = evaluateRing(xp, c, potential);
1040 std::vector<VectorXd> out(n);
1041 for (
size_t j = 0; j < n; ++j) {
1042 out[j] = (e.grad[j] - here.grad[j]) / eps;
1049 VectorXd mean = VectorXd::Zero(x[0].size());
1050 for (
const auto &b : x) {
1053 mean /=
static_cast<double>(n);
1054 std::vector<VectorXd> q(n);
1055 for (
size_t j = 0; j < n; ++j) {
1056 q[j] = (j < m ? 1.0 : -1.0) * (x[j] - mean);
1059 const double qnorm = std::sqrt(dot(q, q));
1060 if (!(qnorm > 1e-12)) {
1061 return std::numeric_limits<double>::infinity();
1063 scale(q, 1.0 / qnorm);
1064 std::vector<std::vector<VectorXd>> basis;
1065 std::vector<double> alpha, beta;
1066 for (
long k = 0; k < steps; ++k) {
1068 std::vector<VectorXd> w = hv(q);
1069 alpha.push_back(dot(w, q));
1070 for (
const auto &b : basis) {
1071 const double p = dot(w, b);
1072 for (
size_t j = 0; j < n; ++j) {
1077 const double bnorm = std::sqrt(dot(w, w));
1078 if (!(bnorm > 1e-12) || k + 1 == steps) {
1081 beta.push_back(bnorm);
1082 scale(w, 1.0 / bnorm);
1085 const long dim =
static_cast<long>(alpha.size());
1086 MatrixXd t = MatrixXd::Zero(dim, dim);
1087 for (
long i = 0; i < dim; ++i) {
1088 t(i, i) = alpha[
static_cast<size_t>(i)];
1090 t(i, i + 1) = t(i + 1, i) = beta[
static_cast<size_t>(i)];
1093 const Eigen::SelfAdjointEigenSolver<MatrixXd> es(t);
1094 const VectorXd y = es.eigenvectors().col(0);
1095 mode.assign(n, VectorXd::Zero(x[0].size()));
1096 for (
long i = 0; i < dim; ++i) {
1097 for (
size_t j = 0; j < n; ++j) {
1098 mode[j] += y(i) * basis[
static_cast<size_t>(i)][j];
1102 scale(mode, 1.0 / std::sqrt(dot(mode, mode)));
1103 return es.eigenvalues()(0);
1109double turningDistance(
const VectorXd &saddle,
const VectorXd &dir,
1110 double vSaddle,
double drop,
double sign,
double h,
1112 std::vector<VectorXd> pts;
1113 for (
long k = 1; k <= maxPoints; ++k) {
1114 pts.push_back(saddle + sign * h *
static_cast<double>(k) * dir);
1116 std::vector<double> v;
1117 std::vector<VectorXd> g;
1118 potential(pts, v, g);
1119 double prevV = vSaddle, prevS = 0.0, best = 0.0, bestV = vSaddle;
1120 for (
long k = 0; k < maxPoints; ++k) {
1121 const double sk = h *
static_cast<double>(k + 1);
1122 const double vk = v[
static_cast<size_t>(k)];
1123 if (vk <= vSaddle - drop) {
1124 const double t = (prevV - (vSaddle - drop)) / (prevV - vk);
1125 return prevS + t * (sk - prevS);
1131 if (vk > prevV + 1e-12 && k > 0) {
1140double sideDrop(
const VectorXd &saddle,
const VectorXd &dir,
double vSaddle,
1141 double sign,
double h,
long maxPoints,
1143 std::vector<VectorXd> pts;
1144 for (
long k = 1; k <= maxPoints; ++k) {
1145 pts.push_back(saddle + sign * h *
static_cast<double>(k) * dir);
1147 std::vector<double> v;
1148 std::vector<VectorXd> g;
1149 potential(pts, v, g);
1150 double lowest = vSaddle;
1151 for (
long k = 0; k < maxPoints; ++k) {
1152 const double vk = v[
static_cast<size_t>(k)];
1153 if (vk > lowest + 1e-12 && k > 0 && lowest < vSaddle) {
1154 return vSaddle - lowest;
1156 lowest = std::min(lowest, vk);
1158 return vSaddle - lowest;
1163 double residual = 0.0;
1164 std::vector<VectorXd> vector;
1169std::vector<RingMode> lowestRingModes(
1170 const std::function<std::vector<VectorXd>(
const std::vector<VectorXd> &)>
1172 std::vector<VectorXd> start,
long steps) {
1173 const size_t n = start.size();
1174 const long f = start.empty() ? 0 : start.front().size();
1175 const double n0 = std::sqrt(dot(start, start));
1176 if (!(n0 > 0.0) || f < 1) {
1177 throw std::runtime_error(
"instantonRate: Lanczos was given a zero vector");
1179 scale(start, 1.0 / n0);
1180 std::vector<std::vector<VectorXd>> basis;
1181 std::vector<double> alpha;
1182 std::vector<double> beta;
1183 std::vector<VectorXd> q = std::move(start);
1184 for (
long k = 0; k < steps; ++k) {
1186 std::vector<VectorXd> w =
apply(q);
1187 alpha.push_back(dot(w, q));
1188 for (
int pass = 0; pass < 2; ++pass) {
1189 for (
const auto &b : basis) {
1190 const double p = dot(w, b);
1191 for (
size_t j = 0; j < n; ++j) {
1196 const double bnorm = std::sqrt(dot(w, w));
1197 if (!(bnorm > 1e-14) || k + 1 == steps) {
1200 beta.push_back(bnorm);
1201 scale(w, 1.0 / bnorm);
1204 const long m =
static_cast<long>(alpha.size());
1205 MatrixXd tridiag = MatrixXd::Zero(m, m);
1206 for (
long i = 0; i < m; ++i) {
1207 tridiag(i, i) = alpha[
static_cast<size_t>(i)];
1209 tridiag(i, i + 1) = tridiag(i + 1, i) = beta[
static_cast<size_t>(i)];
1212 const Eigen::SelfAdjointEigenSolver<MatrixXd> es(tridiag);
1213 std::vector<RingMode> modes;
1214 modes.reserve(
static_cast<size_t>(m));
1215 for (
long i = 0; i < m; ++i) {
1217 mode.theta = es.eigenvalues()(i);
1218 mode.vector.assign(n, VectorXd::Zero(f));
1219 const VectorXd y = es.eigenvectors().col(i);
1220 for (
long s = 0; s < m; ++s) {
1221 for (
size_t j = 0; j < n; ++j) {
1222 mode.vector[j] += y(s) * basis[
static_cast<size_t>(s)][j];
1225 scale(mode.vector, 1.0 / std::sqrt(dot(mode.vector, mode.vector)));
1226 const std::vector<VectorXd> applied =
apply(mode.vector);
1227 double residual = 0.0;
1228 for (
size_t j = 0; j < n; ++j) {
1229 residual += (applied[j] - mode.theta * mode.vector[j]).squaredNorm();
1231 mode.residual = std::sqrt(residual);
1232 modes.push_back(std::move(mode));
1241struct CyclicFactor {
1243 Eigen::PartialPivLU<ColMajorXd> cornerLu;
1250 CyclicFactor(
double cIn,
const std::vector<MatrixXd> &diag)
1253 f(diag.front().rows()),
1254 n(static_cast<long>(diag.size())),
1257 const MatrixXd eye = MatrixXd::Identity(f, f);
1258 const MatrixXd zero = MatrixXd::Zero(f, f);
1259 std::vector<MatrixXd> rhs(
static_cast<size_t>(n), zero);
1261 const std::vector<MatrixXd> fromFirst = open.solve(rhs);
1264 const std::vector<MatrixXd> fromLast = open.solve(rhs);
1266 corner.topLeftCorner(f, f) = fromFirst.front();
1267 corner.bottomLeftCorner(f, f) = fromFirst.back();
1268 corner.topRightCorner(f, f) = fromLast.front();
1269 corner.bottomRightCorner(f, f) = fromLast.back();
1270 corner.topRightCorner(f, f) -= eye / c;
1271 corner.bottomLeftCorner(f, f) -= eye / c;
1272 cornerLu.compute(ColMajorXd(corner));
1273 logAbs = open.logAbsDet() + 2.0 *
static_cast<double>(f) * std::log(c);
1274 const MatrixXd &upper = cornerLu.matrixLU();
1275 for (
long i = 0; i < upper.rows(); ++i) {
1276 const double pivot = upper(i, i);
1279 logAbs = -std::numeric_limits<double>::infinity();
1282 logAbs += std::log(std::abs(pivot));
1286 std::vector<VectorXd> solve(
const std::vector<VectorXd> &rhs)
const {
1287 if (singular ||
static_cast<long>(rhs.size()) != n) {
1288 throw std::runtime_error(
"cyclic ring: singular");
1290 std::vector<VectorXd> y = open.solve(rhs);
1292 g.head(f) = y.front();
1293 g.tail(f) = y.back();
1294 const VectorXd z = cornerLu.solve(g);
1295 std::vector<VectorXd> bump(
static_cast<size_t>(n), VectorXd::Zero(f));
1296 bump.front() = z.head(f);
1297 bump.back() = z.tail(f);
1298 const std::vector<VectorXd> corr = open.solve(bump);
1299 for (
long j = 0; j < n; ++j) {
1300 y[
static_cast<size_t>(j)] -= corr[
static_cast<size_t>(j)];
1310class HaynsworthChain {
1312 HaynsworthChain(
double c,
const std::vector<MatrixXd> &diag,
bool spectrum)
1314 lu_.reserve(diag.size());
1315 for (
size_t k = 0; k < diag.size(); ++k) {
1316 const long f = diag[k].rows();
1317 MatrixXd d = 0.5 * (diag[k] + diag[k].transpose());
1319 const MatrixXd inv = lu_.back().inverse();
1320 d -= c * c * 0.5 * (inv + inv.transpose());
1323 const ColMajorXd
sym = d;
1324 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es(
1325 sym, Eigen::EigenvaluesOnly);
1326 for (
long i = 0; i < f; ++i) {
1327 const double lam = es.eigenvalues()(i);
1329 throw std::runtime_error(
"instanton: singular chain Hessian block");
1334 logAbsDet_ += std::log(std::abs(lam));
1337 lu_.emplace_back(ColMajorXd(d));
1340 double logAbsDet()
const {
return logAbsDet_; }
1341 long negative()
const {
return negative_; }
1342 std::vector<VectorXd> solve(
const std::vector<VectorXd> &b)
const {
1343 const size_t m = b.size();
1344 std::vector<VectorXd> y(m), x(m);
1346 for (
size_t k = 1; k < m; ++k) {
1347 y[k] = b[k] + c_ * lu_[k - 1].solve(y[k - 1]);
1349 x[m - 1] = lu_[m - 1].solve(y[m - 1]);
1350 for (
size_t k = m - 1; k-- > 0;) {
1351 x[k] = lu_[k].solve(y[k] + c_ * x[k + 1]);
1358 std::vector<Eigen::PartialPivLU<ColMajorXd>> lu_;
1359 double logAbsDet_ = 0.0;
1370 WoodburyRing(
double c,
const std::vector<MatrixXd> &diag,
bool closed,
1371 const std::vector<std::vector<VectorXd>> &extras,
1372 const std::vector<double> &kappas,
bool spectrum)
1374 n_(static_cast<long>(diag.size())),
1375 f_(diag.front().rows()),
1377 chain_(c, diag, spectrum),
1379 const long base = closed ? 2 * f_ : 0;
1380 const long m = base +
static_cast<long>(extras.size());
1381 kinv_ = MatrixXd::Zero(m, m);
1384 kinv_.block(0, f_, f_, f_) = -MatrixXd::Identity(f_, f_) / c;
1385 kinv_.block(f_, 0, f_, f_) = -MatrixXd::Identity(f_, f_) / c;
1386 k.block(0, f_, f_, f_) = -c * MatrixXd::Identity(f_, f_);
1387 k.block(f_, 0, f_, f_) = -c * MatrixXd::Identity(f_, f_);
1389 for (
size_t i = 0; i < extras.size(); ++i) {
1390 const long r = base +
static_cast<long>(i);
1391 kinv_(r, r) = 1.0 / kappas[i];
1392 k(r, r) = kappas[i];
1396 logAbsDet_ = chain_.logAbsDet();
1397 negative_ = chain_.negative();
1400 gtg_ = MatrixXd::Zero(m, m);
1401 for (
long col = 0; col < m; ++col) {
1402 gtg_.col(col) = pieces(chain_.solve(column(col)));
1404 woodbury_.compute(ColMajorXd(kinv_ + gtg_));
1405 ok_ = gtg_.array().isFinite().all();
1407 const Eigen::PartialPivLU<ColMajorXd> lu(
1408 ColMajorXd(MatrixXd::Identity(m, m) + k * gtg_));
1409 double logDet = 0.0;
1410 for (
long i = 0; i < m; ++i) {
1411 const double u = lu.matrixLU()(i, i);
1414 logAbsDet_ = -std::numeric_limits<double>::infinity();
1417 logDet += std::log(std::abs(u));
1419 logAbsDet_ = chain_.logAbsDet() + logDet;
1421 const ColMajorXd sMat = -kinv_ - gtg_;
1422 const ColMajorXd sSym = 0.5 * (sMat + sMat.transpose());
1423 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es(
1424 sSym, Eigen::EigenvaluesOnly);
1425 const ColMajorXd kn = -kinv_;
1426 const Eigen::SelfAdjointEigenSolver<ColMajorXd> ek(
1427 kn, Eigen::EigenvaluesOnly);
1428 negative_ = chain_.negative() + (es.eigenvalues().array() < 0.0).count() -
1429 (ek.eigenvalues().array() < 0.0).count();
1432 bool ok()
const {
return ok_; }
1433 std::vector<VectorXd> solve(
const std::vector<VectorXd> &b)
const {
1434 std::vector<VectorXd> tb = chain_.solve(b);
1435 if (kinv_.rows() == 0) {
1438 const VectorXd y = woodbury_.solve(pieces(tb));
1439 const std::vector<VectorXd> gy = chain_.solve(expand(y));
1440 for (
size_t j = 0; j < tb.size(); ++j) {
1445 double logAbsDet()
const {
return logAbsDet_; }
1446 long negative()
const {
return negative_; }
1449 std::vector<VectorXd> column(
long col)
const {
1450 const long base = closed_ ? 2 * f_ : 0;
1452 std::vector<VectorXd> g(
static_cast<size_t>(n_), VectorXd::Zero(f_));
1453 g[
static_cast<size_t>(col < f_ ? 0 : n_ - 1)](col % f_) = 1.0;
1456 return extras_[
static_cast<size_t>(col - base)];
1458 VectorXd pieces(
const std::vector<VectorXd> &x)
const {
1459 const long base = closed_ ? 2 * f_ : 0;
1460 VectorXd out(base +
static_cast<long>(extras_.size()));
1462 out.head(f_) = x[0];
1463 out.segment(f_, f_) = x[
static_cast<size_t>(n_ - 1)];
1465 for (
size_t i = 0; i < extras_.size(); ++i) {
1466 out(base +
static_cast<long>(i)) = dot(extras_[i], x);
1470 std::vector<VectorXd> expand(
const VectorXd &y)
const {
1471 const long base = closed_ ? 2 * f_ : 0;
1472 std::vector<VectorXd> g(
static_cast<size_t>(n_), VectorXd::Zero(f_));
1475 g[
static_cast<size_t>(n_ - 1)] += y.segment(f_, f_);
1477 for (
size_t i = 0; i < extras_.size(); ++i) {
1478 const double w = y(base +
static_cast<long>(i));
1479 for (
long j = 0; j < n_; ++j) {
1480 g[
static_cast<size_t>(j)] += w * extras_[i][
static_cast<size_t>(j)];
1489 HaynsworthChain chain_;
1490 std::vector<std::vector<VectorXd>> extras_;
1492 Eigen::PartialPivLU<ColMajorXd> woodbury_;
1494 double logAbsDet_ = 0.0;
1500std::vector<MatrixXd> ringDiagonal(
const std::vector<MatrixXd> &physical,
1501 double c,
bool half) {
1502 const long beads =
static_cast<long>(physical.size());
1503 const long f = physical.front().rows();
1504 std::vector<MatrixXd> diag(
static_cast<size_t>(beads));
1505 for (
long j = 0; j < beads; ++j) {
1506 const bool end = half && (j == 0 || j + 1 == beads);
1507 MatrixXd block = 0.5 * (physical[
static_cast<size_t>(j)] +
1508 physical[
static_cast<size_t>(j)].transpose());
1512 block += (end ? c : 2.0 * c) * MatrixXd::Identity(f, f);
1513 diag[
static_cast<size_t>(j)] = block;
1519std::vector<VectorXd> applyDiagonal(
const std::vector<MatrixXd> &diag,
double c,
1521 const std::vector<VectorXd> &v) {
1522 const size_t n = v.size();
1523 std::vector<VectorXd> out(n);
1524 for (
size_t j = 0; j < n; ++j) {
1525 out[j] = diag[j] * v[j];
1526 if (j > 0 || closed) {
1527 out[j] -= c * v[(j + n - 1) % n];
1529 if (j + 1 < n || closed) {
1530 out[j] -= c * v[(j + 1) % n];
1540void requireCyclicBlocks(
double c,
const std::vector<MatrixXd> &diag,
1542 if (!(c > 0.0) || diag.empty()) {
1543 throw std::invalid_argument(std::string(what) +
1544 ": need a positive spring constant and blocks");
1546 const long f = diag.front().rows();
1547 const long n =
static_cast<long>(diag.size());
1548 if (f < 1 || n < 2) {
1549 throw std::invalid_argument(std::string(what) +
1550 ": need at least two beads and one coordinate");
1552 for (
const auto &block : diag) {
1553 if (block.rows() != f || block.cols() != f) {
1554 throw std::invalid_argument(std::string(what) +
1555 ": the blocks differ in size");
1563 const std::vector<VectorXd> &tau) {
1564 if (beadHessians.empty() || beadHessians.size() != tau.size()) {
1565 throw std::invalid_argument(
1566 "ringSpectrum: N bead Hessians and N tau blocks");
1568 const std::vector<MatrixXd> diag = ringDiagonal(beadHessians, c,
false);
1569 const WoodburyRing ring(c, diag,
true, {tau}, {1.0},
true);
1571 throw std::runtime_error(
"ringSpectrum: singular ring");
1576 out.
zeroEigenvalue = dot(tau, applyDiagonal(diag, c,
true, tau));
1581 requireCyclicBlocks(c, diag,
"cyclicRingLogAbsDet");
1582 return CyclicFactor(c, diag).logAbs;
1586 const std::vector<MatrixXd> &diag,
1587 const std::vector<VectorXd> &rhs) {
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");
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");
1599 return CyclicFactor(c, diag).solve(rhs);
1607 const long f = q.size();
1608 std::vector<VectorXd> pts;
1609 pts.reserve(
static_cast<size_t>(2 * f));
1610 for (
long a = 0; a < f; ++a) {
1615 pts.push_back(std::move(qp));
1616 pts.push_back(std::move(qm));
1618 std::vector<double> v;
1619 std::vector<VectorXd> g;
1620 potential(pts, v, g);
1621 if (
static_cast<long>(g.size()) != 2 * f) {
1622 throw std::runtime_error(
1623 "rate instanton: the Hessian sample returned the wrong count");
1626 for (
long a = 0; a < f; ++a) {
1628 (g[
static_cast<size_t>(2 * a)] - g[
static_cast<size_t>(2 * a + 1)]) /
1631 return (0.5 * (h + h.transpose())).eval();
1635void bofillUpdate(
MatrixXd &h,
const VectorXd &dq,
const VectorXd &dg) {
1636 const double dq2 = dq.squaredNorm();
1637 if (!(dq2 > 1e-24)) {
1640 const VectorXd r = dg - h * dq;
1641 const double rq = r.dot(dq);
1642 const double r2 = r.squaredNorm();
1643 const double phi = r2 * dq2 > 1e-30 ? (rq * rq) / (r2 * dq2) : 0.0;
1644 const double inv = 1.0 / dq2;
1645 h.noalias() += (1.0 - phi) * inv *
1646 (r * dq.transpose() + dq * r.transpose() -
1647 (rq * inv) * (dq * dq.transpose()));
1648 if (std::abs(rq) > 1e-12 * std::sqrt(std::max(0.0, r2 * dq2))) {
1649 h.noalias() += (phi / rq) * (r * r.transpose());
1651 h = (0.5 * (h + h.transpose())).eval();
1654VectorXd packBeads(
const std::vector<VectorXd> &x) {
1655 const long f = x.front().size();
1656 VectorXd flat(
static_cast<long>(x.size()) * f);
1657 for (
long j = 0; j < static_cast<long>(x.size()); ++j) {
1658 flat.segment(j * f, f) = x[
static_cast<size_t>(j)];
1663void addPacked(std::vector<VectorXd> &x,
const VectorXd &step) {
1664 const long f = x.front().size();
1665 for (
long j = 0; j < static_cast<long>(x.size()); ++j) {
1666 x[
static_cast<size_t>(j)] += step.segment(j * f, f);
1670double packedBeadNorm(
const VectorXd &step,
long f) {
1672 const long n = step.size() / f;
1673 for (
long j = 0; j < n; ++j) {
1674 big = std::max(big, step.segment(j * f, f).norm());
1679bool finiteBeads(
const std::vector<VectorXd> &x) {
1680 for (
const auto &q : x) {
1681 if (!q.array().isFinite().all()) {
1688template <
typename Derived>
1689bool overlapsTau(
const Eigen::MatrixBase<Derived> &mode,
const VectorXd &tau) {
1690 return tau.size() == mode.size() && std::abs(mode.dot(tau)) > 0.5;
1694VectorXd timeTranslation(
const std::vector<VectorXd> &x) {
1695 const long n =
static_cast<long>(x.size());
1696 const long f = x.front().size();
1697 VectorXd tau(n * f);
1698 for (
long j = 0; j < n; ++j) {
1699 const size_t prev =
static_cast<size_t>((j + n - 1) % n);
1700 const size_t next =
static_cast<size_t>((j + 1) % n);
1701 tau.segment(j * f, f) = 0.5 * (x[next] - x[prev]);
1703 const double nrm = tau.norm();
1711std::vector<VectorXd> physicalGradient(
const std::vector<VectorXd> &x,
1712 const std::vector<VectorXd> &ringGrad,
1714 const size_t n = x.size();
1715 std::vector<VectorXd> g(n);
1716 for (
size_t j = 0; j < n; ++j) {
1717 const size_t prev = (j + n - 1) % n;
1718 const size_t next = (j + 1) % n;
1719 g[j] = ringGrad[j] - c * (2.0 * x[j] - x[prev] - x[next]);
1726 double curvature = 0.0;
1732std::vector<VectorXd> cosineSeed(
const VectorXd &saddle,
const VectorXd &dir,
1733 double lambda0,
double temperature,
1734 double crossover,
long nBeads,
1736 std::vector<double> v0;
1737 std::vector<VectorXd> g0;
1738 potential({saddle}, v0, g0);
1739 const double vS = v0.at(0);
1740 const double h = 0.25 * std::sqrt(2.0 *
kBoltzmann * crossover / -lambda0);
1741 const long pts = 200;
1742 const double dPlus = sideDrop(saddle, dir, vS, 1.0, h, pts, potential);
1743 const double dMinus = sideDrop(saddle, dir, vS, -1.0, h, pts, potential);
1744 const double dMin = std::min(dPlus, dMinus);
1745 const double drop = (1.0 - temperature / crossover) *
1746 (dMin > 0.0 ? dMin :
kBoltzmann * crossover);
1747 const double sPlus =
1748 turningDistance(saddle, dir, vS, drop, 1.0, h, pts, potential);
1749 const double sMinus =
1750 turningDistance(saddle, dir, vS, drop, -1.0, h, pts, potential);
1751 std::vector<VectorXd> guess(
static_cast<size_t>(nBeads));
1752 for (
long j = 0; j < nBeads; ++j) {
1753 const double ct = std::cos(2.0 * std::numbers::pi *
static_cast<double>(j) /
1754 static_cast<double>(nBeads));
1755 guess[
static_cast<size_t>(j)] =
1756 saddle + dir * (ct >= 0.0 ? sPlus * ct : sMinus * ct);
1767VectorXd chainIndexOneStep(
const std::vector<RingMode> &ritz,
1769 const std::vector<MatrixXd> &diag,
double spring,
1770 bool closed,
const std::vector<VectorXd> &grad,
1771 const VectorXd &tau,
1772 const std::vector<std::vector<VectorXd>> &nullRing) {
1773 if (climb.index < 0 || ritz.empty() || grad.empty()) {
1776 const long f = grad.front().size();
1777 const long dim =
static_cast<long>(grad.size()) * f;
1778 const double cut = -1e-8 * std::max(1.0, spring);
1779 const double tiny = 1e-8 * std::max(1.0, spring);
1780 const double parked = std::max(1.0, spring);
1781 std::vector<std::vector<VectorXd>> extras;
1782 std::vector<double> kappas;
1783 std::vector<VectorXd> tauRing;
1784 if (tau.size() == dim) {
1785 tauRing.assign(grad.size(), VectorXd::Zero(f));
1786 for (
size_t j = 0; j < grad.size(); ++j) {
1787 tauRing[j] = tau.segment(
static_cast<long>(j) * f, f);
1792 double cycleCurvature = 0.0;
1793 for (
const auto &m : ritz) {
1794 if (std::abs(dot(m.vector, tauRing)) > 0.5) {
1795 cycleCurvature = m.theta;
1799 if (std::abs(cycleCurvature) <= 1e-6 * std::max(1.0, spring)) {
1800 extras.push_back(tauRing);
1801 kappas.push_back(spring);
1805 auto onNull = [&](
const std::vector<VectorXd> &m) {
1806 for (
const auto &r : nullRing) {
1807 if (std::abs(dot(m, r)) > 0.5) {
1813 for (
const auto &r : nullRing) {
1814 extras.push_back(r);
1815 kappas.push_back(spring);
1817 for (
size_t i = 0; i < ritz.size(); ++i) {
1818 if (!tauRing.empty() && std::abs(dot(ritz[i].vector, tauRing)) > 0.5) {
1821 if (onNull(ritz[i].vector)) {
1824 const double li = ritz[i].theta;
1825 const bool isClimb =
static_cast<long>(i) == climb.index;
1826 if ((isClimb && li > 0.0) || (!isClimb && li < cut)) {
1827 extras.push_back(ritz[i].vector);
1828 kappas.push_back(-2.0 * li);
1829 }
else if (!isClimb && std::abs(li) <= tiny) {
1830 extras.push_back(ritz[i].vector);
1831 kappas.push_back(parked - li);
1834 std::vector<VectorXd> rhs = grad;
1837 auto applyShifted = [&](
const std::vector<VectorXd> &x) {
1838 std::vector<VectorXd> out = applyDiagonal(diag, spring, closed, x);
1839 for (
size_t i = 0; i < extras.size(); ++i) {
1840 const double w = kappas[i] * dot(extras[i], x);
1841 for (
size_t j = 0; j < out.size(); ++j) {
1842 out[j] += w * extras[i][j];
1847 const double rhsNorm = std::sqrt(dot(rhs, rhs));
1848 std::vector<VectorXd> stepRing;
1849 bool solved =
false;
1851 if (dim <= kDenseRing) {
1852 throw std::runtime_error(
"small ring: dense solve");
1854 const WoodburyRing ring(spring, diag, closed, extras, kappas,
false);
1856 stepRing = ring.solve(rhs);
1860 for (
int pass = 0; pass < 4; ++pass) {
1861 std::vector<VectorXd> r = applyShifted(stepRing);
1862 for (
size_t j = 0; j < r.size(); ++j) {
1863 r[j] = rhs[j] - r[j];
1865 const double rn = std::sqrt(dot(r, r));
1866 if (!std::isfinite(rn)) {
1869 if (rn <= 1e-10 * std::max(1.0, rhsNorm)) {
1873 const std::vector<VectorXd> dx = ring.solve(r);
1874 for (
size_t j = 0; j < stepRing.size(); ++j) {
1875 stepRing[j] += dx[j];
1879 }
catch (
const std::runtime_error &) {
1882 if (!solved && dim <= kDenseRing) {
1884 const long n =
static_cast<long>(grad.size());
1885 ColMajorXd jt = ColMajorXd::Zero(dim, dim);
1886 for (
long col = 0; col < dim; ++col) {
1887 std::vector<VectorXd> e(
static_cast<size_t>(n), VectorXd::Zero(f));
1888 e[
static_cast<size_t>(col / f)](col % f) = 1.0;
1889 jt.col(col) = packBeads(applyShifted(e));
1891 const Eigen::PartialPivLU<ColMajorXd> lu(jt);
1892 const VectorXd x = lu.solve(packBeads(rhs));
1893 stepRing.assign(
static_cast<size_t>(n), VectorXd::Zero(f));
1894 for (
long j = 0; j < n; ++j) {
1895 stepRing[
static_cast<size_t>(j)] = x.segment(j * f, f);
1897 solved = x.array().isFinite().all();
1899 if (!solved && stepRing.empty()) {
1903 VectorXd step = packBeads(stepRing);
1904 for (
const auto &r : nullRing) {
1905 const VectorXd rf = packBeads(r);
1906 if (rf.size() == step.size()) {
1907 step -= step.dot(rf) * rf;
1910 if (!step.array().isFinite().all()) {
1917 std::vector<VectorXd> beads;
1918 std::vector<double> energies;
1919 double ringPotential = 0.0;
1921 long iterations = 0;
1922 bool converged =
false;
1925 bool stalledHalf =
false;
1930NewtonOut newtonInstanton(std::vector<VectorXd> guess,
double c,
1934 const long nBeads = options.beads;
1937 bool half = options.halfRing && nBeads % 2 == 0 &&
1938 static_cast<long>(guess.size()) == nBeads;
1940 const long m = nBeads / 2;
1941 for (
long j = 1; j < m; ++j) {
1942 if ((guess[
static_cast<size_t>(j)] -
1943 guess[
static_cast<size_t>(nBeads - j)])
1950 std::vector<VectorXd> x;
1952 const long m = nBeads / 2;
1953 x.resize(
static_cast<size_t>(m + 1));
1954 for (
long j = 0; j <= m; ++j) {
1955 x[
static_cast<size_t>(j)] = guess[
static_cast<size_t>(j)];
1958 x = std::move(guess);
1960 const long f = x.front().size();
1961 const MatrixXd hS = (0.5 * (hessSaddle + hessSaddle.transpose())).eval();
1964 const long nAtoms =
static_cast<long>(options.rigidSqrtMasses.size());
1965 const bool quotient = nAtoms > 0 && 3 * nAtoms == hS.rows() &&
1966 options.rigidReference.size() == 3 * nAtoms;
1967 auto ringRigid = [&](
const std::vector<VectorXd> &q) {
1968 std::vector<std::vector<VectorXd>> out;
1972 const long nb =
static_cast<long>(q.size());
1974 Eigen::Vector3d centre = Eigen::Vector3d::Zero();
1976 for (
long j = 0; j < nb; ++j) {
1977 for (
long k = 0; k < nAtoms; ++k) {
1978 const double sm = options.rigidSqrtMasses[
static_cast<size_t>(k)];
1979 const Eigen::Vector3d r =
1980 options.rigidReference.segment<3>(3 * k) +
1981 q[
static_cast<size_t>(j)].segment<3>(3 * k) / sm;
1982 centre += sm * sm * r;
1987 std::vector<int> kinds{0, 1, 2};
1988 for (
int c = 0; c < 3; ++c) {
1989 if (options.rigidRotations[
static_cast<size_t>(c)]) {
1990 kinds.push_back(3 + c);
1993 const long dim = nb * 3 * nAtoms;
1994 MatrixXd g = MatrixXd::Zero(dim,
static_cast<long>(kinds.size()));
1995 for (
size_t col = 0; col < kinds.size(); ++col) {
1996 const int kind = kinds[col];
1997 for (
long j = 0; j < nb; ++j) {
1998 for (
long k = 0; k < nAtoms; ++k) {
1999 const double sm = options.rigidSqrtMasses[
static_cast<size_t>(k)];
2000 Eigen::Vector3d d = Eigen::Vector3d::Zero();
2004 const Eigen::Vector3d r =
2005 options.rigidReference.segment<3>(3 * k) +
2006 q[
static_cast<size_t>(j)].segment<3>(3 * k) / sm;
2007 Eigen::Vector3d e = Eigen::Vector3d::Zero();
2009 d = sm * e.cross(r - centre);
2011 g.block(j * 3 * nAtoms + 3 * k,
static_cast<long>(col), 3, 1) = d;
2015 const ColMajorXd gc = g;
2016 const Eigen::ColPivHouseholderQR<ColMajorXd> qr(gc);
2017 const long rank = qr.rank();
2018 const ColMajorXd basis =
2019 qr.householderQ() * ColMajorXd::Identity(dim, rank);
2020 for (
long r = 0; r < rank; ++r) {
2021 std::vector<VectorXd> u(
static_cast<size_t>(nb));
2022 for (
long j = 0; j < nb; ++j) {
2023 u[
static_cast<size_t>(j)] =
2024 basis.col(r).segment(j * 3 * nAtoms, 3 * nAtoms);
2026 out.push_back(std::move(u));
2030 std::vector<std::vector<VectorXd>> nullRing;
2034 std::vector<MatrixXd> physical;
2035 if (f == 1 || options.initialHessians !=
"finite_difference") {
2036 physical.assign(x.size(), hS);
2038 const double eps = options.lanczosStep > 0.0 ? options.lanczosStep : 1e-4;
2039 physical.resize(x.size());
2040 for (
size_t j = 0; j < x.size(); ++j) {
2041 physical[j] = fdPhysicalHessian(x[j], potential, eps);
2046 std::vector<VectorXd> grad;
2047 std::vector<VectorXd> gradPot;
2048 std::vector<double> energies;
2051 auto objective = [&](
const std::vector<VectorXd> &q) {
2057 const long m =
static_cast<long>(q.size()) - 1;
2058 const long n = 2 * m;
2059 std::vector<double> vu;
2060 std::vector<VectorXd> gu;
2061 potential(q, vu, gu);
2062 for (
double &vj : vu) {
2063 vj -= options.energyShift;
2065 if (vu.size() != q.size() || gu.size() != q.size()) {
2066 throw std::runtime_error(
2067 "rate instanton: potential returned the wrong count");
2069 std::vector<VectorXd> full(
static_cast<size_t>(n));
2070 std::vector<double> vFull(
static_cast<size_t>(n));
2071 std::vector<VectorXd> gFull(
static_cast<size_t>(n));
2072 for (
long j = 0; j <= m; ++j) {
2073 full[
static_cast<size_t>(j)] = q[
static_cast<size_t>(j)];
2074 vFull[
static_cast<size_t>(j)] = vu[
static_cast<size_t>(j)];
2075 gFull[
static_cast<size_t>(j)] = gu[
static_cast<size_t>(j)];
2077 for (
long j = 1; j < m; ++j) {
2078 full[
static_cast<size_t>(n - j)] = q[
static_cast<size_t>(j)];
2079 vFull[
static_cast<size_t>(n - j)] = vu[
static_cast<size_t>(j)];
2080 gFull[
static_cast<size_t>(n - j)] = gu[
static_cast<size_t>(j)];
2082 std::vector<VectorXd> gRing(
static_cast<size_t>(n));
2084 double springE = 0.0;
2085 for (
long j = 0; j < n; ++j) {
2086 const long prev = (j + n - 1) % n;
2087 const long next = (j + 1) % n;
2088 gRing[
static_cast<size_t>(j)] =
2089 gFull[
static_cast<size_t>(j)] +
2090 c * (2.0 * full[
static_cast<size_t>(j)] -
2091 full[
static_cast<size_t>(prev)] -
2092 full[
static_cast<size_t>(next)]);
2094 (full[
static_cast<size_t>(next)] - full[
static_cast<size_t>(j)])
2096 uFull += vFull[
static_cast<size_t>(j)];
2098 uFull += 0.5 * c * springE;
2099 out.u = 0.5 * uFull;
2100 out.grad.resize(q.size());
2101 out.grad.front() = 0.5 * gRing.front();
2102 out.grad.back() = 0.5 * gRing[
static_cast<size_t>(m)];
2103 for (
long j = 1; j < m; ++j) {
2104 out.grad[
static_cast<size_t>(j)] =
2106 (gRing[
static_cast<size_t>(j)] + gRing[
static_cast<size_t>(n - j)]);
2108 out.gradPot = std::move(gu);
2109 out.energies = std::move(vu);
2111 const RingEval ev = evaluateRing(q, c, potential, options.energyShift);
2114 out.gradPot = physicalGradient(q, ev.grad, c);
2115 out.energies = ev.v;
2122 std::vector<RingMode> ritz;
2123 std::vector<MatrixXd> diag;
2130 auto closedGmax = [&](
const Obj &ev) {
2131 if (!half || ev.grad.size() < 2) {
2132 return largestBeadNorm(ev.grad);
2134 double big = 2.0 * ev.grad.front().norm();
2135 big = std::max(big, 2.0 * ev.grad.back().norm());
2136 const long last =
static_cast<long>(ev.grad.size()) - 1;
2137 for (
long j = 1; j < last; ++j) {
2138 big = std::max(big, ev.grad[
static_cast<size_t>(j)].norm());
2144 std::vector<VectorXd> ritzStart(x.size(), VectorXd::Zero(f));
2146 const ColMajorXd hs0 = hS;
2147 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es0(hs0);
2148 for (
auto &v : ritzStart) {
2149 v = es0.eigenvectors().col(0);
2155 std::vector<VectorXd> track = ritzStart;
2157 const double n0 = std::sqrt(dot(track, track));
2159 scale(track, 1.0 / n0);
2162 auto viewOf = [&](
const Obj &ev) {
2163 nullRing = ringRigid(x);
2165 v.tau = half ? VectorXd() : timeTranslation(x);
2166 v.diag = ringDiagonal(physical, c, half);
2167 auto apply = [&](
const std::vector<VectorXd> &vec) {
2168 return applyDiagonal(v.diag, c, !half, vec);
2170 const long dim =
static_cast<long>(x.size()) * f;
2171 if (dim <= kDenseRing) {
2174 const long nb =
static_cast<long>(x.size());
2175 ColMajorXd big = ColMajorXd::Zero(dim, dim);
2176 for (
long col = 0; col < dim; ++col) {
2177 std::vector<VectorXd> e(
static_cast<size_t>(nb), VectorXd::Zero(f));
2178 e[
static_cast<size_t>(col / f)](col % f) = 1.0;
2179 big.col(col) = packBeads(
apply(e));
2181 const ColMajorXd
sym = 0.5 * (big + big.transpose());
2182 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es(sym);
2184 for (
long i = 0; i < dim; ++i) {
2186 m.theta = es.eigenvalues()(i);
2188 m.vector.assign(
static_cast<size_t>(nb), VectorXd::Zero(f));
2189 for (
long j = 0; j < nb; ++j) {
2190 m.vector[
static_cast<size_t>(j)] =
2191 es.eigenvectors().col(i).segment(j * f, f);
2193 v.ritz.push_back(std::move(m));
2200 std::vector<std::vector<VectorXd>> lift;
2201 std::vector<double> kap;
2202 if (v.tau.size() == dim) {
2203 std::vector<VectorXd> tauRing(x.size(), VectorXd::Zero(f));
2204 for (
size_t j = 0; j < x.size(); ++j) {
2205 tauRing[j] = v.tau.segment(
static_cast<long>(j) * f, f);
2207 lift.push_back(std::move(tauRing));
2212 WoodburyRing(c, v.diag, !half, lift, kap,
true).negative();
2213 }
catch (
const std::runtime_error &) {
2220 const double resolvedTol = 1e-8 * std::max(1.0, 4.0 * c);
2221 auto resolvedNegatives = [&](
const std::vector<RingMode> &modes) {
2223 for (
const auto &m : modes) {
2224 if (m.theta < 0.0 && m.residual <= resolvedTol) {
2230 long steps = std::min(dim, std::max(60L, 4 * (negatives + 2)));
2235 std::vector<VectorXd> start = ritzStart;
2236 std::uint64_t h = 0x9E3779B97F4A7C15ULL;
2237 for (
auto &bead : start) {
2238 for (
long a2 = 0; a2 < bead.size(); ++a2) {
2242 bead(a2) += 1e-2 * (
static_cast<double>(h >> 11) * 0x1.0p-53 - 0.5);
2245 v.ritz = lowestRingModes(apply, std::move(start), steps);
2246 if (steps >= std::min(dim, kRitzCap) ||
2247 resolvedNegatives(v.ritz) >= negatives) {
2250 steps = std::min(std::min(dim, kRitzCap), 2 * steps);
2253 v.ritz.erase(std::remove_if(v.ritz.begin(), v.ritz.end(),
2254 [](
const RingMode &m) {
2257 std::max(1.0, std::abs(m.theta));
2261 v.ok = !v.ritz.empty();
2265 const double cut = -1e-8 * std::max(1.0, c);
2267 std::vector<VectorXd> tauRing;
2268 if (v.tau.size() == dim) {
2269 tauRing.assign(x.size(), VectorXd::Zero(f));
2270 for (
size_t j = 0; j < x.size(); ++j) {
2271 tauRing[j] = v.tau.segment(
static_cast<long>(j) * f, f);
2274 auto onCycle = [&](
const std::vector<VectorXd> &m) {
2275 if (!tauRing.empty() && std::abs(dot(m, tauRing)) > 0.5) {
2278 for (
const auto &r : nullRing) {
2279 if (std::abs(dot(m, r)) > 0.5) {
2287 for (
size_t i = 0; i < v.ritz.size(); ++i) {
2288 if (onCycle(v.ritz[i].vector)) {
2292 v.ritz[i].theta < v.ritz[
static_cast<size_t>(lowest)].theta) {
2293 lowest =
static_cast<long>(i);
2295 if (v.ritz[i].theta < 0.0 && track.size() == v.ritz[i].vector.size()) {
2296 const double o = std::abs(dot(v.ritz[i].vector, track));
2299 v.climb.index =
static_cast<long>(i);
2303 if (v.climb.index < 0 || best < kTrackOverlap) {
2304 v.climb.index = lowest;
2306 if (v.climb.index >= 0) {
2307 v.climb.curvature = v.ritz[
static_cast<size_t>(v.climb.index)].theta;
2309 if (v.climb.curvature < 0.0) {
2310 for (
size_t i = 0; i < v.ritz.size(); ++i) {
2311 if (onCycle(v.ritz[i].vector)) {
2314 if (v.ritz[i].theta < cut &&
2315 v.ritz[i].theta <= 1e-3 * v.climb.curvature) {
2320 v.gmax = closedGmax(ev);
2321 if (v.climb.index >= 0) {
2322 ritzStart = v.ritz[
static_cast<size_t>(v.climb.index)].vector;
2323 if (v.climb.curvature < 0.0) {
2325 if (track.size() == ritzStart.size() && dot(track, track) > 0.0) {
2326 scale(track, 1.0 / std::sqrt(dot(track, track)));
2333 auto done = [&](
const View &v) {
2334 return v.ok && v.gmax < options.forceTolerance && v.climb.negative == 1 &&
2335 v.climb.curvature < 0.0;
2338 Obj cur = objective(x);
2339 double trust = options.maxStep;
2341 bool converged =
false;
2342 bool stalledHalf =
false;
2343 const double trustFloor = std::min(1e-4, options.maxStep);
2345 constexpr int kMaxHessianRefreshes = 3;
2347 bool exactAtX =
false;
2348 for (
long it = 0; it < options.maxIterations; ++it) {
2350 const View v = viewOf(cur);
2357 auto accept = [&](VectorXd dir) {
2358 if (dir.size() == 0 || !dir.array().isFinite().all()) {
2361 const double big = packedBeadNorm(dir, f);
2368 std::vector<VectorXd> trial = x;
2369 addPacked(trial, dir);
2370 if (!finiteBeads(trial)) {
2373 Obj next = objective(trial);
2374 if (!finiteBeads(next.grad) || !std::isfinite(next.u)) {
2377 bool ratioOk =
false;
2379 if (v.ok &&
static_cast<long>(x.size()) * f == dir.size()) {
2380 const VectorXd gflat = packBeads(cur.grad);
2381 std::vector<VectorXd> dirRing(x.size(), VectorXd::Zero(f));
2382 for (
size_t k = 0; k < x.size(); ++k) {
2383 dirRing[k] = dir.segment(
static_cast<long>(k) * f, f);
2385 const VectorXd jd = packBeads(applyDiagonal(v.diag, c, !half, dirRing));
2386 const double pred = gflat.dot(dir) + 0.5 * dir.dot(jd);
2387 ratio = std::abs(pred) > 1e-30 ? (next.u - cur.u) / pred : 1.0;
2388 ratioOk = std::isfinite(ratio) && ratio >= 0.1 && ratio <= 3.0;
2391 double magnitude = std::abs(cur.u);
2392 for (
const double vj : cur.energies) {
2393 magnitude += std::abs(vj);
2395 const double roundoff =
2396 1e3 * std::numeric_limits<double>::epsilon() * magnitude;
2397 if (!ratioOk && std::abs(pred) < roundoff &&
2398 closedGmax(next) <= closedGmax(cur)) {
2406 for (
size_t k = 0; k < x.size(); ++k) {
2407 bofillUpdate(physical[k], trial[k] - x[k],
2408 next.gradPot[k] - cur.gradPot[k]);
2410 x = std::move(trial);
2411 cur = std::move(next);
2413 if (ratioOk && ratio > 0.75 && ratio < 1.25 &&
2414 packedBeadNorm(dir, f) >= 0.99 * trust) {
2415 trust = std::min(2.0 * trust, options.maxStep);
2425 if (v.ok && v.gmax < options.forceTolerance && v.climb.negative != 1 &&
2427 const double eps = options.lanczosStep > 0.0 ? options.lanczosStep : 1e-4;
2428 for (
size_t j = 0; j < x.size(); ++j) {
2429 physical[j] = fdPhysicalHessian(x[j], potential, eps);
2434 if (v.ok && v.gmax < options.forceTolerance && v.climb.negative > 1) {
2436 for (
size_t i = 0; i < v.ritz.size(); ++i) {
2437 const auto &m = v.ritz[i];
2438 if (
static_cast<long>(i) == v.climb.index || !(m.theta < 0.0)) {
2442 if (v.tau.size() ==
static_cast<long>(x.size()) * f) {
2443 held = std::abs(packBeads(m.vector).dot(v.tau)) > 0.5;
2445 for (
const auto &r : nullRing) {
2446 held = held || std::abs(dot(m.vector, r)) > 0.5;
2449 (down < 0 || m.theta < v.ritz[
static_cast<size_t>(down)].theta)) {
2450 down =
static_cast<long>(i);
2454 trust = options.maxStep;
2455 const VectorXd mode =
2456 packBeads(v.ritz[
static_cast<size_t>(down)].vector);
2457 if (accept(mode) || accept(-mode)) {
2465 if (half && options.checkOddSector && exactAtX && v.ok &&
2466 v.gmax < options.forceTolerance && v.climb.negative != 1) {
2470 const VectorXd step = chainIndexOneStep(v.ritz, v.climb, v.diag, c, !half,
2471 cur.grad, v.tau, nullRing);
2473 VectorXd dir = step;
2475 if (dir.size() > 0) {
2476 const double big0 = packedBeadNorm(dir, f);
2478 dir *= trust / big0;
2481 for (
int bt = 0; bt < 4 && !moved; ++bt) {
2482 moved = accept(dir);
2486 trust = std::max(0.5 * trust, trustFloor);
2491 if (trust <= trustFloor && refreshes < kMaxHessianRefreshes) {
2494 options.lanczosStep > 0.0 ? options.lanczosStep : 1e-4;
2495 for (
size_t j = 0; j < x.size(); ++j) {
2496 physical[j] = fdPhysicalHessian(x[j], potential, eps);
2498 trust = options.maxStep;
2502 if (!converged && done(viewOf(cur))) {
2507 out.iterations = entries;
2508 out.converged = converged;
2509 out.stalledHalf = stalledHalf;
2511 const long m =
static_cast<long>(x.size()) - 1;
2512 const long n = 2 * m;
2513 out.beads.resize(
static_cast<size_t>(n));
2514 out.energies.assign(
static_cast<size_t>(n), 0.0);
2515 for (
long j = 0; j <= m; ++j) {
2516 out.beads[
static_cast<size_t>(j)] = x[
static_cast<size_t>(j)];
2517 out.energies[
static_cast<size_t>(j)] =
2518 cur.energies[
static_cast<size_t>(j)];
2520 for (
long j = 1; j < m; ++j) {
2521 out.beads[
static_cast<size_t>(n - j)] = x[
static_cast<size_t>(j)];
2522 out.energies[
static_cast<size_t>(n - j)] =
2523 cur.energies[
static_cast<size_t>(j)];
2525 out.ringPotential = 2.0 * cur.u;
2527 out.beads = std::move(x);
2528 out.ringPotential = cur.u;
2529 out.energies = std::move(cur.energies);
2532 for (
size_t j = 0; j < out.beads.size(); ++j) {
2533 const size_t next = (j + 1) % out.beads.size();
2534 out.bN += (out.beads[next] - out.beads[j]).squaredNorm();
2542 const MatrixXd &hessSaddle,
double beta,
2543 std::vector<VectorXd> guess,
2546 const long nBeads = options.
beads;
2547 if (nBeads < 4 || !(beta > 0.0) || hessSaddle.rows() != saddle.size()) {
2548 throw std::invalid_argument(
2549 "optimizeRateInstanton: need N >= 4, beta > 0 and a saddle Hessian "
2550 "of the saddle's dimension");
2554 inst.betaN = beta /
static_cast<double>(nBeads);
2555 inst.temperature = 1.0 / (
kBoltzmann * beta);
2557 if (!(inst.temperature < inst.crossover)) {
2558 throw std::invalid_argument(
2559 "optimizeRateInstanton: T is at or above the crossover temperature; "
2560 "the ring collapses onto the saddle and steepest descent needs the "
2561 "parabolic barrier correction, of which classical transition-state "
2562 "theory is only the one-bead limit");
2565 const bool cool =
static_cast<long>(guess.size()) != nBeads &&
2566 inst.temperature < 0.75 * inst.crossover;
2568 std::vector<double> temps;
2569 for (
double t = 0.85 * inst.crossover; t > inst.temperature * 1.05;
2573 temps.push_back(inst.temperature);
2574 std::vector<VectorXd> beads;
2577 bool targetRan =
false;
2578 const ColMajorXd hS = 0.5 * (hessSaddle + hessSaddle.transpose());
2579 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es(hS);
2580 for (
size_t s = 0; s < temps.size(); ++s) {
2581 const long remain = options.maxIterations - used;
2587 const bool target = s + 1 == temps.size();
2588 opt.checkOddSector = options.checkOddSector && target;
2589 const double betaStage = target ? beta : 1.0 / (
kBoltzmann * temps[s]);
2590 std::vector<VectorXd> stageGuess = beads;
2591 if (
static_cast<long>(stageGuess.size()) != nBeads) {
2593 cosineSeed(saddle, es.eigenvectors().col(0), es.eigenvalues()(0),
2594 temps[s], inst.crossover, nBeads, potential);
2597 std::move(stageGuess), potential, opt);
2598 used += last.iterations;
2604 last.iterations = used;
2607 last.betaN = beta /
static_cast<double>(nBeads);
2608 last.temperature = inst.temperature;
2609 last.crossover = inst.crossover;
2611 last.converged =
false;
2616 if (
static_cast<long>(guess.size()) != nBeads) {
2617 const ColMajorXd hS = 0.5 * (hessSaddle + hessSaddle.transpose());
2618 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es(hS);
2619 guess = cosineSeed(saddle, es.eigenvectors().col(0), es.eigenvalues()(0),
2620 inst.temperature, inst.crossover, nBeads, potential);
2622 const double bnh = inst.betaN *
kHbar;
2623 const double spring = 1.0 / (bnh * bnh);
2625 newtonInstanton(std::move(guess), spring, hessSaddle, options, potential);
2630 const long nGot =
static_cast<long>(got.beads.size());
2631 bool mirrored = (got.converged || got.stalledHalf) && options.halfRing &&
2632 options.checkOddSector && nGot == nBeads && nGot % 2 == 0;
2633 for (
long j = 1; mirrored && j < nGot / 2; ++j) {
2634 mirrored = (got.beads[
static_cast<size_t>(j)] -
2635 got.beads[
static_cast<size_t>(nGot - j)])
2639 const Eigen::SelfAdjointEigenSolver<MatrixXd> es0(
2640 0.5 * (hessSaddle + hessSaddle.transpose()));
2641 const double barrierCurvature = std::abs(es0.eigenvalues()(0));
2642 const RingEval here =
2643 evaluateRing(got.beads, spring, potential, options.energyShift);
2644 std::vector<VectorXd> oddMode;
2645 const double oddCurv =
2646 lowestOddMode(got.beads, here, spring, potential, oddMode,
2647 options.lanczosFirst, options.lanczosStep);
2648 if (oddCurv < -1e-3 * barrierCurvature &&
2649 oddMode.size() == got.beads.size()) {
2650 std::vector<VectorXd> kicked = got.beads;
2651 const double kick = std::sqrt(2.0 / (inst.betaN * -oddCurv));
2652 for (
size_t k = 0; k < kicked.size(); ++k) {
2653 kicked[k] += kick * oddMode[k];
2657 const long before = got.iterations;
2658 got = newtonInstanton(std::move(kicked), spring, hessSaddle, whole,
2660 got.iterations += before;
2663 inst.beads = got.beads;
2664 inst.energies = got.energies;
2665 inst.ringPotential = got.ringPotential;
2667 inst.iterations = got.iterations;
2668 inst.converged = got.converged;
2675 const MatrixXd &hessSaddle,
double beta,
2676 std::vector<VectorXd> guess,
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");
2687 inst.
betaN = beta /
static_cast<double>(N);
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");
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)])
2707 const long active = mirror ? (N / 2 + 1) : N;
2710 return optimizeRateByNewton(saddle, hessSaddle, beta, std::move(guess),
2711 potential, options);
2714 const double c = 1.0 / (bnh * bnh);
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);
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);
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);
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) {
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);
2754 const bool wantMirror = options.
halfRing && N % 2 == 0;
2755 std::vector<VectorXd> x = std::move(guess);
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() >
2770 evalPot = [&](
const std::vector<VectorXd> &q, std::vector<double> &v,
2771 std::vector<VectorXd> &g) {
2772 const long m = N / 2;
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) {
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)];
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)];
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)];
2804 auto symmetrize = [&](std::vector<VectorXd> &q) {
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]);
2819 RingEval cur = evaluateRing(x, c, evalPot, options.
energyShift);
2822 std::vector<VectorXd> mode(x.size(), dir);
2823 double curvature = lowestMode(x, cur, c, evalPot, mode, options.
lanczosFirst,
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];
2838 std::vector<VectorXd> geff = effective(cur.grad);
2840 for (
long it = 0; it < limit; ++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,
2848 if (!(oddCurv < -1e-3 * std::abs(curvature))) {
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];
2863 cur = evaluateRing(x, c, evalPot, options.
energyShift);
2864 curvature = lowestMode(x, cur, c, evalPot, mode, options.
lanczosFirst,
2867 geff = effective(cur.grad);
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];
2880 double gamma = 1.0 / (4.0 * c);
2881 if (!pairs.empty()) {
2882 gamma = dot(pairs.back().first, pairs.back().second) /
2883 dot(pairs.back().second, pairs.back().second);
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];
2893 if (!(dot(d, geff) > 0.0)) {
2896 scale(d, 1.0 / (4.0 * c));
2898 const double big = largestBeadNorm(d);
2900 scale(d, options.
maxStep / big);
2902 for (
size_t k = 0; k < x.size(); ++k) {
2903 trial[k] = x[k] - d[k];
2906 RingEval next = evaluateRing(trial, c, evalPot, options.
energyShift);
2907 const double prevCurv = curvature;
2909 curvature = lowestMode(trial, next, c, evalPot, mode, restart,
2911 std::vector<VectorXd> geffNext = effective(next.grad);
2912 if ((prevCurv < 0.0) != (curvature < 0.0)) {
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];
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) {
2927 x = std::move(trial);
2928 cur = std::move(next);
2929 geff = std::move(geffNext);
2931 if (!inst.
converged && curvature < 0.0 &&
2939 for (
size_t j = 0; j < x.size(); ++j) {
2940 inst.
bN += (x[(j + 1) % x.size()] - x[j]).squaredNorm();
2948std::vector<bool> nearestZero(
const VectorXd &lam,
long count) {
2949 std::vector<long> order(
static_cast<size_t>(lam.size()));
2950 std::iota(order.begin(), order.end(), 0L);
2951 std::sort(order.begin(), order.end(), [&](
long a,
long b) {
2952 return std::abs(lam(a)) < std::abs(lam(b));
2954 std::vector<bool> out(
static_cast<size_t>(lam.size()),
false);
2955 for (
long k = 0; k < std::min<long>(count, lam.size()); ++k) {
2956 out[
static_cast<size_t>(order[
static_cast<size_t>(k)])] =
true;
2964 const MatrixXd &hessReactant,
double vReactant,
2965 const MatrixXd &hessSaddle,
double vSaddle,
long rigidModes,
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");
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");
2977 const double c = 1.0 / (bnh * bnh);
2978 const MatrixXd eye = MatrixXd::Identity(f, f);
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");
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;
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);
3000 for (
long m = 0; m < lr.size(); ++m) {
3001 if (!rigidR[
static_cast<size_t>(m)]) {
3004 nullBasis.conservativeResize(f, nullBasis.cols() + 1);
3005 nullBasis.col(nullBasis.cols() - 1) = er.eigenvectors().col(m);
3007 if (rigidModes >= f) {
3008 throw std::runtime_error(
"instantonRate: every direction is a rigid mode");
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();
3021 if (!(cycleNorm > 0.0)) {
3022 throw std::runtime_error(
3023 "instantonRate: the beads coincide, so the ring has collapsed");
3025 scale(cycle, 1.0 / std::sqrt(cycleNorm));
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);
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");
3043 inst.
zeroEigenvalue = dot(cycle, applyDiagonal(diag, c,
true, cycle));
3047 auto applyFull = [&](
const std::vector<VectorXd> &vec) {
3048 return applyDiagonal(diag, c,
true, vec);
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) {
3059 bead(a2) += 0.1 * (
static_cast<double>(h >> 11) * 0x1.0p-53 - 0.5);
3062 const std::vector<RingMode> modes =
3063 lowestRingModes(applyFull, std::move(start), steps);
3065 for (
const auto &mode : modes) {
3067 std::abs(dot(mode.vector, cycle)) < 0.5) {
3076 for (
const auto &mode : modes) {
3078 std::abs(dot(mode.vector, cycle)) < 0.5) {
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;
3092 0.5 * std::log(inst.
bN / (2.0 * std::numbers::pi *
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");
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)]) {
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);
3121 -std::log(2.0 * std::numbers::pi *
kHbar * inst.
beta) / inst.
beta -
3124 if (hessSaddle.size() > 0) {
3126 hessReactant, hessSaddle, inst.
beta, vSaddle - vReactant, rigidModes);
3132 if (!(temperature > 0.0) || !(crossover > 0.0)) {
3133 throw std::invalid_argument(
3134 "parabolicFactor: temperature and crossover must be positive");
3136 if (!(temperature > crossover)) {
3137 throw std::invalid_argument(
3138 "parabolicFactor: T is at or below the crossover; the factor "
3142 const double phase = std::numbers::pi * crossover / temperature;
3143 const double s = std::sin(phase);
3145 throw std::invalid_argument(
3146 "parabolicFactor: the sine of the barrier phase is not positive");
3152 const MatrixXd &hessSaddle,
double beta,
3153 double barrier,
long rigidModes) {
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");
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");
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));
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)));
3186 return logRatio - std::log(2.0 * std::numbers::pi) - beta * barrier;
3190 const MatrixXd &hessSaddle,
double beta,
3191 double barrier,
long rigidModes) {
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");
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");
3212 const std::vector<bool> rigidR = nearestZero(lr, rigidModes);
3213 const std::vector<bool> rigidS = nearestZero(ls, rigidModes);
3214 const double bh = beta *
kHbar;
3216 auto logTwoSinhHalf = [](
double x) {
3217 return 0.5 * x + std::log1p(-std::exp(-x));
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)));
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))));
3230 return logRatio - std::log(2.0 * std::numbers::pi * bh) - beta * barrier;