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

#include <OHTSTJob.h>

Inheritance diagram for OHTSTJob:

Classes

struct  PlaneAverages

Public Member Functions

 OHTSTJob (std::unique_ptr< Parameters > parameters)
 ~OHTSTJob (void)=default
std::vector< std::string > run (void)
 Virtual run; used solely for dynamic dispatch.
Public Member Functions inherited from eonc::Job
 Job (std::unique_ptr< Parameters > parameters)
 Job (std::shared_ptr< Potential > potPassed, const Parameters &parameters)
virtual ~Job ()=default
JobType getType ()

Private Member Functions

PlaneAverages samplePlane (Matter &matter, 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}

Additional Inherited Members

Protected Attributes inherited from eonc::Job
JobType jtype
Parameters params
std::shared_ptr< Potentialpot

Detailed Description

Definition at line 40 of file OHTSTJob.h.

Constructor & Destructor Documentation

◆ OHTSTJob()

eonc::OHTSTJob::OHTSTJob ( std::unique_ptr< Parameters > parameters)
inline

Definition at line 43 of file OHTSTJob.h.

44 : Job(std::move(parameters)) {}
Job(std::unique_ptr< Parameters > parameters)
Definition Job.h:58

◆ ~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 71 of file OHTSTJob.cpp.

71 {
72 for (long k = 0; k < vel.size(); ++k) {
73 vel[k] = std::sqrt(m_kbt / m_masses3N[k]) * gaussDraw();
74 }
75 if (normal != nullptr) {
76 vel -= (*normal) * normal->dot(vel);
77 }
78}
VectorXd m_masses3N
per-DOF masses of the free atoms (amu)
Definition OHTSTJob.h:81
double m_kbt
k_B T (eV)
Definition OHTSTJob.h:83
double gaussDraw()
standard normal (Box-Muller)
Definition OHTSTJob.cpp:58

◆ 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 m_gaussSpare
Definition OHTSTJob.h:89
bool m_gaussHave
Definition OHTSTJob.h:88
double uniformDraw()
uniform (0,1)
Definition OHTSTJob.cpp:51
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 221 of file OHTSTJob.cpp.

222 {
223 // Eq 22: Q^ZR/Q^R = (dt / t_tot) * sum over plane crossings of
224 // 1 / |(r_{i+1} - r_i).n|. The trajectory is unconstrained,
225 // thermostatted, and stays in the reactant basin by construction
226 // (it starts there and the barrier is >> kT).
227 const long steps = params.oh_tst_options.reactant_md_steps;
228 const long equilSteps = params.oh_tst_options.equil_steps;
229 VectorXd x = matter.getPositionsFreeV();
230 VectorXd v(x.size());
231 drawThermalVelocities(v, nullptr);
232 std::unique_ptr<GleThermostat> gle;
233 if (params.oh_tst_options.thermostat == "gle") {
234 const MatrixXd a =
235 GleThermostat::loadDriftMatrix(params.oh_tst_options.gle_a_file);
236 gle = std::make_unique<GleThermostat>(a, m_kbt, 0.5 * m_dt, x.size());
237 if (!gle->valid()) {
238 throw std::runtime_error("oh_tst: gle thermostat unusable");
239 }
240 }
241 const auto gauss = [this]() { return gaussDraw(); };
242 VectorXd f = matter.getForcesFreeV();
243 double side = normal.dot(x - gammaR);
244 double crossingSum = 0.0;
245 long counted = 0;
246 for (long step = 0; step < equilSteps + steps; ++step) {
247 const VectorXd xOld = x;
248 if (gle) {
249 gle->apply(v, m_masses3N, gauss);
250 }
251 VectorXd a = f.cwiseQuotient(m_masses3N);
252 x += m_dt * v + 0.5 * m_dt * m_dt * a;
253 matter.setPositionsFreeV(x);
254 f = matter.getForcesFreeV();
255 VectorXd aNew = f.cwiseQuotient(m_masses3N);
256 v += 0.5 * m_dt * (a + aNew);
257 if (symmetryReflect(m_symXR, x, v, xOld, nullptr)) {
258 matter.setPositionsFreeV(x);
259 f = matter.getForcesFreeV();
260 }
261 if (gle) {
262 gle->apply(v, m_masses3N, gauss);
263 } else if (uniformDraw() < m_andersenProb) {
264 drawThermalVelocities(v, nullptr);
265 }
266 const double sideNew = normal.dot(x - gammaR);
267 if (step >= equilSteps) {
268 if (side * sideNew < 0.0) {
269 const double proj = std::fabs(normal.dot(x - xOld));
270 if (proj > 1e-14) {
271 crossingSum += 1.0 / proj;
272 }
273 }
274 ++counted;
275 }
276 side = sideNew;
277 }
278 return counted > 0 ? crossingSum / static_cast<double>(counted) : 0.0;
279}
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
double m_andersenProb
per-step Andersen collision probability
Definition OHTSTJob.h:84
double m_dt
integration step, internal units
Definition OHTSTJob.h:82
VectorXd m_symXR
reactant anchor R of the half-lines
Definition OHTSTJob.h:79
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:54
void setPositionsFreeV(const VectorXd &pos)
Definition Matter.cpp:301
VectorXd getForcesFreeV() const
Definition Matter.cpp:354
VectorXd getPositionsFreeV() const
Definition Matter.cpp:268
void drawThermalVelocities(VectorXd &vel, const VectorXd *normal)
Definition OHTSTJob.cpp:71
bool symmetryReflect(const VectorXd &xR, VectorXd &x, VectorXd &v, const VectorXd &xOld, const VectorXd *normal)
Definition OHTSTJob.cpp:80

◆ run()

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

Virtual run; used solely for dynamic dispatch.

Implements eonc::Job.

Definition at line 281 of file OHTSTJob.cpp.

281 {
282 auto reactant = std::make_shared<Matter>(pot, params);
283 auto product = std::make_shared<Matter>(pot, params);
284 if (!eonc::io::io_ok(
285 reactant->con2matter(params.oh_tst_options.reactant_filename))) {
286 EONC_LOG_CRITICAL("OH-TST failed to load {}",
287 params.oh_tst_options.reactant_filename);
288 throw std::runtime_error("oh_tst: failed to load reactant");
289 }
290 if (!eonc::io::io_ok(
291 product->con2matter(params.oh_tst_options.product_filename))) {
292 EONC_LOG_CRITICAL("OH-TST failed to load {}",
293 params.oh_tst_options.product_filename);
294 throw std::runtime_error("oh_tst: failed to load product");
295 }
296
297 const double temperature = params.main_options.temperature;
298 EONC_LOG_INFO("[oh_tst] thermostat = {}{}", params.oh_tst_options.thermostat,
299 params.oh_tst_options.thermostat == "gle"
300 ? std::string(" (drift: ") +
301 params.oh_tst_options.gle_a_file + ")"
302 : std::string());
303 m_kbt = params.constants.kB * temperature;
304 m_dt = params.oh_tst_options.time_step / params.constants.timeUnit;
305 m_seedState = (params.main_options.randomSeed > 0)
306 ? params.main_options.randomSeed
307 : 12345;
308 // Per-step collision probability from the Andersen collision period.
309 const double tcol =
310 params.thermostat_options.andersen_tcol_input / params.constants.timeUnit;
311 m_andersenProb = (tcol > 0.0) ? std::min(1.0, m_dt / tcol) : 0.1;
312
313 // Free-DOF mass vector (amu per coordinate).
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]);
322 }
323 }
324 m_masses3N = VectorXd::Map(m3.data(), static_cast<long>(m3.size()));
325
326 // Straight guideline in 3N space, minimum-image on the endpoint
327 // difference so the plane never strides a cell boundary.
328 const VectorXd xR = reactant->getPositionsFreeV();
329 VectorXd diff = product->getPositionsFreeV() - xR;
330 {
331 AtomMatrix d(AtomMatrix::Map(diff.data(), diff.size() / 3, 3));
332 // Rigid-translation alignment under PBC: AV-embedded state frames
333 // ride the defect (drift = one hop vector, a/2<011> observed), so
334 // atoms near cell boundaries min-image inconsistently until the
335 // drift is gone. Iterate min-image -> de-drift to the fixed point;
336 // the converged residual is the localized reaction coordinate.
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) {
345 break;
346 }
347 }
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());
352 }
353 const double guideLen = diff.norm();
354 EONC_LOG_INFO("[oh_tst] guideline length |P - R| = {:.4f} A over {} free "
355 "DOF",
356 guideLen, xR.size());
357 // A sub-Angstrom guideline is an on-site shuffle (dumbbell rotation,
358 // flicker partner): the plane progression has no room to climb and
359 // the run would "converge" at s = 0 measuring nothing.
360 if (guideLen < 0.5) {
361 EONC_LOG_CRITICAL("[oh_tst] guideline too short ({:.4f} A) -- pick a "
362 "translation-class endpoint pair",
363 guideLen);
364 throw std::runtime_error("oh_tst: degenerate guideline");
365 }
366 const VectorXd u = diff / guideLen;
367
368 // Eqs 13-14: unit vectors to every symmetry-equivalent product;
369 // index 0 is the primary product the guideline points to.
370 m_symXR = xR;
371 m_symDirs.clear();
372 m_symDirs.push_back(u);
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);
379 if (fname.empty()) {
380 continue;
381 }
382 Matter other(pot, params);
383 if (!eonc::io::io_ok(other.con2matter(fname))) {
384 EONC_LOG_CRITICAL("OH-TST failed to load symmetry product {}", fname);
385 throw std::runtime_error("oh_tst: failed to load symmetry product");
386 }
387 VectorXd d = other.getPositionsFreeV() - xR;
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();
392 if (dn > 1e-8) {
393 m_symDirs.push_back(d / dn);
394 }
395 }
396 EONC_LOG_INFO("[oh_tst] symmetry restriction active over {} product "
397 "directions",
398 m_symDirs.size());
399 }
400
401 // Plane state: progression coordinate s, normal n, their conjugate
402 // velocities, and the previous-iteration driving forces for the
403 // two-force velocity Verlet updates (Eqs 6-7 and 9-10).
404 double s = params.oh_tst_options.s_init * guideLen;
405 double vS = 0.0;
406 VectorXd n = u;
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;
413
414 Matter walker(*reactant);
415
416 // Reversible-work accumulators and the previous plane's averages
417 // for the trapezoid rules of Eqs 18-19.
418 double aTrans = 0.0, aRot = 0.0, aBest = 0.0, sBest = s;
419 VectorXd nBest = n;
420 bool havePrev = false;
421 double fnPrev = 0.0;
422 VectorXd rotRawPrev, posPrev, nPrev;
423 double gSPrev = 0.0;
424 VectorXd gRotPrev;
425 int sideSign = 0; // sign of <F.n> at plane 1 (grooming, Sec IIE)
426
427 // RAII: the divergence guard and samplePlane both throw out of the plane
428 // loop below, so the handle has to close itself.
429 std::ofstream prog("oh_tst_progression.dat");
430 if (prog) {
431 prog << "# plane s/L <F.n> (eV/A) dA_trans (eV) dA_rot (eV) "
432 "A (eV) n.u\n";
433 } else {
434 EONC_LOG_ERROR("[oh_tst] cannot open oh_tst_progression.dat");
435 }
436
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;
441 long plane = 0;
442 bool converged = false;
443 // Sec IIC guideline refinement: after the translational force first
444 // changes sign the guideline is re-anchored every step through the
445 // previous plane's average position along the current normal, so a
446 // small rotational force no longer demands a huge s move (the
447 // failure mode that made small-inertia mechanism discovery diverge).
448 bool guidelineMoving = false;
449 VectorXd gOrigin = xR;
450 VectorXd gDir = u;
451 long rotOnlySteps = 0;
452 for (; plane < nPlanes; ++plane) {
453 const VectorXd gamma = gOrigin + s * gDir;
454 PlaneAverages avg = samplePlane(walker, gamma, n);
455
456 // Driving forces: translation climbs against <F.n> (Eq 5 with the
457 // reversed-force convention); the normal is driven along
458 // +<(n.F) R/(alpha |R|^2)> (Appendix A, Eq A1), projected onto
459 // the tangent space of the unit sphere.
460 const double gS = -avg.fn / mS;
461 VectorXd gRot = avg.rotNorm - n * n.dot(avg.rotNorm);
462
463 if (plane == 0) {
464 sideSign = (avg.fn < 0.0) ? -1 : 1;
465 } else if (!guidelineMoving && ((avg.fn < 0.0) ? -1 : 1) != sideSign) {
466 guidelineMoving = true;
467 EONC_LOG_DEBUG("[oh_tst] plane {}: translational force changed "
468 "sign; guideline now follows <r> along the normal",
469 plane);
470 }
471
472 // Reversible work (Eqs 18-19), trapezoid between consecutive
473 // planes: the translational path is the piecewise-linear track of
474 // the average configuration <r>, and the rotational work pairs
475 // the unnormalized <(n.F) R> with the actual change of the
476 // normal. Grooming (Sec IIE): only planes on the reactant side of
477 // the ridge (same sign of <F.n> as plane 1) contribute.
478 if (havePrev) {
479 // Scan mode integrates the whole reactant->product path; the
480 // adaptive run grooms to the reactant side of the ridge only.
481 const bool sameSide = scanMode || ((fnPrev < 0.0 ? -1 : 1) == sideSign &&
482 (avg.fn < 0.0 ? -1 : 1) == sideSign);
483 if (sameSide) {
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);
488 }
489 }
490
491 const double aTotal = aTrans + aRot;
492 if (aTotal > aBest) {
493 aBest = aTotal;
494 sBest = s;
495 nBest = n;
496 }
497 if (prog) {
498 prog << std::format("{:6} {:10.6f} {:14.6e} {:12.6f} {:12.6f} {:12.6f} "
499 "{:10.6f}\n",
500 plane, s / guideLen, avg.fn, aTrans, aRot, aTotal,
501 n.dot(u));
502 prog.flush();
503 }
504 EONC_LOG_DEBUG("[oh_tst] plane {} s/L {:.4f} <F.n> {:.4e} A {:.4f} eV",
505 plane, s / guideLen, avg.fn, aTotal);
506
507 // Divergence guard: reversible work beyond any physical barrier
508 // means the endpoints were not minimized (static relaxation
509 // forces leak into <F.n>) or the plane is chasing a drifting
510 // ensemble; 300 planes of that is pure waste.
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");
517 }
518 // A stationary plane only counts as the variational maximum
519 // after the progression has actually climbed: the reactant basin
520 // bottom is also force-free, and a short guideline puts the first
521 // plane inside it. Two thermal energies of accumulated reversible
522 // work is the cheapest evidence of a ridge.
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) {
526 converged = true;
527 // The converged plane is the optimal one even if sampling noise
528 // put an earlier plane marginally higher.
529 aBest = aTotal;
530 sBest = s;
531 nBest = n;
532 if (prog) {
533 prog << std::format("# converged at plane {}\n", plane);
534 }
535 break;
536 }
537
538 if (scanMode) {
539 // Fixed-increment PMF scan: normal stays along the guideline,
540 // s advances uniformly, and the walker is projected onto the
541 // next constraint plane for samplePlane to re-equilibrate. The
542 // A(s) profile is the PMF; aBest already tracks its maximum.
543 fnPrev = avg.fn;
544 nPrev = n;
545 rotRawPrev = avg.rotRaw;
546 posPrev = avg.pos;
547 gSPrev = gS;
548 gRotPrev = gRot;
549 havePrev = true;
550 s = (double)(plane + 1) * dsScan;
551 const VectorXd gammaNext = xR + s * u;
552 VectorXd xStart = walker.getPositionsFreeV();
553 xStart -= u * (u.dot(xStart - gammaNext));
554 walker.setPositionsFreeV(xStart);
555 continue;
556 }
557 // Damped two-force Verlet on s (Eqs 6-7): the velocity is zeroed
558 // when it opposes the driving force, so the plane settles at the
559 // free-energy maximum instead of oscillating over it ("the
560 // velocity of the plane is zeroed if the plane has gone past the
561 // maximum free energy position").
562 // Pure-rotation step (Sec IIC): right after a guideline change a
563 // spiked rotational force is relaxed at fixed s before the plane
564 // translates again.
565 const bool rotationOnly =
566 guidelineMoving &&
567 gRot.norm() * params.oh_tst_options.alpha_rot > 5.0 * fTol &&
568 rotOnlySteps < 10;
569 if (rotationOnly) {
570 ++rotOnlySteps;
571 } else {
572 rotOnlySteps = 0;
573 }
574 if (havePrev) {
575 vS += 0.5 * dtPlane * (gS + gSPrev);
576 } else {
577 vS += dtPlane * gS;
578 }
579 if (vS * gS < 0.0)
580 vS = 0.0;
581 double ds = dtPlane * vS + 0.5 * dtPlane * dtPlane * gS;
582 ds = std::clamp(ds, -dsMax, dsMax);
583 if (rotationOnly) {
584 ds = 0.0;
585 vS = 0.0;
586 }
587 const double sNew =
588 guidelineMoving ? s + ds : std::clamp(s + ds, 0.0, guideLen);
589
590 // Damped rotation (Eqs 9-10 with the Appendix A driving): the
591 // angular velocity keeps only its projection along the current
592 // driving force while aligned with it, and is zeroed otherwise.
593 if (havePrev && gRotPrev.size() == gRot.size()) {
594 omega += 0.5 * dtPlane * (gRot + gRotPrev);
595 } else {
596 omega += dtPlane * gRot;
597 }
598 omega -= n * n.dot(omega);
599 const double gNorm = gRot.norm();
600 if (gNorm > 1e-14) {
601 const VectorXd gHat = gRot / gNorm;
602 const double along = omega.dot(gHat);
603 if (along > 0.0) {
604 omega = gHat * along;
605 } else {
606 omega.setZero();
607 }
608 } else {
609 omega.setZero();
610 }
611 VectorXd dn = dtPlane * omega + 0.5 * dtPlane * dtPlane * gRot;
612 const double dTheta = dn.norm();
613 if (dTheta > dThetaMax) {
614 dn *= dThetaMax / dTheta;
615 }
616 const VectorXd nOld = n;
617 n = (n + dn).normalized();
618 if (n.dot(gDir) < 0.0) {
619 // Keep the normal pointing towards increasing s.
620 n = -n;
621 }
622
623 // Eq 11-12 restart: place the walker in the new plane at the
624 // rotated image of the average arm, rescaled so rotation does not
625 // change the arm length.
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;
632 }
633 VectorXd gammaNew;
634 if (guidelineMoving) {
635 // Re-anchor: the guideline passes through the previous plane's
636 // average position along the CURRENT normal, and the plane
637 // ADVANCES by this iteration's ds along the fresh line. (A
638 // dead re-assignment here used to pin the plane at <r> forever:
639 // the walker drifted downhill and the trans-work integral ran
640 // away by ~20 eV per plane on the Cu doorway.)
641 gOrigin = avg.pos;
642 gDir = n;
643 s = ds;
644 gammaNew = gOrigin + s * gDir;
645 } else {
646 gammaNew = gOrigin + sNew * gDir;
647 }
648 VectorXd xStart = gammaNew + armNew;
649 xStart -= n * n.dot(xStart - gammaNew);
650 walker.setPositionsFreeV(xStart);
651
652 fnPrev = avg.fn;
653 nPrev = nOld;
654 rotRawPrev = avg.rotRaw;
655 posPrev = avg.pos;
656 gSPrev = gS;
657 gRotPrev = gRot;
658 havePrev = true;
659 if (!guidelineMoving) {
660 s = sNew;
661 }
662 }
663 bool progOk = prog.is_open();
664 if (progOk) {
665 prog.close();
666 progOk = static_cast<bool>(prog);
667 if (!progOk) {
668 EONC_LOG_ERROR("[oh_tst] failed to write oh_tst_progression.dat");
669 }
670 }
671 // A completed scan yields a valid PMF even with no interior
672 // maximum; report it as converged so downstream tooling accepts
673 // the profile (barrier = aBest, the max of A(s)).
674 if (scanMode && plane >= nPlanes)
675 converged = true;
676
677 // Direction-dependent effective mass (Eq 24) and the one-sided
678 // thermal flux factor sqrt(kBT / 2 pi mu) (Eq 23).
679 const double mu = (m_masses3N.array() * nBest.array().square()).sum();
680 const double vFlux = std::sqrt(m_kbt / (2.0 * helpers::pi * mu));
681
682 // Q^ZR/Q^R at the FIRST plane of the progression (Z^R), whose
683 // reversible work to the optimal plane is what aBest measures.
684 Matter rWalker(*reactant);
685 const VectorXd gammaR = xR + (params.oh_tst_options.s_init * guideLen) * u;
686 const double qRatio = reactantQRatio(rWalker, gammaR, u);
687
688 // Rate in internal units (1/internal-time), then SI.
689 const double kInternal = vFlux * qRatio * std::exp(-aBest / m_kbt);
690 const double kSI = kInternal / (params.constants.timeUnit * 1.0e-15);
691
692 std::vector<std::string> returnFiles;
693 std::ofstream out("results.dat");
694 if (!out) {
695 EONC_LOG_ERROR("[oh_tst] cannot open results.dat");
696 throw std::runtime_error("oh_tst: cannot open results.dat");
697 }
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);
711 out.close();
712 if (!out) {
713 EONC_LOG_ERROR("[oh_tst] failed to write results.dat");
714 throw std::runtime_error("oh_tst: failed to write results.dat");
715 }
716 returnFiles.push_back("results.dat");
717 if (progOk) {
718 returnFiles.push_back("oh_tst_progression.dat");
719 }
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);
724 return returnFiles;
725}
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
#define EONC_LOG_DEBUG(...)
Definition EonLogger.h:244
#define EONC_LOG_ERROR(...)
Definition EonLogger.h:262
#define EONC_LOG_INFO(...)
Definition EonLogger.h:250
#define EONC_LOG_CRITICAL(...)
Definition EonLogger.h:268
long m_seedState
LCG state for the thermostat draws.
Definition OHTSTJob.h:85
std::vector< VectorXd > m_symDirs
p-hat_i, index 0 = primary
Definition OHTSTJob.h:78
std::shared_ptr< Potential > pot
Definition Job.h:55
double reactantQRatio(Matter &matter, const VectorXd &gammaR, const VectorXd &normal)
Definition OHTSTJob.cpp:221
PlaneAverages samplePlane(Matter &matter, const VectorXd &gamma, const VectorXd &normal)
Definition OHTSTJob.cpp:125
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38

◆ samplePlane()

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

Definition at line 125 of file OHTSTJob.cpp.

127 {
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;
131
132 // Constrain the current geometry exactly onto the plane.
133 VectorXd x = matter.getPositionsFreeV();
134 x -= normal * normal.dot(x - gamma);
135 matter.setPositionsFreeV(x);
136
137 VectorXd v(x.size());
138 drawThermalVelocities(v, &normal);
139
140 // Colored-noise option: an exact OU half-step before and after each
141 // Verlet step (the auxiliary momenta start fresh per sampling
142 // block); velocities re-project onto the plane after every kick.
143 std::unique_ptr<GleThermostat> gle;
144 if (params.oh_tst_options.thermostat == "gle") {
145 const MatrixXd a =
146 GleThermostat::loadDriftMatrix(params.oh_tst_options.gle_a_file);
147 gle = std::make_unique<GleThermostat>(a, m_kbt, 0.5 * m_dt, x.size());
148 if (!gle->valid()) {
149 EONC_LOG_CRITICAL("OH-TST gle thermostat unusable (gle_a_file = {})",
150 params.oh_tst_options.gle_a_file);
151 throw std::runtime_error("oh_tst: gle thermostat unusable");
152 }
153 }
154 const auto gauss = [this]() { return gaussDraw(); };
155
156 VectorXd f = matter.getForcesFreeV();
157 VectorXd fPlane = f - normal * normal.dot(f);
158
159 PlaneAverages avg;
160 avg.rotNorm = VectorXd::Zero(x.size());
161 avg.rotRaw = VectorXd::Zero(x.size());
162 avg.pos = VectorXd::Zero(x.size());
163 long nAccum = 0;
164
165 VectorXd xPrev = x;
166 for (long step = 0; step < equilSteps + sampleSteps; ++step) {
167 // Velocity Verlet on the plane: forces and velocities projected,
168 // positions corrected back onto the constraint (RATTLE for a
169 // linear constraint is an exact projection).
170 xPrev = x;
171 if (gle) {
172 gle->apply(v, m_masses3N, gauss);
173 v -= normal * normal.dot(v);
174 }
175 VectorXd a = fPlane.cwiseQuotient(m_masses3N);
176 x += m_dt * v + 0.5 * m_dt * m_dt * a;
177 x -= normal * normal.dot(x - gamma);
178 matter.setPositionsFreeV(x);
179 f = matter.getForcesFreeV();
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);
184 // Eqs 14-17: keep the sampling in the primary product subregion.
185 if (symmetryReflect(m_symXR, x, v, xPrev, &normal)) {
186 x -= normal * normal.dot(x - gamma);
187 matter.setPositionsFreeV(x);
188 f = matter.getForcesFreeV();
189 fPlane = f - normal * normal.dot(f);
190 }
191 if (gle) {
192 gle->apply(v, m_masses3N, gauss);
193 v -= normal * normal.dot(v);
194 } else if (uniformDraw() < m_andersenProb) {
195 // Andersen collisions keep the constrained ensemble canonical.
196 drawThermalVelocities(v, &normal);
197 }
198 if (step >= equilSteps) {
199 const double fn = normal.dot(f);
200 const VectorXd arm = x - gamma;
201 const double arm2 = arm.squaredNorm();
202 avg.fn += fn;
203 if (arm2 > 1e-16) {
204 avg.rotNorm.noalias() += (fn / (alphaRot * arm2)) * arm;
205 }
206 avg.rotRaw.noalias() += fn * arm;
207 avg.pos.noalias() += x;
208 ++nAccum;
209 }
210 }
211 if (nAccum > 0) {
212 const double inv = 1.0 / static_cast<double>(nAccum);
213 avg.fn *= inv;
214 avg.rotNorm *= inv;
215 avg.rotRaw *= inv;
216 avg.pos *= inv;
217 }
218 return avg;
219}
VectorXd rotNorm
<(n.F) R / (alpha |R|^2)>, drives rotation
Definition OHTSTJob.h:53

◆ 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 80 of file OHTSTJob.cpp.

81 {
82 if (m_symDirs.size() < 2) {
83 return false;
84 }
85 // Eq 15: distance from the configuration to each half-line
86 // l_i = { R + t p_i, t >= 0 }.
87 const VectorXd rel = x - xR;
88 const double rel2 = rel.squaredNorm();
89 double dPrimary = 0.0;
90 size_t closest = 0;
91 double dMin = 0.0;
92 for (size_t i = 0; i < m_symDirs.size(); ++i) {
93 const double proj = rel.dot(m_symDirs[i]);
94 const double d2 = std::max(0.0, rel2 - proj * proj);
95 const double d = std::sqrt(d2);
96 if (i == 0) {
97 dPrimary = d;
98 dMin = d;
99 } else if (d < dMin) {
100 dMin = d;
101 closest = i;
102 }
103 }
104 if (closest == 0 || dPrimary <= dMin) {
105 return false;
106 }
107 // Eqs 16-17: step back and reflect the velocity about the mirror
108 // that maps p_1 onto the offending p_i; the component of the mirror
109 // normal along the hyperplane normal is removed so the reflected
110 // velocity stays within the plane.
111 VectorXd q = m_symDirs[0] - m_symDirs[closest];
112 if (normal != nullptr) {
113 q -= (*normal) * normal->dot(q);
114 }
115 const double qn = q.norm();
116 if (qn < 1e-12) {
117 return false;
118 }
119 q /= qn;
120 x = xOld;
121 v -= 2.0 * v.dot(q) * q;
122 return true;
123}

◆ 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}

Member Data Documentation

◆ m_andersenProb

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

per-step Andersen collision probability

Definition at line 84 of file OHTSTJob.h.

84{0.0};

◆ m_dt

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

integration step, internal units

Definition at line 82 of file OHTSTJob.h.

82{0.0};

◆ m_gaussHave

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

Definition at line 88 of file OHTSTJob.h.

88{false};

◆ m_gaussSpare

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

Definition at line 89 of file OHTSTJob.h.

89{0.0};

◆ m_kbt

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

k_B T (eV)

Definition at line 83 of file OHTSTJob.h.

83{0.0};

◆ m_masses3N

VectorXd eonc::OHTSTJob::m_masses3N
private

per-DOF masses of the free atoms (amu)

Definition at line 81 of file OHTSTJob.h.

◆ m_seedState

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

LCG state for the thermostat draws.

Definition at line 85 of file OHTSTJob.h.

85{12345};

◆ m_symDirs

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

p-hat_i, index 0 = primary

Definition at line 78 of file OHTSTJob.h.

◆ m_symXR

VectorXd eonc::OHTSTJob::m_symXR
private

reactant anchor R of the half-lines

Definition at line 79 of file OHTSTJob.h.


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