292 {
295 this->
status = NEBStatus::RUNNING;
296
297 QUILL_LOG_DEBUG(
log,
"Nudged elastic band calculation started.");
298
299
300 E_ref = std::max(
path[0]->getPotentialEnergy(),
302
304
305 auto objf = std::make_shared<NEBObjectiveFunction>(
this,
params);
306
307 bool switched{false};
308#ifdef WITH_RGSADDLE
309 std::unique_ptr<XtsciBand> rustBand;
310#endif
311 std::unique_ptr<Optimizer> optim;
312 std::unique_ptr<Optimizer> refine_optim;
313#ifdef WITH_RGSADDLE
314 const auto bandMethod =
params.neb_options().opt_method;
315 if (!
params.neb_options().spring.geometric &&
316 (bandMethod == OptType::XTSCI || bandMethod == OptType::LBFGS ||
317 bandMethod == OptType::FIRE)) {
318 rustBand = std::make_unique<XtsciBand>(*
this,
params);
319 } else
320#endif
321 {
324 if (
params.optimizer_options().refine.method != OptType::None) {
326 objf,
params.optimizer_options().refine.method,
params);
327 }
328 }
329
332 bool zoomDone{false};
333 long zoomStable{0};
334 long zoomPrevCI{-1};
335 long zoomAt{-1};
336
337 double activation_force{0.0};
338
339
340
341
342 const double moveBase =
params.optimizer_options().max_move;
343 const bool adaptCg =
params.neb_options().opt_method == OptType::CG;
344 double moveNow = moveBase;
345 double bestForce = std::numeric_limits<double>::infinity();
346 int stall = 0;
347
348 while (this->
status != NEBStatus::GOOD) {
350 path.front()->pollCancel(
"neb");
351 }
352 if (
params.debug_options().write_movies &&
353 (iteration %
params.debug_options().write_movies_interval == 0)) {
354 bool append = (iteration != 0);
357 params.debug_options().estimate_neb_eigenvalues,
358 std::format("neb_path_{:03d}.con", iteration), iteration,
360 QUILL_LOG_ERROR(
log,
"Failed to write NEB path movie for iteration {}",
361 iteration);
362 }
363
366 maxTang =
367 path[0]->pbc(
path[1]->getPositions() -
path[0]->getPositions());
371 } else {
373 }
374 eonc::safemath::safe_normalize_inplace(maxTang);
375 auto maxImageMetadata = eonc::io::ConFrameMetadata{};
376 maxImageMetadata.frame_index =
static_cast<uint64_t
>(
maxEnergyImage);
378 maxImageMetadata.neb_bead =
static_cast<uint64_t
>(
maxEnergyImage);
379 maxImageMetadata.neb_band = static_cast<uint64_t>(iteration);
380 maxImageMetadata.scalars.push_back(
381 {"relative_energy",
383 maxImageMetadata.scalars.push_back(
384 {"parallel_force",
386 maxImageMetadata.strings.push_back({"movie_kind", "neb_maximage"});
388 "neb_maximage.con", append, &maxImageMetadata))) {
390 }
392 }
393
394 VectorXd pos = objf->getPositions();
396
398
399 if (iteration == 0) {
401 activation_force = convForce;
402 ocineb.initBaseline(convForce);
403
404
405 auto &ci_opt =
params.neb_options().climbing_image;
406 auto &mmf_opt = ci_opt.ocineb;
407 auto fmt_trigger = [](double val) -> std::string {
408 if (val > 1e100)
409 return "INF";
410 return std::format("{:.4f}", val);
411 };
412
413 QUILL_LOG_INFO(
415 "===============================================================");
416 QUILL_LOG_INFO(
log,
" NEB Optimization Configuration");
417 QUILL_LOG_INFO(
419 "===============================================================");
421
422 std::string ci_status = ci_opt.enabled ? "ENABLED" : "DISABLED";
423 QUILL_LOG_INFO(
log,
" {:<25} : {}",
"Climbing Image (CI)", ci_status);
424 if (ci_opt.enabled) {
426 QUILL_LOG_INFO(
log,
" - {:<21} : {} (Factor: {:.2f})",
427 "Relative Trigger", fmt_trigger(ci_rel_val),
428 ci_opt.trigger_factor);
429 QUILL_LOG_INFO(
log,
" - {:<21} : {}",
"Absolute Trigger",
430 fmt_trigger(ci_opt.trigger_force));
431 QUILL_LOG_INFO(
log,
" - {:<21} : {}",
"Converged Only",
432 ci_opt.converged_only);
433 }
434
435 std::string mmf_status =
436 (ci_opt.enabled && mmf_opt.use_mmf) ? "ENABLED" : "DISABLED";
437 QUILL_LOG_INFO(
log,
" {:<25} : {}",
"Hybrid MMF (OCINEB)", mmf_status);
438 if (ci_opt.enabled && mmf_opt.use_mmf) {
439 QUILL_LOG_INFO(
log,
" - {:<21} : {:.4f} (Factor: {:.2f})",
440 "Initial Threshold", ocineb.threshold(),
441 mmf_opt.trigger_factor);
442 QUILL_LOG_INFO(
log,
" - {:<21} : {:.4f}",
"Absolute Floor",
443 mmf_opt.trigger_force);
444 QUILL_LOG_INFO(
log,
" - {:<21} : {:.4f}",
"Angle Tolerance",
445 mmf_opt.angle_tol);
446 }
447 QUILL_LOG_INFO(
449 "---------------------------------------------------------------");
450
451 EONC_LOG_DEBUG(
"{:>10s} {:>12s} {:>14s} {:>11s} {:>12s}",
"iteration",
452 "step size",
453 params.optimizer_options().convergence_metric_label,
454 "max image", "max energy");
455 QUILL_LOG_DEBUG(
457 "---------------------------------------------------------------\n");
458 }
459
460
461 bool ci_active =
462 params.neb_options().climbing_image.enabled &&
463 (activation_force <
465 params.neb_options().climbing_image.trigger_factor ||
466 activation_force <
params.neb_options().climbing_image.trigger_force);
467
468 bool zoomedThisStep = false;
469 if (iteration && !zoomDone &&
params.neb_options().zoom.enabled &&
472 ++zoomStable;
473 } else {
475 zoomStable = 1;
476 }
477 const auto &zoom =
params.neb_options().zoom;
478 const double zoomForce =
479 zoom.activation_threshold > 0.0
480 ? zoom.activation_threshold
481 : 10.0 *
params.neb_options().force_tolerance;
482 if (zoomStable >= zoom.stability_count && convForce < zoomForce) {
483 std::vector<double> energy;
484 energy.reserve(
path.size());
485 for (
const auto &image :
path) {
486 energy.push_back(image->getPotentialEnergy());
487 }
488 const auto window =
491 zoom.interpolation)) {
495 zoomDone = true;
496 zoomedThisStep = true;
497 zoomAt = iteration;
498 QUILL_LOG_INFO(
log,
"Zoom-NEB: packed the band onto images [{}, {}]",
499 window.lo, window.hi);
500 }
501 }
502 }
503
504 if (iteration) {
505
506
507 if (!zoomedThisStep &&
509 ocineb.stabilityCount())) {
510 auto result = ocineb.run(*this, convForce);
511
512 if (result.convergedAfterMMF) {
514 break;
515 }
516
517
518
519
520 bool didResample = false;
521 if (!result.convergedAfterMMF && result.newForce < convForce &&
523 path[0]->numberOfAtoms() > 6) {
525 std::span{
path.data(),
path.size()});
527 didResample = true;
528 }
529
530
531
532 if (result.shouldResetOptimizer || didResample) {
533#ifdef WITH_RGSADDLE
534 if (rustBand) {
535 rustBand->syncFromPath();
536 rustBand->reset();
537 } else
538#endif
539 {
542 }
543 }
544 }
545
546 long iterLimit =
params.neb_options().max_iterations;
547 if (zoomDone &&
params.neb_options().zoom.max_iterations > 0 &&
548 zoomAt >= 0) {
549 iterLimit = zoomAt +
params.neb_options().zoom.max_iterations;
550 }
551 if (iteration >= iterLimit) {
552 status = NEBStatus::BAD_MAX_ITERATIONS;
553 break;
554 }
555
556 if (zoomedThisStep) {
558 iteration++;
559 continue;
560 }
561
562
563
565
566#ifdef WITH_RGSADDLE
567 if (rustBand) {
568 rustBand->step(moveNow);
569 } else
570#endif
571 {
572 auto &activeOptim =
573 (refine_optim &&
574 convForce <=
params.optimizer_options().refine.threshold)
575 ? refine_optim
576 : optim;
577 if (refine_optim &&
578 convForce <=
params.optimizer_options().refine.threshold &&
579 !switched) {
580 switched = true;
582 magic_enum::enum_name<OptType>(
583 params.optimizer_options().refine.method));
584 }
585 activeOptim->step(moveNow);
586 }
587
589 if (adaptCg && activation_force < bestForce * 0.98) {
590 bestForce = activation_force;
591 stall = 0;
592 if (moveNow < moveBase && convForce > 0.0 &&
593 activation_force < 0.8 * convForce) {
594 moveNow = std::min(moveBase, moveNow * 1.5);
595 }
596 } else if (adaptCg) {
597 ++stall;
598 if (convForce > 0.0 && activation_force > 2.0 * convForce) {
599 moveNow = std::max(moveNow * 0.5, moveBase * 0.2);
600 }
601 if (stall >= 12) {
602 moveNow = std::max(moveBase * 0.2, moveNow * 0.5);
603 stall = 0;
604#ifdef WITH_RGSADDLE
605 if (rustBand) {
606 rustBand->reset();
607 } else
608#endif
609 {
612 if (refine_optim) {
614 objf,
params.optimizer_options().refine.method,
params);
615 }
616 }
617 QUILL_LOG_DEBUG(
log,
"Band stalled; reset optimizer, step {:.4e}",
618 moveNow);
619 }
620 }
622 }
623
624 iteration++;
625
627 double stepSize = 0.0;
629 const VectorXd delta = objf->difference(objf->getPositions(), pos);
630 const long seg = 3L *
atoms + 9L;
631 for (
long image = 0; image <
numImages; ++image) {
632 stepSize = std::max(
633 stepSize, delta.segment(image * seg, seg).cwiseAbs().maxCoeff());
634 }
635 } else {
637 path[0]->pbcV(objf->getPositions() - pos));
638 }
639 QUILL_LOG_DEBUG(
log,
"{:>10} {:>12.4e} {:>14.4e} {:>11} {:>12.4}",
641 dE);
642
643 if (
pot->getType() == PotType::CatLearn) {
644 if (objf->isUncertain()) {
645 QUILL_LOG_DEBUG(
log,
"NEB failed due to high uncertainty");
646 status = NEBStatus::MAX_UNCERTAINTY;
647 break;
648 } else if (objf->isConverged()) {
649 QUILL_LOG_DEBUG(
log,
"NEB converged\n");
651 break;
652 }
653 } else {
654 if (objf->isConverged()) {
655 QUILL_LOG_DEBUG(
log,
"NEB converged\n");
657 break;
658 }
659 }
660 }
662}
double matDot(const AtomMatrix &a, const AtomMatrix &b)
SIMD-optimized dot product for contiguous Eigen matrices.
#define EONC_LOG_DEBUG(...)
#define EONC_LOG_WARNING(...)
void printImageData(bool writeToFile=false, size_t idx=0)
double convergenceForce(void)
void setCIEnabled(bool enabled)
friend class eonc::neb::OCINEBController
static Config fromParams(const Parameters ¶ms)
double maxAtomMotionV(const VectorXd v1)
void resamplePathInPlace(std::span< std::shared_ptr< Matter > > path)
In-place path reparameterization for NEB shared_ptr paths.
IoStatus matter2con(Matter &m, std::string filename, bool append, const ConFrameMetadata *metadata)
Append a frame to a .con, or truncate and write one frame.
constexpr bool io_ok(IoStatus s) noexcept
Window selectWindow(const std::vector< double > &energy, std::size_t climbingImage, const neb_options_t::zoom_options_t &cfg)
Auto: contiguous images around the climbing image whose energy is above E_ref + alpha * barrier.
bool redistributePath(std::vector< std::shared_ptr< Matter > > &path, Window window, neb_options_t::zoom_options_t::Interpolation how)
Place every band image on the window by equal arc length.
eonc::io::IoStatus writePathCon(const std::vector< std::shared_ptr< Matter > > &path, const std::vector< std::shared_ptr< AtomMatrix > > &tangent, const std::vector< std::shared_ptr< EigenmodeStrategy > > &eigenmode_solvers, long numImages, bool estimateEigenvalues, std::string filename, std::optional< size_t > bandIndex, double referenceEnergy, const std::vector< double > *quantumFreeEnergy, const eonc::tunneling::BandWalls *walls)
Write a NEB band as a multi-frame .con via readcon ConFrameBuilder::clone().