150 const VectorXd &gamma,
151 const VectorXd &normal) {
152 const long equilSteps =
params.oh_tst_options().equil_steps;
153 const long sampleSteps =
params.oh_tst_options().sample_steps;
154 const double alphaRot =
params.oh_tst_options().alpha_rot;
160 x -= normal * normal.dot(x - gamma);
163 VectorXd v(x.size());
169 std::unique_ptr<GleThermostat> gle;
170 if (
params.oh_tst_options().thermostat ==
"gle") {
173 gle = std::make_unique<GleThermostat>(a,
m_kbt, 0.5 *
m_dt, x.size());
176 params.oh_tst_options().gle_a_file);
177 throw std::runtime_error(
"oh_tst: gle thermostat unusable");
180 const auto gauss = [
this]() {
return gaussDraw(); };
183 VectorXd fPlane = f - normal * normal.dot(f);
186 avg.
rotNorm = VectorXd::Zero(x.size());
187 avg.
rotRaw = VectorXd::Zero(x.size());
188 avg.
pos = VectorXd::Zero(x.size());
192 for (
long step = 0; step < equilSteps + sampleSteps; ++step) {
199 v -= normal * normal.dot(v);
201 VectorXd a = fPlane.cwiseQuotient(
m_masses3N);
203 x -= normal * normal.dot(x - gamma);
206 fPlane = f - normal * normal.dot(f);
207 VectorXd aNew = fPlane.cwiseQuotient(
m_masses3N);
208 v += 0.5 *
m_dt * (a + aNew);
209 v -= normal * normal.dot(v);
212 x -= normal * normal.dot(x - gamma);
215 fPlane = f - normal * normal.dot(f);
219 v -= normal * normal.dot(v);
224 if (step >= equilSteps) {
225 const double fn = normal.dot(f);
226 const VectorXd arm = x - gamma;
227 const double arm2 = arm.squaredNorm();
230 avg.
rotNorm.noalias() += (fn / (alphaRot * arm2)) * arm;
232 avg.
rotRaw.noalias() += fn * arm;
233 avg.
pos.noalias() += x;
238 const double inv = 1.0 /
static_cast<double>(nAccum);
308 auto reactant = std::make_shared<Matter>(
pot,
params);
309 auto product = std::make_shared<Matter>(
pot,
params);
311 reactant->con2matter(
params.oh_tst_options().reactant_filename))) {
313 params.oh_tst_options().reactant_filename);
314 throw std::runtime_error(
"oh_tst: failed to load reactant");
317 product->con2matter(
params.oh_tst_options().product_filename))) {
319 params.oh_tst_options().product_filename);
320 throw std::runtime_error(
"oh_tst: failed to load product");
323 const double temperature =
params.main_options().temperature;
325 "[oh_tst] thermostat = {}{}",
params.oh_tst_options().thermostat,
326 params.oh_tst_options().thermostat ==
"gle"
327 ? std::string(
" (drift: ") +
params.oh_tst_options().gle_a_file +
")"
332 ?
params.main_options().randomSeed
335 const double tcol =
params.thermostat_options().andersen_tcol_input /
336 params.constants().timeUnit;
340 const long nAtoms = reactant->numberOfAtoms();
341 auto masses = reactant->getMasses();
342 std::vector<double> m3;
343 m3.reserve(3 * nAtoms);
344 for (
long i = 0; i < nAtoms; ++i) {
345 if (!reactant->getFixed(i)) {
346 for (
int j = 0; j < 3; ++j)
347 m3.push_back(masses[i]);
350 m_masses3N = VectorXd::Map(m3.data(),
static_cast<long>(m3.size()));
354 const VectorXd xR = reactant->getPositionsFreeV();
355 VectorXd diff = product->getPositionsFreeV() - xR;
357 AtomMatrix d(AtomMatrix::Map(diff.data(), diff.size() / 3, 3));
358 Eigen::RowVector3d total_drift = Eigen::RowVector3d::Zero();
360 EONC_LOG_INFO(
"[oh_tst] rigid drift removed: ({:.4f}, {:.4f}, "
361 "{:.4f}) A per atom",
362 total_drift[0], total_drift[1], total_drift[2]);
363 diff = VectorXd::Map(d.data(), diff.size());
365 const double guideLen = diff.norm();
366 EONC_LOG_INFO(
"[oh_tst] guideline length |P - R| = {:.4f} A over {} free "
368 guideLen, xR.size());
372 if (guideLen < 0.5) {
374 "translation-class endpoint pair",
376 throw std::runtime_error(
"oh_tst: degenerate guideline");
378 const VectorXd u = diff / guideLen;
385 if (!
params.oh_tst_options().symmetry_products.empty()) {
386 std::string rest =
params.oh_tst_options().symmetry_products;
387 while (!rest.empty()) {
388 const auto comma = rest.find(
',');
389 std::string fname = rest.substr(0, comma);
390 rest = (comma == std::string::npos) ?
"" : rest.substr(comma + 1);
397 throw std::runtime_error(
"oh_tst: failed to load symmetry product");
400 AtomMatrix dm(AtomMatrix::Map(d.data(), d.size() / 3, 3));
402 d = VectorXd::Map(dm.data(), d.size());
403 const double dn = d.norm();
408 EONC_LOG_INFO(
"[oh_tst] symmetry restriction active over {} product "
416 double s =
params.oh_tst_options().s_init * guideLen;
419 VectorXd omega = VectorXd::Zero(n.size());
420 const double mS =
params.oh_tst_options().plane_mass;
421 const double dtPlane =
params.oh_tst_options().plane_time_step;
422 const double dsMax =
params.oh_tst_options().ds_max;
423 const double dThetaMax =
params.oh_tst_options().dtheta_max;
424 const double fTol =
params.oh_tst_options().force_tol;
433 double aTrans = 0.0, aRot = 0.0, aBest = 0.0, sBest = s;
435 bool havePrev =
false;
437 VectorXd rotRawPrev, posPrev, nPrev;
444 std::ofstream prog(
"oh_tst_progression.dat");
446 prog <<
"# plane s/L <F.n> (eV/A) dA_trans (eV) dA_rot (eV) "
452 const bool scanMode =
params.oh_tst_options().pmf_scan;
453 const long nScan = std::max(2L,
params.oh_tst_options().scan_planes);
454 const long nPlanes = scanMode ? nScan :
params.oh_tst_options().max_planes;
462 bool converged =
false;
468 bool guidelineMoving =
false;
469 VectorXd gOrigin = xR;
471 long rotOnlySteps = 0;
472 for (; plane < nPlanes; ++plane) {
474 s =
pmfScanS(plane, nScan, guideLen);
476 const VectorXd gamma = gOrigin + s * gDir;
483 const double gS = -avg.
fn / mS;
487 sideSign = (avg.
fn < 0.0) ? -1 : 1;
488 }
else if (!guidelineMoving && ((avg.
fn < 0.0) ? -1 : 1) != sideSign) {
489 guidelineMoving =
true;
491 "sign; guideline now follows <r> along the normal",
504 const bool sameSide = scanMode || ((fnPrev < 0.0 ? -1 : 1) == sideSign &&
505 (avg.
fn < 0.0 ? -1 : 1) == sideSign);
507 const VectorXd fParMean = 0.5 * (fnPrev * nPrev + avg.
fn * n);
508 aTrans += -fParMean.dot(avg.
pos - posPrev);
509 const VectorXd rotMean = 0.5 * (rotRawPrev + avg.
rotRaw);
510 aRot += rotMean.dot(n - nPrev);
514 const double aTotal = aTrans + aRot;
515 if (aTotal > aBest) {
521 prog << std::format(
"{:6} {:10.6f} {:14.6e} {:12.6f} {:12.6f} {:12.6f} "
523 plane, s / guideLen, avg.
fn, aTrans, aRot, aTotal,
527 EONC_LOG_DEBUG(
"[oh_tst] plane {} s/L {:.4f} <F.n> {:.4e} A {:.4f} eV",
528 plane, s / guideLen, avg.
fn, aTotal);
534 if (aTotal >
params.oh_tst_options().max_delta_a) {
536 "[oh_tst] accumulated work {:.2f} eV exceeds max_delta_a "
537 "{:.2f} eV at plane {} -- endpoints likely unminimized",
538 aTotal,
params.oh_tst_options().max_delta_a, plane);
539 throw std::runtime_error(
"oh_tst: diverging reversible work");
546 if (!scanMode && plane > 2 && aTotal > 2.0 *
m_kbt &&
547 std::fabs(avg.
fn) < fTol &&
548 gRot.norm() *
params.oh_tst_options().alpha_rot < fTol) {
556 prog << std::format(
"# converged at plane {}\n", plane);
573 s =
pmfScanS(plane + 1, nScan, guideLen);
574 const VectorXd gammaNext = xR + s * u;
575 xFree -= u * (u.dot(xFree - gammaNext));
587 const bool rotationOnly =
589 gRot.norm() *
params.oh_tst_options().alpha_rot > 5.0 * fTol &&
597 vS += 0.5 * dtPlane * (gS + gSPrev);
603 double ds = dtPlane * vS + 0.5 * dtPlane * dtPlane * gS;
604 ds = std::clamp(ds, -dsMax, dsMax);
610 guidelineMoving ? s + ds : std::clamp(s + ds, 0.0, guideLen);
615 if (havePrev && gRotPrev.size() == gRot.size()) {
616 omega += 0.5 * dtPlane * (gRot + gRotPrev);
618 omega += dtPlane * gRot;
620 omega -= n * n.dot(omega);
621 const double gNorm = gRot.norm();
623 const VectorXd gHat = gRot / gNorm;
624 const double along = omega.dot(gHat);
626 omega = gHat * along;
633 VectorXd dn = dtPlane * omega + 0.5 * dtPlane * dtPlane * gRot;
634 const double dTheta = dn.norm();
635 if (dTheta > dThetaMax) {
636 dn *= dThetaMax / dTheta;
638 const VectorXd nOld = n;
639 n = (n + dn).normalized();
640 if (n.dot(gDir) < 0.0) {
648 VectorXd arm = avg.
pos - gamma;
649 VectorXd armNew = arm - nOld * arm.dot(n - nOld);
650 const double armLen = arm.norm();
651 const double armNewLen = armNew.norm();
652 if (armLen > 1e-12 && armNewLen > 1e-12) {
653 armNew *= armLen / armNewLen;
656 if (guidelineMoving) {
666 gammaNew = gOrigin + s * gDir;
668 gammaNew = gOrigin + sNew * gDir;
670 xFree = gammaNew + armNew;
671 xFree -= n * n.dot(xFree - gammaNew);
681 if (!guidelineMoving) {
685 bool progOk = prog.is_open();
688 progOk =
static_cast<bool>(prog);
690 EONC_LOG_ERROR(
"[oh_tst] failed to write oh_tst_progression.dat");
696 if (scanMode && plane >= nPlanes)
702 const long planesUsed = (plane < nPlanes) ? plane + 1 : plane;
706 const double mu = (
m_masses3N.array() * nBest.array().square()).sum();
711 Matter rWalker(*reactant);
712 const double sFirst = scanMode ?
pmfScanS(0, nScan, guideLen)
713 :
params.oh_tst_options().s_init * guideLen;
714 const VectorXd gammaR = xR + sFirst * u;
718 const double kInternal = vFlux * qRatio * std::exp(-aBest /
m_kbt);
719 const double kSI = kInternal / (
params.constants().timeUnit * 1.0e-15);
721 std::vector<std::string> returnFiles;
722 std::ofstream out(
"results.dat");
725 throw std::runtime_error(
"oh_tst: cannot open results.dat");
727 out <<
"oh_tst job_type\n";
728 out << std::format(
"{} converged\n", converged ? 1 : 0);
729 out << std::format(
"{} planes_used\n", planesUsed);
730 out << std::format(
"{:.8f} free_energy_barrier_eV\n", aBest);
731 out << std::format(
"{:.8f} delta_a_trans_eV\n", aTrans);
732 out << std::format(
"{:.8f} delta_a_rot_eV\n", aRot);
733 out << std::format(
"{:.8f} s_star_over_L\n", sBest / guideLen);
734 out << std::format(
"{:.8f} guideline_length_A\n", guideLen);
735 out << std::format(
"{:.8f} normal_overlap_with_guideline\n", nBest.dot(u));
736 out << std::format(
"{:.8e} effective_mass_amu\n", mu);
737 out << std::format(
"{:.8e} q_ratio_per_A\n", qRatio);
738 out << std::format(
"{:.8e} rate_ohtst_per_s\n", kSI);
739 out << std::format(
"{:.4f} temperature_K\n", temperature);
743 throw std::runtime_error(
"oh_tst: failed to write results.dat");
745 returnFiles.push_back(
"results.dat");
747 returnFiles.push_back(
"oh_tst_progression.dat");
749 EONC_LOG_INFO(
"[oh_tst] {} after {} planes: A = {:.4f} eV at s/L = {:.4f}, "
750 "k = {:.4e} 1/s at {:.1f} K",
751 converged ?
"converged" :
"max planes", planesUsed, aBest,
752 sBest / guideLen, kSI, temperature);