126 const VectorXd &gamma,
127 const VectorXd &normal) {
128 const long equilSteps =
params.oh_tst_options.equil_steps;
129 const long sampleSteps =
params.oh_tst_options.sample_steps;
130 const double alphaRot =
params.oh_tst_options.alpha_rot;
134 x -= normal * normal.dot(x - gamma);
137 VectorXd v(x.size());
143 std::unique_ptr<GleThermostat> gle;
144 if (
params.oh_tst_options.thermostat ==
"gle") {
147 gle = std::make_unique<GleThermostat>(a,
m_kbt, 0.5 *
m_dt, x.size());
150 params.oh_tst_options.gle_a_file);
151 throw std::runtime_error(
"oh_tst: gle thermostat unusable");
154 const auto gauss = [
this]() {
return gaussDraw(); };
157 VectorXd fPlane = f - normal * normal.dot(f);
160 avg.
rotNorm = VectorXd::Zero(x.size());
161 avg.
rotRaw = VectorXd::Zero(x.size());
162 avg.
pos = VectorXd::Zero(x.size());
166 for (
long step = 0; step < equilSteps + sampleSteps; ++step) {
173 v -= normal * normal.dot(v);
175 VectorXd a = fPlane.cwiseQuotient(
m_masses3N);
177 x -= normal * normal.dot(x - gamma);
180 fPlane = f - normal * normal.dot(f);
181 VectorXd aNew = fPlane.cwiseQuotient(
m_masses3N);
182 v += 0.5 *
m_dt * (a + aNew);
183 v -= normal * normal.dot(v);
186 x -= normal * normal.dot(x - gamma);
189 fPlane = f - normal * normal.dot(f);
193 v -= normal * normal.dot(v);
198 if (step >= equilSteps) {
199 const double fn = normal.dot(f);
200 const VectorXd arm = x - gamma;
201 const double arm2 = arm.squaredNorm();
204 avg.
rotNorm.noalias() += (fn / (alphaRot * arm2)) * arm;
206 avg.
rotRaw.noalias() += fn * arm;
207 avg.
pos.noalias() += x;
212 const double inv = 1.0 /
static_cast<double>(nAccum);
282 auto reactant = std::make_shared<Matter>(
pot,
params);
283 auto product = std::make_shared<Matter>(
pot,
params);
285 reactant->con2matter(
params.oh_tst_options.reactant_filename))) {
287 params.oh_tst_options.reactant_filename);
288 throw std::runtime_error(
"oh_tst: failed to load reactant");
291 product->con2matter(
params.oh_tst_options.product_filename))) {
293 params.oh_tst_options.product_filename);
294 throw std::runtime_error(
"oh_tst: failed to load product");
297 const double temperature =
params.main_options.temperature;
299 params.oh_tst_options.thermostat ==
"gle"
300 ? std::string(
" (drift: ") +
301 params.oh_tst_options.gle_a_file +
")"
306 ?
params.main_options.randomSeed
310 params.thermostat_options.andersen_tcol_input /
params.constants.timeUnit;
314 const long nAtoms = reactant->numberOfAtoms();
315 auto masses = reactant->getMasses();
316 std::vector<double> m3;
317 m3.reserve(3 * nAtoms);
318 for (
long i = 0; i < nAtoms; ++i) {
319 if (!reactant->getFixed(i)) {
320 for (
int j = 0; j < 3; ++j)
321 m3.push_back(masses[i]);
324 m_masses3N = VectorXd::Map(m3.data(),
static_cast<long>(m3.size()));
328 const VectorXd xR = reactant->getPositionsFreeV();
329 VectorXd diff = product->getPositionsFreeV() - xR;
331 AtomMatrix d(AtomMatrix::Map(diff.data(), diff.size() / 3, 3));
337 Eigen::RowVector3d total_drift = Eigen::RowVector3d::Zero();
338 for (
int pass = 0; pass < 6; ++pass) {
339 d = reactant->pbc(d);
340 const Eigen::RowVector3d drift =
341 d.colwise().sum() /
static_cast<double>(d.rows());
342 d.rowwise() -= drift;
343 total_drift += drift;
344 if (drift.norm() < 1e-6) {
348 EONC_LOG_INFO(
"[oh_tst] rigid drift removed: ({:.4f}, {:.4f}, "
349 "{:.4f}) A per atom",
350 total_drift[0], total_drift[1], total_drift[2]);
351 diff = VectorXd::Map(d.data(), diff.size());
353 const double guideLen = diff.norm();
354 EONC_LOG_INFO(
"[oh_tst] guideline length |P - R| = {:.4f} A over {} free "
356 guideLen, xR.size());
360 if (guideLen < 0.5) {
362 "translation-class endpoint pair",
364 throw std::runtime_error(
"oh_tst: degenerate guideline");
366 const VectorXd u = diff / guideLen;
373 if (!
params.oh_tst_options.symmetry_products.empty()) {
374 std::string rest =
params.oh_tst_options.symmetry_products;
375 while (!rest.empty()) {
376 const auto comma = rest.find(
',');
377 std::string fname = rest.substr(0, comma);
378 rest = (comma == std::string::npos) ?
"" : rest.substr(comma + 1);
385 throw std::runtime_error(
"oh_tst: failed to load symmetry product");
388 AtomMatrix dm(AtomMatrix::Map(d.data(), d.size() / 3, 3));
389 dm = reactant->pbc(dm);
390 d = VectorXd::Map(dm.data(), d.size());
391 const double dn = d.norm();
396 EONC_LOG_INFO(
"[oh_tst] symmetry restriction active over {} product "
404 double s =
params.oh_tst_options.s_init * guideLen;
407 VectorXd omega = VectorXd::Zero(n.size());
408 const double mS =
params.oh_tst_options.plane_mass;
409 const double dtPlane =
params.oh_tst_options.plane_time_step;
410 const double dsMax =
params.oh_tst_options.ds_max;
411 const double dThetaMax =
params.oh_tst_options.dtheta_max;
412 const double fTol =
params.oh_tst_options.force_tol;
418 double aTrans = 0.0, aRot = 0.0, aBest = 0.0, sBest = s;
420 bool havePrev =
false;
422 VectorXd rotRawPrev, posPrev, nPrev;
429 std::ofstream prog(
"oh_tst_progression.dat");
431 prog <<
"# plane s/L <F.n> (eV/A) dA_trans (eV) dA_rot (eV) "
437 const bool scanMode =
params.oh_tst_options.pmf_scan;
438 const long nScan = std::max(2L,
params.oh_tst_options.scan_planes);
439 const double dsScan = scanMode ? (guideLen / (double)(nScan - 1)) : 0.0;
440 const long nPlanes = scanMode ? nScan :
params.oh_tst_options.max_planes;
442 bool converged =
false;
448 bool guidelineMoving =
false;
449 VectorXd gOrigin = xR;
451 long rotOnlySteps = 0;
452 for (; plane < nPlanes; ++plane) {
453 const VectorXd gamma = gOrigin + s * gDir;
460 const double gS = -avg.
fn / mS;
464 sideSign = (avg.
fn < 0.0) ? -1 : 1;
465 }
else if (!guidelineMoving && ((avg.
fn < 0.0) ? -1 : 1) != sideSign) {
466 guidelineMoving =
true;
468 "sign; guideline now follows <r> along the normal",
481 const bool sameSide = scanMode || ((fnPrev < 0.0 ? -1 : 1) == sideSign &&
482 (avg.
fn < 0.0 ? -1 : 1) == sideSign);
484 const VectorXd fParMean = 0.5 * (fnPrev * nPrev + avg.
fn * n);
485 aTrans += -fParMean.dot(avg.
pos - posPrev);
486 const VectorXd rotMean = 0.5 * (rotRawPrev + avg.
rotRaw);
487 aRot += rotMean.dot(n - nPrev);
491 const double aTotal = aTrans + aRot;
492 if (aTotal > aBest) {
498 prog << std::format(
"{:6} {:10.6f} {:14.6e} {:12.6f} {:12.6f} {:12.6f} "
500 plane, s / guideLen, avg.
fn, aTrans, aRot, aTotal,
504 EONC_LOG_DEBUG(
"[oh_tst] plane {} s/L {:.4f} <F.n> {:.4e} A {:.4f} eV",
505 plane, s / guideLen, avg.
fn, aTotal);
511 if (aTotal >
params.oh_tst_options.max_delta_a) {
513 "[oh_tst] accumulated work {:.2f} eV exceeds max_delta_a "
514 "{:.2f} eV at plane {} -- endpoints likely unminimized",
515 aTotal,
params.oh_tst_options.max_delta_a, plane);
516 throw std::runtime_error(
"oh_tst: diverging reversible work");
523 if (!scanMode && plane > 2 && aTotal > 2.0 *
m_kbt &&
524 std::fabs(avg.
fn) < fTol &&
525 gRot.norm() *
params.oh_tst_options.alpha_rot < fTol) {
533 prog << std::format(
"# converged at plane {}\n", plane);
550 s = (double)(plane + 1) * dsScan;
551 const VectorXd gammaNext = xR + s * u;
553 xStart -= u * (u.dot(xStart - gammaNext));
565 const bool rotationOnly =
567 gRot.norm() *
params.oh_tst_options.alpha_rot > 5.0 * fTol &&
575 vS += 0.5 * dtPlane * (gS + gSPrev);
581 double ds = dtPlane * vS + 0.5 * dtPlane * dtPlane * gS;
582 ds = std::clamp(ds, -dsMax, dsMax);
588 guidelineMoving ? s + ds : std::clamp(s + ds, 0.0, guideLen);
593 if (havePrev && gRotPrev.size() == gRot.size()) {
594 omega += 0.5 * dtPlane * (gRot + gRotPrev);
596 omega += dtPlane * gRot;
598 omega -= n * n.dot(omega);
599 const double gNorm = gRot.norm();
601 const VectorXd gHat = gRot / gNorm;
602 const double along = omega.dot(gHat);
604 omega = gHat * along;
611 VectorXd dn = dtPlane * omega + 0.5 * dtPlane * dtPlane * gRot;
612 const double dTheta = dn.norm();
613 if (dTheta > dThetaMax) {
614 dn *= dThetaMax / dTheta;
616 const VectorXd nOld = n;
617 n = (n + dn).normalized();
618 if (n.dot(gDir) < 0.0) {
626 VectorXd arm = avg.
pos - gamma;
627 VectorXd armNew = arm - nOld * arm.dot(n - nOld);
628 const double armLen = arm.norm();
629 const double armNewLen = armNew.norm();
630 if (armLen > 1e-12 && armNewLen > 1e-12) {
631 armNew *= armLen / armNewLen;
634 if (guidelineMoving) {
644 gammaNew = gOrigin + s * gDir;
646 gammaNew = gOrigin + sNew * gDir;
648 VectorXd xStart = gammaNew + armNew;
649 xStart -= n * n.dot(xStart - gammaNew);
659 if (!guidelineMoving) {
663 bool progOk = prog.is_open();
666 progOk =
static_cast<bool>(prog);
668 EONC_LOG_ERROR(
"[oh_tst] failed to write oh_tst_progression.dat");
674 if (scanMode && plane >= nPlanes)
679 const double mu = (
m_masses3N.array() * nBest.array().square()).sum();
684 Matter rWalker(*reactant);
685 const VectorXd gammaR = xR + (
params.oh_tst_options.s_init * guideLen) * u;
689 const double kInternal = vFlux * qRatio * std::exp(-aBest /
m_kbt);
690 const double kSI = kInternal / (
params.constants.timeUnit * 1.0e-15);
692 std::vector<std::string> returnFiles;
693 std::ofstream out(
"results.dat");
696 throw std::runtime_error(
"oh_tst: cannot open results.dat");
698 out <<
"oh_tst job_type\n";
699 out << std::format(
"{} converged\n", converged ? 1 : 0);
700 out << std::format(
"{} planes_used\n", plane);
701 out << std::format(
"{:.8f} free_energy_barrier_eV\n", aBest);
702 out << std::format(
"{:.8f} delta_a_trans_eV\n", aTrans);
703 out << std::format(
"{:.8f} delta_a_rot_eV\n", aRot);
704 out << std::format(
"{:.8f} s_star_over_L\n", sBest / guideLen);
705 out << std::format(
"{:.8f} guideline_length_A\n", guideLen);
706 out << std::format(
"{:.8f} normal_overlap_with_guideline\n", nBest.dot(u));
707 out << std::format(
"{:.8e} effective_mass_amu\n", mu);
708 out << std::format(
"{:.8e} q_ratio_per_A\n", qRatio);
709 out << std::format(
"{:.8e} rate_ohtst_per_s\n", kSI);
710 out << std::format(
"{:.4f} temperature_K\n", temperature);
714 throw std::runtime_error(
"oh_tst: failed to write results.dat");
716 returnFiles.push_back(
"results.dat");
718 returnFiles.push_back(
"oh_tst_progression.dat");
720 EONC_LOG_INFO(
"[oh_tst] {} after {} planes: A = {:.4f} eV at s/L = {:.4f}, "
721 "k = {:.4e} 1/s at {:.1f} K",
722 converged ?
"converged" :
"max planes", plane, aBest,
723 sBest / guideLen, kSI, temperature);