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

#include <OHTSTJob.h>

Inheritance diagram for eonc::OHTSTJob:

Classes

struct  PlaneAverages

Public Member Functions

 OHTSTJob (std::unique_ptr< Parameters > parameters, Runtime &rt)
 ~OHTSTJob (void)=default
std::vector< std::string > run (void)
 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.

Private Member Functions

PlaneAverages samplePlane (Matter &matter, VectorXd &x, const VectorXd &gamma, const VectorXd &normal)
double reactantQRatio (Matter &matter, const VectorXd &gammaR, const VectorXd &normal)
void drawThermalVelocities (VectorXd &vel, const VectorXd *normal)
bool symmetryReflect (const VectorXd &xR, VectorXd &x, VectorXd &v, const VectorXd &xOld, const VectorXd *normal)
double uniformDraw ()
 uniform (0,1)
double gaussDraw ()
 standard normal (Box-Muller)

Private Attributes

std::vector< VectorXd > m_symDirs
 p-hat_i, index 0 = primary
VectorXd m_symXR
 reactant anchor R of the half-lines
VectorXd m_masses3N
 per-DOF masses of the free atoms (amu)
double m_dt {0.0}
 integration step, internal units
double m_kbt {0.0}
 k_B T (eV)
double m_andersenProb {0.0}
 per-step Andersen collision probability
long m_seedState {12345}
 LCG state for the thermostat draws.
bool m_gaussHave {false}
double m_gaussSpare {0.0}

Friends

struct ::tests::OHTSTPlaneWrapTest
struct OHTSTSymmetryTest

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

Definition at line 51 of file OHTSTJob.h.

Constructor & Destructor Documentation

◆ OHTSTJob()

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

Definition at line 54 of file OHTSTJob.h.

55 : 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

◆ ~OHTSTJob()

eonc::OHTSTJob::~OHTSTJob ( void )
default

Member Function Documentation

◆ drawThermalVelocities()

void eonc::OHTSTJob::drawThermalVelocities ( VectorXd & vel,
const VectorXd * normal )
private

Maxwell-Boltzmann draw on the free DOF, then projection onto the plane (n.v = 0) when a normal is supplied.

Definition at line 94 of file OHTSTJob.cpp.

94 {
95 for (long k = 0; k < vel.size(); ++k) {
96 vel[k] = std::sqrt(m_kbt / m_masses3N[k]) * gaussDraw();
97 }
98 if (normal != nullptr) {
99 vel -= (*normal) * normal->dot(vel);
100 }
101}
VectorXd m_masses3N
per-DOF masses of the free atoms (amu)
Definition OHTSTJob.h:98
double gaussDraw()
standard normal (Box-Muller)
Definition OHTSTJob.cpp:58
double m_kbt
k_B T (eV)
Definition OHTSTJob.h:100

◆ gaussDraw()

double eonc::OHTSTJob::gaussDraw ( )
private

standard normal (Box-Muller)

Definition at line 58 of file OHTSTJob.cpp.

58 {
59 if (m_gaussHave) {
60 m_gaussHave = false;
61 return m_gaussSpare;
62 }
63 double u1 = std::max(uniformDraw(), 1e-12);
64 double u2 = uniformDraw();
65 double r = std::sqrt(-2.0 * std::log(u1));
66 m_gaussSpare = r * std::sin(2.0 * helpers::pi * u2);
67 m_gaussHave = true;
68 return r * std::cos(2.0 * helpers::pi * u2);
69}
double uniformDraw()
uniform (0,1)
Definition OHTSTJob.cpp:51
double m_gaussSpare
Definition OHTSTJob.h:106
constexpr double pi

◆ reactantQRatio()

double eonc::OHTSTJob::reactantQRatio ( Matter & matter,
const VectorXd & gammaR,
const VectorXd & normal )
private

Eq 22: Q^ZR/Q^R from crossing statistics of an unconstrained reactant-basin trajectory through the plane (gammaR, normal).

Definition at line 247 of file OHTSTJob.cpp.

248 {
249 // Eq 22: Q^ZR/Q^R = (dt / t_tot) * sum over plane crossings of
250 // 1 / |(r_{i+1} - r_i).n|. The trajectory is unconstrained,
251 // thermostatted, and stays in the reactant basin by construction
252 // (it starts there and the barrier is >> kT).
253 const long steps = params.oh_tst_options().reactant_md_steps;
254 const long equilSteps = params.oh_tst_options().equil_steps;
255 VectorXd x = matter.getPositionsFreeV();
256 VectorXd v(x.size());
257 drawThermalVelocities(v, nullptr);
258 std::unique_ptr<GleThermostat> gle;
259 if (params.oh_tst_options().thermostat == "gle") {
260 const MatrixXd a =
261 GleThermostat::loadDriftMatrix(params.oh_tst_options().gle_a_file);
262 gle = std::make_unique<GleThermostat>(a, m_kbt, 0.5 * m_dt, x.size());
263 if (!gle->valid()) {
264 throw std::runtime_error("oh_tst: gle thermostat unusable");
265 }
266 }
267 const auto gauss = [this]() { return gaussDraw(); };
268 VectorXd f = matter.getForcesFreeV();
269 double side = normal.dot(x - gammaR);
270 double crossingSum = 0.0;
271 long counted = 0;
272 for (long step = 0; step < equilSteps + steps; ++step) {
273 const VectorXd xOld = x;
274 if (gle) {
275 gle->apply(v, m_masses3N, gauss);
276 }
277 VectorXd a = f.cwiseQuotient(m_masses3N);
278 x += m_dt * v + 0.5 * m_dt * m_dt * a;
279 matter.setPositionsFreeV(x);
280 f = matter.getForcesFreeV();
281 VectorXd aNew = f.cwiseQuotient(m_masses3N);
282 v += 0.5 * m_dt * (a + aNew);
283 if (symmetryReflect(m_symXR, x, v, xOld, nullptr)) {
284 matter.setPositionsFreeV(x);
285 f = matter.getForcesFreeV();
286 }
287 if (gle) {
288 gle->apply(v, m_masses3N, gauss);
289 } else if (uniformDraw() < m_andersenProb) {
290 drawThermalVelocities(v, nullptr);
291 }
292 const double sideNew = normal.dot(x - gammaR);
293 if (step >= equilSteps) {
294 if (side * sideNew < 0.0) {
295 const double proj = std::fabs(normal.dot(x - xOld));
296 if (proj > 1e-14) {
297 crossingSum += 1.0 / proj;
298 }
299 }
300 ++counted;
301 }
302 side = sideNew;
303 }
304 return counted > 0 ? crossingSum / static_cast<double>(counted) : 0.0;
305}
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
static MatrixXd loadDriftMatrix(const std::string &path)
Load a drift matrix from a gle4md-layout text file ('#' starts a comment).
Parameters params
Definition Job.h:58
double m_andersenProb
per-step Andersen collision probability
Definition OHTSTJob.h:101
void drawThermalVelocities(VectorXd &vel, const VectorXd *normal)
Definition OHTSTJob.cpp:94
double m_dt
integration step, internal units
Definition OHTSTJob.h:99
VectorXd m_symXR
reactant anchor R of the half-lines
Definition OHTSTJob.h:96
bool symmetryReflect(const VectorXd &xR, VectorXd &x, VectorXd &v, const VectorXd &xOld, const VectorXd *normal)
Definition OHTSTJob.cpp:103

◆ run()

std::vector< std::string > eonc::OHTSTJob::run ( void )
virtual

Virtual run; used solely for dynamic dispatch.

Implements eonc::Job.

Definition at line 307 of file OHTSTJob.cpp.

307 {
308 auto reactant = std::make_shared<Matter>(pot, params);
309 auto product = std::make_shared<Matter>(pot, params);
310 if (!eonc::io::io_ok(
311 reactant->con2matter(params.oh_tst_options().reactant_filename))) {
312 EONC_LOG_CRITICAL("OH-TST failed to load {}",
313 params.oh_tst_options().reactant_filename);
314 throw std::runtime_error("oh_tst: failed to load reactant");
315 }
316 if (!eonc::io::io_ok(
317 product->con2matter(params.oh_tst_options().product_filename))) {
318 EONC_LOG_CRITICAL("OH-TST failed to load {}",
319 params.oh_tst_options().product_filename);
320 throw std::runtime_error("oh_tst: failed to load product");
321 }
322
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 + ")"
328 : std::string());
329 m_kbt = params.constants().kB * temperature;
330 m_dt = params.oh_tst_options().time_step / params.constants().timeUnit;
331 m_seedState = (params.main_options().randomSeed > 0)
332 ? params.main_options().randomSeed
333 : 12345;
334 // Per-step collision probability from the Andersen collision period.
335 const double tcol = params.thermostat_options().andersen_tcol_input /
336 params.constants().timeUnit;
337 m_andersenProb = (tcol > 0.0) ? std::min(1.0, m_dt / tcol) : 0.1;
338
339 // Free-DOF mass vector (amu per coordinate).
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]);
348 }
349 }
350 m_masses3N = VectorXd::Map(m3.data(), static_cast<long>(m3.size()));
351
352 // Straight guideline in 3N space, minimum-image on the endpoint
353 // difference so the plane never strides a cell boundary.
354 const VectorXd xR = reactant->getPositionsFreeV();
355 VectorXd diff = product->getPositionsFreeV() - xR;
356 {
357 AtomMatrix d(AtomMatrix::Map(diff.data(), diff.size() / 3, 3));
358 Eigen::RowVector3d total_drift = Eigen::RowVector3d::Zero();
359 d = minImageRemoveRigidDrift(*reactant, std::move(d), &total_drift);
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());
364 }
365 const double guideLen = diff.norm();
366 EONC_LOG_INFO("[oh_tst] guideline length |P - R| = {:.4f} A over {} free "
367 "DOF",
368 guideLen, xR.size());
369 // A sub-Angstrom guideline is an on-site shuffle (dumbbell rotation,
370 // flicker partner): the plane progression has no room to climb and
371 // the run would "converge" at s = 0 measuring nothing.
372 if (guideLen < 0.5) {
373 EONC_LOG_CRITICAL("[oh_tst] guideline too short ({:.4f} A) -- pick a "
374 "translation-class endpoint pair",
375 guideLen);
376 throw std::runtime_error("oh_tst: degenerate guideline");
377 }
378 const VectorXd u = diff / guideLen;
379
380 // Eqs 13-14: unit vectors to every symmetry-equivalent product;
381 // index 0 is the primary product the guideline points to.
382 m_symXR = xR;
383 m_symDirs.clear();
384 m_symDirs.push_back(u);
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);
391 if (fname.empty()) {
392 continue;
393 }
394 Matter other(pot, params);
395 if (!eonc::io::io_ok(other.con2matter(fname))) {
396 EONC_LOG_CRITICAL("OH-TST failed to load symmetry product {}", fname);
397 throw std::runtime_error("oh_tst: failed to load symmetry product");
398 }
399 VectorXd d = other.getPositionsFreeV() - xR;
400 AtomMatrix dm(AtomMatrix::Map(d.data(), d.size() / 3, 3));
401 dm = minImageRemoveRigidDrift(*reactant, std::move(dm), nullptr);
402 d = VectorXd::Map(dm.data(), d.size());
403 const double dn = d.norm();
404 if (dn > 1e-8) {
405 m_symDirs.push_back(d / dn);
406 }
407 }
408 EONC_LOG_INFO("[oh_tst] symmetry restriction active over {} product "
409 "directions",
410 m_symDirs.size());
411 }
412
413 // Plane state: progression coordinate s, normal n, their conjugate
414 // velocities, and the previous-iteration driving forces for the
415 // two-force velocity Verlet updates (Eqs 6-7 and 9-10).
416 double s = params.oh_tst_options().s_init * guideLen;
417 double vS = 0.0;
418 VectorXd n = u;
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;
425
426 Matter walker(*reactant);
427 // Carry the unwrapped free coordinates. The next plane must not reload
428 // them after setPositionsFreeV has wrapped the configuration.
429 VectorXd xFree = walker.getPositionsFreeV();
430
431 // Reversible-work accumulators and the previous plane's averages
432 // for the trapezoid rules of Eqs 18-19.
433 double aTrans = 0.0, aRot = 0.0, aBest = 0.0, sBest = s;
434 VectorXd nBest = n;
435 bool havePrev = false;
436 double fnPrev = 0.0;
437 VectorXd rotRawPrev, posPrev, nPrev;
438 double gSPrev = 0.0;
439 VectorXd gRotPrev;
440 int sideSign = 0; // sign of <F.n> at plane 1 (grooming, Sec IIE)
441
442 // RAII: the divergence guard and samplePlane both throw out of the plane
443 // loop below, so the handle has to close itself.
444 std::ofstream prog("oh_tst_progression.dat");
445 if (prog) {
446 prog << "# plane s/L <F.n> (eV/A) dA_trans (eV) dA_rot (eV) "
447 "A (eV) n.u\n";
448 } else {
449 EONC_LOG_ERROR("[oh_tst] cannot open oh_tst_progression.dat");
450 }
451
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;
455 // Adaptive runs start at s_init, off the force-free reactant. A PMF
456 // scan is a uniform reactant->product grid, so plane 0 is s = 0.
457 if (scanMode) {
458 s = pmfScanS(0, nScan, guideLen);
459 sBest = s;
460 }
461 long plane = 0;
462 bool converged = false;
463 // Sec IIC guideline refinement: after the translational force first
464 // changes sign the guideline is re-anchored every step through the
465 // previous plane's average position along the current normal, so a
466 // small rotational force no longer demands a huge s move (the
467 // failure mode that made small-inertia mechanism discovery diverge).
468 bool guidelineMoving = false;
469 VectorXd gOrigin = xR;
470 VectorXd gDir = u;
471 long rotOnlySteps = 0;
472 for (; plane < nPlanes; ++plane) {
473 if (scanMode) {
474 s = pmfScanS(plane, nScan, guideLen);
475 }
476 const VectorXd gamma = gOrigin + s * gDir;
477 PlaneAverages avg = samplePlane(walker, xFree, gamma, n);
478
479 // Driving forces: translation climbs against <F.n> (Eq 5 with the
480 // reversed-force convention); the normal is driven along
481 // +<(n.F) R/(alpha |R|^2)> (Appendix A, Eq A1), projected onto
482 // the tangent space of the unit sphere.
483 const double gS = -avg.fn / mS;
484 VectorXd gRot = avg.rotNorm - n * n.dot(avg.rotNorm);
485
486 if (plane == 0) {
487 sideSign = (avg.fn < 0.0) ? -1 : 1;
488 } else if (!guidelineMoving && ((avg.fn < 0.0) ? -1 : 1) != sideSign) {
489 guidelineMoving = true;
490 EONC_LOG_DEBUG("[oh_tst] plane {}: translational force changed "
491 "sign; guideline now follows <r> along the normal",
492 plane);
493 }
494
495 // Reversible work (Eqs 18-19), trapezoid between consecutive
496 // planes: the translational path is the piecewise-linear track of
497 // the average configuration <r>, and the rotational work pairs
498 // the unnormalized <(n.F) R> with the actual change of the
499 // normal. Grooming (Sec IIE): only planes on the reactant side of
500 // the ridge (same sign of <F.n> as plane 1) contribute.
501 if (havePrev) {
502 // Scan mode integrates the whole reactant->product path; the
503 // adaptive run grooms to the reactant side of the ridge only.
504 const bool sameSide = scanMode || ((fnPrev < 0.0 ? -1 : 1) == sideSign &&
505 (avg.fn < 0.0 ? -1 : 1) == sideSign);
506 if (sameSide) {
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);
511 }
512 }
513
514 const double aTotal = aTrans + aRot;
515 if (aTotal > aBest) {
516 aBest = aTotal;
517 sBest = s;
518 nBest = n;
519 }
520 if (prog) {
521 prog << std::format("{:6} {:10.6f} {:14.6e} {:12.6f} {:12.6f} {:12.6f} "
522 "{:10.6f}\n",
523 plane, s / guideLen, avg.fn, aTrans, aRot, aTotal,
524 n.dot(u));
525 prog.flush();
526 }
527 EONC_LOG_DEBUG("[oh_tst] plane {} s/L {:.4f} <F.n> {:.4e} A {:.4f} eV",
528 plane, s / guideLen, avg.fn, aTotal);
529
530 // Divergence guard: reversible work beyond any physical barrier
531 // means the endpoints were not minimized (static relaxation
532 // forces leak into <F.n>) or the plane is chasing a drifting
533 // ensemble; 300 planes of that is pure waste.
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");
540 }
541 // A stationary plane only counts as the variational maximum
542 // after the progression has actually climbed: the reactant basin
543 // bottom is also force-free, and a short guideline puts the first
544 // plane inside it. Two thermal energies of accumulated reversible
545 // work is the cheapest evidence of a ridge.
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) {
549 converged = true;
550 // The converged plane is the optimal one even if sampling noise
551 // put an earlier plane marginally higher.
552 aBest = aTotal;
553 sBest = s;
554 nBest = n;
555 if (prog) {
556 prog << std::format("# converged at plane {}\n", plane);
557 }
558 break;
559 }
560
561 if (scanMode) {
562 // Fixed-increment PMF scan: normal stays along the guideline,
563 // s advances uniformly, and the walker is projected onto the
564 // next constraint plane for samplePlane to re-equilibrate. The
565 // A(s) profile is the PMF; aBest already tracks its maximum.
566 fnPrev = avg.fn;
567 nPrev = n;
568 rotRawPrev = avg.rotRaw;
569 posPrev = avg.pos;
570 gSPrev = gS;
571 gRotPrev = gRot;
572 havePrev = true;
573 s = pmfScanS(plane + 1, nScan, guideLen);
574 const VectorXd gammaNext = xR + s * u;
575 xFree -= u * (u.dot(xFree - gammaNext));
576 walker.setPositionsFreeV(xFree);
577 continue;
578 }
579 // Damped two-force Verlet on s (Eqs 6-7): the velocity is zeroed
580 // when it opposes the driving force, so the plane settles at the
581 // free-energy maximum instead of oscillating over it ("the
582 // velocity of the plane is zeroed if the plane has gone past the
583 // maximum free energy position").
584 // Pure-rotation step (Sec IIC): right after a guideline change a
585 // spiked rotational force is relaxed at fixed s before the plane
586 // translates again.
587 const bool rotationOnly =
588 guidelineMoving &&
589 gRot.norm() * params.oh_tst_options().alpha_rot > 5.0 * fTol &&
590 rotOnlySteps < 10;
591 if (rotationOnly) {
592 ++rotOnlySteps;
593 } else {
594 rotOnlySteps = 0;
595 }
596 if (havePrev) {
597 vS += 0.5 * dtPlane * (gS + gSPrev);
598 } else {
599 vS += dtPlane * gS;
600 }
601 if (vS * gS < 0.0)
602 vS = 0.0;
603 double ds = dtPlane * vS + 0.5 * dtPlane * dtPlane * gS;
604 ds = std::clamp(ds, -dsMax, dsMax);
605 if (rotationOnly) {
606 ds = 0.0;
607 vS = 0.0;
608 }
609 const double sNew =
610 guidelineMoving ? s + ds : std::clamp(s + ds, 0.0, guideLen);
611
612 // Damped rotation (Eqs 9-10 with the Appendix A driving): the
613 // angular velocity keeps only its projection along the current
614 // driving force while aligned with it, and is zeroed otherwise.
615 if (havePrev && gRotPrev.size() == gRot.size()) {
616 omega += 0.5 * dtPlane * (gRot + gRotPrev);
617 } else {
618 omega += dtPlane * gRot;
619 }
620 omega -= n * n.dot(omega);
621 const double gNorm = gRot.norm();
622 if (gNorm > 1e-14) {
623 const VectorXd gHat = gRot / gNorm;
624 const double along = omega.dot(gHat);
625 if (along > 0.0) {
626 omega = gHat * along;
627 } else {
628 omega.setZero();
629 }
630 } else {
631 omega.setZero();
632 }
633 VectorXd dn = dtPlane * omega + 0.5 * dtPlane * dtPlane * gRot;
634 const double dTheta = dn.norm();
635 if (dTheta > dThetaMax) {
636 dn *= dThetaMax / dTheta;
637 }
638 const VectorXd nOld = n;
639 n = (n + dn).normalized();
640 if (n.dot(gDir) < 0.0) {
641 // Keep the normal pointing towards increasing s.
642 n = -n;
643 }
644
645 // Eq 11-12 restart: place the walker in the new plane at the
646 // rotated image of the average arm, rescaled so rotation does not
647 // change the arm length.
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;
654 }
655 VectorXd gammaNew;
656 if (guidelineMoving) {
657 // Re-anchor: the guideline passes through the previous plane's
658 // average position along the CURRENT normal, and the plane
659 // ADVANCES by this iteration's ds along the fresh line. (A
660 // dead re-assignment here used to pin the plane at <r> forever:
661 // the walker drifted downhill and the trans-work integral ran
662 // away by ~20 eV per plane on the Cu doorway.)
663 gOrigin = avg.pos;
664 gDir = n;
665 s = ds;
666 gammaNew = gOrigin + s * gDir;
667 } else {
668 gammaNew = gOrigin + sNew * gDir;
669 }
670 xFree = gammaNew + armNew;
671 xFree -= n * n.dot(xFree - gammaNew);
672 walker.setPositionsFreeV(xFree);
673
674 fnPrev = avg.fn;
675 nPrev = nOld;
676 rotRawPrev = avg.rotRaw;
677 posPrev = avg.pos;
678 gSPrev = gS;
679 gRotPrev = gRot;
680 havePrev = true;
681 if (!guidelineMoving) {
682 s = sNew;
683 }
684 }
685 bool progOk = prog.is_open();
686 if (progOk) {
687 prog.close();
688 progOk = static_cast<bool>(prog);
689 if (!progOk) {
690 EONC_LOG_ERROR("[oh_tst] failed to write oh_tst_progression.dat");
691 }
692 }
693 // A completed scan yields a valid PMF even with no interior
694 // maximum; report it as converged so downstream tooling accepts
695 // the profile (barrier = aBest, the max of A(s)).
696 if (scanMode && plane >= nPlanes)
697 converged = true;
698
699 // `break` leaves `plane` at the 0-based index of the plane just
700 // sampled. A finished loop leaves it at nPlanes. planes_used is
701 // the number of planes sampled in either case.
702 const long planesUsed = (plane < nPlanes) ? plane + 1 : plane;
703
704 // Direction-dependent effective mass (Eq 24) and the one-sided
705 // thermal flux factor sqrt(kBT / 2 pi mu) (Eq 23).
706 const double mu = (m_masses3N.array() * nBest.array().square()).sum();
707 const double vFlux = std::sqrt(m_kbt / (2.0 * helpers::pi * mu));
708
709 // Q^ZR/Q^R at the FIRST plane of the progression (Z^R), whose
710 // reversible work to the optimal plane is what aBest measures.
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;
715 const double qRatio = reactantQRatio(rWalker, gammaR, u);
716
717 // Rate in internal units (1/internal-time), then SI.
718 const double kInternal = vFlux * qRatio * std::exp(-aBest / m_kbt);
719 const double kSI = kInternal / (params.constants().timeUnit * 1.0e-15);
720
721 std::vector<std::string> returnFiles;
722 std::ofstream out("results.dat");
723 if (!out) {
724 EONC_LOG_ERROR("[oh_tst] cannot open results.dat");
725 throw std::runtime_error("oh_tst: cannot open results.dat");
726 }
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);
740 out.close();
741 if (!out) {
742 EONC_LOG_ERROR("[oh_tst] failed to write results.dat");
743 throw std::runtime_error("oh_tst: failed to write results.dat");
744 }
745 returnFiles.push_back("results.dat");
746 if (progOk) {
747 returnFiles.push_back("oh_tst_progression.dat");
748 }
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);
753 return returnFiles;
754}
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
#define EONC_LOG_DEBUG(...)
Definition EonLogger.h:243
#define EONC_LOG_ERROR(...)
Definition EonLogger.h:261
#define EONC_LOG_INFO(...)
Definition EonLogger.h:249
#define EONC_LOG_CRITICAL(...)
Definition EonLogger.h:267
std::shared_ptr< Potential > pot
Definition Job.h:63
double reactantQRatio(Matter &matter, const VectorXd &gammaR, const VectorXd &normal)
Definition OHTSTJob.cpp:247
PlaneAverages samplePlane(Matter &matter, VectorXd &x, const VectorXd &gamma, const VectorXd &normal)
Definition OHTSTJob.cpp:149
long m_seedState
LCG state for the thermostat draws.
Definition OHTSTJob.h:102
std::vector< VectorXd > m_symDirs
p-hat_i, index 0 = primary
Definition OHTSTJob.h:95
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
AtomMatrix minImageRemoveRigidDrift(const Matter &reference, AtomMatrix diff, Eigen::RowVector3d *totalDrift)
Minimum-image a free-atom difference and remove rigid translation.
Definition OHTSTJob.cpp:71
double pmfScanS(long plane, long nScan, double guideLen)
Uniform PMF-scan coordinate.
Definition OHTSTJob.h:26

◆ samplePlane()

OHTSTJob::PlaneAverages eonc::OHTSTJob::samplePlane ( Matter & matter,
VectorXd & x,
const VectorXd & gamma,
const VectorXd & normal )
private

Definition at line 149 of file OHTSTJob.cpp.

151 {
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;
155
156 // Constrain the unwrapped free coordinates onto the plane. Matter
157 // stores the periodic image: a coordinate just outside the cell comes
158 // back from getPositionsFreeV shifted by a lattice vector, and
159 // projecting that image lands on a different 3N point.
160 x -= normal * normal.dot(x - gamma);
161 matter.setPositionsFreeV(x);
162
163 VectorXd v(x.size());
164 drawThermalVelocities(v, &normal);
165
166 // Colored-noise option: an exact OU half-step before and after each
167 // Verlet step (the auxiliary momenta start fresh per sampling
168 // block); velocities re-project onto the plane after every kick.
169 std::unique_ptr<GleThermostat> gle;
170 if (params.oh_tst_options().thermostat == "gle") {
171 const MatrixXd a =
172 GleThermostat::loadDriftMatrix(params.oh_tst_options().gle_a_file);
173 gle = std::make_unique<GleThermostat>(a, m_kbt, 0.5 * m_dt, x.size());
174 if (!gle->valid()) {
175 EONC_LOG_CRITICAL("OH-TST gle thermostat unusable (gle_a_file = {})",
176 params.oh_tst_options().gle_a_file);
177 throw std::runtime_error("oh_tst: gle thermostat unusable");
178 }
179 }
180 const auto gauss = [this]() { return gaussDraw(); };
181
182 VectorXd f = matter.getForcesFreeV();
183 VectorXd fPlane = f - normal * normal.dot(f);
184
185 PlaneAverages avg;
186 avg.rotNorm = VectorXd::Zero(x.size());
187 avg.rotRaw = VectorXd::Zero(x.size());
188 avg.pos = VectorXd::Zero(x.size());
189 long nAccum = 0;
190
191 VectorXd xPrev = x;
192 for (long step = 0; step < equilSteps + sampleSteps; ++step) {
193 // Velocity Verlet on the plane: forces and velocities projected,
194 // positions corrected back onto the constraint (RATTLE for a
195 // linear constraint is an exact projection).
196 xPrev = x;
197 if (gle) {
198 gle->apply(v, m_masses3N, gauss);
199 v -= normal * normal.dot(v);
200 }
201 VectorXd a = fPlane.cwiseQuotient(m_masses3N);
202 x += m_dt * v + 0.5 * m_dt * m_dt * a;
203 x -= normal * normal.dot(x - gamma);
204 matter.setPositionsFreeV(x);
205 f = matter.getForcesFreeV();
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);
210 // Eqs 14-17: keep the sampling in the primary product subregion.
211 if (symmetryReflect(m_symXR, x, v, xPrev, &normal)) {
212 x -= normal * normal.dot(x - gamma);
213 matter.setPositionsFreeV(x);
214 f = matter.getForcesFreeV();
215 fPlane = f - normal * normal.dot(f);
216 }
217 if (gle) {
218 gle->apply(v, m_masses3N, gauss);
219 v -= normal * normal.dot(v);
220 } else if (uniformDraw() < m_andersenProb) {
221 // Andersen collisions keep the constrained ensemble canonical.
222 drawThermalVelocities(v, &normal);
223 }
224 if (step >= equilSteps) {
225 const double fn = normal.dot(f);
226 const VectorXd arm = x - gamma;
227 const double arm2 = arm.squaredNorm();
228 avg.fn += fn;
229 if (arm2 > 1e-16) {
230 avg.rotNorm.noalias() += (fn / (alphaRot * arm2)) * arm;
231 }
232 avg.rotRaw.noalias() += fn * arm;
233 avg.pos.noalias() += x;
234 ++nAccum;
235 }
236 }
237 if (nAccum > 0) {
238 const double inv = 1.0 / static_cast<double>(nAccum);
239 avg.fn *= inv;
240 avg.rotNorm *= inv;
241 avg.rotRaw *= inv;
242 avg.pos *= inv;
243 }
244 return avg;
245}
VectorXd rotNorm
<(n.F) R / (alpha |R|^2)>, drives rotation
Definition OHTSTJob.h:64

◆ symmetryReflect()

bool eonc::OHTSTJob::symmetryReflect ( const VectorXd & xR,
VectorXd & x,
VectorXd & v,
const VectorXd & xOld,
const VectorXd * normal )
private

Eqs 14-17 symmetry restriction: if the configuration is closer to another equivalent product half-line than to the primary one, revert the position and reflect the velocity about the mirror that maps the primary direction onto the offending one. Returns true when a reflection was applied.

Definition at line 103 of file OHTSTJob.cpp.

104 {
105 if (m_symDirs.size() < 2) {
106 return false;
107 }
108 // Eq 15: distance from the configuration to each half-line
109 // l_i = { R + t p_i, t >= 0 }. For t < 0 the closest point is the
110 // reactant endpoint, not the foot on the infinite line.
111 const VectorXd rel = x - xR;
112 const double rel2 = rel.squaredNorm();
113 double dPrimary = 0.0;
114 size_t closest = 0;
115 double dMin = 0.0;
116 for (size_t i = 0; i < m_symDirs.size(); ++i) {
117 const double proj = rel.dot(m_symDirs[i]);
118 const double d2 = (proj < 0.0) ? rel2 : std::max(0.0, rel2 - proj * proj);
119 const double d = std::sqrt(d2);
120 if (i == 0) {
121 dPrimary = d;
122 dMin = d;
123 } else if (d < dMin) {
124 dMin = d;
125 closest = i;
126 }
127 }
128 if (closest == 0 || dPrimary <= dMin) {
129 return false;
130 }
131 // Eqs 16-17: step back and reflect the velocity about the mirror
132 // that maps p_1 onto the offending p_i; the component of the mirror
133 // normal along the hyperplane normal is removed so the reflected
134 // velocity stays within the plane.
135 VectorXd q = m_symDirs[0] - m_symDirs[closest];
136 if (normal != nullptr) {
137 q -= (*normal) * normal->dot(q);
138 }
139 const double qn = q.norm();
140 if (qn < 1e-12) {
141 return false;
142 }
143 q /= qn;
144 x = xOld;
145 v -= 2.0 * v.dot(q) * q;
146 return true;
147}

◆ uniformDraw()

double eonc::OHTSTJob::uniformDraw ( )
private

uniform (0,1)

Definition at line 51 of file OHTSTJob.cpp.

51 {
52 // Deterministic LCG: the job must be reproducible for a given
53 // random_seed across MPI farm workers.
54 m_seedState = (1664525L * m_seedState + 1013904223L) & 0x7fffffffL;
55 return static_cast<double>(m_seedState) / 2147483648.0;
56}

◆ ::tests::OHTSTPlaneWrapTest

friend struct ::tests::OHTSTPlaneWrapTest
friend

Definition at line 75 of file OHTSTJob.h.

◆ OHTSTSymmetryTest

friend struct OHTSTSymmetryTest
friend

Definition at line 93 of file OHTSTJob.h.

Member Data Documentation

◆ m_andersenProb

double eonc::OHTSTJob::m_andersenProb {0.0}
private

per-step Andersen collision probability

Definition at line 101 of file OHTSTJob.h.

101{0.0};

◆ m_dt

double eonc::OHTSTJob::m_dt {0.0}
private

integration step, internal units

Definition at line 99 of file OHTSTJob.h.

99{0.0};

◆ m_gaussHave

bool eonc::OHTSTJob::m_gaussHave {false}
private

Definition at line 105 of file OHTSTJob.h.

105{false};

◆ m_gaussSpare

double eonc::OHTSTJob::m_gaussSpare {0.0}
private

Definition at line 106 of file OHTSTJob.h.

106{0.0};

◆ m_kbt

double eonc::OHTSTJob::m_kbt {0.0}
private

k_B T (eV)

Definition at line 100 of file OHTSTJob.h.

100{0.0};

◆ m_masses3N

VectorXd eonc::OHTSTJob::m_masses3N
private

per-DOF masses of the free atoms (amu)

Definition at line 98 of file OHTSTJob.h.

◆ m_seedState

long eonc::OHTSTJob::m_seedState {12345}
private

LCG state for the thermostat draws.

Definition at line 102 of file OHTSTJob.h.

102{12345};

◆ m_symDirs

std::vector<VectorXd> eonc::OHTSTJob::m_symDirs
private

p-hat_i, index 0 = primary

Definition at line 95 of file OHTSTJob.h.

◆ m_symXR

VectorXd eonc::OHTSTJob::m_symXR
private

reactant anchor R of the half-lines

Definition at line 96 of file OHTSTJob.h.


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