Virtual run; used solely for dynamic dispatch.
281 {
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");
289 }
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");
295 }
296
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 +
")"
302 : std::string());
306 ?
params.main_options.randomSeed
307 : 12345;
308
309 const double tcol =
310 params.thermostat_options.andersen_tcol_input /
params.constants.timeUnit;
312
313
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
327
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
333
334
335
336
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
358
359
360 if (guideLen < 0.5) {
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
369
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 }
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) {
394 }
395 }
396 EONC_LOG_INFO(
"[oh_tst] symmetry restriction active over {} product "
397 "directions",
399 }
400
401
402
403
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
417
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;
426
427
428
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 {
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
444
445
446
447
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;
455
456
457
458
459
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;
468 "sign; guideline now follows <r> along the normal",
469 plane);
470 }
471
472
473
474
475
476
477
478 if (havePrev) {
479
480
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
508
509
510
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
519
520
521
522
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
528
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
540
541
542
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
558
559
560
561
562
563
564
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
591
592
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
620 n = -n;
621 }
622
623
624
625
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
636
637
638
639
640
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
672
673
674 if (scanMode && plane >= nPlanes)
675 converged = true;
676
677
678
679 const double mu = (
m_masses3N.array() * nBest.array().square()).sum();
681
682
683
684 Matter rWalker(*reactant);
685 const VectorXd gammaR = xR + (
params.oh_tst_options.s_init * guideLen) * u;
687
688
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) {
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) {
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
#define EONC_LOG_DEBUG(...)
#define EONC_LOG_ERROR(...)
#define EONC_LOG_INFO(...)
#define EONC_LOG_CRITICAL(...)
long m_seedState
LCG state for the thermostat draws.
std::vector< VectorXd > m_symDirs
p-hat_i, index 0 = primary
std::shared_ptr< Potential > pot
double reactantQRatio(Matter &matter, const VectorXd &gammaR, const VectorXd &normal)
PlaneAverages samplePlane(Matter &matter, const VectorXd &gamma, const VectorXd &normal)
constexpr bool io_ok(IoStatus s) noexcept