47constexpr double kTimeUnitFs = 10.180505717871193;
52constexpr double kRotationZero = 1e-2;
58 explicit MassWeighted(
const Matter &reference)
60 for (
long i = 0; i < reference.numberOfAtoms(); ++i) {
61 if (reference.getFixed(i)) {
64 const double m = reference.getMass(i);
66 throw std::invalid_argument(
"instanton: a free atom without a mass");
69 sqrtMass_.push_back(std::sqrt(m));
72 throw std::invalid_argument(
"instanton: every atom is fixed");
75 long dimension()
const {
return 3 *
static_cast<long>(free_.size()); }
76 VectorXi freeAtoms()
const {
77 VectorXi out(
static_cast<long>(free_.size()));
78 for (
size_t k = 0; k < free_.size(); ++k) {
79 out(
static_cast<long>(k)) =
static_cast<int>(free_[k]);
86 std::vector<double> spreadAbout(
const std::vector<VectorXd> &beads,
87 VectorXd ¢roid,
long atoms)
const {
88 centroid = VectorXd::Zero(dimension());
89 for (
const auto &q : beads) {
92 centroid /=
static_cast<double>(std::max<size_t>(1, beads.size()));
93 std::vector<double> out(
static_cast<size_t>(3 * atoms), 0.0);
94 for (
size_t k = 0; k < free_.size(); ++k) {
95 for (
int c = 0; c < 3; ++c) {
96 const long i =
static_cast<long>(3 * k) + c;
98 for (
const auto &q : beads) {
99 const double d = q(i) - centroid(i);
102 out[
static_cast<size_t>(3 * free_[k] + c)] =
104 static_cast<double>(std::max<size_t>(1, beads.size()))) /
110 VectorXd toQ(
const Matter &m)
const {
111 const AtomMatrix d = ref_.pbc(m.getPositions() - ref_.getPositions());
112 VectorXd q(dimension());
113 for (
size_t k = 0; k < free_.size(); ++k) {
114 for (
int c = 0; c < 3; ++c) {
115 q(
static_cast<long>(3 * k) + c) = sqrtMass_[k] * d(free_[k], c);
120 void place(
const VectorXd &q, Matter &m)
const {
122 for (
size_t k = 0; k < free_.size(); ++k) {
123 for (
int c = 0; c < 3; ++c) {
124 r(free_[k], c) += q(
static_cast<long>(3 * k) + c) / sqrtMass_[k];
131 MatrixXd rigidGenerators(
const Matter &m)
const {
132 const long n = dimension();
135 Eigen::RowVector3d com = Eigen::RowVector3d::Zero();
137 for (
size_t k = 0; k < free_.size(); ++k) {
138 const double w = sqrtMass_[k] * sqrtMass_[k];
139 com += w * r.row(free_[k]);
143 for (
size_t k = 0; k < free_.size(); ++k) {
144 const long i =
static_cast<long>(3 * k);
145 const Eigen::Vector3d x = (r.row(free_[k]) - com).transpose();
146 for (
int c = 0; c < 3; ++c) {
147 b(i + c, c) = sqrtMass_[k];
148 Eigen::Vector3d e = Eigen::Vector3d::Zero();
150 b.block(i, 3 + c, 3, 1) = sqrtMass_[k] * e.cross(x);
158 void markRotationZeroModes(
const MatrixXd &hess,
const MatrixXd &generators,
159 std::array<bool, 3> &keep,
160 std::array<double, 3> &residual)
const {
161 const MatrixXd h = 0.5 * (hess + hess.transpose());
162 const double hn = h.norm();
163 for (
int c = 0; c < 3; ++c) {
164 const VectorXd r = generators.col(3 + c);
165 const double rn = r.norm();
167 keep[
static_cast<size_t>(c)] =
false;
168 residual[
static_cast<size_t>(c)] = 0.0;
171 const double rel = hn > 0.0 ? (h * r).norm() / (hn * rn) : 0.0;
172 residual[
static_cast<size_t>(c)] = rel;
173 keep[
static_cast<size_t>(c)] = rel <= kRotationZero;
180 MatrixXd rigidBasis(
const Matter &m,
181 const std::array<bool, 3> &rotations)
const {
182 if (
static_cast<long>(free_.size()) != m.numberOfAtoms()) {
185 const MatrixXd g = rigidGenerators(m);
186 const long n = g.rows();
187 std::vector<int> cols{0, 1, 2};
188 for (
int c = 0; c < 3; ++c) {
189 if (rotations[
static_cast<size_t>(c)]) {
190 cols.push_back(3 + c);
193 MatrixXd b(n,
static_cast<long>(cols.size()));
194 for (
size_t k = 0; k < cols.size(); ++k) {
195 b.col(
static_cast<long>(k)) = g.col(cols[k]);
197 const Eigen::ColPivHouseholderQR<MatrixXd> qr(b);
198 const long rank = qr.rank();
202 return qr.householderQ() * MatrixXd::Identity(n, rank);
204 const std::vector<double> &sqrtMasses()
const {
return sqrtMass_; }
206 VectorXd referenceFree()
const {
208 VectorXd out(dimension());
209 for (
size_t k = 0; k < free_.size(); ++k) {
210 for (
int c = 0; c < 3; ++c) {
211 out(
static_cast<long>(3 * k) + c) = r(free_[k], c);
216 VectorXd gradient(
const AtomMatrix &forces)
const {
217 VectorXd g(dimension());
218 for (
size_t k = 0; k < free_.size(); ++k) {
219 for (
int c = 0; c < 3; ++c) {
220 g(
static_cast<long>(3 * k) + c) = -forces(free_[k], c) / sqrtMass_[k];
228 std::vector<long> free_;
229 std::vector<double> sqrtMass_;
237 const long n = ref.numberOfAtoms();
238 for (
long i = 0; i < n; ++i) {
239 if (ref.getFixed(i)) {
243 AtomMatrix d = ref.pbc(m.getPositions() - ref.getPositions());
245 for (
long i = 0; i < n; ++i) {
246 w(i) = ref.getMass(i);
248 const double total = w.sum();
249 const Eigen::RowVector3d shift = (w.transpose() * d) / total;
250 d.rowwise() -= shift;
251 if (!ref.getPeriodic()) {
252 const Eigen::RowVector3d com = (w.transpose() * ref.getPositions()) / total;
256 const Eigen::Matrix3d h = y.transpose() * w.asDiagonal() * x;
257 Eigen::JacobiSVD<Eigen::Matrix3d> svd(h, Eigen::ComputeFullU |
258 Eigen::ComputeFullV);
259 Eigen::Matrix3d fix = Eigen::Matrix3d::Identity();
260 fix(2, 2) = (svd.matrixV() * svd.matrixU().transpose()).determinant() < 0.0
263 const Eigen::Matrix3d rot = svd.matrixV() * fix * svd.matrixU().transpose();
264 d = (y * rot.transpose()) - x;
266 m.setPositions(ref.getPositions() + d);
272void writeCentroid(
const std::string &file,
const std::vector<VectorXd> &beads,
273 const MassWeighted &mw,
const Matter &reactant,
274 std::vector<io::ConMetadataValue> scalars) {
278 meta.
spreads = mw.spreadAbout(beads, centroid, reactant.numberOfAtoms());
279 mw.place(centroid, frame);
280 meta.frame_index = 0;
281 meta.write_con_forces =
false;
282 double largest = 0.0;
283 for (
const double s : meta.spreads) {
284 largest = std::max(largest, s);
286 scalars.push_back({
"beads",
static_cast<double>(beads.size())});
287 scalars.push_back({
"spread_max", largest});
288 meta.scalars = std::move(scalars);
289 if (!
io::io_ok(frame.matter2con(file,
false, &meta))) {
290 throw std::runtime_error(
"instanton: cannot write " + file);
295std::vector<VectorXd> doubledRing(
const std::vector<VectorXd> &ring) {
296 const size_t n = ring.size();
297 std::vector<VectorXd> fine(2 * n);
298 for (
size_t j = 0; j < n; ++j) {
299 fine[2 * j] = ring[j];
300 fine[2 * j + 1] = 0.5 * (ring[j] + ring[(j + 1) % n]);
316void steepestDescentPath(
const VectorXd &qSaddle,
double vSaddle,
318 const std::vector<double> &sqrtMass,
320 std::vector<VectorXd> &pathQ,
321 std::vector<double> &pathV) {
322 constexpr double cartStep = 0.01;
323 constexpr double gradTol = 1e-3;
324 constexpr long maxSteps = 4000;
325 const long n = qSaddle.size();
326 const Eigen::SelfAdjointEigenSolver<MatrixXd> es(
327 0.5 * (hSaddle + hSaddle.transpose()));
328 const VectorXd mode = es.eigenvectors().col(0);
329 const double stiff = es.eigenvalues().cwiseAbs().maxCoeff();
331 auto cartesian = [&](
const VectorXd &dq) {
333 for (
long i = 0; i < n; ++i) {
334 const size_t a =
static_cast<size_t>(i / 3);
335 const double m = a < sqrtMass.size() ? sqrtMass[a] : 1.0;
336 big = std::max(big, std::abs(dq(i)) / m);
340 auto capped = [&](VectorXd dq) {
341 const double big = cartesian(dq);
342 if (big > cartStep) {
343 dq *= cartStep / big;
347 auto at = [&](
const VectorXd &q,
double &v, VectorXd &g) {
348 std::vector<VectorXd> one{q};
349 std::vector<double> vs;
350 std::vector<VectorXd> gs;
351 evaluate(one, vs, gs);
352 if (vs.empty() || gs.empty() || !std::isfinite(vs[0]) ||
353 !gs[0].array().isFinite().all()) {
360 std::vector<std::vector<VectorXd>> sideQ(2);
361 std::vector<std::vector<double>> sideV(2);
362 for (
int side = 0; side < 2; ++side) {
363 const double big0 = cartesian(mode);
364 VectorXd q = qSaddle + (side == 0 ? -1.0 : 1.0) * (cartStep / big0) * mode;
367 if (!at(q, v, g) || !(v < vSaddle)) {
370 sideQ[side].push_back(q);
371 sideV[side].push_back(v);
372 double alpha = stiff > 0.0 ? 1.0 / stiff : 1.0;
373 for (
long k = 0; k < maxSteps && g.norm() > gradTol; ++k) {
374 bool lowered =
false;
375 for (
int halving = 0; halving < 30 && !lowered; ++halving) {
376 const VectorXd trial = q + capped(-alpha * g);
379 if (at(trial, vt, gt) && vt < v) {
392 sideQ[side].push_back(q);
393 sideV[side].push_back(v);
396 pathQ.assign(sideQ[0].rbegin(), sideQ[0].rend());
397 pathV.assign(sideV[0].rbegin(), sideV[0].rend());
398 pathQ.push_back(qSaddle);
399 pathV.push_back(vSaddle);
400 pathQ.insert(pathQ.end(), sideQ[1].begin(), sideQ[1].end());
401 pathV.insert(pathV.end(), sideV[1].begin(), sideV[1].end());
403 if (pathQ.back().norm() < pathQ.front().norm()) {
404 std::reverse(pathQ.begin(), pathQ.end());
405 std::reverse(pathV.begin(), pathV.end());
412bool straddlesSaddle(
const std::vector<VectorXd> &beads,
413 const VectorXd &qSaddle,
const MatrixXd &hSaddle) {
414 const Eigen::SelfAdjointEigenSolver<MatrixXd> es(
415 0.5 * (hSaddle + hSaddle.transpose()));
416 const VectorXd mode = es.eigenvectors().col(0);
417 double lo = std::numeric_limits<double>::infinity();
419 for (
const auto &b : beads) {
420 const double s = (b - qSaddle).dot(mode);
421 lo = std::min(lo, s);
422 hi = std::max(hi, s);
424 return lo < 0.0 && hi > 0.0;
429std::vector<std::string>
430runRate(
const Parameters ¶ms,
const std::shared_ptr<Potential> &
pot,
431 const Matter &reactant,
const MassWeighted &mw,
433 const std::function<
MatrixXd(
const VectorXd &)> &hessianAt,
434 const std::array<bool, 3> &rotationZero,
435 const std::array<double, 3> &rotationResidual) {
436 const auto &o = params.instanton_options();
437 const std::string resultsFile =
"results.dat";
438 const std::string pathFile =
"instanton.con";
439 std::vector<std::string> returnFiles{resultsFile};
442 if (!
io::io_ok(saddle.con2matter(o.saddle_filename))) {
443 throw std::runtime_error(
"instanton: cannot read " + o.saddle_filename);
445 if (saddle.numberOfAtoms() != reactant.numberOfAtoms()) {
446 throw std::runtime_error(
447 "instanton: the saddle and the reactant differ in atom count");
449 if (o.hessian_final !=
"recomputed") {
450 throw std::invalid_argument(
"instanton: hessian_final must be recomputed");
452 std::vector<double> temperatures = o.temperatures;
453 if (temperatures.empty()) {
454 temperatures.push_back(o.temperature);
456 for (
const double t : temperatures) {
458 throw std::invalid_argument(
459 "instanton: mode rate needs [Instanton] temperature or "
460 "temperatures in K");
464 std::sort(temperatures.begin(), temperatures.end(), std::greater<>());
465 alignRigid(reactant, saddle);
466 const long n = mw.dimension();
467 const VectorXd qSaddle = mw.toQ(saddle);
469 const double vSaddle = saddle.getPotentialEnergy();
470 const MatrixXd hReactant = hessianAt(VectorXd::Zero(n));
471 const MatrixXd hSaddle = hessianAt(qSaddle);
473 const long rigidModes = mw.rigidBasis(reactant, rotationZero).cols();
474 if (temperatures.size() == 1) {
475 EONC_LOG_INFO(
"[Instanton] rate: {} beads at {:.4g} K, crossover {:.4g} K, "
476 "barrier {:.6f} eV, {} degrees of freedom, {} rigid modes",
477 o.beads, temperatures.front(), tc, vSaddle - vReactant, n,
480 EONC_LOG_INFO(
"[Instanton] rate: {} beads at {} temperatures from {:.4g} K "
481 "down to {:.4g} K, crossover {:.4g} K, barrier {:.6f} eV, "
482 "{} degrees of freedom, {} rigid modes",
483 o.beads, temperatures.size(), temperatures.front(),
484 temperatures.back(), tc, vSaddle - vReactant, n, rigidModes);
486 EONC_LOG_INFO(
"[Instanton] rotation residuals {:.3g}, {:.3g}, {:.3g}",
487 rotationResidual[0], rotationResidual[1], rotationResidual[2]);
489 std::vector<std::pair<std::string, double>> extras{
490 {
"instanton_crossover_K", tc},
491 {
"barrier_classical", vSaddle - vReactant}};
494 status, params.potential_options().potential,
496 env.job_type =
"instanton";
497 env.extras.emplace_back(
"force_calls",
498 static_cast<double>(env.force_calls));
499 for (
const auto &kv : extras) {
500 env.extras.push_back(kv);
502 env.writeResultsDat(resultsFile);
507 std::vector<VectorXd> pathQ;
508 std::vector<double> pathV;
509 std::unique_ptr<tunneling::Profile> profile;
511 if (!o.initial_path.empty()) {
512 const auto frames = readcon::read_all_frames(o.initial_path);
513 bool haveEnergies =
true;
514 for (
const auto &frame : frames) {
517 throw std::runtime_error(
"instanton: cannot read " + o.initial_path);
519 if (image.numberOfAtoms() != reactant.numberOfAtoms()) {
520 throw std::runtime_error(
"instanton: " + o.initial_path +
521 " differs in atom count");
523 alignRigid(reactant, image);
524 pathQ.push_back(mw.toQ(image));
525 const auto energy = frame.energy_opt();
526 haveEnergies = haveEnergies && energy.has_value();
527 pathV.push_back(energy.value_or(0.0));
529 if (pathQ.size() < 3) {
530 throw std::runtime_error(
"instanton: " + o.initial_path +
531 " holds fewer than three frames");
534 std::vector<VectorXd> grads;
535 evaluate(pathQ, pathV, grads);
537 std::vector<double> arc(pathQ.size(), 0.0);
538 for (
size_t k = 1; k < pathQ.size(); ++k) {
539 arc[k] = arc[k - 1] + (pathQ[k] - pathQ[k - 1]).norm();
542 profile = std::make_unique<tunneling::Profile>(std::move(arc), pathV);
543 }
catch (
const std::invalid_argument &ex) {
550 }
catch (
const std::exception &ex) {
564 steepestDescentPath(qSaddle, vSaddle, hSaddle, mw.sqrtMasses(), evaluate,
566 std::vector<double> arc(pathQ.size(), 0.0);
567 for (
size_t k = 1; k < pathQ.size(); ++k) {
568 arc[k] = arc[k - 1] + (pathQ[k] - pathQ[k - 1]).norm();
571 profile = std::make_unique<tunneling::Profile>(std::move(arc), pathV);
573 EONC_LOG_INFO(
"[Instanton] steepest-descent path of {} points, {} "
577 }
catch (
const std::exception &ex) {
579 "seeding from the saddle mode",
588 const std::string tableFile =
"rate_instanton.dat";
589 std::ofstream table(tableFile);
591 throw std::runtime_error(
"instanton: cannot write " + tableFile);
593 table <<
"# T_K T_c_K beads converged iterations U_N_eV negative_modes "
594 "ln_k_per_s k_per_s ln_k_htst_per_s barrier_effective_eV "
595 "ln_k_wkb_path_per_s ln_k_parabolic_per_s parabolic_factor\n";
596 table << std::setprecision(10);
597 returnFiles.push_back(tableFile);
599 const double nan = std::numeric_limits<double>::quiet_NaN();
600 auto wkbAt = [&](
double beta) {
601 if (!(profile && hwPath > 0.0)) {
606 }
catch (
const std::exception &ex) {
612 std::vector<VectorXd> ring;
614 bool rateFailed =
false;
615 for (
size_t ti = 0; ti < temperatures.size(); ++ti) {
616 const double temperature = temperatures[ti];
618 const bool last = ti + 1 == temperatures.size();
619 const double wkbLog = wkbAt(beta);
620 if (!(temperature < tc)) {
626 if (temperature > tc) {
630 hReactant, hSaddle, beta, vSaddle - vReactant, rigidModes);
632 hReactant, hSaddle, beta, vSaddle - vReactant, rigidModes);
633 const double logPar = logQhtst + std::log(factor);
636 EONC_LOG_INFO(
"[Instanton] {:.4g} K is above the crossover {:.4g} "
637 "K; parabolic factor {:.6g}, ln(k s) = {:.4f}",
638 temperature, tc, factor, logPar - logSecond);
641 "[Instanton] parabolic factor {:.6g} is large; this close "
642 "to the crossover a uniform theory is the finite rate",
645 table << temperature <<
' ' << tc <<
' ' << o.beads
646 <<
" 0 0 nan 0 nan nan " << (logHtst - logSecond) <<
" nan "
647 << wkbLog <<
' ' << (logPar - logSecond) <<
' ' << factor
650 extras.emplace_back(
"instanton_temperature_K", temperature);
651 extras.emplace_back(
"parabolic_factor", factor);
652 extras.emplace_back(
"rate_parabolic", kPar);
653 extras.emplace_back(
"rate_parabolic_log", logPar - logSecond);
654 extras.emplace_back(
"rate_htst", kHtst);
655 extras.emplace_back(
"rate_htst_log", logHtst - logSecond);
656 if (std::isfinite(wkbLog)) {
657 extras.emplace_back(
"rate_wkb_path_log", wkbLog);
661 }
catch (
const std::exception &ex) {
666 if (!(temperature > tc)) {
667 EONC_LOG_ERROR(
"[Instanton] {:.4g} K is at the crossover temperature "
668 "{:.4g} K; the parabolic factor diverges there",
671 table << temperature <<
' ' << tc <<
' ' << o.beads
672 <<
" 0 0 nan 0 nan nan nan nan " << wkbLog <<
" nan nan\n";
676 extras.emplace_back(
"instanton_temperature_K", temperature);
677 if (std::isfinite(wkbLog)) {
678 extras.emplace_back(
"rate_wkb_path_log", wkbLog);
687 ro.maxIterations = o.max_iterations;
688 ro.forceTolerance = o.force_tolerance;
689 ro.halfRing = o.half_ring;
690 ro.initialHessians = o.initial_hessians;
691 ro.energyShift = o.energy_shift;
692 if (
static_cast<long>(mw.sqrtMasses().size()) == reactant.numberOfAtoms()) {
693 ro.rigidSqrtMasses = mw.sqrtMasses();
694 ro.rigidReference = mw.referenceFree();
695 ro.rigidRotations = rotationZero;
697 std::vector<VectorXd> guess = ring;
698 if (guess.empty() && profile) {
702 EONC_LOG_INFO(
"[Instanton] ring seeded from {} by the period condition",
703 o.initial_path.empty() ?
"the steepest-descent path"
705 }
catch (
const std::invalid_argument &ex) {
711 long ladderIterations = 0;
712 if (guess.empty() && o.bead_ladder && o.beads >= 16) {
713 long coarse = o.beads / 4;
714 if (coarse % 2 != 0) {
720 std::vector<VectorXd> rung;
721 for (
long nb = coarse; nb < o.beads; nb *= 2) {
727 ladderIterations += rungInst.iterations;
728 EONC_LOG_INFO(
"[Instanton] ladder rung {} beads: U_N {:.6f} eV after "
730 nb, rungInst.ringPotential, rungInst.iterations,
731 rungInst.converged ?
"" :
" (not converged)");
732 rung = rungInst.beads;
733 if (
static_cast<long>(rung.size()) != nb) {
737 rung = doubledRing(rung);
738 if (
static_cast<long>(rung.size()) > o.beads) {
743 if (
static_cast<long>(rung.size()) == o.beads) {
744 guess = std::move(rung);
748 qSaddle, hSaddle, beta, guess, evaluate, ro);
749 inst.iterations += ladderIterations;
750 EONC_LOG_INFO(
"[Instanton] {:.4g} K: ring U_N {:.6f} eV after {} "
752 temperature, inst.ringPotential, inst.iterations,
753 inst.converged ?
"" :
" (not converged)");
755 if (inst.converged && !straddlesSaddle(inst.beads, qSaddle, hSaddle)) {
756 EONC_LOG_ERROR(
"[Instanton] {:.4g} K: the ring converged off the "
757 "saddle's dividing plane, onto another saddle; no rate",
759 inst.converged =
false;
762 if (inst.converged) {
765 const long stride = std::max<long>(1, o.hessian_stride);
766 const long nBeads = o.beads;
767 std::map<long, MatrixXd> anchors;
770 auto anchor = [&](
long j) ->
const MatrixXd & {
771 const long n =
static_cast<long>(inst.beads.size());
772 if (j > n / 2 && (inst.beads[
static_cast<size_t>(j)] -
773 inst.beads[
static_cast<size_t>(n - j)])
777 auto it = anchors.find(j);
778 if (it == anchors.end()) {
779 it = anchors.emplace(j, hessianAt(inst.beads[
static_cast<size_t>(j)]))
784 auto beadHessian = [&](
long j,
const VectorXd &) ->
MatrixXd {
785 const long lo = (j / stride) * stride;
789 const long hi = lo + stride < nBeads ? lo + stride : 0;
790 const long span = (hi == 0 ? nBeads : hi) - lo;
792 static_cast<double>(j - lo) /
static_cast<double>(span);
793 return (1.0 - t) * anchor(lo) + t * anchor(hi);
797 vReactant - o.energy_shift, hSaddle,
798 vSaddle - o.energy_shift, rigidModes);
799 rateOk = std::isfinite(inst.logRate) && inst.negativeModes == 1;
800 if (inst.negativeModes != 1) {
801 EONC_LOG_ERROR(
"[Instanton] the ring Hessian has {} negative modes, "
802 "not one: the ring is not a first-order saddle of U_N",
805 }
catch (
const std::runtime_error &ex) {
810 EONC_LOG_INFO(
"[Instanton] {:.4g} K: ln(k s) = {:.4f}, harmonic TST "
811 "{:.4f}, effective barrier {:.4f} eV",
812 temperature, inst.logRate - logSecond,
813 inst.classicalLogRate - logSecond, inst.effectiveBarrier);
815 table << temperature <<
' ' << tc <<
' ' << o.beads <<
' '
816 << (inst.converged ? 1 : 0) <<
' ' << inst.iterations <<
' '
817 << inst.ringPotential <<
' ' << inst.negativeModes <<
' ';
819 table << inst.logRate - logSecond <<
' ' << inst.rate <<
' '
820 << inst.classicalLogRate - logSecond <<
' '
821 << inst.effectiveBarrier;
823 table <<
"nan nan nan nan";
825 table <<
' ' << wkbLog <<
" nan nan\n";
827 std::vector<std::string> files;
829 files.push_back(pathFile);
831 if (temperatures.size() > 1) {
832 std::ostringstream name;
833 name << std::defaultfloat << std::setprecision(6) <<
"instanton_"
834 << temperature <<
"K.con";
835 files.push_back(name.str());
837 if (!inst.beads.empty()) {
839 for (
const auto &file : files) {
840 for (
size_t j = 0; j < inst.beads.size(); ++j) {
841 mw.place(inst.beads[j], frame);
844 meta.energy = inst.energies[j];
845 meta.write_con_forces =
false;
847 {
"imaginary_time_fs",
static_cast<double>(j) * inst.betaN *
850 meta.scalars.push_back({
"instanton_temperature_K", temperature});
851 meta.scalars.push_back({
"instanton_crossover_K", tc});
852 meta.scalars.push_back(
853 {
"instanton_converged", inst.converged ? 1.0 : 0.0});
855 meta.scalars.push_back(
856 {
"rate_instanton_log", inst.logRate - logSecond});
857 meta.scalars.push_back(
858 {
"barrier_effective_instanton", inst.effectiveBarrier});
861 if (!
io::io_ok(frame.matter2con(file, j > 0, &meta))) {
862 throw std::runtime_error(
"instanton: cannot write " + file);
865 returnFiles.push_back(file);
869 const std::string centroidFile =
870 last ?
"instanton_centroid.con"
871 :
"instanton_centroid_" +
872 files.back().substr(std::string(
"instanton_").size());
873 writeCentroid(centroidFile, inst.beads, mw, reactant,
874 {{
"instanton_temperature_K", temperature},
875 {
"instanton_crossover_K", tc},
876 {
"instanton_converged", inst.converged ? 1.0 : 0.0}});
877 returnFiles.push_back(centroidFile);
884 extras.emplace_back(
"instanton_temperature_K", temperature);
885 extras.emplace_back(
"instanton_iterations",
886 static_cast<double>(inst.iterations));
887 extras.emplace_back(
"instanton_ring_potential", inst.ringPotential);
888 extras.emplace_back(
"instanton_bN", inst.bN);
889 if (std::isfinite(wkbLog)) {
890 extras.emplace_back(
"rate_wkb_path_log", wkbLog);
893 extras.emplace_back(
"rate_instanton", inst.rate);
894 extras.emplace_back(
"rate_instanton_log", inst.logRate - logSecond);
895 extras.emplace_back(
"rate_htst", inst.classicalRate);
896 extras.emplace_back(
"rate_htst_log", inst.classicalLogRate - logSecond);
897 extras.emplace_back(
"barrier_effective_instanton", inst.effectiveBarrier);
898 extras.emplace_back(
"instanton_negative_modes",
899 static_cast<double>(inst.negativeModes));
900 extras.emplace_back(
"instanton_zero_mode", inst.zeroEigenvalue);
910 if (o.pi_planes > 0) {
912 params, *
pot, reactant, saddle, hSaddle, pathQ, temperatures, extras);
913 returnFiles.insert(returnFiles.end(), planeFiles.begin(), planeFiles.end());
922 const auto &o =
params.instanton_options();
924 std::vector<std::string> returnFiles;
925 const std::string resultsFile =
"results.dat";
926 const std::string pathFile =
"instanton.con";
927 returnFiles.push_back(resultsFile);
929 auto reactant = std::make_unique<Matter>(
pot,
params);
930 if (!
io::io_ok(reactant->con2matter(o.reactant_filename))) {
931 throw std::runtime_error(
"instanton: cannot read " + o.reactant_filename);
933 const MassWeighted mw(*reactant);
934 const long n = mw.dimension();
935 const VectorXd qStart = VectorXd::Zero(n);
939 std::vector<std::unique_ptr<Matter>> pool;
940 auto evaluate = [&](
const std::vector<VectorXd> &q, std::vector<double> &v,
941 std::vector<VectorXd> &grad) {
942 while (pool.size() < q.size()) {
943 pool.push_back(std::make_unique<Matter>(*reactant));
945 for (
size_t j = 0; j < q.size(); ++j) {
946 mw.place(q[j], *pool[j]);
949 grad.resize(q.size());
950 if (
pot->supportsBatchEvaluation() && q.size() > 1) {
951 const long atoms = reactant->numberOfAtoms();
952 std::vector<VectorXi> nrs(q.size());
953 std::vector<Matrix3d> boxes(q.size());
954 std::vector<const double *> posPtr, boxPtr;
955 std::vector<const int *> nrsPtr;
956 std::vector<double *> frcPtr;
957 for (
size_t j = 0; j < q.size(); ++j) {
958 nrs[j] = pool[j]->getAtomicNrs();
959 boxes[j] = pool[j]->getPeriodic() ? pool[j]->getCell()
960 : Matrix3d::Zero().eval();
962 for (
size_t j = 0; j < q.size(); ++j) {
963 posPtr.push_back(pool[j]->getPositions().data());
964 nrsPtr.push_back(nrs[j].data());
965 frcPtr.push_back(pool[j]->forcesData());
966 boxPtr.push_back(boxes[j].data());
968 std::vector<double> energies(q.size()), variances(q.size());
969 pot->forceBatch(
static_cast<long>(q.size()), atoms, posPtr.data(),
970 nrsPtr.data(), frcPtr.data(), energies.data(),
971 variances.data(), boxPtr.data());
972 for (
size_t j = 0; j < q.size(); ++j) {
973 pool[j]->setComputedPotential(energies[j], variances[j]);
976 for (
size_t j = 0; j < q.size(); ++j) {
977 v[j] = pool[j]->getPotentialEnergy();
978 grad[j] = mw.gradient(pool[j]->getForces());
984 std::array<bool, 3> rotationZero{{
false,
false,
false}};
985 std::array<double, 3> rotationResidual{{0.0, 0.0, 0.0}};
986 bool rotationsKnown =
false;
987 auto hessianAt = [&](
const VectorXd &q) {
993 if (out.rows() != n) {
994 throw std::runtime_error(
"instanton: a bead Hessian failed");
996 if (!rotationsKnown) {
997 mw.markRotationZeroModes(out, mw.rigidGenerators(m), rotationZero,
999 rotationsKnown =
true;
1003 const MatrixXd rigid = mw.rigidBasis(m, rotationZero);
1004 if (rigid.cols() > 0) {
1005 const MatrixXd p = MatrixXd::Identity(n, n) - rigid * rigid.transpose();
1011 if (o.mode ==
"rate") {
1012 return runRate(
params,
pot, *reactant, mw, evaluate, hessianAt,
1013 rotationZero, rotationResidual);
1016 auto product = std::make_unique<Matter>(
pot,
params);
1017 if (!
io::io_ok(product->con2matter(o.product_filename))) {
1018 throw std::runtime_error(
"instanton: cannot read " + o.product_filename);
1020 if (reactant->numberOfAtoms() != product->numberOfAtoms()) {
1021 throw std::runtime_error(
"instanton: the minima differ in atom count");
1023 alignRigid(*reactant, *product);
1024 const VectorXd qEnd = mw.toQ(*product);
1027 std::vector<VectorXd> guess;
1028 if (!o.initial_path.empty()) {
1029 const auto frames = readcon::read_all_frames(o.initial_path);
1030 for (
const auto &frame : frames) {
1033 throw std::runtime_error(
"instanton: cannot read " + o.initial_path);
1035 alignRigid(*reactant, m);
1036 guess.push_back(mw.toQ(m));
1038 if (guess.size() >= 2) {
1039 guess.front() = qStart;
1040 guess.back() = qEnd;
1044 const MatrixXd hStart = hessianAt(qStart);
1045 const MatrixXd hEnd = hessianAt(qEnd);
1047 const double betaHbar = o.beta_hbar_omega / omega;
1049 opt.
beads = o.beads;
1053 EONC_LOG_INFO(
"[Instanton] {} beads over beta hbar = {:.4f} fs, {} degrees "
1055 o.beads, betaHbar * kTimeUnitFs, n);
1058 qStart, qEnd, betaHbar, guess, evaluate, opt);
1062 bool splitOk =
false;
1063 std::string failure;
1066 const double betaAsymmetry =
1070 "{:.4g} eV, too far for the splitting; the path and "
1071 "action are written, the splitting is not",
1075 const long stride = std::max<long>(1, o.hessian_stride);
1076 const long P = o.beads;
1077 std::map<long, MatrixXd> anchors;
1078 auto anchor = [&](
long j) ->
const MatrixXd & {
1079 auto it = anchors.find(j);
1080 if (it == anchors.end()) {
1081 it = anchors.emplace(j, hessianAt(inst.
path[
static_cast<size_t>(j)]))
1086 auto beadHessian = [&](
long j,
const VectorXd &) ->
MatrixXd {
1090 const long lo = 1 + ((j - 1) / stride) * stride;
1091 const long hi = std::min(lo + stride, P - 1);
1092 if (j == lo || hi == lo) {
1096 static_cast<double>(j - lo) /
static_cast<double>(hi - lo);
1097 return (1.0 - t) * anchor(lo) + t * anchor(hi);
1101 splitOk = std::isfinite(inst.
delta0);
1102 }
catch (
const std::runtime_error &ex) {
1103 failure = ex.what();
1111 for (
size_t j = 0; j < inst.
path.size(); ++j) {
1112 mw.place(inst.
path[j], frame);
1117 meta.
scalars = {{
"imaginary_time_fs",
1118 static_cast<double>(j) * inst.
dtau * kTimeUnitFs}};
1122 {
"instanton_beta_hbar_fs", betaHbar * kTimeUnitFs});
1123 meta.
scalars.push_back({
"instanton_temperature_K", kelvin});
1125 {
"instanton_converged", inst.
converged ? 1.0 : 0.0});
1128 meta.
scalars.push_back({
"tunnel_splitting_instanton", inst.
delta0});
1139 throw std::runtime_error(
"instanton: cannot write " + pathFile);
1142 returnFiles.push_back(pathFile);
1143 writeCentroid(
"instanton_centroid.con", inst.
path, mw, *reactant,
1144 {{
"instanton_temperature_K", kelvin},
1145 {
"instanton_converged", inst.converged ? 1.0 : 0.0}});
1146 returnFiles.push_back(
"instanton_centroid.con");
1150 const bool good = splitOk || (inst.converged && !inst.symmetricEnough);
1155 status, params.potential_options().potential,
1157 env.job_type =
"instanton";
1158 env.extras.emplace_back(
"force_calls",
static_cast<double>(env.force_calls));
1159 env.extras.emplace_back(
"instanton_iterations",
1160 static_cast<double>(inst.iterations));
1161 env.extras.emplace_back(
"instanton_action", inst.action);
1162 env.extras.emplace_back(
"instanton_temperature_K", kelvin);
1163 env.extras.emplace_back(
"tunnel_asymmetry", inst.asymmetry);
1164 env.extras.emplace_back(
"instanton_beta_asymmetry", betaAsymmetry);
1165 env.extras.emplace_back(
"instanton_symmetric",
1166 inst.symmetricEnough ? 1.0 : 0.0);
1168 env.extras.emplace_back(
"tunnel_splitting_instanton", inst.delta0);
1169 env.extras.emplace_back(
"instanton_mode_separation", inst.modeSeparation);
1171 env.writeResultsDat(resultsFile);
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
#define EONC_LOG_ERROR(...)
#define EONC_LOG_WARNING(...)
#define EONC_LOG_INFO(...)
void writeHessianFile(bool on) noexcept
Whether a finished Hessian goes to hessian.dat (on by default).
MatrixXd getHessian(Matter *matterIn, const VectorXi &atomsIn)
std::vector< std::string > run(void) override
Virtual run; used solely for dynamic dispatch.
std::shared_ptr< Potential > pot
double getPotentialEnergy() const
io::IoStatus matter2con(std::string filename, bool append=false, const io::ConFrameMetadata *metadata=nullptr)
static PotRegistry & get() noexcept
Process-lifetime singleton.
size_t total_force_calls() const noexcept
IoStatus con2matter(Matter &m, std::string filename)
constexpr bool io_ok(IoStatus s) noexcept
void requireTrotterSprings(const std::string &springs, const char *use)
Economised springs and a normal-mode GLE are refused.
std::vector< std::string > runAfterInstanton(const Parameters ¶ms, Potential &pot, const Matter &reactant, const Matter &saddle, const MatrixXd &hSaddle, const std::vector< VectorXd > &pathQ, const std::vector< double > &temperatures, std::vector< std::pair< std::string, double > > &extras)
[Instanton] mode rate with pi_planes > 0: the planes and rate at each temperature,...
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.
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::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....
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...
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...
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.
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.
RAII resource manager for the ARTn C library with global synchronization.
static JobResultEnvelope fromMinimization(RunStatus status, PotType pot, std::uint64_t fcalls, bool hasE, double energy)
long maxIterations
L-BFGS iterations.
double forceTolerance
largest per-bead |dS/dq| / dtau, eV / (amu^0.5 Angstrom)
long beads
P: segments from one minimum to the other.
double betaHbarOmega
beta hbar omega of the stiffer end along the path; sets the imaginary time
double asymmetry
V(end) - V(start), eV.
std::vector< double > energies
V at every bead, eV.
double zeroMode
the eigenvalue det' leaves out
double action
(S - S_well) / hbar
bool symmetricEnough
beta |asymmetry| < 0.1: the propagator ratio reads delta0 only when the wells lie within a small frac...
double modeSeparation
The second smallest eigenvalue of J over the zero mode's: small means the kink is not isolated in ima...
double delta0
tunnelling splitting, eV
std::vector< VectorXd > path
P + 1 beads, ends at the minima.
long beads
N, beads on the ring.