Loading...
Searching...
No Matches
Tunneling.cpp
Go to the documentation of this file.
1/*
2** This file is part of eOn.
3**
4** SPDX-License-Identifier: BSD-3-Clause
5**
6** Copyright (c) 2010--present, eOn Development Team
7** All rights reserved.
8**
9** Repo:
10** https://github.com/TheochemUI/eOn
11**
12** The half-ring springs and the banded ring Hessian are adapted from i-PI
13** under the MIT licence.
14** i-PI Copyright (C) 2014-2015 i-PI developers
15** Algorithms implemented by Yair Litman and Mariana Rossi, 2017.
16*/
17#include "eon/Tunneling.h"
18
19#include <Eigen/Eigenvalues>
20#include <Eigen/LU>
21
22#include <algorithm>
23#include <cmath>
24#include <cstdint>
25#include <deque>
26#include <limits>
27#include <numbers>
28#include <numeric>
29#include <stdexcept>
30#include <utility>
31
32namespace eonc::tunneling {
33
34double massWeightedDistance(const Matter &a, const Matter &b) {
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}
50
51std::vector<double>
52massWeightedPath(const std::vector<std::shared_ptr<Matter>> &band) {
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}
60
61Profile::Profile(std::vector<double> s, std::vector<double> v)
62 : s_(std::move(s)),
63 v_(std::move(v)),
64 m_(s_.size(), 0.0) {
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");
69 }
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");
74 }
75 }
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;
81 if (d0 * d1 <= 0.0) {
82 m_[k] = 0.0;
83 } else {
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);
87 }
88 }
89}
90
91double Profile::operator()(double x) const {
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];
102}
103
104double wellCurvature(const Profile &p, bool leftEnd) {
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}
143
144double hbarOmega(double curvature) {
145 if (!(curvature > 0.0)) {
146 throw std::invalid_argument("a well needs a positive curvature");
147 }
148 return kHbar * std::sqrt(curvature);
149}
150
151double wkbAction(const Profile &p, double energy, int points) {
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}
163
164double Splitting::tlsEnergy() const { return std::hypot(delta, delta0); }
165
166Splitting wkbSplitting(const Profile &p, double hwReactant, double hwProduct) {
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}
183
184namespace {
185
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();
190 }
191 return s;
192}
193
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]);
202}
203
204// Crossings V(s) = e nearest the barrier top, one on each side.
205std::pair<double, double> turningPoints(const Profile &p, double sTop,
206 double energy) {
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;
211 double a = from;
212 double b = to;
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;
217 b = sk;
218 break;
219 }
220 if (k == steps) {
221 return to;
222 }
223 }
224 for (int k = 0; k < 60; ++k) {
225 const double m = 0.5 * (a + b);
226 if (p(m) > energy) {
227 a = m;
228 } else {
229 b = m;
230 }
231 }
232 return 0.5 * (a + b);
233 };
234 return {cross(sTop, s0), cross(sTop, s1)};
235}
236
237// int_{s-}^{s+} ds / sqrt(2 (V - E)). The cosine substitution keeps the
238// integrand finite at the turning points. Optional tables are the running
239// integral and the arc length at the end of each panel.
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);
246 double total = 0.0;
247 if (cumulative != nullptr) {
248 cumulative->assign(1, 0.0);
249 positions->assign(1, sMinus);
250 }
251 for (int k = 0; k < panels; ++k) {
252 const double phi =
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));
263 }
264 }
265 return total;
266}
267
268} // namespace
269
270std::vector<VectorXd> ringFromPath(const std::vector<VectorXd> &path,
271 const std::vector<double> &energies,
272 double betaHbar, long beads) {
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}
385
386double wkbLogRateAlongPath(const Profile &profile, double beta,
387 double hwReactant) {
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}
457
458Splitting bandSplitting(const std::vector<std::shared_ptr<Matter>> &band,
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);
464 }
465 const Profile p(massWeightedPath(band), std::move(v));
466 return wkbSplitting(p, hbarOmega(wellCurvature(p, true)),
467 hbarOmega(wellCurvature(p, false)));
468}
469
470namespace {
471
474constexpr long kDenseRing = 4096;
475
478// Overlap with the last climb below which the lowest mode takes over.
479constexpr double kTrackOverlap = 0.3;
480constexpr long kRitzCap = 400;
481
482using ColMajorXd =
483 Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor>;
484
485// J = c T (x) I + blockdiag(A_k), T the tridiagonal (2, -1) spring matrix;
486// the diagonal blocks hold 2c already, the off-diagonal blocks are -c I.
487// Block LU: D_1 = A_1, D_k = A_k - c^2 D_{k-1}^-1.
488class BlockChain {
489public:
490 BlockChain(double c, const std::vector<MatrixXd> &diag)
491 : c_(c) {
492 lu_.reserve(diag.size());
493 for (size_t k = 0; k < diag.size(); ++k) {
494 ColMajorXd d = diag[k];
495 if (k > 0) {
496 d -= c_ * c_ * lu_.back().inverse();
497 }
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);
503 if (u == 0.0) {
504 throw std::runtime_error("instanton: singular chain Hessian block");
505 }
506 sign_ *= u < 0.0 ? -1 : 1;
507 logAbsDet_ += std::log(std::abs(u));
508 }
509 }
510 }
511 double logAbsDet() const { return logAbsDet_; }
512 int sign() const { return sign_; }
513 // x = J^-1 b, b and x stacked by bead.
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);
517 y[0] = b[0];
518 for (size_t k = 1; k < m; ++k) {
519 y[k] = b[k] + c_ * lu_[k - 1].solve(y[k - 1]);
520 }
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]);
524 }
525 return x;
526 }
527 // Same recurrence, several right-hand sides per bead.
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);
531 y[0] = b[0];
532 for (size_t k = 1; k < m; ++k) {
533 y[k] = b[k] + c_ * lu_[k - 1].solve(y[k - 1]);
534 }
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]);
538 }
539 return x;
540 }
541
542private:
543 double c_;
544 std::vector<Eigen::PartialPivLU<ColMajorXd>> lu_;
545 double logAbsDet_ = 0.0;
546 int sign_ = 1;
547};
548
549double dot(const std::vector<VectorXd> &a, const std::vector<VectorXd> &b) {
550 double s = 0.0;
551 for (size_t k = 0; k < a.size(); ++k) {
552 s += a[k].dot(b[k]);
553 }
554 return s;
555}
556
557void scale(std::vector<VectorXd> &a, double f) {
558 for (auto &v : a) {
559 v *= f;
560 }
561}
562
563// Point at arc-length fraction f along a polyline.
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]);
573}
574
575struct ActionEval {
576 double action = 0.0;
577 std::vector<double> v; // interior beads
578 std::vector<VectorXd> grad; // dS/dq over interior beads
579 std::vector<VectorXd> potGrad; // dV/dq over interior beads
580};
581
582ActionEval evaluateAction(const std::vector<VectorXd> &interior,
583 const VectorXd &start, const VectorXd &end,
584 double vStart, double vEnd, double dtau,
585 const BatchPotential &potential) {
586 ActionEval out;
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");
591 }
592 auto bead = [&](size_t j) -> const VectorXd & {
593 return j == 0 ? start : (j == m + 1 ? end : interior[j - 1]);
594 };
595 double kinetic = 0.0;
596 for (size_t j = 0; j <= m; ++j) {
597 kinetic += (bead(j + 1) - bead(j)).squaredNorm();
598 }
599 double pot = 0.5 * (vStart + vEnd);
600 for (double vj : out.v) {
601 pot += vj;
602 }
603 out.action = 0.5 * kinetic / dtau + dtau * pot;
604 out.grad.resize(m);
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];
608 }
609 return out;
610}
611
612double largestBeadNorm(const std::vector<VectorXd> &g) {
613 double m = 0.0;
614 for (const auto &v : g) {
615 m = std::max(m, v.norm());
616 }
617 return m;
618}
619
620} // namespace
621
622double pathOmega(const MatrixXd &hessStart, const MatrixXd &hessEnd,
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));
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}
632
633Instanton optimizeInstanton(const VectorXd &start, const VectorXd &end,
634 double betaHbar, std::vector<VectorXd> guess,
635 const BatchPotential &potential,
636 const InstantonOptions &options) {
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}
780
781void instantonSplitting(Instanton &inst, const BeadHessian &hessian,
782 const MatrixXd &hessStart, const MatrixXd &hessEnd) {
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}
859
860double crossoverTemperature(const MatrixXd &hessSaddle) {
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}
870
871namespace {
872
873struct RingEval {
874 double u = 0.0; // U_N
875 std::vector<double> v; // V at each bead
876 std::vector<VectorXd> grad; // dU_N/dq at each bead
877};
878
879RingEval evaluateRing(const std::vector<VectorXd> &x, double c,
880 const BatchPotential &potential,
881 double energyShift = 0.0) {
882 RingEval out;
883 std::vector<VectorXd> gv;
884 potential(x, out.v, gv);
885 for (double &v : out.v) {
886 v -= energyShift;
887 }
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");
892 }
893 out.grad.resize(n);
894 double spring = 0.0;
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();
900 out.u += out.v[j];
901 }
902 out.u += 0.5 * c * spring;
903 return out;
904}
905
906// Lowest eigenpair of the ring Hessian by Lanczos on finite-difference
907// products, started from `start`; full reorthogonalisation.
908double lowestMode(const std::vector<VectorXd> &x, const RingEval &here,
909 double c, const BatchPotential &potential,
910 std::vector<VectorXd> &mode, long steps, double eps,
911 bool mirror = false) {
912 const size_t n = x.size();
913 // The thermal instanton retraces, so the unstable mode is even. Probes
914 // and products stay on that mirror and the potential call sees one half.
915 const bool reflect = mirror && n % 2 == 0;
916 auto snap = [&](std::vector<VectorXd> &q) {
917 if (!reflect) {
918 return;
919 }
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]);
925 q[a] = mid;
926 q[b] = mid;
927 }
928 };
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];
933 }
934 snap(xp);
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;
939 }
940 snap(out);
941 return out;
942 };
943 std::vector<std::vector<VectorXd>> basis;
944 std::vector<double> alpha, beta;
945 std::vector<VectorXd> q = mode;
946 snap(q);
947 scale(q, 1.0 / std::sqrt(dot(q, q)));
948 for (long k = 0; k < steps; ++k) {
949 basis.push_back(q);
950 std::vector<VectorXd> w = hv(q);
951 const double a = dot(w, q);
952 alpha.push_back(a);
953 for (const auto &b : basis) {
954 const double p = dot(w, b);
955 for (size_t j = 0; j < n; ++j) {
956 w[j] -= p * b[j];
957 }
958 }
959 snap(w);
960 const double bnorm = std::sqrt(dot(w, w));
961 if (!(bnorm > 1e-12) || k + 1 == steps) {
962 break;
963 }
964 beta.push_back(bnorm);
965 scale(w, 1.0 / bnorm);
966 q = std::move(w);
967 }
968 const long m = static_cast<long>(alpha.size());
969 MatrixXd t = MatrixXd::Zero(m, m);
970 for (long i = 0; i < m; ++i) {
971 t(i, i) = alpha[static_cast<size_t>(i)];
972 if (i + 1 < m) {
973 t(i, i + 1) = t(i + 1, i) = beta[static_cast<size_t>(i)];
974 }
975 }
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());
981 }
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];
985 }
986 }
987 scale(ritz, 1.0 / std::sqrt(dot(ritz, ritz)));
988 snap(ritz);
989 const double rnorm = std::sqrt(dot(ritz, ritz));
990 if (rnorm > 0.0) {
991 scale(ritz, 1.0 / rnorm);
992 }
993 mode = std::move(ritz);
994 return es.eigenvalues()(0);
995}
996
997// Lowest curvature of the ring Hessian over vectors odd under j -> N-j and
998// orthogonal to the cycle mode x[j+1] - x[j-1], at a mirror-symmetric ring
999// of even N. A search confined to the even sector cannot see these. The
1000// products move the ring off the mirror, so `potential` evaluates every bead.
1001// Returns +inf when no odd direction is left to probe.
1002double lowestOddMode(const std::vector<VectorXd> &x, const RingEval &here,
1003 double c, const BatchPotential &potential,
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];
1010 }
1011 auto odd = [&](std::vector<VectorXd> &q) {
1012 q[0].setZero();
1013 q[m].setZero();
1014 for (size_t a = 1; a < m; ++a) {
1015 const VectorXd half = 0.5 * (q[a] - q[n - a]);
1016 q[a] = half;
1017 q[n - a] = -half;
1018 }
1019 };
1020 odd(cycle);
1021 const double cnorm = std::sqrt(dot(cycle, cycle));
1022 if (cnorm > 0.0) {
1023 scale(cycle, 1.0 / cnorm);
1024 }
1025 auto project = [&](std::vector<VectorXd> &q) {
1026 odd(q);
1027 if (cnorm > 0.0) {
1028 const double p = dot(q, cycle);
1029 for (size_t j = 0; j < n; ++j) {
1030 q[j] -= p * cycle[j];
1031 }
1032 }
1033 };
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];
1038 }
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;
1043 }
1044 project(out);
1045 return out;
1046 };
1047 // Start from the bead displacements with opposite signs on the two
1048 // halves: two copies of the turning region moving against each other.
1049 VectorXd mean = VectorXd::Zero(x[0].size());
1050 for (const auto &b : x) {
1051 mean += b;
1052 }
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);
1057 }
1058 project(q);
1059 const double qnorm = std::sqrt(dot(q, q));
1060 if (!(qnorm > 1e-12)) {
1061 return std::numeric_limits<double>::infinity();
1062 }
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) {
1067 basis.push_back(q);
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) {
1073 w[j] -= p * b[j];
1074 }
1075 }
1076 project(w);
1077 const double bnorm = std::sqrt(dot(w, w));
1078 if (!(bnorm > 1e-12) || k + 1 == steps) {
1079 break;
1080 }
1081 beta.push_back(bnorm);
1082 scale(w, 1.0 / bnorm);
1083 q = std::move(w);
1084 }
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)];
1089 if (i + 1 < dim) {
1090 t(i, i + 1) = t(i + 1, i) = beta[static_cast<size_t>(i)];
1091 }
1092 }
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];
1099 }
1100 }
1101 project(mode);
1102 scale(mode, 1.0 / std::sqrt(dot(mode, mode)));
1103 return es.eigenvalues()(0);
1104}
1105
1106// Distance along +dir (sign = 1) or -dir (sign = -1) from the saddle at which
1107// V has dropped by `drop`, from a scan in steps of h; the lowest point found
1108// when V never drops that far.
1109double turningDistance(const VectorXd &saddle, const VectorXd &dir,
1110 double vSaddle, double drop, double sign, double h,
1111 long maxPoints, const BatchPotential &potential) {
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);
1115 }
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);
1126 }
1127 if (vk < bestV) {
1128 bestV = vk;
1129 best = sk;
1130 }
1131 if (vk > prevV + 1e-12 && k > 0) {
1132 break; // past the minimum on this side
1133 }
1134 prevV = vk;
1135 prevS = sk;
1136 }
1137 return best;
1138}
1139
1140double sideDrop(const VectorXd &saddle, const VectorXd &dir, double vSaddle,
1141 double sign, double h, long maxPoints,
1142 const BatchPotential &potential) {
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);
1146 }
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; // found this side's minimum
1155 }
1156 lowest = std::min(lowest, vk);
1157 }
1158 return vSaddle - lowest; // still falling: the drop to the last point
1159}
1160
1161struct RingMode {
1162 double theta = 0.0;
1163 double residual = 0.0;
1164 std::vector<VectorXd> vector;
1165};
1166
1167// Lowest Ritz pairs of a symmetric ring operator, full reorthogonalisation.
1168// `steps` at the dimension is the whole spectrum.
1169std::vector<RingMode> lowestRingModes(
1170 const std::function<std::vector<VectorXd>(const std::vector<VectorXd> &)>
1171 &apply,
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");
1178 }
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) {
1185 basis.push_back(q);
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) {
1192 w[j] -= p * b[j];
1193 }
1194 }
1195 }
1196 const double bnorm = std::sqrt(dot(w, w));
1197 if (!(bnorm > 1e-14) || k + 1 == steps) {
1198 break;
1199 }
1200 beta.push_back(bnorm);
1201 scale(w, 1.0 / bnorm);
1202 q = std::move(w);
1203 }
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)];
1208 if (i + 1 < m) {
1209 tridiag(i, i + 1) = tridiag(i + 1, i) = beta[static_cast<size_t>(i)];
1210 }
1211 }
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) {
1216 RingMode mode;
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];
1223 }
1224 }
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();
1230 }
1231 mode.residual = std::sqrt(residual);
1232 modes.push_back(std::move(mode));
1233 }
1234 return modes;
1235}
1236
1237// Factored cyclic ring Hessian: the open chain plus the corner coupling
1238// M between bead 0 and bead N-1.
1239// det(H_open + P M P^T) = det(H_open) det(M) det(M^{-1} + P^T H_open^{-1} P),
1240// and |det M| = c^{2f}. A singular ring makes the last factor zero.
1241struct CyclicFactor {
1242 BlockChain open;
1243 Eigen::PartialPivLU<ColMajorXd> cornerLu;
1244 double c;
1245 long f;
1246 long n;
1247 double logAbs;
1248 bool singular;
1249
1250 CyclicFactor(double cIn, const std::vector<MatrixXd> &diag)
1251 : open(cIn, diag),
1252 c(cIn),
1253 f(diag.front().rows()),
1254 n(static_cast<long>(diag.size())),
1255 logAbs(0.0),
1256 singular(false) {
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);
1260 rhs.front() = eye;
1261 const std::vector<MatrixXd> fromFirst = open.solve(rhs);
1262 rhs.front() = zero;
1263 rhs.back() = eye;
1264 const std::vector<MatrixXd> fromLast = open.solve(rhs);
1265 MatrixXd corner(2 * f, 2 * f);
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);
1277 if (pivot == 0.0) {
1278 singular = true;
1279 logAbs = -std::numeric_limits<double>::infinity();
1280 return;
1281 }
1282 logAbs += std::log(std::abs(pivot));
1283 }
1284 }
1285
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");
1289 }
1290 std::vector<VectorXd> y = open.solve(rhs);
1291 VectorXd g(2 * f);
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)];
1301 }
1302 return y;
1303 }
1304};
1305
1306// Block LU of an open block-tridiagonal chain with given diagonal blocks
1307// and -c I between neighbours. Solves, and the inertia and log-determinant
1308// from the Schur complements (Haynsworth: the inertia of the chain is the
1309// sum over its Schur blocks).
1310class HaynsworthChain {
1311public:
1312 HaynsworthChain(double c, const std::vector<MatrixXd> &diag, bool spectrum)
1313 : c_(c) {
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());
1318 if (k > 0) {
1319 const MatrixXd inv = lu_.back().inverse();
1320 d -= c * c * 0.5 * (inv + inv.transpose());
1321 }
1322 if (spectrum) {
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);
1328 if (lam == 0.0) {
1329 throw std::runtime_error("instanton: singular chain Hessian block");
1330 }
1331 if (lam < 0.0) {
1332 ++negative_;
1333 }
1334 logAbsDet_ += std::log(std::abs(lam));
1335 }
1336 }
1337 lu_.emplace_back(ColMajorXd(d));
1338 }
1339 }
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);
1345 y[0] = b[0];
1346 for (size_t k = 1; k < m; ++k) {
1347 y[k] = b[k] + c_ * lu_[k - 1].solve(y[k - 1]);
1348 }
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]);
1352 }
1353 return x;
1354 }
1355
1356private:
1357 double c_;
1358 std::vector<Eigen::PartialPivLU<ColMajorXd>> lu_;
1359 double logAbsDet_ = 0.0;
1360 long negative_ = 0;
1361};
1362
1363// J = T + G K G^T over the chain T: the closure blocks (-c I between the
1364// last bead and the first) when `closed`, and symmetric rank-one terms
1365// kappa_i u_i u_i^T. Woodbury gives J^{-1} b, the determinant lemma
1366// ln|det J| and Haynsworth the inertia, all O(N f^3), so the N f by N f
1367// matrix is never formed.
1368class WoodburyRing {
1369public:
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)
1373 : c_(c),
1374 n_(static_cast<long>(diag.size())),
1375 f_(diag.front().rows()),
1376 closed_(closed),
1377 chain_(c, diag, spectrum),
1378 extras_(extras) {
1379 const long base = closed ? 2 * f_ : 0;
1380 const long m = base + static_cast<long>(extras.size());
1381 kinv_ = MatrixXd::Zero(m, m);
1382 MatrixXd k = MatrixXd::Zero(m, m);
1383 if (closed) {
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_);
1388 }
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];
1393 }
1394 if (m == 0) {
1395 ok_ = true;
1396 logAbsDet_ = chain_.logAbsDet();
1397 negative_ = chain_.negative();
1398 return;
1399 }
1400 gtg_ = MatrixXd::Zero(m, m);
1401 for (long col = 0; col < m; ++col) {
1402 gtg_.col(col) = pieces(chain_.solve(column(col)));
1403 }
1404 woodbury_.compute(ColMajorXd(kinv_ + gtg_));
1405 ok_ = gtg_.array().isFinite().all();
1406 if (spectrum) {
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);
1412 if (u == 0.0) {
1413 ok_ = false;
1414 logAbsDet_ = -std::numeric_limits<double>::infinity();
1415 return;
1416 }
1417 logDet += std::log(std::abs(u));
1418 }
1419 logAbsDet_ = chain_.logAbsDet() + logDet;
1420 // neg(J) = neg(T) + neg(S) - neg(-K^{-1}), S = -K^{-1} - G^T T^{-1} G.
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();
1430 }
1431 }
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) {
1436 return tb;
1437 }
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) {
1441 tb[j] -= gy[j];
1442 }
1443 return tb;
1444 }
1445 double logAbsDet() const { return logAbsDet_; }
1446 long negative() const { return negative_; }
1447
1448private:
1449 std::vector<VectorXd> column(long col) const {
1450 const long base = closed_ ? 2 * f_ : 0;
1451 if (col < base) {
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;
1454 return g;
1455 }
1456 return extras_[static_cast<size_t>(col - base)];
1457 }
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()));
1461 if (closed_) {
1462 out.head(f_) = x[0];
1463 out.segment(f_, f_) = x[static_cast<size_t>(n_ - 1)];
1464 }
1465 for (size_t i = 0; i < extras_.size(); ++i) {
1466 out(base + static_cast<long>(i)) = dot(extras_[i], x);
1467 }
1468 return out;
1469 }
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_));
1473 if (closed_) {
1474 g[0] += y.head(f_);
1475 g[static_cast<size_t>(n_ - 1)] += y.segment(f_, f_);
1476 }
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)];
1481 }
1482 }
1483 return g;
1484 }
1485
1486 double c_;
1487 long n_, f_;
1488 bool closed_;
1489 HaynsworthChain chain_;
1490 std::vector<std::vector<VectorXd>> extras_;
1491 MatrixXd kinv_, gtg_;
1492 Eigen::PartialPivLU<ColMajorXd> woodbury_;
1493 bool ok_ = false;
1494 double logAbsDet_ = 0.0;
1495 long negative_ = 0;
1496};
1497
1498// Diagonal blocks of the closed ring (H + 2 c I) or of the half chain from
1499// one turning point to the other (end blocks H / 2 + c I).
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());
1509 if (end) {
1510 block *= 0.5;
1511 }
1512 block += (end ? c : 2.0 * c) * MatrixXd::Identity(f, f);
1513 diag[static_cast<size_t>(j)] = block;
1514 }
1515 return diag;
1516}
1517
1518// (diag - c neighbours) v for the closed ring or the open half chain.
1519std::vector<VectorXd> applyDiagonal(const std::vector<MatrixXd> &diag, double c,
1520 bool closed,
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];
1528 }
1529 if (j + 1 < n || closed) {
1530 out[j] -= c * v[(j + 1) % n];
1531 }
1532 }
1533 return out;
1534}
1535
1536} // namespace
1537
1538namespace {
1539
1540void requireCyclicBlocks(double c, const std::vector<MatrixXd> &diag,
1541 const char *what) {
1542 if (!(c > 0.0) || diag.empty()) {
1543 throw std::invalid_argument(std::string(what) +
1544 ": need a positive spring constant and blocks");
1545 }
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");
1551 }
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");
1556 }
1557 }
1558}
1559
1560} // namespace
1561
1562RingSpectrum ringSpectrum(const std::vector<MatrixXd> &beadHessians, double c,
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");
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}
1579
1580double cyclicRingLogAbsDet(double c, const std::vector<MatrixXd> &diag) {
1581 requireCyclicBlocks(c, diag, "cyclicRingLogAbsDet");
1582 return CyclicFactor(c, diag).logAbs;
1583}
1584
1585std::vector<VectorXd> cyclicRingSolve(double c,
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");
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}
1601
1602namespace {
1603
1604// Central difference of dV/dq. One batch of 2 f displaced beads.
1605MatrixXd fdPhysicalHessian(const VectorXd &q, const BatchPotential &potential,
1606 double eps) {
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) {
1611 VectorXd qp = q;
1612 VectorXd qm = q;
1613 qp[a] += eps;
1614 qm[a] -= eps;
1615 pts.push_back(std::move(qp));
1616 pts.push_back(std::move(qm));
1617 }
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");
1624 }
1625 MatrixXd h(f, f);
1626 for (long a = 0; a < f; ++a) {
1627 h.col(a) =
1628 (g[static_cast<size_t>(2 * a)] - g[static_cast<size_t>(2 * a + 1)]) /
1629 (2.0 * eps);
1630 }
1631 return (0.5 * (h + h.transpose())).eval();
1632}
1633
1634// Bofill mix of Powell's symmetric Broyden and SR1. H is d2V/dq2.
1635void bofillUpdate(MatrixXd &h, const VectorXd &dq, const VectorXd &dg) {
1636 const double dq2 = dq.squaredNorm();
1637 if (!(dq2 > 1e-24)) {
1638 return;
1639 }
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());
1650 }
1651 h = (0.5 * (h + h.transpose())).eval();
1652}
1653
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)];
1659 }
1660 return flat;
1661}
1662
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);
1667 }
1668}
1669
1670double packedBeadNorm(const VectorXd &step, long f) {
1671 double big = 0.0;
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());
1675 }
1676 return big;
1677}
1678
1679bool finiteBeads(const std::vector<VectorXd> &x) {
1680 for (const auto &q : x) {
1681 if (!q.array().isFinite().all()) {
1682 return false;
1683 }
1684 }
1685 return true;
1686}
1687
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;
1691}
1692
1693// Normalised bead velocity q_{j+1} - q_{j-1}. Empty when the beads coincide.
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]);
1702 }
1703 const double nrm = tau.norm();
1704 if (!(nrm > 0.0)) {
1705 return VectorXd();
1706 }
1707 tau /= nrm;
1708 return tau;
1709}
1710
1711std::vector<VectorXd> physicalGradient(const std::vector<VectorXd> &x,
1712 const std::vector<VectorXd> &ringGrad,
1713 double c) {
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]);
1720 }
1721 return g;
1722}
1723
1724struct Climb {
1725 long index = -1;
1726 double curvature = 0.0;
1727 long negative = 0;
1728};
1729
1730// Cosine between the turning points, opened by (1 - T / Tc) of the lower
1731// barrier. The far turning point sits on bead 0.
1732std::vector<VectorXd> cosineSeed(const VectorXd &saddle, const VectorXd &dir,
1733 double lambda0, double temperature,
1734 double crossover, long nBeads,
1735 const BatchPotential &potential) {
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);
1757 }
1758 return guess;
1759}
1760
1761// Index-1 Newton step through the block chain: a negative climb eigenvalue
1762// stays and a raw Newton step climbs it; a positive one is flipped, as is
1763// every other negative Ritz value off the cycle; a tiny one is parked at a
1764// spring-sized curvature; the cycle itself is held with a spring-sized
1765// curvature and its component removed from the step. Each flip is a
1766// rank-one term in the Woodbury correction, so the solve stays O(N f^3).
1767VectorXd chainIndexOneStep(const std::vector<RingMode> &ritz,
1768 const Climb &climb,
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()) {
1774 return VectorXd();
1775 }
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);
1788 }
1789 // On a discrete ring the time shift has a small curvature of its own,
1790 // and the stationary ring is where Newton on that curvature leads; the
1791 // lift is only for a cycle too flat to solve with.
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;
1796 break;
1797 }
1798 }
1799 if (std::abs(cycleCurvature) <= 1e-6 * std::max(1.0, spring)) {
1800 extras.push_back(tauRing);
1801 kappas.push_back(spring);
1802 }
1803 }
1804 // The rigid ring motions are lifted like the cycle.
1805 auto onNull = [&](const std::vector<VectorXd> &m) {
1806 for (const auto &r : nullRing) {
1807 if (std::abs(dot(m, r)) > 0.5) {
1808 return true;
1809 }
1810 }
1811 return false;
1812 };
1813 for (const auto &r : nullRing) {
1814 extras.push_back(r);
1815 kappas.push_back(spring);
1816 }
1817 for (size_t i = 0; i < ritz.size(); ++i) {
1818 if (!tauRing.empty() && std::abs(dot(ritz[i].vector, tauRing)) > 0.5) {
1819 continue;
1820 }
1821 if (onNull(ritz[i].vector)) {
1822 continue;
1823 }
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);
1832 }
1833 }
1834 std::vector<VectorXd> rhs = grad;
1835 scale(rhs, -1.0);
1836 // The operator the step solves with: the ring plus every rank-one term.
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];
1843 }
1844 }
1845 return out;
1846 };
1847 const double rhsNorm = std::sqrt(dot(rhs, rhs));
1848 std::vector<VectorXd> stepRing;
1849 bool solved = false;
1850 try {
1851 if (dim <= kDenseRing) {
1852 throw std::runtime_error("small ring: dense solve");
1853 }
1854 const WoodburyRing ring(spring, diag, closed, extras, kappas, false);
1855 if (ring.ok()) {
1856 stepRing = ring.solve(rhs);
1857 // Cutting the ring open can leave a Schur pivot near zero next to
1858 // the barrier, and the Woodbury solve then loses digits; iterative
1859 // refinement against the exact operator recovers them.
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];
1864 }
1865 const double rn = std::sqrt(dot(r, r));
1866 if (!std::isfinite(rn)) {
1867 break;
1868 }
1869 if (rn <= 1e-10 * std::max(1.0, rhsNorm)) {
1870 solved = true;
1871 break;
1872 }
1873 const std::vector<VectorXd> dx = ring.solve(r);
1874 for (size_t j = 0; j < stepRing.size(); ++j) {
1875 stepRing[j] += dx[j];
1876 }
1877 }
1878 }
1879 } catch (const std::runtime_error &) {
1880 solved = false;
1881 }
1882 if (!solved && dim <= kDenseRing) {
1883 // A small ring is cheap to solve densely when the chain cannot.
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));
1890 }
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);
1896 }
1897 solved = x.array().isFinite().all();
1898 }
1899 if (!solved && stepRing.empty()) {
1900 return VectorXd();
1901 }
1902 // The cycle component stays: a gradient along it has to be stepped out.
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;
1908 }
1909 }
1910 if (!step.array().isFinite().all()) {
1911 return VectorXd();
1912 }
1913 return step;
1914}
1915
1916struct NewtonOut {
1917 std::vector<VectorXd> beads;
1918 std::vector<double> energies;
1919 double ringPotential = 0.0;
1920 double bN = 0.0;
1921 long iterations = 0;
1922 bool converged = false;
1923 // Half ring, stationary, and not index 1. The odd-mode probe decides
1924 // whether the search continues on the whole ring.
1925 bool stalledHalf = false;
1926};
1927
1928// Index-1 Newton on one ring. `x` holds N beads. A half ring optimises beads
1929// 0..N/2 and mirrors them. Trust is the largest bead displacement.
1930NewtonOut newtonInstanton(std::vector<VectorXd> guess, double c,
1931 const MatrixXd &hessSaddle,
1932 const RateInstantonOptions &options,
1933 const BatchPotential &potential) {
1934 const long nBeads = options.beads;
1935 // Fold only an even ring that already matches under j -> N - j. An empty
1936 // guess is seeded into that shape before this call.
1937 bool half = options.halfRing && nBeads % 2 == 0 &&
1938 static_cast<long>(guess.size()) == nBeads;
1939 if (half) {
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)])
1944 .norm() > 1e-8) {
1945 half = false;
1946 break;
1947 }
1948 }
1949 }
1950 std::vector<VectorXd> x;
1951 if (half) {
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)];
1956 }
1957 } else {
1958 x = std::move(guess);
1959 }
1960 const long f = x.front().size();
1961 const MatrixXd hS = (0.5 * (hessSaddle + hessSaddle.transpose())).eval();
1962 // The rigid quotient: translations and free rotations of the whole ring
1963 // about its centre of mass, from the current beads, orthonormalised.
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;
1969 if (!quotient) {
1970 return out;
1971 }
1972 const long nb = static_cast<long>(q.size());
1973 // Cartesian positions and the ring's centre of mass.
1974 Eigen::Vector3d centre = Eigen::Vector3d::Zero();
1975 double total = 0.0;
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;
1983 total += sm * sm;
1984 }
1985 }
1986 centre /= total;
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);
1991 }
1992 }
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();
2001 if (kind < 3) {
2002 d(kind) = sm;
2003 } else {
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();
2008 e(kind - 3) = 1.0;
2009 d = sm * e.cross(r - centre);
2010 }
2011 g.block(j * 3 * nAtoms + 3 * k, static_cast<long>(col), 3, 1) = d;
2012 }
2013 }
2014 }
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);
2025 }
2026 out.push_back(std::move(u));
2027 }
2028 return out;
2029 };
2030 std::vector<std::vector<VectorXd>> nullRing;
2031 // A one-dimensional well is already the saddle curvature. In more
2032 // dimensions the turning points are not the saddle, so each bead starts
2033 // from its own curvature and the Bofill update carries it.
2034 std::vector<MatrixXd> physical;
2035 if (f == 1 || options.initialHessians != "finite_difference") {
2036 physical.assign(x.size(), hS);
2037 } else {
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);
2042 }
2043 }
2044
2045 struct Obj {
2046 std::vector<VectorXd> grad;
2047 std::vector<VectorXd> gradPot;
2048 std::vector<double> energies;
2049 double u = 0.0;
2050 };
2051 auto objective = [&](const std::vector<VectorXd> &q) {
2052 Obj out;
2053 if (half) {
2054 // The potential is evaluated on beads 0..N/2. The closed ring is that
2055 // chain plus its mirror, and the folded gradient is half the derivative
2056 // of the closed-ring energy.
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;
2064 }
2065 if (vu.size() != q.size() || gu.size() != q.size()) {
2066 throw std::runtime_error(
2067 "rate instanton: potential returned the wrong count");
2068 }
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)];
2076 }
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)];
2081 }
2082 std::vector<VectorXd> gRing(static_cast<size_t>(n));
2083 double uFull = 0.0;
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)]);
2093 springE +=
2094 (full[static_cast<size_t>(next)] - full[static_cast<size_t>(j)])
2095 .squaredNorm();
2096 uFull += vFull[static_cast<size_t>(j)];
2097 }
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)] =
2105 0.5 *
2106 (gRing[static_cast<size_t>(j)] + gRing[static_cast<size_t>(n - j)]);
2107 }
2108 out.gradPot = std::move(gu);
2109 out.energies = std::move(vu);
2110 } else {
2111 const RingEval ev = evaluateRing(q, c, potential, options.energyShift);
2112 out.u = ev.u;
2113 out.grad = ev.grad;
2114 out.gradPot = physicalGradient(q, ev.grad, c);
2115 out.energies = ev.v;
2116 }
2117 return out;
2118 };
2119
2120 struct View {
2121 bool ok = false;
2122 std::vector<RingMode> ritz; // lowest Ritz pairs, ascending
2123 std::vector<MatrixXd> diag; // ring blocks the step solves with
2124 VectorXd tau;
2125 Climb climb;
2126 double gmax = 0.0;
2127 };
2128 // Turning-point blocks store half the closed-ring derivative. The
2129 // residual that stops the climb is the closed-ring residual.
2130 auto closedGmax = [&](const Obj &ev) {
2131 if (!half || ev.grad.size() < 2) {
2132 return largestBeadNorm(ev.grad);
2133 }
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());
2139 }
2140 return big;
2141 };
2142 // Lowest modes of the ring Hessian from matrix-vector products alone,
2143 // started along the saddle's unstable direction on every bead.
2144 std::vector<VectorXd> ritzStart(x.size(), VectorXd::Zero(f));
2145 {
2146 const ColMajorXd hs0 = hS;
2147 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es0(hs0);
2148 for (auto &v : ritzStart) {
2149 v = es0.eigenvectors().col(0);
2150 }
2151 }
2152 // The climb follows the mode that overlaps the last one, as in dimer and
2153 // minimum-mode following: the lowest curvature can switch to another
2154 // channel, and climbing it walks the ring to a neighbouring saddle.
2155 std::vector<VectorXd> track = ritzStart;
2156 {
2157 const double n0 = std::sqrt(dot(track, track));
2158 if (n0 > 0.0) {
2159 scale(track, 1.0 / n0);
2160 }
2161 }
2162 auto viewOf = [&](const Obj &ev) {
2163 nullRing = ringRigid(x);
2164 View v;
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);
2169 };
2170 const long dim = static_cast<long>(x.size()) * f;
2171 if (dim <= kDenseRing) {
2172 // A small ring takes its whole spectrum densely: every negative
2173 // curvature in every symmetry sector, exactly.
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));
2180 }
2181 const ColMajorXd sym = 0.5 * (big + big.transpose());
2182 const Eigen::SelfAdjointEigenSolver<ColMajorXd> es(sym);
2183 v.ritz.clear();
2184 for (long i = 0; i < dim; ++i) {
2185 RingMode m;
2186 m.theta = es.eigenvalues()(i);
2187 m.residual = 0.0;
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);
2192 }
2193 v.ritz.push_back(std::move(m));
2194 }
2195 } else {
2196 // The number of negative curvatures off the cycle, exactly, from the
2197 // inertia of the same chain with the cycle lifted by c.
2198 long negatives = 0;
2199 {
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);
2206 }
2207 lift.push_back(std::move(tauRing));
2208 kap.push_back(c);
2209 }
2210 try {
2211 negatives =
2212 WoodburyRing(c, v.diag, !half, lift, kap, true).negative();
2213 } catch (const std::runtime_error &) {
2214 negatives = 0;
2215 }
2216 }
2217 // Lanczos deepens until every one of those negative curvatures is a
2218 // resolved Ritz pair; a flip on an unresolved vector corrupts the step.
2219 // The ring spectrum reaches 4 c, so residuals scale with c.
2220 const double resolvedTol = 1e-8 * std::max(1.0, 4.0 * c);
2221 auto resolvedNegatives = [&](const std::vector<RingMode> &modes) {
2222 long count = 0;
2223 for (const auto &m : modes) {
2224 if (m.theta < 0.0 && m.residual <= resolvedTol) {
2225 ++count;
2226 }
2227 }
2228 return count;
2229 };
2230 long steps = std::min(dim, std::max(60L, 4 * (negatives + 2)));
2231 for (;;) {
2232 // J commutes with the ring's mirror about any bead, so a start that
2233 // is mirror-symmetric spans no antisymmetric mode at all; a fixed
2234 // pseudo-random admixture reaches every symmetry sector.
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) {
2239 h ^= h << 13;
2240 h ^= h >> 7;
2241 h ^= h << 17;
2242 bead(a2) += 1e-2 * (static_cast<double>(h >> 11) * 0x1.0p-53 - 0.5);
2243 }
2244 }
2245 v.ritz = lowestRingModes(apply, std::move(start), steps);
2246 if (steps >= std::min(dim, kRitzCap) ||
2247 resolvedNegatives(v.ritz) >= negatives) {
2248 break;
2249 }
2250 steps = std::min(std::min(dim, kRitzCap), 2 * steps);
2251 }
2252 // Only resolved pairs enter the classification and the flips.
2253 v.ritz.erase(std::remove_if(v.ritz.begin(), v.ritz.end(),
2254 [](const RingMode &m) {
2255 return m.residual >
2256 1e-6 *
2257 std::max(1.0, std::abs(m.theta));
2258 }),
2259 v.ritz.end());
2260 }
2261 v.ok = !v.ritz.empty();
2262 if (v.ok) {
2263 // Classify from the Ritz values: the lowest mode off the cycle is
2264 // the climb; further negatives below a thousandth of it count.
2265 const double cut = -1e-8 * std::max(1.0, c);
2266 v.climb = Climb{};
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);
2272 }
2273 }
2274 auto onCycle = [&](const std::vector<VectorXd> &m) {
2275 if (!tauRing.empty() && std::abs(dot(m, tauRing)) > 0.5) {
2276 return true;
2277 }
2278 for (const auto &r : nullRing) {
2279 if (std::abs(dot(m, r)) > 0.5) {
2280 return true;
2281 }
2282 }
2283 return false;
2284 };
2285 double best = 0.0;
2286 long lowest = -1;
2287 for (size_t i = 0; i < v.ritz.size(); ++i) {
2288 if (onCycle(v.ritz[i].vector)) {
2289 continue;
2290 }
2291 if (lowest < 0 ||
2292 v.ritz[i].theta < v.ritz[static_cast<size_t>(lowest)].theta) {
2293 lowest = static_cast<long>(i);
2294 }
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));
2297 if (o > best) {
2298 best = o;
2299 v.climb.index = static_cast<long>(i);
2300 }
2301 }
2302 }
2303 if (v.climb.index < 0 || best < kTrackOverlap) {
2304 v.climb.index = lowest;
2305 }
2306 if (v.climb.index >= 0) {
2307 v.climb.curvature = v.ritz[static_cast<size_t>(v.climb.index)].theta;
2308 }
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)) {
2312 continue;
2313 }
2314 if (v.ritz[i].theta < cut &&
2315 v.ritz[i].theta <= 1e-3 * v.climb.curvature) {
2316 ++v.climb.negative;
2317 }
2318 }
2319 }
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) {
2324 track = ritzStart;
2325 if (track.size() == ritzStart.size() && dot(track, track) > 0.0) {
2326 scale(track, 1.0 / std::sqrt(dot(track, track)));
2327 }
2328 }
2329 }
2330 }
2331 return v;
2332 };
2333 auto done = [&](const View &v) {
2334 return v.ok && v.gmax < options.forceTolerance && v.climb.negative == 1 &&
2335 v.climb.curvature < 0.0;
2336 };
2337
2338 Obj cur = objective(x);
2339 double trust = options.maxStep;
2340 long entries = 0;
2341 bool converged = false;
2342 bool stalledHalf = false;
2343 const double trustFloor = std::min(1e-4, options.maxStep);
2344 // Finite-difference rebuilds of the bead blocks when the search stalls.
2345 constexpr int kMaxHessianRefreshes = 3;
2346 int refreshes = 0;
2347 bool exactAtX = false;
2348 for (long it = 0; it < options.maxIterations; ++it) {
2349 ++entries;
2350 const View v = viewOf(cur);
2351 if (done(v)) {
2352 converged = true;
2353 break;
2354 }
2355 // A shorter Newton step when the quadratic model does not match. A step
2356 // that only reduces the residual is a walk into a well.
2357 auto accept = [&](VectorXd dir) {
2358 if (dir.size() == 0 || !dir.array().isFinite().all()) {
2359 return false;
2360 }
2361 const double big = packedBeadNorm(dir, f);
2362 if (!(big > 0.0)) {
2363 return false;
2364 }
2365 if (big > trust) {
2366 dir *= trust / big;
2367 }
2368 std::vector<VectorXd> trial = x;
2369 addPacked(trial, dir);
2370 if (!finiteBeads(trial)) {
2371 return false;
2372 }
2373 Obj next = objective(trial);
2374 if (!finiteBeads(next.grad) || !std::isfinite(next.u)) {
2375 return false;
2376 }
2377 bool ratioOk = false;
2378 double ratio = 0.0;
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);
2384 }
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;
2389 // Near the saddle the predicted change sits at the round-off of
2390 // U_N and the ratio is noise; there the step stands on the residual.
2391 double magnitude = std::abs(cur.u);
2392 for (const double vj : cur.energies) {
2393 magnitude += std::abs(vj);
2394 }
2395 const double roundoff =
2396 1e3 * std::numeric_limits<double>::epsilon() * magnitude;
2397 if (!ratioOk && std::abs(pred) < roundoff &&
2398 closedGmax(next) <= closedGmax(cur)) {
2399 ratioOk = true;
2400 ratio = 1.0;
2401 }
2402 }
2403 if (!ratioOk) {
2404 return false;
2405 }
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]);
2409 }
2410 x = std::move(trial);
2411 cur = std::move(next);
2412 exactAtX = false;
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);
2416 }
2417 return true;
2418 };
2419 // A converged gradient is classified with exact bead Hessians, as
2420 // i-PI's hessian_final does: the Bofill blocks can carry negative
2421 // curvatures the surface does not have. A second negative curvature
2422 // that survives the rebuild is a higher-index stationary ring, where
2423 // the flipped Newton step vanishes; a trust-sized displacement down
2424 // that mode leaves it.
2425 if (v.ok && v.gmax < options.forceTolerance && v.climb.negative != 1 &&
2426 !exactAtX) {
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);
2430 }
2431 exactAtX = true;
2432 continue;
2433 }
2434 if (v.ok && v.gmax < options.forceTolerance && v.climb.negative > 1) {
2435 long down = -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)) {
2439 continue;
2440 }
2441 bool held = false;
2442 if (v.tau.size() == static_cast<long>(x.size()) * f) {
2443 held = std::abs(packBeads(m.vector).dot(v.tau)) > 0.5;
2444 }
2445 for (const auto &r : nullRing) {
2446 held = held || std::abs(dot(m.vector, r)) > 0.5;
2447 }
2448 if (!held &&
2449 (down < 0 || m.theta < v.ritz[static_cast<size_t>(down)].theta)) {
2450 down = static_cast<long>(i);
2451 }
2452 }
2453 if (down >= 0) {
2454 trust = options.maxStep;
2455 const VectorXd mode =
2456 packBeads(v.ritz[static_cast<size_t>(down)].vector);
2457 if (accept(mode) || accept(-mode)) {
2458 continue;
2459 }
2460 }
2461 }
2462 // A half ring that is stationary at the wrong index cannot see a mode
2463 // odd under the mirror. Leave it for the odd-mode probe instead of
2464 // spending the remaining iterations on this point.
2465 if (half && options.checkOddSector && exactAtX && v.ok &&
2466 v.gmax < options.forceTolerance && v.climb.negative != 1) {
2467 stalledHalf = true;
2468 break;
2469 }
2470 const VectorXd step = chainIndexOneStep(v.ritz, v.climb, v.diag, c, !half,
2471 cur.grad, v.tau, nullRing);
2472 bool moved = false;
2473 VectorXd dir = step;
2474 // Clip to the trust radius first, so each halving is a new trial point.
2475 if (dir.size() > 0) {
2476 const double big0 = packedBeadNorm(dir, f);
2477 if (big0 > trust) {
2478 dir *= trust / big0;
2479 }
2480 }
2481 for (int bt = 0; bt < 4 && !moved; ++bt) {
2482 moved = accept(dir);
2483 dir *= 0.5;
2484 }
2485 if (!moved) {
2486 trust = std::max(0.5 * trust, trustFloor);
2487 // At the trust floor the Bofill blocks no longer model the ring and
2488 // every step is refused; rebuild them from finite differences, 2 f
2489 // gradient calls per bead, a few times at most, and start the trust
2490 // region again.
2491 if (trust <= trustFloor && refreshes < kMaxHessianRefreshes) {
2492 ++refreshes;
2493 const double eps =
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);
2497 }
2498 trust = options.maxStep;
2499 }
2500 }
2501 }
2502 if (!converged && done(viewOf(cur))) {
2503 converged = true;
2504 }
2505
2506 NewtonOut out;
2507 out.iterations = entries;
2508 out.converged = converged;
2509 out.stalledHalf = stalledHalf;
2510 if (half) {
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)];
2519 }
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)];
2524 }
2525 out.ringPotential = 2.0 * cur.u;
2526 } else {
2527 out.beads = std::move(x);
2528 out.ringPotential = cur.u;
2529 out.energies = std::move(cur.energies);
2530 }
2531 out.bN = 0.0;
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();
2535 }
2536 return out;
2537}
2538
2539// Below 0.75 Tc an empty guess walks down from 0.85 Tc. Each stage passes a
2540// full-length ring onward, so the walk is not repeated on the way back in.
2541RateInstanton optimizeRateByNewton(const VectorXd &saddle,
2542 const MatrixXd &hessSaddle, double beta,
2543 std::vector<VectorXd> guess,
2544 const BatchPotential &potential,
2545 const RateInstantonOptions &options) {
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");
2551 }
2552 RateInstanton inst;
2553 inst.beta = beta;
2554 inst.betaN = beta / static_cast<double>(nBeads);
2555 inst.temperature = 1.0 / (kBoltzmann * beta);
2556 inst.crossover = crossoverTemperature(hessSaddle);
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");
2563 }
2564
2565 const bool cool = static_cast<long>(guess.size()) != nBeads &&
2566 inst.temperature < 0.75 * inst.crossover;
2567 if (cool) {
2568 std::vector<double> temps;
2569 for (double t = 0.85 * inst.crossover; t > inst.temperature * 1.05;
2570 t *= 0.75) {
2571 temps.push_back(t);
2572 }
2573 temps.push_back(inst.temperature);
2574 std::vector<VectorXd> beads;
2575 RateInstanton last;
2576 long used = 0;
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;
2582 if (remain <= 0) {
2583 break;
2584 }
2585 RateInstantonOptions opt = options;
2586 opt.maxIterations = remain;
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) {
2592 stageGuess =
2593 cosineSeed(saddle, es.eigenvectors().col(0), es.eigenvalues()(0),
2594 temps[s], inst.crossover, nBeads, potential);
2595 }
2596 last = optimizeRateInstanton(saddle, hessSaddle, betaStage,
2597 std::move(stageGuess), potential, opt);
2598 used += last.iterations;
2599 beads = last.beads;
2600 if (target) {
2601 targetRan = true;
2602 }
2603 }
2604 last.iterations = used;
2605 if (targetRan) {
2606 last.beta = beta;
2607 last.betaN = beta / static_cast<double>(nBeads);
2608 last.temperature = inst.temperature;
2609 last.crossover = inst.crossover;
2610 } else {
2611 last.converged = false;
2612 }
2613 return last;
2614 }
2615
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);
2621 }
2622 const double bnh = inst.betaN * kHbar;
2623 const double spring = 1.0 / (bnh * bnh);
2624 NewtonOut got =
2625 newtonInstanton(std::move(guess), spring, hessSaddle, options, potential);
2626 // A half ring cannot see modes odd under j -> N-j, so two copies of the
2627 // instanton are a stationary point it can accept or sit on. An unstable
2628 // odd mode there sends the search onto the whole ring from a kick along
2629 // that mode.
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)])
2636 .norm() <= 1e-8;
2637 }
2638 if (mirrored) {
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];
2654 }
2655 RateInstantonOptions whole = options;
2656 whole.halfRing = false;
2657 const long before = got.iterations;
2658 got = newtonInstanton(std::move(kicked), spring, hessSaddle, whole,
2659 potential);
2660 got.iterations += before;
2661 }
2662 }
2663 inst.beads = got.beads;
2664 inst.energies = got.energies;
2665 inst.ringPotential = got.ringPotential;
2666 inst.bN = got.bN;
2667 inst.iterations = got.iterations;
2668 inst.converged = got.converged;
2669 return inst;
2670}
2671
2672} // namespace
2673
2675 const MatrixXd &hessSaddle, double beta,
2676 std::vector<VectorXd> guess,
2677 const BatchPotential &potential,
2678 const RateInstantonOptions &options) {
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}
2944
2945namespace {
2946
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));
2953 });
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;
2957 }
2958 return out;
2959}
2960
2961} // namespace
2962
2964 const MatrixXd &hessReactant, double vReactant,
2965 const MatrixXd &hessSaddle, double vSaddle, long rigidModes,
2966 long denseLimit) {
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}
3130
3131double parabolicFactor(double temperature, double crossover) {
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}
3150
3151double harmonicTstLogRate(const MatrixXd &hessReactant,
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");
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}
3188
3189double quantumHarmonicTstLogRate(const MatrixXd &hessReactant,
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");
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}
3232
3233} // namespace eonc::tunneling
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
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
Energy along the path, interpolated as a monotone cubic (Fritsch and Carlson), flat at both ends beca...
Definition Tunneling.h:54
std::vector< double > m_
Definition Tunneling.h:62
std::vector< double > s_
Definition Tunneling.h:62
std::vector< double > v_
Definition Tunneling.h:62
Profile(std::vector< double > s, std::vector< double > v)
Definition Tunneling.cpp:61
const std::vector< double > & v() const
Definition Tunneling.h:59
double operator()(double x) const
Definition Tunneling.cpp:91
const std::vector< double > & s() const
Definition Tunneling.h:58
void * sym(Handle h, const char *name) noexcept
Definition DynLib.h:75
AtomMatrix apply(const AtomMatrix &diff, const Matrix3d &cell, const Matrix3d &cellInverse)
Definition Matter.h:44
constexpr double eps
Definition SafeMath.h:19
std::function< MatrixXd(long j, const VectorXd &q)> BeadHessian
The mass-weighted Hessian d2V/dq2 at interior bead j (1..P-1).
Definition Tunneling.h:157
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...
double cyclicRingLogAbsDet(double c, const std::vector< MatrixXd > &diag)
log|det| of the cyclic block-tridiagonal ring Hessian.
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.
std::vector< VectorXd > cyclicRingSolve(double c, const std::vector< MatrixXd > &diag, const std::vector< VectorXd > &rhs)
Solves that same cyclic ring Hessian.
double wkbAction(const Profile &p, double energy, int points)
(1/hbar) integral sqrt(2 (V(s) - E)) ds over the path where V > E.
double parabolicFactor(double temperature, double crossover)
(pi T_c / T) / sin(pi T_c / T).
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.
constexpr double kTimeUnitSeconds
One unit of time, sqrt(amu Angstrom^2 / eV), in seconds.
Definition Tunneling.h:224
void instantonRate(RateInstanton &inst, const RingBeadHessian &hessian, const MatrixXd &hessReactant, double vReactant, const MatrixXd &hessSaddle, double vSaddle, long rigidModes, long denseLimit)
Fills the rate from the bead Hessians, the reactant minimum's Hessian and energy, and optionally the ...
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,...
std::function< MatrixXd(long j, const VectorXd &q)> RingBeadHessian
The mass-weighted Hessian d2V/dq2 at ring bead j (0..N-1).
Definition Tunneling.h:359
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.
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).
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
Splitting wkbSplitting(const Profile &p, double hwReactant, double hwProduct)
delta0 = (hbar omega / pi) exp(-S) with the Landau and Lifshitz prefactor.
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 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 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
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 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...
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...
double wkbLogRateAlongPath(const Profile &profile, double beta, double hwReactant)
ln(k), k in 1/time, for the one-dimensional thermal rate along profile.
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
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 ...
constexpr double kBoltzmann
Boltzmann constant in eV / K, correctly rounded from the exact 1.380649e-23 J / K.
Definition Tunneling.h:41
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
double asymmetry
V(end) - V(start), eV.
Definition Tunneling.h:168
std::vector< double > energies
V at every bead, eV.
Definition Tunneling.h:161
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
bool symmetricEnough
beta |asymmetry| < 0.1: the propagator ratio reads delta0 only when the wells lie within a small frac...
Definition Tunneling.h:173
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 betaHbar
imaginary time the path spans
Definition Tunneling.h:162
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
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
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
std::vector< double > energies
V at every bead, eV.
Definition Tunneling.h:318
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
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
long negativeModes
Eigenvalues below zero, the one along tau left out.
Definition Tunneling.h:302
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 tlsEnergy() const
sqrt(delta^2 + delta0^2), eV
double hwProduct
hbar omega of the product well, eV
Definition Tunneling.h:81
double delta
product minus reactant, eV
Definition Tunneling.h:78