24#include "magic_enum/magic_enum.hpp"
31namespace fs = std::filesystem;
36 std::shared_ptr<Matter> finalPassed,
38 std::shared_ptr<Potential> potPassed)
45 const size_t relax_count =
47 ? base_count * init_opt.oversampling_factor
50 std::vector<Matter>
path;
51 switch (init_opt.method) {
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);
61 *initialPassed, *finalPassed, relax_count, parametersPassed);
65 *initialPassed, *finalPassed, relax_count, parametersPassed);
70 *initialPassed, *finalPassed, relax_count, parametersPassed,
76 *initialPassed, *finalPassed, base_count);
81 if (init_opt.oversampling && path.size() > (base_count + 2)) {
82 auto *log = eonc::log::get();
84 "Decimating oversampled path ({} images) to "
85 "{} images via cubic spline.",
86 path.size() - 2, base_count);
89 path = eonc::helpers::neb_paths::resamplePath(path, base_count);
94 log,
"Relaxing decimated path to restore IDPP surface...");
97 std::shared_ptr<ObjectiveFunction> post_decim_objf =
98 std::make_shared<CollectiveIDPPObjectiveFunction>(
99 path, parametersPassed);
102 bool use_zbl = (init_opt.method == NEBInit::SIDPP_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);
110 post_decim_objf, parametersPassed.neb_options.opt_method,
115 parametersPassed.neb_options.initialization.max_iterations,
116 parametersPassed.neb_options.initialization.max_move);
120 parametersPassed, potPassed) {}
125 std::shared_ptr<Potential> potPassed)
126 :
ci_enabled_{parametersPassed.neb_options.climbing_image.enabled},
133 params.optimizer_options.convergence_metric,
"[Nudged Elastic Band]");
136 atoms = initPath.front().numberOfAtoms();
149 pot->needsPerImageInstance() &&
params.main_options.parallel;
152 "NEB: Creating per-image potential instances for "
153 "parallel force evaluation ({} images)",
157 for (
long i = 0; i <=
numImages + 1; i++) {
158 path[i] = std::make_shared<Matter>(std::move(initPath[i]));
167 tangent[i] = std::make_shared<AtomMatrix>();
177 path[0]->getPotentialEnergy();
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) {
187 ksp =
params.neb_options.spring.constant;
195 if (
params.debug_options.estimate_neb_eigenvalues) {
197 for (
long i = 0; i <=
numImages + 1; i++) {
208 QUILL_LOG_DEBUG(
log,
"Nudged elastic band calculation started.");
211 E_ref = std::min(
path[0]->getPotentialEnergy(),
216 auto objf = std::make_shared<NEBObjectiveFunction>(
this,
params);
218 bool switched{
false};
221 std::unique_ptr<Optimizer> refine_optim{
nullptr};
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 {}",
246 path[0]->pbc(
path[1]->getPositions() -
path[0]->getPositions());
257 maxImageMetadata.neb_bead =
static_cast<uint64_t
>(
maxEnergyImage);
258 maxImageMetadata.neb_band =
static_cast<uint64_t
>(iteration);
259 maxImageMetadata.scalars.push_back(
261 path[0]->getPotentialEnergy()});
262 maxImageMetadata.scalars.push_back(
265 maxImageMetadata.strings.push_back({
"movie_kind",
"neb_maximage"});
267 "neb_maximage.con", append, &maxImageMetadata))) {
273 VectorXd pos = objf->getPositions();
278 if (iteration == 0) {
283 auto &ci_opt =
params.neb_options.climbing_image;
284 auto &mmf_opt = ci_opt.ocineb;
285 auto fmt_trigger = [](
double val) -> std::string {
288 return std::format(
"{:.4f}", val);
293 "===============================================================");
294 QUILL_LOG_INFO(
log,
" NEB Optimization Configuration");
297 "===============================================================");
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) {
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);
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})",
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",
327 "---------------------------------------------------------------");
329 EONC_LOG_DEBUG(
"{:>10s} {:>12s} {:>14s} {:>11s} {:>12s}",
"iteration",
331 params.optimizer_options.convergence_metric_label,
332 "max image",
"max energy");
335 "---------------------------------------------------------------\n");
340 params.neb_options.climbing_image.enabled &&
342 params.neb_options.climbing_image.trigger_factor ||
343 convForce <
params.neb_options.climbing_image.trigger_force);
349 auto result = ocineb.
run(*
this, convForce);
351 if (result.convergedAfterMMF) {
359 bool didResample =
false;
360 if (!result.convergedAfterMMF && result.newForce < convForce) {
362 std::span{
path.data(),
path.size()});
369 if (result.shouldResetOptimizer || didResample) {
375 if (iteration >=
params.neb_options.max_iterations) {
386 convForce <=
params.optimizer_options.refine.threshold)
390 convForce <=
params.optimizer_options.refine.threshold && !switched) {
393 magic_enum::enum_name<OptType>(
394 params.optimizer_options.refine.method));
396 activeOptim->step(
params.optimizer_options.max_move);
404 path[0]->getPotentialEnergy();
406 path[0]->pbcV(objf->getPositions() - pos));
407 QUILL_LOG_DEBUG(
log,
"{:>10} {:>12.4e} {:>14.4e} {:>11} {:>12.4}",
412 if (objf->isUncertain()) {
413 QUILL_LOG_DEBUG(
log,
"NEB failed due to high uncertainty");
416 }
else if (objf->isConverged()) {
417 QUILL_LOG_DEBUG(
log,
"NEB converged\n");
422 if (objf->isConverged()) {
423 QUILL_LOG_DEBUG(
log,
"NEB converged\n");
439 bool ciOnly =
params.neb_options.climbing_image.converged_only &&
444 for (
long i = iStart; i <= iEnd; i++) {
445 if (
params.optimizer_options.convergence_metric ==
"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))
453 }
else if (
params.optimizer_options.convergence_metric ==
"max_component") {
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));
480 std::vector<long> dirty;
483 if (
path[i]->needsForceUpdate()) {
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));
502 for (
long idx : dirty) {
503 nrsStore.push_back(
path[idx]->getAtomicNrs());
504 boxStore.push_back(
path[idx]->getCell());
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());
514 std::vector<double> energies(nDirty), variances(nDirty);
515 pot->forceBatch(nDirty,
atoms, posVec.data(), nrsVec.data(),
516 frcVec.data(), energies.data(), variances.data(),
518 for (
long j = 0; j < nDirty; j++) {
519 path[dirty[j]]->setComputedPotential(energies[j], variances[j]);
530 std::vector<std::thread> threads;
531 threads.reserve(
static_cast<size_t>(
numImages));
534 threads.emplace_back([
this, i] {
path[i]->getForcesRaw(); });
536 for (
auto &t : threads)
539 for (
auto &t : threads)
546 path[i]->getForcesRaw();
552 auto first =
path.begin() + 1;
554 auto it = std::max_element(
556 [](
const std::shared_ptr<Matter> &a,
const std::shared_ptr<Matter> &b) {
557 return a->getPotentialEnergy() < b->getPotentialEnergy();
560 double maxEnergy = (*it)->getPotentialEnergy();
563 if (
params.neb_options.spring.weighting.enabled) {
564 E_ref = std::min(
path[0]->getPotentialEnergy(),
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();
599 return t.compute(posDiffNext, posDiffPrev, energy, energyPrev,
607 using T = std::decay_t<
decltype(s)>;
608 if constexpr (std::is_same_v<T, eonc::neb::UniformSpring>) {
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);
615 return s.compute(i, *
tangent[i], posNext, posPrev, pos,
path[i]);
625 if (std::holds_alternative<eonc::neb::DNEB_Projection>(
635 path[i]->numberOfFreeAtoms(),
636 path[i]->numberOfAtoms()};
637 *
projectedForce[i] = std::visit([&](
auto &p) {
return p.project(data); },
642 path[i]->numberOfAtoms());
652 params.debug_options.estimate_neb_eigenvalues,
653 writeToFile, idx,
log);
664std::vector<readcon::ConFrame>
668 params.debug_options.estimate_neb_eigenvalues, bandIndex);
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(...)
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 ¶metersPassed, std::shared_ptr< Potential > potPassed)
std::vector< double > extremumEnergy
void printImageData(bool writeToFile=false, size_t idx=0)
std::size_t maxEnergyImage
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)
double convergenceForce(void)
NudgedElasticBand(std::shared_ptr< Matter > initialPassed, std::shared_ptr< Matter > finalPassed, const Parameters ¶metersPassed, 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 ¶ms)
void updateStability(long climbingImage)
MMFResult run(eonc::NudgedElasticBand &neb, double convForce)
void initBaseline(double baseline_force)
int stabilityCount() const
std::unique_ptr< Optimizer > mkOptim(std::shared_ptr< ObjectiveFunction > a_objf, OptType a_otype, const Parameters &a_params)
std::vector< Matter > sidppPath(const Matter &initImg, const Matter &finalImg, size_t target_nimgs, const Parameters ¶ms, 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 ¶ms, bool use_zbl)
std::vector< Matter > idppCollectivePath(const Matter &initImg, const Matter &finalImg, size_t nimgs, const Parameters ¶ms, 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 ¶ms)
double maxAtomMotionV(const VectorXd v1)
constexpr bool io_ok(IoStatus s) noexcept
quill::Logger * get() noexcept
Get or create the default "combi" logger.
quill::Logger * traceback() noexcept
Get or create the "_traceback" logger for traceback logging.
void zeroTranslation(AtomMatrix &projectedForce, int nFreeAtoms, int nAtoms)
Zero net translational force for fully free systems.
SpringStrategy buildSpringStrategy(const Parameters ¶ms, 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 ¶ms)
Build the tangent strategy from parameters.
ProjectionStrategy buildProjectionStrategy(const Parameters ¶ms)
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 ¶ms, std::shared_ptr< Potential > pot)
Build the eigenmode solver from parameters.
struct eonc::Parameters::neb_options_t::path_initialization_t initialization
Data for a single image needed by projection strategies.
Result of spring force computation for a single image.