Loading...
Searching...
No Matches
eonc::InstantonJob Class Reference

Ring-polymer instanton ([Instanton]): the tunnelling splitting between two minima beyond one-dimensional WKB (mode splitting), or the thermal rate through a saddle (mode rate): the ring below the crossover temperature, and the parabolic barrier factor times harmonic transition-state theory above it, written to instanton.con and results.dat. More...

#include <InstantonJob.h>

Inheritance diagram for eonc::InstantonJob:

Public Member Functions

 InstantonJob (std::unique_ptr< Parameters > parameters, Runtime &rt)
 ~InstantonJob (void)=default
std::vector< std::string > run (void) override
 Virtual run; used solely for dynamic dispatch.
Public Member Functions inherited from eonc::Job
void adoptRuntime (std::unique_ptr< Runtime > rt)
 Take ownership of a Runtime previously passed as Runtime&.
 Job (std::unique_ptr< Parameters > parameters, Runtime &rt)
 Borrow: caller keeps Runtime alive (CLI stack / Python Session).
 Job (std::unique_ptr< Parameters > parameters, std::unique_ptr< Runtime > rt)
 Own a Runtime (one-shot makeJob / rvalue).
 Job (std::unique_ptr< Parameters > parameters)
 Own a default-constructed Runtime.
 Job (std::shared_ptr< Potential > potPassed, const Parameters &parameters)
virtual ~Job ()=default
JobType getType ()
PotRegistry & pots () noexcept
void releasePotential ()
 Drop the Potential so on_destroyed is recorded before Runtime dies.

Additional Inherited Members

Protected Attributes inherited from eonc::Job
JobType jtype
Parameters params
std::unique_ptr< Runtime > owned_runtime_
 Non-null when this Job owns the composition root (one-shot makeJob).
Runtime * runtime_
 Always valid: either owned_runtime_.get() or a caller-owned Runtime.
std::shared_ptr< Potential > pot

Detailed Description

Ring-polymer instanton ([Instanton]): the tunnelling splitting between two minima beyond one-dimensional WKB (mode splitting), or the thermal rate through a saddle (mode rate): the ring below the crossover temperature, and the parabolic barrier factor times harmonic transition-state theory above it, written to instanton.con and results.dat.

Definition at line 24 of file InstantonJob.h.

Constructor & Destructor Documentation

◆ InstantonJob()

eonc::InstantonJob::InstantonJob ( std::unique_ptr< Parameters > parameters,
Runtime & rt )
inline

Definition at line 26 of file InstantonJob.h.

27 : Job(std::move(parameters), rt) {}
Job(std::unique_ptr< Parameters > parameters, Runtime &rt)
Borrow: caller keeps Runtime alive (CLI stack / Python Session).
Definition Job.h:76

◆ ~InstantonJob()

eonc::InstantonJob::~InstantonJob ( void )
default

Member Function Documentation

◆ run()

std::vector< std::string > eonc::InstantonJob::run ( void )
overridevirtual

Virtual run; used solely for dynamic dispatch.

Implements eonc::Job.

Definition at line 921 of file InstantonJob.cpp.

921 {
922 const auto &o = params.instanton_options();
923 pathintegral::requireTrotterSprings(o.springs, "instanton");
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);
928
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);
932 }
933 const MassWeighted mw(*reactant);
934 const long n = mw.dimension();
935 const VectorXd qStart = VectorXd::Zero(n);
936
937 // Bead evaluations: one batch per call, through forceBatch when the
938 // potential spreads a batch over calculators.
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));
944 }
945 for (size_t j = 0; j < q.size(); ++j) {
946 mw.place(q[j], *pool[j]);
947 }
948 v.resize(q.size());
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();
961 }
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());
967 }
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]);
974 }
975 }
976 for (size_t j = 0; j < q.size(); ++j) {
977 v[j] = pool[j]->getPotentialEnergy();
978 grad[j] = mw.gradient(pool[j]->getForces());
979 }
980 };
981
982 // The first call is the reactant. Its Hessian decides which rotations
983 // are zero modes; later beads reuse that decision.
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) {
988 Matter m(*reactant);
989 mw.place(q, m);
990 Hessian h(params, &m);
991 h.writeHessianFile(false);
992 MatrixXd out = h.getHessian(&m, mw.freeAtoms());
993 if (out.rows() != n) {
994 throw std::runtime_error("instanton: a bead Hessian failed");
995 }
996 if (!rotationsKnown) {
997 mw.markRotationZeroModes(out, mw.rigidGenerators(m), rotationZero,
998 rotationResidual);
999 rotationsKnown = true;
1000 }
1001 // A finite-difference Hessian of a free structure has small nonzero
1002 // rigid eigenvalues of either sign; project them to zero.
1003 const MatrixXd rigid = mw.rigidBasis(m, rotationZero);
1004 if (rigid.cols() > 0) {
1005 const MatrixXd p = MatrixXd::Identity(n, n) - rigid * rigid.transpose();
1006 out = p * out * p;
1007 }
1008 return out;
1009 };
1010
1011 if (o.mode == "rate") {
1012 return runRate(params, pot, *reactant, mw, evaluate, hessianAt,
1013 rotationZero, rotationResidual);
1014 }
1015
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);
1019 }
1020 if (reactant->numberOfAtoms() != product->numberOfAtoms()) {
1021 throw std::runtime_error("instanton: the minima differ in atom count");
1022 }
1023 alignRigid(*reactant, *product);
1024 const VectorXd qEnd = mw.toQ(*product);
1025
1026 // Starting path: a band from file, else the straight line.
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) {
1031 Matter m(*reactant);
1032 if (!io::io_ok(io::con2matter(m, frame))) {
1033 throw std::runtime_error("instanton: cannot read " + o.initial_path);
1034 }
1035 alignRigid(*reactant, m);
1036 guess.push_back(mw.toQ(m));
1037 }
1038 if (guess.size() >= 2) {
1039 guess.front() = qStart;
1040 guess.back() = qEnd;
1041 }
1042 }
1043
1044 const MatrixXd hStart = hessianAt(qStart);
1045 const MatrixXd hEnd = hessianAt(qEnd);
1046 const double omega = tunneling::pathOmega(hStart, hEnd, qStart, qEnd);
1047 const double betaHbar = o.beta_hbar_omega / omega;
1048 tunneling::InstantonOptions opt;
1049 opt.beads = o.beads;
1050 opt.betaHbarOmega = o.beta_hbar_omega;
1051 opt.maxIterations = o.max_iterations;
1052 opt.forceTolerance = o.force_tolerance;
1053 EONC_LOG_INFO("[Instanton] {} beads over beta hbar = {:.4f} fs, {} degrees "
1054 "of freedom",
1055 o.beads, betaHbar * kTimeUnitFs, n);
1056
1057 tunneling::Instanton inst = tunneling::optimizeInstanton(
1058 qStart, qEnd, betaHbar, guess, evaluate, opt);
1059 EONC_LOG_INFO("[Instanton] action {:.6f} after {} iterations{}", inst.action,
1060 inst.iterations, inst.converged ? "" : " (not converged)");
1061
1062 bool splitOk = false;
1063 std::string failure;
1064 // beta |delta|: the propagator ratio reads delta0 only when the wells
1065 // lie within a small fraction of kB T of each other.
1066 const double betaAsymmetry =
1067 std::abs(inst.asymmetry) * betaHbar / tunneling::kHbar;
1068 if (inst.converged && !inst.symmetricEnough) {
1069 EONC_LOG_WARNING("[Instanton] beta |delta| = {:.3g}: the wells differ by "
1070 "{:.4g} eV, too far for the splitting; the path and "
1071 "action are written, the splitting is not",
1072 betaAsymmetry, inst.asymmetry);
1073 }
1074 if (inst.converged && inst.symmetricEnough) {
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)]))
1082 .first;
1083 }
1084 return it->second;
1085 };
1086 auto beadHessian = [&](long j, const VectorXd &) -> MatrixXd {
1087 if (stride == 1) {
1088 return anchor(j);
1089 }
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) {
1093 return anchor(lo);
1094 }
1095 const double t =
1096 static_cast<double>(j - lo) / static_cast<double>(hi - lo);
1097 return (1.0 - t) * anchor(lo) + t * anchor(hi);
1098 };
1099 try {
1100 tunneling::instantonSplitting(inst, beadHessian, hStart, hEnd);
1101 splitOk = std::isfinite(inst.delta0);
1102 } catch (const std::runtime_error &ex) {
1103 failure = ex.what();
1104 EONC_LOG_ERROR("[Instanton] {}", failure);
1105 }
1106 }
1107
1108 // The path, one frame per bead; the splitting on the first frame.
1109 const double kelvin = tunneling::kHbar / (tunneling::kBoltzmann * betaHbar);
1110 Matter frame(*reactant);
1111 for (size_t j = 0; j < inst.path.size(); ++j) {
1112 mw.place(inst.path[j], frame);
1113 io::ConFrameMetadata meta;
1114 meta.frame_index = static_cast<uint64_t>(j);
1115 meta.energy = inst.energies[j];
1116 meta.write_con_forces = false;
1117 meta.scalars = {{"imaginary_time_fs",
1118 static_cast<double>(j) * inst.dtau * kTimeUnitFs}};
1119 if (j == 0) {
1120 meta.scalars.push_back({"instanton_action", inst.action});
1121 meta.scalars.push_back(
1122 {"instanton_beta_hbar_fs", betaHbar * kTimeUnitFs});
1123 meta.scalars.push_back({"instanton_temperature_K", kelvin});
1124 meta.scalars.push_back(
1125 {"instanton_converged", inst.converged ? 1.0 : 0.0});
1126 meta.scalars.push_back({"tunnel_asymmetry", inst.asymmetry});
1127 if (splitOk) {
1128 meta.scalars.push_back({"tunnel_splitting_instanton", inst.delta0});
1129 meta.scalars.push_back(
1130 {"tls_energy_instanton", std::hypot(inst.asymmetry, inst.delta0)});
1131 meta.scalars.push_back({"instanton_zero_mode", inst.zeroMode});
1132 meta.scalars.push_back(
1133 {"instanton_mode_separation", inst.modeSeparation});
1134 meta.scalars.push_back(
1135 {"instanton_symmetric", inst.symmetricEnough ? 1.0 : 0.0});
1136 }
1137 }
1138 if (!io::io_ok(frame.matter2con(pathFile, j > 0, &meta))) {
1139 throw std::runtime_error("instanton: cannot write " + pathFile);
1140 }
1141 }
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");
1147
1148 // A converged path between wells too far apart is a result, not a
1149 // failure: the flags say why no splitting was written.
1150 const bool good = splitOk || (inst.converged && !inst.symmetricEnough);
1151 const auto status = good ? RunStatus::GOOD
1152 : (inst.converged ? RunStatus::FAIL_POTENTIAL_FAILED
1155 status, params.potential_options().potential,
1156 PotRegistry::get().total_force_calls(), false, 0.0);
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);
1167 if (splitOk) {
1168 env.extras.emplace_back("tunnel_splitting_instanton", inst.delta0);
1169 env.extras.emplace_back("instanton_mode_separation", inst.modeSeparation);
1170 }
1171 env.writeResultsDat(resultsFile);
1172 return returnFiles;
1173}
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
#define EONC_LOG_ERROR(...)
Definition EonLogger.h:261
#define EONC_LOG_WARNING(...)
Definition EonLogger.h:255
#define EONC_LOG_INFO(...)
Definition EonLogger.h:249
std::shared_ptr< Potential > pot
Definition Job.h:63
Parameters params
Definition Job.h:58
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
Definition ConFileIO.h:38
void requireTrotterSprings(const std::string &springs, const char *use)
Economised springs and a normal-mode GLE are refused.
void instantonSplitting(Instanton &inst, const BeadHessian &hessian, const MatrixXd &hessStart, const MatrixXd &hessEnd)
Fills delta0, zeroMode and modeSeparation from the bead Hessians and the Hessians of the two minima.
double 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,...
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
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
static JobResultEnvelope fromMinimization(RunStatus status, PotType pot, std::uint64_t fcalls, bool hasE, double energy)
Definition JobResult.h:101

The documentation for this class was generated from the following files: