280 {
281 long iteration = 0;
283
284 QUILL_LOG_DEBUG(
log,
"Nudged elastic band calculation started.");
285
286
287 E_ref = std::max(
path[0]->getPotentialEnergy(),
289
291
292 auto objf = std::make_shared<NEBObjectiveFunction>(
this,
params);
293
294 bool switched{false};
297 std::unique_ptr<Optimizer> refine_optim{nullptr};
300 objf,
params.optimizer_options().refine.method,
params);
301 }
302
305 bool zoomDone{false};
306 long zoomStable{0};
307 long zoomPrevCI{-1};
308 long zoomAt{-1};
309
311 if (
params.debug_options().write_movies &&
312 (iteration %
params.debug_options().write_movies_interval == 0)) {
313 bool append = (iteration != 0);
316 params.debug_options().estimate_neb_eigenvalues,
317 std::format("neb_path_{:03d}.con", iteration), iteration,
319 QUILL_LOG_ERROR(
log,
"Failed to write NEB path movie for iteration {}",
320 iteration);
321 }
322
325 maxTang =
326 path[0]->pbc(
path[1]->getPositions() -
path[0]->getPositions());
330 } else {
332 }
333 eonc::safemath::safe_normalize_inplace(maxTang);
334 auto maxImageMetadata = eonc::io::ConFrameMetadata{};
335 maxImageMetadata.frame_index =
static_cast<uint64_t
>(
maxEnergyImage);
337 maxImageMetadata.neb_bead =
static_cast<uint64_t
>(
maxEnergyImage);
338 maxImageMetadata.neb_band = static_cast<uint64_t>(iteration);
339 maxImageMetadata.scalars.push_back(
340 {"relative_energy",
342 maxImageMetadata.scalars.push_back(
343 {"parallel_force",
345 maxImageMetadata.strings.push_back({"movie_kind", "neb_maximage"});
347 "neb_maximage.con", append, &maxImageMetadata))) {
349 }
351 }
352
353 VectorXd pos = objf->getPositions();
355
357
358 if (iteration == 0) {
360 ocineb.initBaseline(convForce);
361
362
363 auto &ci_opt =
params.neb_options().climbing_image;
364 auto &mmf_opt = ci_opt.ocineb;
365 auto fmt_trigger = [](double val) -> std::string {
366 if (val > 1e100)
367 return "INF";
368 return std::format("{:.4f}", val);
369 };
370
371 QUILL_LOG_INFO(
373 "===============================================================");
374 QUILL_LOG_INFO(
log,
" NEB Optimization Configuration");
375 QUILL_LOG_INFO(
377 "===============================================================");
379
380 std::string ci_status = ci_opt.enabled ? "ENABLED" : "DISABLED";
381 QUILL_LOG_INFO(
log,
" {:<25} : {}",
"Climbing Image (CI)", ci_status);
382 if (ci_opt.enabled) {
384 QUILL_LOG_INFO(
log,
" - {:<21} : {} (Factor: {:.2f})",
385 "Relative Trigger", fmt_trigger(ci_rel_val),
386 ci_opt.trigger_factor);
387 QUILL_LOG_INFO(
log,
" - {:<21} : {}",
"Absolute Trigger",
388 fmt_trigger(ci_opt.trigger_force));
389 QUILL_LOG_INFO(
log,
" - {:<21} : {}",
"Converged Only",
390 ci_opt.converged_only);
391 }
392
393 std::string mmf_status =
394 (ci_opt.enabled && mmf_opt.use_mmf) ? "ENABLED" : "DISABLED";
395 QUILL_LOG_INFO(
log,
" {:<25} : {}",
"Hybrid MMF (OCINEB)", mmf_status);
396 if (ci_opt.enabled && mmf_opt.use_mmf) {
397 QUILL_LOG_INFO(
log,
" - {:<21} : {:.4f} (Factor: {:.2f})",
398 "Initial Threshold", ocineb.threshold(),
399 mmf_opt.trigger_factor);
400 QUILL_LOG_INFO(
log,
" - {:<21} : {:.4f}",
"Absolute Floor",
401 mmf_opt.trigger_force);
402 QUILL_LOG_INFO(
log,
" - {:<21} : {:.4f}",
"Angle Tolerance",
403 mmf_opt.angle_tol);
404 }
405 QUILL_LOG_INFO(
407 "---------------------------------------------------------------");
408
409 EONC_LOG_DEBUG(
"{:>10s} {:>12s} {:>14s} {:>11s} {:>12s}",
"iteration",
410 "step size",
411 params.optimizer_options().convergence_metric_label,
412 "max image", "max energy");
413 QUILL_LOG_DEBUG(
415 "---------------------------------------------------------------\n");
416 }
417
418
419 bool ci_active =
420 params.neb_options().climbing_image.enabled &&
422 params.neb_options().climbing_image.trigger_factor ||
423 convForce <
params.neb_options().climbing_image.trigger_force);
424
425 bool zoomedThisStep = false;
426 if (iteration && !zoomDone &&
params.neb_options().zoom.enabled &&
429 ++zoomStable;
430 } else {
432 zoomStable = 1;
433 }
434 const auto &zoom =
params.neb_options().zoom;
435 const double zoomForce =
436 zoom.activation_threshold > 0.0
437 ? zoom.activation_threshold
438 : 10.0 *
params.neb_options().force_tolerance;
439 if (zoomStable >= zoom.stability_count && convForce < zoomForce) {
440 std::vector<double> energy;
441 energy.reserve(
path.size());
442 for (
const auto &image :
path) {
443 energy.push_back(image->getPotentialEnergy());
444 }
445 const auto window =
448 zoom.interpolation)) {
452 zoomDone = true;
453 zoomedThisStep = true;
454 zoomAt = iteration;
455 QUILL_LOG_INFO(
log,
"Zoom-NEB: packed the band onto images [{}, {}]",
456 window.lo, window.hi);
457 }
458 }
459 }
460
461 if (iteration) {
462
463
464 if (!zoomedThisStep &&
466 ocineb.stabilityCount())) {
467 auto result = ocineb.run(*this, convForce);
468
469 if (result.convergedAfterMMF) {
471 break;
472 }
473
474
475
476
477 bool didResample = false;
478 if (!result.convergedAfterMMF && result.newForce < convForce &&
480 path[0]->numberOfAtoms() > 6) {
482 std::span{
path.data(),
path.size()});
484 didResample = true;
485 }
486
487
488
489 if (result.shouldResetOptimizer || didResample) {
492 }
493 }
494
495 long iterLimit =
params.neb_options().max_iterations;
496 if (zoomDone &&
params.neb_options().zoom.max_iterations > 0 &&
497 zoomAt >= 0) {
498 iterLimit = zoomAt +
params.neb_options().zoom.max_iterations;
499 }
500 if (iteration >= iterLimit) {
502 break;
503 }
504
505 if (zoomedThisStep) {
506 iteration++;
507 continue;
508 }
509
510
511
513
514 auto &activeOptim =
515 (refine_optim &&
516 convForce <=
params.optimizer_options().refine.threshold)
517 ? refine_optim
518 : optim;
519 if (refine_optim &&
520 convForce <=
params.optimizer_options().refine.threshold &&
521 !switched) {
522 switched = true;
524 magic_enum::enum_name<OptType>(
525 params.optimizer_options().refine.method));
526 }
527 activeOptim->step(
params.optimizer_options().max_move);
528
530 }
531
532 iteration++;
533
535 double stepSize = 0.0;
537 const VectorXd delta = objf->difference(objf->getPositions(), pos);
538 const long seg = 3L *
atoms + 9L;
539 for (
long image = 0; image <
numImages; ++image) {
540 stepSize = std::max(
541 stepSize, delta.segment(image * seg, seg).cwiseAbs().maxCoeff());
542 }
543 } else {
545 path[0]->pbcV(objf->getPositions() - pos));
546 }
547 QUILL_LOG_DEBUG(
log,
"{:>10} {:>12.4e} {:>14.4e} {:>11} {:>12.4}",
549 dE);
550
552 if (objf->isUncertain()) {
553 QUILL_LOG_DEBUG(
log,
"NEB failed due to high uncertainty");
555 break;
556 } else if (objf->isConverged()) {
557 QUILL_LOG_DEBUG(
log,
"NEB converged\n");
559 break;
560 }
561 } else {
562 if (objf->isConverged()) {
563 QUILL_LOG_DEBUG(
log,
"NEB converged\n");
565 break;
566 }
567 }
568 }
570}
double matDot(const AtomMatrix &a, const AtomMatrix &b)
SIMD-optimized dot product for contiguous Eigen matrices.
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
#define EONC_LOG_DEBUG(...)
#define EONC_LOG_WARNING(...)
std::size_t maxEnergyImage
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)
Write a NEB band as a multi-frame .con via readcon ConFrameBuilder::clone().