Virtual run; used solely for dynamic dispatch.
307 {
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");
315 }
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");
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());
332 ?
params.main_options().randomSeed
333 : 12345;
334
335 const double tcol =
params.thermostat_options().andersen_tcol_input /
336 params.constants().timeUnit;
338
339
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
353
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();
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
370
371
372 if (guideLen < 0.5) {
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
381
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 }
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));
402 d = VectorXd::Map(dm.data(), d.size());
403 const double dn = d.norm();
404 if (dn > 1e-8) {
406 }
407 }
408 EONC_LOG_INFO(
"[oh_tst] symmetry restriction active over {} product "
409 "directions",
411 }
412
413
414
415
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
428
429 VectorXd xFree = walker.getPositionsFreeV();
430
431
432
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;
441
442
443
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 {
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
456
457 if (scanMode) {
459 sBest = s;
460 }
461 long plane = 0;
462 bool converged = false;
463
464
465
466
467
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;
478
479
480
481
482
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;
491 "sign; guideline now follows <r> along the normal",
492 plane);
493 }
494
495
496
497
498
499
500
501 if (havePrev) {
502
503
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
531
532
533
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
542
543
544
545
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
551
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
563
564
565
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
580
581
582
583
584
585
586
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
613
614
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
642 n = -n;
643 }
644
645
646
647
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
658
659
660
661
662
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
694
695
696 if (scanMode && plane >= nPlanes)
697 converged = true;
698
699
700
701
702 const long planesUsed = (plane < nPlanes) ? plane + 1 : plane;
703
704
705
706 const double mu = (
m_masses3N.array() * nBest.array().square()).sum();
708
709
710
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;
716
717
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) {
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) {
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
#define EONC_LOG_DEBUG(...)
#define EONC_LOG_ERROR(...)
#define EONC_LOG_INFO(...)
#define EONC_LOG_CRITICAL(...)
std::shared_ptr< Potential > pot
double reactantQRatio(Matter &matter, const VectorXd &gammaR, const VectorXd &normal)
PlaneAverages samplePlane(Matter &matter, VectorXd &x, const VectorXd &gamma, const VectorXd &normal)
long m_seedState
LCG state for the thermostat draws.
std::vector< VectorXd > m_symDirs
p-hat_i, index 0 = primary
constexpr bool io_ok(IoStatus s) noexcept
AtomMatrix minImageRemoveRigidDrift(const Matter &reference, AtomMatrix diff, Eigen::RowVector3d *totalDrift)
Minimum-image a free-atom difference and remove rigid translation.
double pmfScanS(long plane, long nScan, double guideLen)
Uniform PMF-scan coordinate.