Loading...
Searching...
No Matches
NudgedElasticBand.cpp
Go to the documentation of this file.
1/*
2** This file is part of eOn.
3**
4** SPDX-License-Identifier: BSD-3-Clause
5**
6** Copyright (c) 2010--present, eOn Development Team
7** All rights reserved.
8**
9** Repo:
10** https://github.com/TheochemUI/eOn
11*/
13#include "eon/BaseStructures.h"
19#include "eon/NEBProjection.h"
21#include "eon/NEBSpringForce.h"
22#include "eon/NEBTangent.h"
23#include "eon/Optimizer.h"
24#include "magic_enum/magic_enum.hpp"
25
26#include "eon/EonLogger.h"
27#include <format>
28#include <stdexcept>
29#include <thread>
30using namespace eonc::helpers;
31namespace fs = std::filesystem;
32
33// Nudged Elastic Band definitions
34// First constructor: Now delegates to the second constructor
35NudgedElasticBand::NudgedElasticBand(std::shared_ptr<Matter> initialPassed,
36 std::shared_ptr<Matter> finalPassed,
37 const Parameters &parametersPassed,
38 std::shared_ptr<Potential> potPassed)
40 [&]() {
41 auto &init_opt = parametersPassed.neb_options.initialization;
42 const size_t base_count = parametersPassed.neb_options.image_count;
43
44 // Apply oversampling factor if flag exists
45 const size_t relax_count =
46 init_opt.oversampling
47 ? base_count * init_opt.oversampling_factor
48 : base_count;
49
50 std::vector<Matter> path;
51 switch (init_opt.method) {
52 case NEBInit::FILE: {
53 std::vector<fs::path> file_paths =
54 eonc::helpers::neb_paths::readFilePaths(init_opt.input_path);
55 path = eonc::helpers::neb_paths::filePathInit(
56 file_paths, *initialPassed, base_count);
57 break;
58 }
59 case NEBInit::IDPP:
61 *initialPassed, *finalPassed, relax_count, parametersPassed);
62 break;
65 *initialPassed, *finalPassed, relax_count, parametersPassed);
66 break;
67 case NEBInit::SIDPP:
70 *initialPassed, *finalPassed, relax_count, parametersPassed,
71 (init_opt.method == NEBInit::SIDPP_ZBL));
72 break;
73 case NEBInit::LINEAR:
74 default:
76 *initialPassed, *finalPassed, base_count);
77 break;
78 }
79
80 // Decimate path back to the target count using the cubic spline
81 if (init_opt.oversampling && path.size() > (base_count + 2)) {
82 auto *log = eonc::log::get();
83 QUILL_LOG_INFO(log,
84 "Decimating oversampled path ({} images) to "
85 "{} images via cubic spline.",
86 path.size() - 2, base_count);
87
88 // Perform the Spline Resampling
89 path = eonc::helpers::neb_paths::resamplePath(path, base_count);
90
91 // POST-DECIMATION RE-RELAXATION
92 // The spline might have placed atoms in high-energy positions.
93 QUILL_LOG_INFO(
94 log, "Relaxing decimated path to restore IDPP surface...");
95
96 // Collective IDPP objective for the new reduced path
97 std::shared_ptr<ObjectiveFunction> post_decim_objf =
98 std::make_shared<CollectiveIDPPObjectiveFunction>(
99 path, parametersPassed);
100
101 // Wrap with ZBL if the original method used ZBL options
102 bool use_zbl = (init_opt.method == NEBInit::SIDPP_ZBL);
103 if (use_zbl) {
104 auto zbl_pot = eonc::helpers::neb_paths::createZBLPotential();
105 post_decim_objf = std::make_shared<ZBLRepulsiveIDPPObjective>(
106 post_decim_objf, zbl_pot, path, parametersPassed, 1.0);
107 }
108
110 post_decim_objf, parametersPassed.neb_options.opt_method,
111 parametersPassed);
112
113 // Run a short optimization
114 optim->run(
115 parametersPassed.neb_options.initialization.max_iterations,
116 parametersPassed.neb_options.initialization.max_move);
117 }
118 return path;
119 }(),
120 parametersPassed, potPassed) {}
121
122// Second constructor: Contains all the common setup code
123NudgedElasticBand::NudgedElasticBand(std::vector<Matter> initPath,
124 const Parameters &parametersPassed,
125 std::shared_ptr<Potential> potPassed)
126 : ci_enabled_{parametersPassed.neb_options.climbing_image.enabled},
127 params{parametersPassed},
128 pot{potPassed},
129 E_ref{0.0} {
130
133 params.optimizer_options.convergence_metric, "[Nudged Elastic Band]");
134 this->status = NEBStatus::INIT;
135 numImages = params.neb_options.image_count;
136 atoms = initPath.front().numberOfAtoms();
137
138 // Common initialization logic
139 path.resize(numImages + 2);
140 tangent.resize(numImages + 2);
141 projectedForce.resize(numImages + 2);
142 extremumPosition.resize(2 * (numImages + 1));
143 extremumEnergy.resize(2 * (numImages + 1));
144 extremumCurvature.resize(2 * (numImages + 1));
145 numExtrema = 0;
146
147 // Create per-image potentials if needed for true parallel force evaluation
149 pot->needsPerImageInstance() && params.main_options.parallel;
151 QUILL_LOG_INFO(log,
152 "NEB: Creating per-image potential instances for "
153 "parallel force evaluation ({} images)",
154 numImages + 2);
155 }
156
157 for (long i = 0; i <= numImages + 1; i++) {
158 path[i] = std::make_shared<Matter>(std::move(initPath[i]));
159
160 // Give each INTERMEDIATE image its own potential for true parallelism.
161 // Endpoints (i=0, i=numImages+1) keep the shared pot -- they are only
162 // evaluated once during initialization and never in parallel.
163 if (perImagePotentials_ && i > 0 && i <= numImages) {
164 path[i]->setPotential(eonc::helpers::makePotential(params));
165 }
166
167 tangent[i] = std::make_shared<AtomMatrix>();
168 tangent[i]->resize(atoms, 3);
169 tangent[i]->setZero();
170 projectedForce[i] = std::make_shared<AtomMatrix>();
171 projectedForce[i]->resize(atoms, 3);
172 projectedForce[i]->setZero();
173 }
174
175 // Common final setup
176 movedAfterForceCall = true;
177 path[0]->getPotentialEnergy();
178 path[numImages + 1]->getPotentialEnergy();
179 climbingImage = 0;
180
181 // Setup springs
182 k_u = params.neb_options.spring.weighting.k_max;
183 k_l = params.neb_options.spring.weighting.k_min;
184 if (params.neb_options.spring.weighting.enabled) {
185 ksp = k_l;
186 } else {
187 ksp = params.neb_options.spring.constant;
188 }
189
190 // Cache strategies that are constant across iterations
193
194 // Optional debugging setup
195 if (params.debug_options.estimate_neb_eigenvalues) {
196 eigenmode_solvers.resize(numImages + 2);
197 for (long i = 0; i <= numImages + 1; i++) {
199 eonc::buildEigenmodeStrategy(path[i], parametersPassed, pot);
200 }
201 }
202}
203
205 long iteration = 0;
207
208 QUILL_LOG_DEBUG(log, "Nudged elastic band calculation started.");
209
210 // Initialize E_ref for energy weighting
211 E_ref = std::min(path[0]->getPotentialEnergy(),
212 path[numImages + 1]->getPotentialEnergy());
213
214 updateForces();
215
216 auto objf = std::make_shared<NEBObjectiveFunction>(this, params);
217
218 bool switched{false};
220 objf, params.neb_options.opt_method, params);
221 std::unique_ptr<Optimizer> refine_optim{nullptr};
222 if (params.optimizer_options.refine.method != OptType::None) {
223 refine_optim = eonc::helpers::create::mkOptim(
224 objf, params.optimizer_options.refine.method, params);
225 }
226
227 // OCINEB controller
229 eonc::neb::OCINEBController ocineb(ocinebCfg);
230
231 while (this->status != NEBStatus::GOOD) {
232 if (params.debug_options.write_movies &&
233 (iteration % params.debug_options.write_movies_interval == 0)) {
234 bool append = (iteration != 0);
237 params.debug_options.estimate_neb_eigenvalues,
238 std::format("neb_path_{:03d}.con", iteration), iteration))) {
239 QUILL_LOG_ERROR(log, "Failed to write NEB path movie for iteration {}",
240 iteration);
241 }
242
243 AtomMatrix maxTang;
244 if (maxEnergyImage == 0) {
245 maxTang =
246 path[0]->pbc(path[1]->getPositions() - path[0]->getPositions());
247 } else if (maxEnergyImage == static_cast<size_t>(numImages + 1)) {
248 maxTang = path[numImages]->pbc(path[numImages + 1]->getPositions() -
249 path[numImages]->getPositions());
250 } else {
251 maxTang = *tangent[maxEnergyImage];
252 }
253 maxTang.normalize();
254 auto maxImageMetadata = eonc::io::ConFrameMetadata{};
255 maxImageMetadata.frame_index = static_cast<uint64_t>(maxEnergyImage);
256 maxImageMetadata.energy = path[maxEnergyImage]->getPotentialEnergy();
257 maxImageMetadata.neb_bead = static_cast<uint64_t>(maxEnergyImage);
258 maxImageMetadata.neb_band = static_cast<uint64_t>(iteration);
259 maxImageMetadata.scalars.push_back(
260 {"relative_energy", path[maxEnergyImage]->getPotentialEnergy() -
261 path[0]->getPotentialEnergy()});
262 maxImageMetadata.scalars.push_back(
263 {"parallel_force",
264 matDot(path[maxEnergyImage]->getForces(), maxTang)});
265 maxImageMetadata.strings.push_back({"movie_kind", "neb_maximage"});
266 if (!eonc::io::io_ok(path[maxEnergyImage]->matter2con(
267 "neb_maximage.con", append, &maxImageMetadata))) {
268 EONC_LOG_WARNING("Failed to write neb_maximage.con");
269 }
270 printImageData(true, iteration);
271 }
272
273 VectorXd pos = objf->getPositions();
274 double convForce = convergenceForce();
275
277
278 if (iteration == 0) {
279 baseline_force = convForce;
280 ocineb.initBaseline(convForce);
281
282 // Log configuration banner
283 auto &ci_opt = params.neb_options.climbing_image;
284 auto &mmf_opt = ci_opt.ocineb;
285 auto fmt_trigger = [](double val) -> std::string {
286 if (val > 1e100)
287 return "INF";
288 return std::format("{:.4f}", val);
289 };
290
291 QUILL_LOG_INFO(
292 log,
293 "===============================================================");
294 QUILL_LOG_INFO(log, " NEB Optimization Configuration");
295 QUILL_LOG_INFO(
296 log,
297 "===============================================================");
298 QUILL_LOG_INFO(log, " {:<25} : {:.4f}", "Baseline Force", baseline_force);
299
300 std::string ci_status = ci_opt.enabled ? "ENABLED" : "DISABLED";
301 QUILL_LOG_INFO(log, " {:<25} : {}", "Climbing Image (CI)", ci_status);
302 if (ci_opt.enabled) {
303 double ci_rel_val = baseline_force * ci_opt.trigger_factor;
304 QUILL_LOG_INFO(log, " - {:<21} : {} (Factor: {:.2f})",
305 "Relative Trigger", fmt_trigger(ci_rel_val),
306 ci_opt.trigger_factor);
307 QUILL_LOG_INFO(log, " - {:<21} : {}", "Absolute Trigger",
308 fmt_trigger(ci_opt.trigger_force));
309 QUILL_LOG_INFO(log, " - {:<21} : {}", "Converged Only",
310 ci_opt.converged_only);
311 }
312
313 std::string mmf_status =
314 (ci_opt.enabled && mmf_opt.use_mmf) ? "ENABLED" : "DISABLED";
315 QUILL_LOG_INFO(log, " {:<25} : {}", "Hybrid MMF (OCINEB)", mmf_status);
316 if (ci_opt.enabled && mmf_opt.use_mmf) {
317 QUILL_LOG_INFO(log, " - {:<21} : {:.4f} (Factor: {:.2f})",
318 "Initial Threshold", ocineb.threshold(),
319 mmf_opt.trigger_factor);
320 QUILL_LOG_INFO(log, " - {:<21} : {:.4f}", "Absolute Floor",
321 mmf_opt.trigger_force);
322 QUILL_LOG_INFO(log, " - {:<21} : {:.4f}", "Angle Tolerance",
323 mmf_opt.angle_tol);
324 }
325 QUILL_LOG_INFO(
326 log,
327 "---------------------------------------------------------------");
328
329 EONC_LOG_DEBUG("{:>10s} {:>12s} {:>14s} {:>11s} {:>12s}", "iteration",
330 "step size",
331 params.optimizer_options.convergence_metric_label,
332 "max image", "max energy");
333 QUILL_LOG_DEBUG(
335 "---------------------------------------------------------------\n");
336 }
337
338 // CI active when force drops below relative threshold
339 bool ci_active =
340 params.neb_options.climbing_image.enabled &&
341 (convForce < baseline_force *
342 params.neb_options.climbing_image.trigger_factor ||
343 convForce < params.neb_options.climbing_image.trigger_force);
344
345 if (iteration) {
346 // MMF triggering via controller
347 if (ocineb.shouldTrigger(convForce, ci_active, climbingImage, numImages,
348 ocineb.stabilityCount())) {
349 auto result = ocineb.run(*this, convForce);
350
351 if (result.convergedAfterMMF) {
353 break;
354 }
355 // Post-MMF arc-length reparameterization: pass full path
356 // (endpoints are fixed by resamplePathInPlace, only interior
357 // images are redistributed). Zero force-call cost; next NEB
358 // iteration recomputes all forces anyway.
359 bool didResample = false;
360 if (!result.convergedAfterMMF && result.newForce < convForce) {
362 std::span{path.data(), path.size()});
363 movedAfterForceCall = true;
364 didResample = true;
365 }
366
367 // Reset optimizer AFTER reparameterization so fresh L-BFGS
368 // starts from the redistributed positions.
369 if (result.shouldResetOptimizer || didResample) {
371 objf, params.neb_options.opt_method, params);
372 }
373 }
374
375 if (iteration >= params.neb_options.max_iterations) {
377 break;
378 }
379
380 // Set CI state so updateForces() inside the optimizer step
381 // applies the correct force projection.
382 setCIEnabled(ci_active);
383
384 auto &activeOptim =
385 (refine_optim &&
386 convForce <= params.optimizer_options.refine.threshold)
387 ? refine_optim
388 : optim;
389 if (refine_optim &&
390 convForce <= params.optimizer_options.refine.threshold && !switched) {
391 switched = true;
392 EONC_LOG_DEBUG("Switched to {}",
393 magic_enum::enum_name<OptType>(
394 params.optimizer_options.refine.method));
395 }
396 activeOptim->step(params.optimizer_options.max_move);
397
398 setCIEnabled(params.neb_options.climbing_image.enabled);
399 }
400
401 iteration++;
402
403 double dE = path[maxEnergyImage]->getPotentialEnergy() -
404 path[0]->getPotentialEnergy();
405 double stepSize = eonc::helpers::maxAtomMotionV(
406 path[0]->pbcV(objf->getPositions() - pos));
407 QUILL_LOG_DEBUG(log, "{:>10} {:>12.4e} {:>14.4e} {:>11} {:>12.4}",
408 iteration, stepSize, convergenceForce(), maxEnergyImage,
409 dE);
410
411 if (pot->getType() == PotType::CatLearn) {
412 if (objf->isUncertain()) {
413 QUILL_LOG_DEBUG(log, "NEB failed due to high uncertainty");
415 break;
416 } else if (objf->isConverged()) {
417 QUILL_LOG_DEBUG(log, "NEB converged\n");
419 break;
420 }
421 } else {
422 if (objf->isConverged()) {
423 QUILL_LOG_DEBUG(log, "NEB converged\n");
425 break;
426 }
427 }
428 }
429 return status;
430}
431
432// generate the force value that is compared to the convergence criterion
435 updateForces();
436 double fmax = 0;
437
438 // Determine which images to check for convergence
439 bool ciOnly = params.neb_options.climbing_image.converged_only &&
441 long iStart = ciOnly ? climbingImage : 1;
442 long iEnd = ciOnly ? climbingImage : numImages;
443
444 for (long i = iStart; i <= iEnd; i++) {
445 if (params.optimizer_options.convergence_metric == "norm") {
446 fmax = std::max(fmax, projectedForce[i]->norm());
447 } else if (params.optimizer_options.convergence_metric == "max_atom") {
448 for (int j = 0; j < path[0]->numberOfAtoms(); j++) {
449 if (path[0]->getFixed(j))
450 continue;
451 fmax = std::max(fmax, projectedForce[i]->row(j).norm());
452 }
453 } else if (params.optimizer_options.convergence_metric == "max_component") {
454 fmax = std::max(fmax, projectedForce[i]->maxCoeff());
455 } else {
457 QUILL_LOG_CRITICAL(
458 log, "[Nudged Elastic Band] unknown opt_convergence_metric: {}",
459 params.optimizer_options.convergence_metric);
460 throw std::invalid_argument(
461 std::format("[Nudged Elastic Band] unknown convergence_metric: {}",
462 params.optimizer_options.convergence_metric));
463 }
464 }
465 return fmax;
466}
467
468// Update the forces, do the projections, and add spring forces
470 // Update forces for all intermediate images. Prefer batched evaluation
471 // (single model.forward() over all dirty images, e.g. MetatomicPotential
472 // on GPU). Else fall back to per-image evaluation, which is itself
473 // thread-parallel when (a) the potential is thread-safe on the same
474 // instance, or (b) per-image instances were created (separate models).
475 if (pot->supportsBatchEvaluation() && numImages > 1) {
476 // Collect only images that need recomputation (positions changed).
477 // Materialize atomic numbers and cells first, then build the raw-pointer
478 // arrays after storage is stable. Otherwise vector growth can invalidate
479 // earlier .data() pointers and hand garbage cells/types to forceBatch().
480 std::vector<long> dirty; // indices into path[] (1-based)
481 dirty.reserve(numImages);
482 for (long i = 1; i <= numImages; i++) {
483 if (path[i]->needsForceUpdate()) {
484 dirty.push_back(i);
485 }
486 }
487
488 if (!dirty.empty()) {
489 auto nDirty = static_cast<long>(dirty.size());
490 std::vector<VectorXi> nrsStore;
491 std::vector<Matrix3d> boxStore;
492 std::vector<const double *> posVec, boxVec;
493 std::vector<const int *> nrsVec;
494 std::vector<double *> frcVec;
495 nrsStore.reserve(static_cast<size_t>(nDirty));
496 boxStore.reserve(static_cast<size_t>(nDirty));
497 posVec.reserve(static_cast<size_t>(nDirty));
498 boxVec.reserve(static_cast<size_t>(nDirty));
499 nrsVec.reserve(static_cast<size_t>(nDirty));
500 frcVec.reserve(static_cast<size_t>(nDirty));
501
502 for (long idx : dirty) {
503 nrsStore.push_back(path[idx]->getAtomicNrs());
504 boxStore.push_back(path[idx]->getCell());
505 }
506 for (long j = 0; j < nDirty; j++) {
507 auto idx = dirty[static_cast<size_t>(j)];
508 posVec.push_back(path[idx]->getPositions().data());
509 nrsVec.push_back(nrsStore[static_cast<size_t>(j)].data());
510 frcVec.push_back(path[idx]->forcesData());
511 boxVec.push_back(boxStore[static_cast<size_t>(j)].data());
512 }
513
514 std::vector<double> energies(nDirty), variances(nDirty);
515 pot->forceBatch(nDirty, atoms, posVec.data(), nrsVec.data(),
516 frcVec.data(), energies.data(), variances.data(),
517 boxVec.data());
518 for (long j = 0; j < nDirty; j++) {
519 path[dirty[j]]->setComputedPotential(energies[j], variances[j]);
520 }
521 }
522 } else {
523 // Per-image evaluation (sequential or parallel threads)
524 bool canParallel = pot->isSharedInstanceThreadSafe() || perImagePotentials_;
525 if (numImages > 1 && params.main_options.parallel && canParallel) {
526 // std::thread rather than std::jthread -- Apple Clang libc++ lacks the
527 // latter. Wrap launch + join so a throw from any lambda still joins the
528 // remaining threads before we rethrow; otherwise the unjoined std::thread
529 // destructors call std::terminate().
530 std::vector<std::thread> threads;
531 threads.reserve(static_cast<size_t>(numImages));
532 try {
533 for (long i = 1; i <= numImages; i++) {
534 threads.emplace_back([this, i] { path[i]->getForcesRaw(); });
535 }
536 for (auto &t : threads)
537 t.join();
538 } catch (...) {
539 for (auto &t : threads)
540 if (t.joinable())
541 t.join();
542 throw;
543 }
544 } else {
545 for (long i = 1; i <= numImages; i++) {
546 path[i]->getForcesRaw();
547 }
548 }
549 }
550
551 // Find the highest energy non-endpoint image
552 auto first = path.begin() + 1;
553 auto last = path.begin() + numImages + 1;
554 auto it = std::max_element(
555 first, last,
556 [](const std::shared_ptr<Matter> &a, const std::shared_ptr<Matter> &b) {
557 return a->getPotentialEnergy() < b->getPotentialEnergy();
558 });
559 maxEnergyImage = std::distance(path.begin(), it);
560 double maxEnergy = (*it)->getPotentialEnergy();
561
562 // Update E_ref for energy weighting
563 if (params.neb_options.spring.weighting.enabled) {
564 E_ref = std::min(path[0]->getPotentialEnergy(),
565 path[numImages + 1]->getPotentialEnergy());
566 }
567
568 if (!ci_active) {
569 climbingImage = 0;
570 }
571
572 // Spring strategy must be rebuilt each iteration (depends on maxEnergy,
573 // E_ref). Tangent and projection strategies are cached as members.
575 maxEnergy, E_ref);
576
577 // Pre-allocate temporaries outside the loop to avoid repeated heap
578 // allocation of Nx3 matrices (each ~8KB for 337 atoms).
579 AtomMatrix posDiffNext(atoms, 3), posDiffPrev(atoms, 3);
580
581 for (long i = 1; i <= numImages; i++) {
582 const AtomMatrix &force = path[i]->getForces();
583 const AtomMatrix &pos = path[i]->getPositions();
584 const AtomMatrix &posPrev = path[i - 1]->getPositions();
585 const AtomMatrix &posNext = path[i + 1]->getPositions();
586 double energy = path[i]->getPotentialEnergy();
587 double energyPrev = path[i - 1]->getPotentialEnergy();
588 double energyNext = path[i + 1]->getPotentialEnergy();
589 posDiffNext.noalias() = posNext - pos;
590 posDiffNext = path[i]->pbc(posDiffNext);
591 posDiffPrev.noalias() = pos - posPrev;
592 posDiffPrev = path[i]->pbc(posDiffPrev);
593 double distNext = posDiffNext.norm();
594 double distPrev = posDiffPrev.norm();
595
596 // Tangent via strategy dispatch
597 *tangent[i] = std::visit(
598 [&](auto &t) {
599 return t.compute(posDiffNext, posDiffPrev, energy, energyPrev,
600 energyNext);
601 },
603
604 // Spring forces via strategy dispatch
605 eonc::neb::SpringResult springResult = std::visit(
606 [&](auto &s) -> eonc::neb::SpringResult {
607 using T = std::decay_t<decltype(s)>;
608 if constexpr (std::is_same_v<T, eonc::neb::UniformSpring>) {
609 this->ksp = s.ksp;
610 return s.compute(i, *tangent[i], distNext, distPrev, posDiffNext,
611 posDiffPrev, path[i]);
612 } else if constexpr (std::is_same_v<T, eonc::neb::WeightedSpring>) {
613 return s.compute(i, *tangent[i], distNext, distPrev);
614 } else {
615 return s.compute(i, *tangent[i], posNext, posPrev, pos, path[i]);
616 }
617 },
618 spring);
619
620 // Climbing image or projected force
621 if (ci_active && i == static_cast<long>(maxEnergyImage)) {
623 // CI force: F - 2*(F.t)*t, plus DNEB correction if active
624 AtomMatrix forceDNEB = AtomMatrix::Zero(atoms, 3);
625 if (std::holds_alternative<eonc::neb::DNEB_Projection>(
627 AtomMatrix fPerp = eonc::neb::forcePerp(force, *tangent[i]);
628 forceDNEB = eonc::neb::computeDNEBComponent(springResult.forceSpring,
629 *tangent[i], fPerp);
630 }
631 *projectedForce[i] =
632 eonc::neb::climbingImageForce(force, *tangent[i], forceDNEB);
633 } else {
634 eonc::neb::ImageForceData data{force, *tangent[i], springResult,
635 path[i]->numberOfFreeAtoms(),
636 path[i]->numberOfAtoms()};
637 *projectedForce[i] = std::visit([&](auto &p) { return p.project(data); },
639 }
640
641 eonc::neb::zeroTranslation(*projectedForce[i], path[i]->numberOfFreeAtoms(),
642 path[i]->numberOfAtoms());
643 }
644
645 movedAfterForceCall = false;
646}
647
648// Thin wrappers delegating to eonc::neb:: free functions
649
650void NudgedElasticBand::printImageData(bool writeToFile, size_t idx) {
652 params.debug_options.estimate_neb_eigenvalues,
653 writeToFile, idx, log);
654}
655
658 numExtrema = result.numExtrema;
659 extremumPosition = std::move(result.positions);
660 extremumEnergy = std::move(result.energies);
661 extremumCurvature = std::move(result.curvatures);
662}
663
664std::vector<readcon::ConFrame>
665NudgedElasticBand::pathFrames(std::optional<size_t> bandIndex) {
668 params.debug_options.estimate_neb_eigenvalues, bandIndex);
669}
double matDot(const AtomMatrix &a, const AtomMatrix &b)
SIMD-optimized dot product for contiguous Eigen matrices.
Definition Eigen.h:50
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
#define EONC_LOG_DEBUG(...)
Definition EonLogger.h:244
#define EONC_LOG_WARNING(...)
Definition EonLogger.h:256
The optimizer class is used to serve as an abstract class for all optimizers, as well as to call an o...
NudgedElasticBand(std::shared_ptr< Matter > initialPassed, std::shared_ptr< Matter > finalPassed, const Parameters &parametersPassed, std::shared_ptr< Potential > potPassed)
std::vector< double > extremumEnergy
void printImageData(bool writeToFile=false, size_t idx=0)
std::vector< std::shared_ptr< AtomMatrix > > tangent
std::vector< readcon::ConFrame > pathFrames(std::optional< size_t > bandIndex=std::nullopt)
In-memory ConFrames with the same NEB stamps as writePathCon / neb.con.
std::vector< std::shared_ptr< Matter > > path
bool perImagePotentials_
Whether per-image potential instances exist.
std::vector< double > extremumPosition
std::vector< std::shared_ptr< EigenmodeStrategy > > eigenmode_solvers
NudgedElasticBand::NEBStatus compute(void)
NudgedElasticBand(std::shared_ptr< Matter > initialPassed, std::shared_ptr< Matter > finalPassed, const Parameters &parametersPassed, std::shared_ptr< Potential > potPassed)
neb::TangentStrategy tangentStrat_
std::vector< std::shared_ptr< AtomMatrix > > projectedForce
void setCIEnabled(bool enabled)
std::shared_ptr< Potential > pot
neb::ProjectionStrategy projectionStrat_
std::vector< double > extremumCurvature
struct eonc::Parameters::neb_options_t neb_options
bool shouldTrigger(double convForce, bool ci_active, long climbingImage, long numImages, int ciStabilityCounter) const
static Config fromParams(const Parameters &params)
void updateStability(long climbingImage)
MMFResult run(eonc::NudgedElasticBand &neb, double convForce)
void initBaseline(double baseline_force)
std::unique_ptr< Optimizer > mkOptim(std::shared_ptr< ObjectiveFunction > a_objf, OptType a_otype, const Parameters &a_params)
Definition Optimizer.cpp:21
std::vector< Matter > sidppPath(const Matter &initImg, const Matter &finalImg, size_t target_nimgs, const Parameters &params, bool use_zbl)
void resamplePathInPlace(std::span< std::shared_ptr< Matter > > path)
In-place path reparameterization for NEB shared_ptr paths.
std::vector< Matter > linearPath(const Matter &initImg, const Matter &finalImg, const size_t nimgs)
std::vector< Matter > idppPath(const Matter &initImg, const Matter &finalImg, const size_t nimgs, const Parameters &params, bool use_zbl)
std::vector< Matter > idppCollectivePath(const Matter &initImg, const Matter &finalImg, size_t nimgs, const Parameters &params, bool use_zbl)
void requireKnownConvergenceMetric(std::string_view metric, std::string_view context)
Throws std::invalid_argument naming context when metric is unrecognized.
std::shared_ptr< Potential > makePotential(const Parameters &params)
double maxAtomMotionV(const VectorXd v1)
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
quill::Logger * get() noexcept
Get or create the default "combi" logger.
Definition EonLogger.h:44
quill::Logger * traceback() noexcept
Get or create the "_traceback" logger for traceback logging.
Definition EonLogger.h:88
void zeroTranslation(AtomMatrix &projectedForce, int nFreeAtoms, int nAtoms)
Zero net translational force for fully free systems.
SpringStrategy buildSpringStrategy(const Parameters &params, const std::vector< std::shared_ptr< Matter > > &path, long numImages, int atoms, double maxEnergy, double E_ref)
Build the appropriate spring strategy from parameters and current path state.
TangentStrategy buildTangentStrategy(const Parameters &params)
Build the tangent strategy from parameters.
ProjectionStrategy buildProjectionStrategy(const Parameters &params)
Build the projection strategy from parameters.
AtomMatrix climbingImageForce(const AtomMatrix &force, const AtomMatrix &tangent, const AtomMatrix &forceDNEB)
Compute the climbing image projected force.
std::vector< readcon::ConFrame > pathToConFrames(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::optional< size_t > bandIndex)
Build stamped ConFrames for a NEB band (same metadata as writePathCon).
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)
Write a NEB band as a multi-frame .con via readcon ConFrameBuilder::clone().
AtomMatrix computeDNEBComponent(const AtomMatrix &forceSpring, const AtomMatrix &tangent, const AtomMatrix &fPerp)
Compute the DNEB force component for a given image.
void printImageData(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, bool writeToFile, size_t idx, eonc::log::Scoped log)
Print NEB image data to log and optionally to file.
AtomMatrix forcePerp(const AtomMatrix &force, const AtomMatrix &tangent)
Compute the perpendicular component of force relative to the tangent.
ExtremaResult findSplineExtrema(const std::vector< std::shared_ptr< Matter > > &path, const std::vector< std::shared_ptr< AtomMatrix > > &tangent, long numImages)
Find extrema along the MEP using cubic spline interpolation.
std::shared_ptr< EigenmodeStrategy > buildEigenmodeStrategy(std::shared_ptr< Matter > matter, const Parameters &params, std::shared_ptr< Potential > pot)
Build the eigenmode solver from parameters.
struct eonc::Parameters::neb_options_t::path_initialization_t initialization
std::optional< uint64_t > frame_index
Definition ConFileIO.h:72
Data for a single image needed by projection strategies.
Result of spring force computation for a single image.