Loading...
Searching...
No Matches
NudgedElasticBand Class Reference

#include <NudgedElasticBand.h>

Public Types

enum class  NEBStatus {
  GOOD = 0 , INIT = 1 , BAD_MAX_ITERATIONS = 2 , RUNNING ,
  MAX_UNCERTAINTY
}

Public Member Functions

 NudgedElasticBand (std::shared_ptr< Matter > initialPassed, std::shared_ptr< Matter > finalPassed, const Parameters &parametersPassed, std::shared_ptr< Potential > potPassed)
 NudgedElasticBand (std::vector< Matter > initPath, const Parameters &parametersPassed, std::shared_ptr< Potential > potPassed)
 ~NudgedElasticBand ()=default
NudgedElasticBand::NEBStatus compute (void)
NudgedElasticBand::NEBStatus getStatus ()
long lastIteration () const noexcept
 Iteration count from the last compute(). Zero before compute() runs.
bool solidState () const noexcept
double solidJacobian () const noexcept
const Matrix3d & cellForce (long image) const
void updateForces (bool ci_active)
void updateForces (void)
void setCIEnabled (bool enabled)
double convergenceForce (void)
void adoptSessionProjection (long ciIndex)
 Host bookkeeping for a band whose projected force came from the rgsaddle session: the highest interior image, the climbing image (ciIndex, or none when it is not an interior image) and the tangents, from the images' current energies and positions.
void findExtrema (void)
void printImageData (bool writeToFile=false, size_t idx=0)
std::vector< readcon::ConFrame > pathFrames (std::optional< size_t > bandIndex=std::nullopt)
 In-memory ConFrames with the same NEB stamps as writePathCon / neb.con.
long imageOwner (std::size_t i) const noexcept
 The identity band image i carries in a batch (forceBatchOwned): interior image i is i - 1, the reactant numImages and the product numImages + 1, so no image shares a key with an endpoint.

Public Attributes

std::vector< std::shared_ptr< EigenmodeStrategy > > eigenmode_solvers
int atoms {0}
long numImages {0}
long climbingImage {0}
long numExtrema {0}
double reactantEnergy {0.0}
std::vector< std::shared_ptr< Matter > > path
std::vector< std::shared_ptr< AtomMatrix > > tangent
std::vector< std::shared_ptr< AtomMatrix > > projectedForce
std::vector< double > extremumEnergy
std::vector< double > extremumPosition
std::vector< double > extremumCurvature
std::size_t maxEnergyImage {0}
bool movedAfterForceCall {false}
bool perImagePotentials_
 Whether per-image potential instances exist.
double ksp {0.0}
double k_u {0.0}
double k_l {0.0}
double E_ref

Private Member Functions

void prepareSolidState ()
void projectSolidState (bool ci_active)

Private Attributes

bool ci_enabled_ {false}
double baseline_force {-1.0}
Parameters params
std::shared_ptr< Potential > pot
NEBStatus status
long iteration_ {0}
eonc::log::Scoped log
neb::TangentStrategy tangentStrat_
neb::ProjectionStrategy projectionStrat_
bool solidState_ {false}
double solidJacobian_ {1.0}
std::vector< Matrix3d > projectedCellForce

Friends

class eonc::neb::OCINEBController

Detailed Description

Definition at line 38 of file NudgedElasticBand.h.

Member Enumeration Documentation

◆ NEBStatus

Enumerator
GOOD 
INIT 
BAD_MAX_ITERATIONS 
RUNNING 
MAX_UNCERTAINTY 

Definition at line 42 of file NudgedElasticBand.h.

42 {
43 GOOD = 0,
44 INIT = 1,
45 BAD_MAX_ITERATIONS = 2,
46 RUNNING,
47 MAX_UNCERTAINTY
48 };

Constructor & Destructor Documentation

◆ NudgedElasticBand() [1/2]

eonc::NudgedElasticBand::NudgedElasticBand ( std::shared_ptr< Matter > initialPassed,
std::shared_ptr< Matter > finalPassed,
const Parameters & parametersPassed,
std::shared_ptr< Potential > potPassed )

Definition at line 58 of file NudgedElasticBand.cpp.

63 [&]() {
64 auto &init_opt = parametersPassed.neb_options().initialization;
65 const size_t base_count =
66 parametersPassed.neb_options().image_count;
67 if (parametersPassed.neb_options().solid_state.enabled &&
68 init_opt.method != NEBInit::LINEAR &&
69 init_opt.method != NEBInit::FILE) {
70 throw std::invalid_argument(
71 "solid_state accepts initializer linear or file");
72 }
73 if (parametersPassed.neb_options().match_endpoints) {
75 *initialPassed, *finalPassed, 1.0);
76 auto *log = eonc::log::get();
77 if (aligned.error != 0) {
78 QUILL_LOG_WARNING(
79 log,
80 "match_endpoints: IRA align failed (error {}), "
81 "interpolating the input order",
82 aligned.error);
83 } else {
84 QUILL_LOG_INFO(log,
85 "match_endpoints: Hausdorff {:.4f} A after IRA "
86 "permute+rotate of the reactant",
87 aligned.hausdorffDistance);
88 }
89 }
90
91 // Apply oversampling factor if flag exists
92 const size_t relax_count =
93 init_opt.oversampling
94 ? base_count * init_opt.oversampling_factor
95 : base_count;
96
97 std::vector<Matter> path;
98 switch (init_opt.method) {
99 case NEBInit::FILE: {
100 std::vector<fs::path> file_paths =
101 eonc::helpers::neb_paths::readFilePaths(init_opt.input_path);
103 file_paths, *initialPassed, base_count);
104 // filePathInit reloads every frame, including ends the caller
105 // may already have minimized.
106 path.front() = Matter(*initialPassed);
107 path.back() = Matter(*finalPassed);
108 break;
109 }
110 case NEBInit::IDPP:
112 *initialPassed, *finalPassed, relax_count, parametersPassed);
113 break;
114 case NEBInit::IDPP_COLLECTIVE:
116 *initialPassed, *finalPassed, relax_count, parametersPassed);
117 break;
118 case NEBInit::SIDPP:
119 case NEBInit::SIDPP_ZBL:
121 *initialPassed, *finalPassed, relax_count, parametersPassed,
122 (init_opt.method == NEBInit::SIDPP_ZBL));
123 break;
124 case NEBInit::LINEAR:
125 default:
127 *initialPassed, *finalPassed, base_count);
128 break;
129 }
130
131 // Decimate path back to the target count using the cubic spline
132 if (init_opt.oversampling && path.size() > (base_count + 2)) {
133 auto *log = eonc::log::get();
134 QUILL_LOG_INFO(log,
135 "Decimating oversampled path ({} images) to "
136 "{} images via cubic spline.",
137 path.size() - 2, base_count);
138
139 // Perform the Spline Resampling
141
142 // POST-DECIMATION RE-RELAXATION
143 // The spline might have placed atoms in high-energy positions.
144 QUILL_LOG_INFO(
145 log, "Relaxing decimated path to restore IDPP surface...");
146
147 // Collective IDPP objective for the new reduced path
148 std::shared_ptr<ObjectiveFunction> post_decim_objf =
149 std::make_shared<CollectiveIDPPObjectiveFunction>(
150 path, parametersPassed);
151
152 // Wrap with ZBL if the original method used ZBL options
153 bool use_zbl = (init_opt.method == NEBInit::SIDPP_ZBL);
154 if (use_zbl) {
156 post_decim_objf = std::make_shared<ZBLRepulsiveIDPPObjective>(
157 post_decim_objf, zbl_pot, path, parametersPassed, 1.0);
158 }
159
161 post_decim_objf, parametersPassed.neb_options().opt_method,
162 parametersPassed);
163
164 // Run a short optimization
165 optim->run(
166 parametersPassed.neb_options().initialization.max_iterations,
167 parametersPassed.neb_options().initialization.max_move);
168 }
169 if (parametersPassed.neb_options().solid_state.enabled &&
170 init_opt.method == NEBInit::LINEAR) {
171 if (init_opt.oversampling) {
172 throw std::invalid_argument(
173 "solid_state NEB does not oversample the initial path");
174 }
175 for (Matter &image : path) {
177 }
179 } else if (parametersPassed.neb_options().solid_state.enabled &&
180 init_opt.method == NEBInit::FILE) {
181 for (Matter &image : path) {
183 }
184 }
185 return path;
186 }(),
187 parametersPassed, potPassed) {}
NudgedElasticBand(std::shared_ptr< Matter > initialPassed, std::shared_ptr< Matter > finalPassed, const Parameters &parametersPassed, std::shared_ptr< Potential > potPassed)
eonc::log::Scoped log
std::vector< std::shared_ptr< Matter > > path
const neb_options_t & neb_options() const
static MatchResult alignReactantToProduct(Matter &reactant, const Matter &product, double distThreshold)
Rigid-align + permute reactant onto product.
std::unique_ptr< Optimizer > mkOptim(std::shared_ptr< ObjectiveFunction > a_objf, OptType a_otype, const Parameters &a_params)
Definition Optimizer.cpp:55
std::vector< Matter > resamplePath(const std::vector< Matter > &densePath, size_t targetCount)
std::vector< Matter > sidppPath(const Matter &initImg, const Matter &finalImg, size_t target_nimgs, const Parameters &params, bool use_zbl)
std::shared_ptr< Potential > createZBLPotential()
std::vector< Matter > linearPath(const Matter &initImg, const Matter &finalImg, const size_t nimgs)
std::vector< Matter > filePathInit(const std::vector< fs::path > &fsrcs, const Matter &refImg, 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)
std::vector< fs::path > readFilePaths(const std::string &listFilePath)
Reads a file where each line contains a path to another file.
quill::Logger * get() noexcept
Get or create the default "combi" logger.
Definition EonLogger.h:111
void interpolateSolidStateLinear(std::vector< Matter > &images)
Replace interior images by a fractional linear interpolation of the oriented endpoints.
void orientSolidStateMatter(Matter &image)
struct eonc::neb_options_t::solid_state_options_t solid_state
bool match_endpoints
Permute+rotate the reactant onto the product before interpolation.
struct eonc::neb_options_t::path_initialization_t initialization

◆ NudgedElasticBand() [2/2]

eonc::NudgedElasticBand::NudgedElasticBand ( std::vector< Matter > initPath,
const Parameters & parametersPassed,
std::shared_ptr< Potential > potPassed )

Definition at line 190 of file NudgedElasticBand.cpp.

193 : ci_enabled_{parametersPassed.neb_options().climbing_image.enabled},
194 params{parametersPassed},
195 pot{potPassed},
196 E_ref{0.0} {
197
200 params.optimizer_options().convergence_metric, "[Nudged Elastic Band]");
201 this->status = NEBStatus::INIT;
202 numImages = params.neb_options().image_count;
203 if (initPath.empty()) {
204 throw std::invalid_argument("NEB: initPath is empty");
205 }
206 if (initPath.size() != static_cast<size_t>(numImages + 2)) {
207 throw std::invalid_argument("NEB: initPath.size() must be image_count + 2");
208 }
209 atoms = initPath.front().numberOfAtoms();
210 for (size_t i = 1; i < initPath.size(); ++i) {
212 initPath[i], "path images");
213 }
214
215 // Common initialization logic
216 path.resize(numImages + 2);
217 tangent.resize(numImages + 2);
218 projectedForce.resize(numImages + 2);
219 extremumPosition.resize(2 * (numImages + 1));
220 extremumEnergy.resize(2 * (numImages + 1));
221 extremumCurvature.resize(2 * (numImages + 1));
222 numExtrema = 0;
223
224 // A potential that keeps per-geometry state (a neighbour table, a
225 // working directory) gets one instance per interior image, threads or
226 // not: a shared instance would rebuild that state for every image of
227 // every step. [Main] parallel only decides whether the images run on
228 // the image pool.
229 perImagePotentials_ = pot->needsPerImageInstance();
231 QUILL_LOG_INFO(log, "NEB: one potential instance per image ({} images)",
232 numImages);
233 }
234
235 for (long i = 0; i <= numImages + 1; i++) {
236 path[i] = std::make_shared<Matter>(std::move(initPath[i]));
237
238 // Each interior image gets its own instance. The endpoints keep the
239 // shared one: they are evaluated once, before the band moves.
240 if (perImagePotentials_ && i > 0 && i <= numImages) {
242 }
243
244 tangent[i] = std::make_shared<AtomMatrix>();
245 tangent[i]->resize(atoms, 3);
246 tangent[i]->setZero();
247 projectedForce[i] = std::make_shared<AtomMatrix>();
248 projectedForce[i]->resize(atoms, 3);
249 projectedForce[i]->setZero();
250 }
251
252 // Common final setup
253 movedAfterForceCall = true;
255 // Name images before the endpoint evaluation. The host matches a
256 // system to a band index by the position pointer.
257 if (pot) {
258 pot->bindBand(path);
259 }
260 // Both endpoints in one call, so two calculator groups take one each.
261 {
262 Matter *const ends[] = {path[0].get(), path[numImages + 1].get()};
263 const long owners[] = {imageOwner(0), imageOwner(numImages + 1)};
264 eonc::evaluateTogether(*pot, ends, false, owners);
265 }
266 reactantEnergy = path[0]->getPotentialEnergy();
267 climbingImage = 0;
268
269 // Setup springs
270 k_u = params.neb_options().spring.weighting.k_max;
271 k_l = params.neb_options().spring.weighting.k_min;
272 if (params.neb_options().spring.weighting.enabled) {
273 ksp = k_l;
274 } else {
275 ksp = params.neb_options().spring.constant;
276 }
277
278 // Cache strategies that are constant across iterations
281
282 // Optional debugging setup
283 if (params.debug_options().estimate_neb_eigenvalues) {
284 eigenmode_solvers.resize(numImages + 2);
285 for (long i = 0; i <= numImages + 1; i++) {
287 eonc::buildEigenmodeStrategy(path[i], parametersPassed, pot);
288 }
289 }
290}
std::vector< double > extremumEnergy
std::vector< std::shared_ptr< AtomMatrix > > tangent
bool perImagePotentials_
Whether per-image potential instances exist.
std::vector< double > extremumPosition
std::vector< std::shared_ptr< EigenmodeStrategy > > eigenmode_solvers
neb::TangentStrategy tangentStrat_
std::vector< std::shared_ptr< AtomMatrix > > projectedForce
long imageOwner(std::size_t i) const noexcept
The identity band image i carries in a batch (forceBatchOwned): interior image i is i - 1,...
std::shared_ptr< Potential > pot
neb::ProjectionStrategy projectionStrat_
std::vector< double > extremumCurvature
void requireSameAtomCount(const Matter &a, const Matter &b, std::string_view what)
Abort before Eigen subtracts two position matrices of different size.
std::shared_ptr< Potential > makeImagePotential(const Potential &pot, const Parameters &params)
A separate instance of pot for one image of a band, dimer or pair of minimizations: a clone when the ...
void requireKnownConvergenceMetric(std::string_view metric, std::string_view context)
Throws std::invalid_argument naming context when metric is unrecognized.
TangentStrategy buildTangentStrategy(const Parameters &params)
Build the tangent strategy from parameters.
ProjectionStrategy buildProjectionStrategy(const Parameters &params)
Build the projection strategy from parameters.
std::shared_ptr< LowestEigenmode > buildEigenmodeStrategy(std::shared_ptr< Matter > matter, const Parameters &params, std::shared_ptr< Potential > pot)
void evaluateTogether(Potential &pot, std::span< Matter *const > systems, bool threads, std::span< const long > owners)
Evaluates every system that needs a force update.
Definition Matter.cpp:995
struct eonc::neb_options_t::climbing_image_options_t climbing_image

◆ ~NudgedElasticBand()

Member Function Documentation

◆ adoptSessionProjection()

Host bookkeeping for a band whose projected force came from the rgsaddle session: the highest interior image, the climbing image (ciIndex, or none when it is not an interior image) and the tangents, from the images' current energies and positions.

The projected force stays as the session assembled it.

Definition at line 970 of file NudgedElasticBand.cpp.

970 {
971 long highest = 1;
972 for (long i = 2; i <= numImages; ++i) {
973 if (path[i]->getPotentialEnergy() > path[highest]->getPotentialEnergy()) {
974 highest = i;
975 }
976 }
977 maxEnergyImage = static_cast<size_t>(highest);
978 climbingImage = (ciIndex > 0 && ciIndex <= numImages) ? ciIndex : 0;
979 for (long i = 1; i <= numImages; ++i) {
980 const AtomMatrix &pos = path[i]->getPositions();
981 const AtomMatrix posDiffNext =
982 path[i]->pbc(path[i + 1]->getPositions() - pos);
983 const AtomMatrix posDiffPrev =
984 path[i]->pbc(pos - path[i - 1]->getPositions());
985 const double energy = path[i]->getPotentialEnergy();
986 const double energyPrev = path[i - 1]->getPotentialEnergy();
987 const double energyNext = path[i + 1]->getPotentialEnergy();
988 *tangent[i] = std::visit(
989 [&](auto &t) {
990 return t.compute(posDiffNext, posDiffPrev, energy, energyPrev,
991 energyNext);
992 },
994 }
995}
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37

◆ cellForce()

const Matrix3d & eonc::NudgedElasticBand::cellForce ( long image) const
inlinenodiscard

Definition at line 64 of file NudgedElasticBand.h.

64 {
65 return projectedCellForce.at(static_cast<size_t>(image));
66 }
std::vector< Matrix3d > projectedCellForce

◆ compute()

Definition at line 292 of file NudgedElasticBand.cpp.

292 {
293 iteration_ = 0;
294 long &iteration = iteration_;
295 this->status = NEBStatus::RUNNING;
296
297 QUILL_LOG_DEBUG(log, "Nudged elastic band calculation started.");
298
299 // Higher endpoint: springs stay at k_min until an image exceeds it.
300 E_ref = std::max(path[0]->getPotentialEnergy(),
301 path[numImages + 1]->getPotentialEnergy());
302
303 updateForces();
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 {
323 objf, params.neb_options().opt_method, params);
324 if (params.optimizer_options().refine.method != OptType::None) {
325 refine_optim = eonc::helpers::create::mkOptim(
326 objf, params.optimizer_options().refine.method, params);
327 }
328 }
329 // OCINEB controller
331 eonc::neb::OCINEBController ocineb(ocinebCfg);
332 bool zoomDone{false};
333 long zoomStable{0};
334 long zoomPrevCI{-1};
335 long zoomAt{-1};
336 // Activation uses the stepping projection; acceptance uses configured CI.
337 double activation_force{0.0};
338 // Conjugate gradient has no curvature history to damp a bad step.
339 // A jump in the band force shortens the next CG step, and a stall
340 // drops the search direction. LBFGS keeps the configured length:
341 // shortening it here left bands short of the cap.
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) {
349 if (!path.empty()) {
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,
359 reactantEnergy))) {
360 QUILL_LOG_ERROR(log, "Failed to write NEB path movie for iteration {}",
361 iteration);
362 }
363
364 AtomMatrix maxTang;
365 if (maxEnergyImage == 0) {
366 maxTang =
367 path[0]->pbc(path[1]->getPositions() - path[0]->getPositions());
368 } else if (maxEnergyImage == static_cast<size_t>(numImages + 1)) {
369 maxTang = path[numImages]->pbc(path[numImages + 1]->getPositions() -
370 path[numImages]->getPositions());
371 } else {
372 maxTang = *tangent[maxEnergyImage];
373 }
374 eonc::safemath::safe_normalize_inplace(maxTang);
375 auto maxImageMetadata = eonc::io::ConFrameMetadata{};
376 maxImageMetadata.frame_index = static_cast<uint64_t>(maxEnergyImage);
377 maxImageMetadata.energy = path[maxEnergyImage]->getPotentialEnergy();
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",
382 path[maxEnergyImage]->getPotentialEnergy() - reactantEnergy});
383 maxImageMetadata.scalars.push_back(
384 {"parallel_force",
385 matDot(path[maxEnergyImage]->getForces(), maxTang)});
386 maxImageMetadata.strings.push_back({"movie_kind", "neb_maximage"});
388 "neb_maximage.con", append, &maxImageMetadata))) {
389 EONC_LOG_WARNING("Failed to write neb_maximage.con");
390 }
391 printImageData(true, iteration);
392 }
393
394 VectorXd pos = objf->getPositions();
395 double convForce = convergenceForce();
396
397 ocineb.updateStability(climbingImage);
398
399 if (iteration == 0) {
400 baseline_force = convForce;
401 activation_force = convForce;
402 ocineb.initBaseline(convForce);
403
404 // Log configuration banner
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(
414 log,
415 "===============================================================");
416 QUILL_LOG_INFO(log, " NEB Optimization Configuration");
417 QUILL_LOG_INFO(
418 log,
419 "===============================================================");
420 QUILL_LOG_INFO(log, " {:<25} : {:.4f}", "Baseline Force", baseline_force);
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) {
425 double ci_rel_val = baseline_force * ci_opt.trigger_factor;
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(
448 log,
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 // CI active when force drops below relative threshold
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 &&
470 climbingImage > 0 && climbingImage + 1 < path.size()) {
471 if (static_cast<long>(climbingImage) == zoomPrevCI) {
472 ++zoomStable;
473 } else {
474 zoomPrevCI = static_cast<long>(climbingImage);
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)) {
492 movedAfterForceCall = true;
494 objf, params.neb_options().opt_method, params);
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 // MMF triggering via controller. Skipped on the zoom step so the
506 // dimer sees the redistributed band, not the pre-zoom geometry.
507 if (!zoomedThisStep &&
508 ocineb.shouldTrigger(convForce, ci_active, climbingImage, numImages,
509 ocineb.stabilityCount())) {
510 auto result = ocineb.run(*this, convForce);
511
512 if (result.convergedAfterMMF) {
513 status = NEBStatus::GOOD;
514 break;
515 }
516 // Post-MMF arc-length reparameterization: pass full path
517 // (endpoints are fixed by resamplePathInPlace, only interior
518 // images are redistributed). Zero force-call cost; next NEB
519 // iteration recomputes all forces anyway.
520 bool didResample = false;
521 if (!result.convergedAfterMMF && result.newForce < convForce &&
522 path[climbingImage]->getPeriodic() &&
523 path[0]->numberOfAtoms() > 6) {
525 std::span{path.data(), path.size()});
526 movedAfterForceCall = true;
527 didResample = true;
528 }
529
530 // Reset optimizer AFTER reparameterization so fresh L-BFGS
531 // starts from the redistributed positions.
532 if (result.shouldResetOptimizer || didResample) {
533#ifdef WITH_RGSADDLE
534 if (rustBand) {
535 rustBand->syncFromPath();
536 rustBand->reset();
537 } else
538#endif
539 {
541 objf, params.neb_options().opt_method, params);
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) {
557 activation_force = convergenceForce();
558 iteration++;
559 continue;
560 }
561
562 // Set CI state so updateForces() inside the optimizer step
563 // applies the correct force projection.
564 setCIEnabled(ci_active);
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;
581 EONC_LOG_DEBUG("Switched to {}",
582 magic_enum::enum_name<OptType>(
583 params.optimizer_options().refine.method));
584 }
585 activeOptim->step(moveNow);
586 }
587
588 activation_force = convergenceForce();
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 {
611 objf, params.neb_options().opt_method, params);
612 if (refine_optim) {
613 refine_optim = eonc::helpers::create::mkOptim(
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 }
621 setCIEnabled(params.neb_options().climbing_image.enabled);
622 }
623
624 iteration++;
625
626 double dE = path[maxEnergyImage]->getPotentialEnergy() - reactantEnergy;
627 double stepSize = 0.0;
628 if (solidState_) {
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}",
640 iteration, stepSize, convergenceForce(), maxEnergyImage,
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");
650 status = NEBStatus::GOOD;
651 break;
652 }
653 } else {
654 if (objf->isConverged()) {
655 QUILL_LOG_DEBUG(log, "NEB converged\n");
656 status = NEBStatus::GOOD;
657 break;
658 }
659 }
660 }
661 return status;
662}
double matDot(const AtomMatrix &a, const AtomMatrix &b)
SIMD-optimized dot product for contiguous Eigen matrices.
Definition Eigen.h:50
#define EONC_LOG_DEBUG(...)
Definition EonLogger.h:310
#define EONC_LOG_WARNING(...)
Definition EonLogger.h:322
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 &params)
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
Definition ConFileIO.h:38
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.
Definition NEBZoom.cpp:74
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.
Definition NEBZoom.cpp:118
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().

◆ convergenceForce()

Definition at line 665 of file NudgedElasticBand.cpp.

665 {
667 updateForces();
668
669 auto imageForce = [&](long i) -> double {
670 const double cellNorm =
671 solidState_ ? projectedCellForce[static_cast<size_t>(i)].norm() : 0.0;
672 if (params.optimizer_options().convergence_metric == "norm") {
673 return std::hypot(projectedForce[i]->norm(), cellNorm);
674 }
675 if (params.optimizer_options().convergence_metric == "max_atom") {
676 // Every image shares the reactant constraint mask.
677 return std::max(path[0]->maxFreeAtomForce(*projectedForce[i]), cellNorm);
678 }
679 if (params.optimizer_options().convergence_metric == "max_component") {
680 double component = projectedForce[i]->cwiseAbs().maxCoeff();
681 if (solidState_) {
682 component = std::max(
683 component,
684 projectedCellForce[static_cast<size_t>(i)].cwiseAbs().maxCoeff());
685 }
686 return component;
687 }
689 QUILL_LOG_CRITICAL(
690 log, "[Nudged Elastic Band] unknown opt_convergence_metric: {}",
691 params.optimizer_options().convergence_metric);
692 throw std::invalid_argument(
693 std::format("[Nudged Elastic Band] unknown convergence_metric: {}",
694 params.optimizer_options().convergence_metric));
695 };
696
697 double bandMax = 0;
698 for (long i = 1; i <= numImages; ++i) {
699 bandMax = std::max(bandMax, imageForce(i));
700 }
701
702 const bool ciOnly = params.neb_options().climbing_image.converged_only &&
704 if (!ciOnly) {
705 return bandMax;
706 }
707
708 const double ciForce = imageForce(climbingImage);
709 const double slack = params.neb_options().climbing_image.band_slack;
710 const double tol = params.neb_options().force_tolerance;
711 if (slack > 0.0 && bandMax > slack * tol) {
712 return bandMax;
713 }
714 return ciForce;
715}
double norm(const Eigen::MatrixBase< A > &x)
sqrt of the correctly rounded squared norm; IEEE sqrt is itself correctly rounded,...
quill::Logger * traceback() noexcept
Get or create the "_traceback" logger for traceback logging.
Definition EonLogger.h:155

◆ findExtrema()

Definition at line 1005 of file NudgedElasticBand.cpp.

1005 {
1007 numExtrema = result.numExtrema;
1008 extremumPosition = std::move(result.positions);
1009 extremumEnergy = std::move(result.energies);
1010 extremumCurvature = std::move(result.curvatures);
1011}
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.

◆ getStatus()

Definition at line 59 of file NudgedElasticBand.h.

59{ return this->status; };

◆ imageOwner()

long eonc::NudgedElasticBand::imageOwner ( std::size_t i) const
inlinenodiscardnoexcept

The identity band image i carries in a batch (forceBatchOwned): interior image i is i - 1, the reactant numImages and the product numImages + 1, so no image shares a key with an endpoint.

Definition at line 97 of file NudgedElasticBand.h.

97 {
98 const auto idx = static_cast<long>(i);
99 if (idx == 0)
100 return numImages;
101 return idx == numImages + 1 ? numImages + 1 : idx - 1;
102 }

◆ lastIteration()

long eonc::NudgedElasticBand::lastIteration ( ) const
inlinenodiscardnoexcept

Iteration count from the last compute(). Zero before compute() runs.

Definition at line 61 of file NudgedElasticBand.h.

61{ return iteration_; }

◆ pathFrames()

std::vector< readcon::ConFrame > eonc::NudgedElasticBand::pathFrames ( std::optional< size_t > bandIndex = std::nullopt)
nodiscard

In-memory ConFrames with the same NEB stamps as writePathCon / neb.con.

Empty if path is incomplete. Does not write to disk.

Definition at line 1014 of file NudgedElasticBand.cpp.

1014 {
1017 params.debug_options().estimate_neb_eigenvalues, bandIndex,
1019}
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, double referenceEnergy, const std::vector< double > *quantumFreeEnergy, const eonc::tunneling::BandWalls *walls)
Build stamped ConFrames for a NEB band (same metadata as writePathCon).

◆ prepareSolidState()

Definition at line 1085 of file NudgedElasticBand.cpp.

1085 {
1086 if (!params.neb_options().solid_state.enabled) {
1087 return;
1088 }
1089 refuseSolidStateCombination(params);
1090 for (const auto &image : path) {
1091 if (!image->getPeriodic()) {
1092 throw std::invalid_argument(
1093 "solid_state NEB requires periodic boundaries on every image");
1094 }
1096 }
1097 const double volume0 = std::abs(path.front()->getCell().determinant());
1098 const double volume1 = std::abs(path.back()->getCell().determinant());
1100 eonc::neb::solidStateJacobian(0.5 * (volume0 + volume1), atoms,
1101 params.neb_options().solid_state.weight);
1102 projectedCellForce.assign(static_cast<size_t>(numImages + 2),
1103 Matrix3d::Zero());
1104 solidState_ = true;
1105 QUILL_LOG_INFO(log, "Solid-state NEB Jacobian {:.6f} Angstrom",
1107}
double solidStateJacobian(double meanVolume, int nAtoms, double weight)
J = (V/N)^{1/3} * N^{1/2} * weight, with V the mean endpoint volume.

◆ printImageData()

void eonc::NudgedElasticBand::printImageData ( bool writeToFile = false,
size_t idx = 0 )

Definition at line 999 of file NudgedElasticBand.cpp.

999 {
1001 params.debug_options().estimate_neb_eigenvalues,
1002 writeToFile, idx, log, reactantEnergy);
1003}
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, double referenceEnergy)
Print NEB image data to log and optionally to file.

◆ projectSolidState()

void eonc::NudgedElasticBand::projectSolidState ( bool ci_active)
private

Definition at line 1109 of file NudgedElasticBand.cpp.

1109 {
1110 // Each image keeps one cell. The band does not store a cell on each bead.
1111 const double pressure = params.neb_options().solid_state.pressure;
1112 const Matrix3d external = Matrix3d::Identity() * pressure;
1113 std::vector<double> enthalpy(static_cast<size_t>(numImages + 2));
1114 for (long i = 0; i <= numImages + 1; ++i) {
1115 enthalpy[static_cast<size_t>(i)] =
1116 eonc::neb::solidStateEnthalpy(*path[i], *path[0], pressure);
1117 }
1118
1119 long highest = 1;
1120 for (long i = 2; i <= numImages; ++i) {
1121 if (enthalpy[static_cast<size_t>(i)] >
1122 enthalpy[static_cast<size_t>(highest)]) {
1123 highest = i;
1124 }
1125 }
1126 maxEnergyImage = static_cast<size_t>(highest);
1127 const double endpointEnthalpy = std::max(enthalpy.front(), enthalpy.back());
1128 const bool climb =
1129 ci_active && enthalpy[static_cast<size_t>(highest)] > endpointEnthalpy;
1130 climbingImage = 0;
1131
1132 if (params.neb_options().spring.weighting.enabled) {
1133 E_ref = std::max(path[0]->getPotentialEnergy(),
1134 path[numImages + 1]->getPotentialEnergy());
1135 }
1136 const double maxEnergy = path[highest]->getPotentialEnergy();
1138 maxEnergy, E_ref);
1139 if (const auto *uniform = std::get_if<eonc::neb::UniformSpring>(&spring)) {
1140 ksp = uniform->ksp;
1141 }
1142
1143 if (!pot->computesStress()) {
1144 throw std::invalid_argument(
1145 "solid_state NEB requires a potential that reports the Cauchy stress");
1146 }
1147
1148 for (long i = 1; i <= numImages; ++i) {
1149 const double volume = std::abs(path[i]->getCell().determinant());
1150 Matrix3d stress = path[i]->cauchyStress();
1151 Matrix3d cellTrue =
1152 eonc::neb::cellNebForce(stress, volume, solidJacobian_, external);
1153 AtomMatrix atomicTrue = path[i]->getForces();
1154 for (long atom = 0; atom < atoms; ++atom) {
1155 if (path[i]->getFixed(atom)) {
1156 atomicTrue.row(atom).setZero();
1157 }
1158 }
1159
1160 const eonc::neb::JointBlock toNext =
1162 const eonc::neb::JointBlock toPrev =
1164 const AtomMatrix packedNext = packJoint(toNext.atomic, toNext.cell);
1165 const AtomMatrix packedPrev = packJoint(toPrev.atomic, toPrev.cell);
1166 const double energy = enthalpy[static_cast<size_t>(i)];
1167 const double energyPrev = enthalpy[static_cast<size_t>(i - 1)];
1168 const double energyNext = enthalpy[static_cast<size_t>(i + 1)];
1169 const AtomMatrix packedTangent = std::visit(
1170 [&](auto &tangentStrategy) {
1171 return tangentStrategy.compute(packedNext, packedPrev, energy,
1172 energyPrev, energyNext);
1173 },
1175 *tangent[i] = packedTangent.topRows(atoms);
1176
1177 AtomMatrix packedForce = packJoint(atomicTrue, cellTrue);
1178 AtomMatrix total;
1179 if (climb && i == highest) {
1180 climbingImage = static_cast<size_t>(highest);
1181 const double parallel = matDot(packedForce, packedTangent);
1182 total = packedForce - 2.0 * parallel * packedTangent;
1183 } else {
1184 const double parallel = matDot(packedForce, packedTangent);
1185 const AtomMatrix perpendicular = packedForce - parallel * packedTangent;
1186 const double scale = springScale(spring, i, eonc::neb::jointNorm(toNext),
1187 eonc::neb::jointNorm(toPrev));
1188 total = perpendicular + scale * packedTangent;
1189 }
1190 *projectedForce[i] = total.topRows(atoms);
1191 projectedCellForce[static_cast<size_t>(i)] = total.bottomRows(3);
1192 projectedCellForce[static_cast<size_t>(i)](0, 1) = 0.0;
1193 projectedCellForce[static_cast<size_t>(i)](0, 2) = 0.0;
1194 projectedCellForce[static_cast<size_t>(i)](1, 2) = 0.0;
1195 for (long atom = 0; atom < atoms; ++atom) {
1196 if (path[i]->getFixed(atom)) {
1197 projectedForce[i]->row(atom).setZero();
1198 }
1199 }
1200 eonc::neb::zeroTranslation(*projectedForce[i], path[i]->numberOfFreeAtoms(),
1201 path[i]->numberOfAtoms());
1202 }
1203}
Eigen::Matrix< double, 3, 3, eOnStorageOrder > Matrix3d
Definition Eigen.h:35
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.
double jointNorm(const JointBlock &block)
JointBlock jointDisplacement(const Matter &from, const Matter &to, double jacobian)
Displacement of to relative to from in the joint metric.
double solidStateEnthalpy(const Matter &image, const Matter &reference, double pressure)
Potential energy plus P : (h0^{-1} (h-h0)) * V0.
Matrix3d cellNebForce(const Matrix3d &cauchy, double volume, double jacobian, const Matrix3d &externalStress)
NEB force on the Jacobian-scaled strain.

◆ setCIEnabled()

void eonc::NudgedElasticBand::setCIEnabled ( bool enabled)
inline

Definition at line 70 of file NudgedElasticBand.h.

70 {
71 if (ci_enabled_ != enabled) {
72 ci_enabled_ = enabled;
73 // CI changes the projection while physical force caches remain valid.
75 }
76 }

◆ solidJacobian()

double eonc::NudgedElasticBand::solidJacobian ( ) const
inlinenodiscardnoexcept

Definition at line 63 of file NudgedElasticBand.h.

63{ return solidJacobian_; }

◆ solidState()

bool eonc::NudgedElasticBand::solidState ( ) const
inlinenodiscardnoexcept

Definition at line 62 of file NudgedElasticBand.h.

62{ return solidState_; }

◆ updateForces() [1/2]

void eonc::NudgedElasticBand::updateForces ( bool ci_active)

Definition at line 718 of file NudgedElasticBand.cpp.

718 {
719 // Update forces for all intermediate images. Prefer batched evaluation
720 // (single model.forward() over all dirty images, e.g. MetatomicPotential
721 // on GPU). Else fall back to per-image evaluation, which is itself
722 // thread-parallel when (a) the potential is thread-safe on the same
723 // instance, or (b) per-image instances were created (separate models).
724 if (pot->supportsBatchEvaluation() && numImages > 1) {
725 // Collect only images that need recomputation (positions changed).
726 // Materialize atomic numbers and cells first, then build the raw-pointer
727 // arrays after storage is stable. Otherwise vector growth can invalidate
728 // earlier .data() pointers and hand garbage cells/types to forceBatch().
729 std::vector<long> dirty; // indices into path[] (1-based)
730 dirty.reserve(numImages);
731 for (long i = 1; i <= numImages; i++) {
732 if (path[i]->needsForceUpdate()) {
733 dirty.push_back(i);
734 }
735 }
736
737 if (!dirty.empty()) {
738 if (!path.empty()) {
739 path.front()->pollCancel("neb");
740 }
741 auto nDirty = static_cast<long>(dirty.size());
742 std::vector<VectorXi> nrsStore;
743 std::vector<Matrix3d> boxStore;
744 std::vector<const double *> posVec, boxVec;
745 std::vector<const int *> nrsVec;
746 std::vector<double *> frcVec;
747 nrsStore.reserve(static_cast<size_t>(nDirty));
748 boxStore.reserve(static_cast<size_t>(nDirty));
749 posVec.reserve(static_cast<size_t>(nDirty));
750 boxVec.reserve(static_cast<size_t>(nDirty));
751 nrsVec.reserve(static_cast<size_t>(nDirty));
752 frcVec.reserve(static_cast<size_t>(nDirty));
753
754 for (long idx : dirty) {
755 nrsStore.push_back(path[idx]->getAtomicNrs());
756 // Isolated molecules still store a box for I/O. Pots that infer
757 // PBC from a non-zero cell must see a zero box, as
758 // Matter::computePotential does on the endpoints.
759 boxStore.push_back(
760 (path[idx]->getPeriodic() || pot->forwardsStoredCell())
761 ? path[idx]->getCell()
762 : Matrix3d::Zero());
763 }
764 for (long j = 0; j < nDirty; j++) {
765 auto idx = dirty[static_cast<size_t>(j)];
766 posVec.push_back(path[idx]->getPositions().data());
767 nrsVec.push_back(nrsStore[static_cast<size_t>(j)].data());
768 frcVec.push_back(path[idx]->forcesData());
769 boxVec.push_back(boxStore[static_cast<size_t>(j)].data());
770 }
771
772 std::vector<double> energies(nDirty), variances(nDirty);
773 // Image i is system i - 1 to the potential's router (imageOwner),
774 // whether or not the images before it are dirty.
775 std::vector<long> owners(static_cast<size_t>(nDirty));
776 for (long j = 0; j < nDirty; j++) {
777 owners[static_cast<size_t>(j)] =
778 imageOwner(static_cast<size_t>(dirty[static_cast<size_t>(j)]));
779 }
780 pot->forceBatchOwned(nDirty, atoms, posVec.data(), nrsVec.data(),
781 frcVec.data(), energies.data(), variances.data(),
782 boxVec.data(), owners.data());
783 for (long j = 0; j < nDirty; j++) {
784 path[dirty[j]]->setComputedPotential(energies[j], variances[j]);
785 }
786 }
787 } else {
788 // Per-image evaluation (sequential or parallel threads)
789 bool canParallel =
791 if (numImages > 1 && params.main_options().parallel && canParallel) {
792#ifdef EON_PARALLEL_NEB
793 // nvc++ -stdpar=multicore|gpu (meson -Dstdpar=cpu|gpu).
794 std::vector<long> beads(static_cast<size_t>(numImages));
795 std::iota(beads.begin(), beads.end(), 1);
796 std::exception_ptr neb_cancel;
797 std::mutex neb_cancel_mu;
798 std::for_each(std::execution::par, beads.begin(), beads.end(),
799 [this, &neb_cancel, &neb_cancel_mu](long i) {
800 try {
801 path[i]->getForcesRaw();
802 } catch (const JobCancelled &) {
803 std::lock_guard<std::mutex> lock(neb_cancel_mu);
804 if (!neb_cancel) {
805 neb_cancel = std::current_exception();
806 }
807 }
808 });
809 if (neb_cancel) {
810 std::rethrow_exception(neb_cancel);
811 }
812#else
814 [this](long i) { path[i]->getForcesRaw(); });
815#endif
816 } else {
817 for (long i = 1; i <= numImages; i++) {
818 path[i]->getForcesRaw();
819 }
820 }
821 }
822
823 if (solidState_) {
824 projectSolidState(ci_active);
825 movedAfterForceCall = ci_active != ci_enabled_;
826 return;
827 }
828
829 // Find the highest energy non-endpoint image
830 auto first = path.begin() + 1;
831 auto last = path.begin() + numImages + 1;
832 auto it = std::max_element(
833 first, last,
834 [](const std::shared_ptr<Matter> &a, const std::shared_ptr<Matter> &b) {
835 return a->getPotentialEnergy() < b->getPotentialEnergy();
836 });
837 maxEnergyImage = std::distance(path.begin(), it);
838 double maxEnergy = (*it)->getPotentialEnergy();
839
840 // Update E_ref for energy weighting. The higher endpoint keeps the
841 // soft spring on the side below that minimum.
842 if (params.neb_options().spring.weighting.enabled) {
843 E_ref = std::max(path[0]->getPotentialEnergy(),
844 path[numImages + 1]->getPotentialEnergy());
845 }
846
847 // Climbing requires an interior peak above both fixed endpoints.
848 // A monotonic path retains the spring force on every interior image.
849 const double endpointEnergy = std::max(path.front()->getPotentialEnergy(),
850 path.back()->getPotentialEnergy());
851 const bool climb = ci_active && maxEnergy > endpointEnergy;
852 const long previousCI = static_cast<long>(climbingImage);
853 climbingImage = 0;
854
855 // Spring strategy must be rebuilt each iteration (depends on maxEnergy,
856 // E_ref). Tangent and projection strategies are cached as members.
858 maxEnergy, E_ref);
859
860 if (const auto *uniform = std::get_if<eonc::neb::UniformSpring>(&spring)) {
861 ksp = uniform->ksp;
862 }
863 // The climbing image, chosen before the per-image work so that work
864 // writes nothing outside its own image.
865 // Re-picking the highest image every call hops the climber onto a
866 // shoulder, so the current one stays while it is a strict interior
867 // local maximum.
868 long ciTarget = static_cast<long>(maxEnergyImage);
869 if (climb && previousCI > 0 && previousCI <= numImages) {
870 const double eCI = path[previousCI]->getPotentialEnergy();
871 if (eCI > path[previousCI - 1]->getPotentialEnergy() &&
872 eCI > path[previousCI + 1]->getPotentialEnergy()) {
873 ciTarget = previousCI;
874 }
875 }
876 if (climb) {
877 climbingImage = static_cast<size_t>(ciTarget);
878 }
879
880 // Each image reads its neighbours' positions and energies and writes
881 // only its own tangent and projected force, so with [Main] parallel the
882 // images are projected on the image pool like their forces.
883 auto projectImage = [&](long i) {
884 AtomMatrix posDiffNext(atoms, 3), posDiffPrev(atoms, 3);
885 const AtomMatrix &force = path[i]->getForces();
886 const AtomMatrix &pos = path[i]->getPositions();
887 const AtomMatrix &posPrev = path[i - 1]->getPositions();
888 const AtomMatrix &posNext = path[i + 1]->getPositions();
889 double energy = path[i]->getPotentialEnergy();
890 double energyPrev = path[i - 1]->getPotentialEnergy();
891 double energyNext = path[i + 1]->getPotentialEnergy();
892 posDiffNext.noalias() = posNext - pos;
893 posDiffNext = path[i]->pbc(posDiffNext);
894 posDiffPrev.noalias() = pos - posPrev;
895 posDiffPrev = path[i]->pbc(posDiffPrev);
896 double distNext = posDiffNext.norm();
897 double distPrev = posDiffPrev.norm();
898
899 // Tangent via strategy dispatch
900 *tangent[i] = std::visit(
901 [&](auto &t) {
902 return t.compute(posDiffNext, posDiffPrev, energy, energyPrev,
903 energyNext);
904 },
906
907 // Spring forces via strategy dispatch
908 eonc::neb::SpringResult springResult = std::visit(
909 [&](auto &s) -> eonc::neb::SpringResult {
910 using T = std::decay_t<decltype(s)>;
911 if constexpr (std::is_same_v<T, eonc::neb::UniformSpring>) {
912 return s.compute(i, *tangent[i], distNext, distPrev, posDiffNext,
913 posDiffPrev, path[i]);
914 } else if constexpr (std::is_same_v<T, eonc::neb::WeightedSpring>) {
915 return s.compute(i, *tangent[i], distNext, distPrev);
916 } else if constexpr (std::is_same_v<T, eonc::neb::GeometricSpring>) {
917 return s.compute(i, *tangent[i], distNext, distPrev);
918 } else {
919 return s.compute(i, *tangent[i], posNext, posPrev, pos, path[i]);
920 }
921 },
922 spring);
923
924 // Climbing image or projected force
925 if (climb && i == ciTarget) {
926 // CI force: F - 2*(F.t)*t, plus DNEB correction if active
927 AtomMatrix forceDNEB = AtomMatrix::Zero(atoms, 3);
928 if (const auto *dnebProj =
929 std::get_if<eonc::neb::DNEB_Projection>(&projectionStrat_)) {
930 AtomMatrix fPerp = eonc::neb::forcePerp(force, *tangent[i]);
931 forceDNEB = eonc::neb::computeDNEBComponent(springResult.forceSpring,
932 *tangent[i], fPerp,
933 dnebProj->use_switching);
934 }
935 *projectedForce[i] =
936 eonc::neb::climbingImageForce(force, *tangent[i], forceDNEB);
937 } else {
938 eonc::neb::ImageForceData data{force, *tangent[i], springResult,
939 path[i]->numberOfFreeAtoms(),
940 path[i]->numberOfAtoms()};
941 *projectedForce[i] = std::visit([&](auto &p) { return p.project(data); },
943 }
944
945 // The spring force acts along the tangent, which has a component on a
946 // fixed atom whenever the endpoints place it differently; a fixed atom
947 // carries no force on the band.
948 if (path[i]->numberOfFreeAtoms() < path[i]->numberOfAtoms()) {
949 for (long j = 0; j < atoms; j++) {
950 if (path[i]->getFixed(j)) {
951 projectedForce[i]->row(j).setZero();
952 }
953 }
954 }
955
956 eonc::neb::zeroTranslation(*projectedForce[i], path[i]->numberOfFreeAtoms(),
957 path[i]->numberOfAtoms());
958 };
959 if (numImages > 1 && params.main_options().parallel) {
960 eonc::forEachImage(numImages, projectImage);
961 } else {
962 for (long i = 1; i <= numImages; i++) {
963 projectImage(i);
964 }
965 }
966
967 movedAfterForceCall = ci_active != ci_enabled_;
968}
void projectSolidState(bool ci_active)
AtomMatrix computeDNEBComponent(const AtomMatrix &forceSpring, const AtomMatrix &tangent, const AtomMatrix &fPerp, bool useSwitching)
Compute the DNEB force component for a given image.
AtomMatrix climbingImageForce(const AtomMatrix &force, const AtomMatrix &tangent, const AtomMatrix &forceDNEB)
Compute the climbing image projected force.
AtomMatrix forcePerp(const AtomMatrix &force, const AtomMatrix &tangent)
Compute the perpendicular component of force relative to the tangent.
bool potAllowsSharedInstance(const P &p) noexcept
void forEachImage(long n, Work &&work)

◆ updateForces() [2/2]

Definition at line 69 of file NudgedElasticBand.h.

◆ eonc::neb::OCINEBController

friend class eonc::neb::OCINEBController
friend

Definition at line 39 of file NudgedElasticBand.h.

Member Data Documentation

◆ atoms

Definition at line 104 of file NudgedElasticBand.h.

104{0};

◆ baseline_force

Definition at line 129 of file NudgedElasticBand.h.

129{-1.0};

◆ ci_enabled_

Definition at line 128 of file NudgedElasticBand.h.

128{false}; // runtime CI state, set by compute()

◆ climbingImage

Definition at line 105 of file NudgedElasticBand.h.

105{0}, climbingImage{0}, numExtrema{0};

◆ E_ref

Definition at line 124 of file NudgedElasticBand.h.

◆ eigenmode_solvers

std::vector<std::shared_ptr<EigenmodeStrategy> > eonc::NudgedElasticBand::eigenmode_solvers

Definition at line 92 of file NudgedElasticBand.h.

◆ extremumCurvature

Definition at line 114 of file NudgedElasticBand.h.

◆ extremumEnergy

Definition at line 112 of file NudgedElasticBand.h.

◆ extremumPosition

Definition at line 113 of file NudgedElasticBand.h.

◆ iteration_

Definition at line 133 of file NudgedElasticBand.h.

133{0};

◆ k_l

Definition at line 123 of file NudgedElasticBand.h.

123{0.0}; // Lower-bound value for the spring constant

◆ k_u

Definition at line 122 of file NudgedElasticBand.h.

122{0.0}; // Upper-bound value for the spring constant

◆ ksp

Definition at line 121 of file NudgedElasticBand.h.

121{0.0};

◆ log

◆ maxEnergyImage

Definition at line 116 of file NudgedElasticBand.h.

116{0};

◆ movedAfterForceCall

Definition at line 118 of file NudgedElasticBand.h.

118{false};

◆ numExtrema

Definition at line 105 of file NudgedElasticBand.h.

105{0}, climbingImage{0}, numExtrema{0};

◆ numImages

Definition at line 105 of file NudgedElasticBand.h.

105{0}, climbingImage{0}, numExtrema{0};

◆ params

◆ path

std::vector<std::shared_ptr<Matter> > eonc::NudgedElasticBand::path

Definition at line 109 of file NudgedElasticBand.h.

◆ perImagePotentials_

Initial value:
{
false}

Whether per-image potential instances exist.

Definition at line 119 of file NudgedElasticBand.h.

119 {
120 false};

◆ pot

std::shared_ptr<Potential> eonc::NudgedElasticBand::pot
private

Definition at line 131 of file NudgedElasticBand.h.

◆ projectedCellForce

Definition at line 141 of file NudgedElasticBand.h.

◆ projectedForce

std::vector<std::shared_ptr<AtomMatrix> > eonc::NudgedElasticBand::projectedForce

Definition at line 111 of file NudgedElasticBand.h.

◆ projectionStrat_

neb::ProjectionStrategy eonc::NudgedElasticBand::projectionStrat_
private

Definition at line 138 of file NudgedElasticBand.h.

◆ reactantEnergy

Definition at line 108 of file NudgedElasticBand.h.

108{0.0};

◆ solidJacobian_

Definition at line 140 of file NudgedElasticBand.h.

140{1.0};

◆ solidState_

Definition at line 139 of file NudgedElasticBand.h.

139{false};

◆ status

◆ tangent

std::vector<std::shared_ptr<AtomMatrix> > eonc::NudgedElasticBand::tangent

Definition at line 110 of file NudgedElasticBand.h.

◆ tangentStrat_

neb::TangentStrategy eonc::NudgedElasticBand::tangentStrat_
private

Definition at line 137 of file NudgedElasticBand.h.


The documentation for this class was generated from the following files: