Loading...
Searching...
No Matches
eonc::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 ()
void updateForces (bool ci_active)
void updateForces (void)
void setCIEnabled (bool enabled)
double convergenceForce (void)
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.

Public Attributes

std::vector< std::shared_ptr< EigenmodeStrategy > > eigenmode_solvers
int atoms {0}
long numImages {0}
long climbingImage {0}
long numExtrema {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 Attributes

bool ci_enabled_ {false}
double baseline_force {-1.0}
Parameters params
std::shared_ptr< Potentialpot
NEBStatus status
eonc::log::Scoped log
neb::TangentStrategy tangentStrat_
neb::ProjectionStrategy projectionStrat_

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]

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

Definition at line 35 of file NudgedElasticBand.cpp.

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);
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
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) {
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(
116 parametersPassed.neb_options.initialization.max_move);
117 }
118 return path;
119 }(),
120 parametersPassed, potPassed) {}
std::vector< std::shared_ptr< Matter > > path
NudgedElasticBand(std::shared_ptr< Matter > initialPassed, std::shared_ptr< Matter > finalPassed, const Parameters &parametersPassed, std::shared_ptr< Potential > potPassed)
struct eonc::Parameters::neb_options_t neb_options
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 > 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:44
struct eonc::Parameters::neb_options_t::path_initialization_t initialization

◆ NudgedElasticBand() [2/2]

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

Definition at line 123 of file NudgedElasticBand.cpp.

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}
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
std::shared_ptr< Potential > pot
neb::ProjectionStrategy projectionStrat_
std::vector< double > extremumCurvature
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)
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< 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::climbing_image_options_t climbing_image

◆ ~NudgedElasticBand()

eonc::NudgedElasticBand::~NudgedElasticBand ( )
default

Member Function Documentation

◆ compute()

NudgedElasticBand::NEBStatus NudgedElasticBand::compute ( void )

Definition at line 204 of file NudgedElasticBand.cpp.

204 {
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"});
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
276 ocineb.updateStability(climbingImage);
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}
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
void printImageData(bool writeToFile=false, size_t idx=0)
void setCIEnabled(bool enabled)
friend class eonc::neb::OCINEBController
static Config fromParams(const Parameters &params)
void resamplePathInPlace(std::span< std::shared_ptr< Matter > > path)
In-place path reparameterization for NEB shared_ptr paths.
double maxAtomMotionV(const VectorXd v1)
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
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().

◆ convergenceForce()

double NudgedElasticBand::convergenceForce ( void )

Definition at line 433 of file NudgedElasticBand.cpp.

433 {
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}
quill::Logger * traceback() noexcept
Get or create the "_traceback" logger for traceback logging.
Definition EonLogger.h:88

◆ findExtrema()

void NudgedElasticBand::findExtrema ( void )

Definition at line 656 of file NudgedElasticBand.cpp.

656 {
658 numExtrema = result.numExtrema;
659 extremumPosition = std::move(result.positions);
660 extremumEnergy = std::move(result.energies);
661 extremumCurvature = std::move(result.curvatures);
662}
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()

NudgedElasticBand::NEBStatus eonc::NudgedElasticBand::getStatus ( )
inline

Definition at line 59 of file NudgedElasticBand.h.

59{ return this->status; };

◆ pathFrames()

std::vector< readcon::ConFrame > 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 665 of file NudgedElasticBand.cpp.

665 {
668 params.debug_options.estimate_neb_eigenvalues, bandIndex);
669}
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).

◆ printImageData()

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

Definition at line 650 of file NudgedElasticBand.cpp.

650 {
652 params.debug_options.estimate_neb_eigenvalues,
653 writeToFile, idx, log);
654}
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.

◆ setCIEnabled()

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

Definition at line 62 of file NudgedElasticBand.h.

62{ ci_enabled_ = enabled; }

◆ updateForces() [1/2]

void NudgedElasticBand::updateForces ( bool ci_active)

Definition at line 469 of file NudgedElasticBand.cpp.

469 {
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}
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.
AtomMatrix climbingImageForce(const AtomMatrix &force, const AtomMatrix &tangent, const AtomMatrix &forceDNEB)
Compute the climbing image projected force.
AtomMatrix computeDNEBComponent(const AtomMatrix &forceSpring, const AtomMatrix &tangent, const AtomMatrix &fPerp)
Compute the DNEB force component for a given image.
AtomMatrix forcePerp(const AtomMatrix &force, const AtomMatrix &tangent)
Compute the perpendicular component of force relative to the tangent.

◆ updateForces() [2/2]

void eonc::NudgedElasticBand::updateForces ( void )
inline

Definition at line 61 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

int eonc::NudgedElasticBand::atoms {0}

Definition at line 74 of file NudgedElasticBand.h.

74{0};

◆ baseline_force

double eonc::NudgedElasticBand::baseline_force {-1.0}
private

Definition at line 95 of file NudgedElasticBand.h.

95{-1.0};

◆ ci_enabled_

bool eonc::NudgedElasticBand::ci_enabled_ {false}
private

Definition at line 94 of file NudgedElasticBand.h.

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

◆ climbingImage

long eonc::NudgedElasticBand::climbingImage {0}

Definition at line 75 of file NudgedElasticBand.h.

75{0}, climbingImage{0}, numExtrema{0};

◆ E_ref

double eonc::NudgedElasticBand::E_ref

Definition at line 90 of file NudgedElasticBand.h.

◆ eigenmode_solvers

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

Definition at line 72 of file NudgedElasticBand.h.

◆ extremumCurvature

std::vector<double> eonc::NudgedElasticBand::extremumCurvature

Definition at line 81 of file NudgedElasticBand.h.

◆ extremumEnergy

std::vector<double> eonc::NudgedElasticBand::extremumEnergy

Definition at line 79 of file NudgedElasticBand.h.

◆ extremumPosition

std::vector<double> eonc::NudgedElasticBand::extremumPosition

Definition at line 80 of file NudgedElasticBand.h.

◆ k_l

double eonc::NudgedElasticBand::k_l {0.0}

Definition at line 89 of file NudgedElasticBand.h.

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

◆ k_u

double eonc::NudgedElasticBand::k_u {0.0}

Definition at line 88 of file NudgedElasticBand.h.

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

◆ ksp

double eonc::NudgedElasticBand::ksp {0.0}

Definition at line 87 of file NudgedElasticBand.h.

87{0.0};

◆ log

eonc::log::Scoped eonc::NudgedElasticBand::log
private

Definition at line 99 of file NudgedElasticBand.h.

◆ maxEnergyImage

std::size_t eonc::NudgedElasticBand::maxEnergyImage {0}

Definition at line 83 of file NudgedElasticBand.h.

83{0};

◆ movedAfterForceCall

bool eonc::NudgedElasticBand::movedAfterForceCall {false}

Definition at line 84 of file NudgedElasticBand.h.

84{false};

◆ numExtrema

long eonc::NudgedElasticBand::numExtrema {0}

Definition at line 75 of file NudgedElasticBand.h.

75{0}, climbingImage{0}, numExtrema{0};

◆ numImages

long eonc::NudgedElasticBand::numImages {0}

Definition at line 75 of file NudgedElasticBand.h.

75{0}, climbingImage{0}, numExtrema{0};

◆ params

Parameters eonc::NudgedElasticBand::params
private

Definition at line 96 of file NudgedElasticBand.h.

◆ path

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

Definition at line 76 of file NudgedElasticBand.h.

◆ perImagePotentials_

bool eonc::NudgedElasticBand::perImagePotentials_
Initial value:
{
false}

Whether per-image potential instances exist.

Definition at line 85 of file NudgedElasticBand.h.

85 {
86 false};

◆ pot

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

Definition at line 97 of file NudgedElasticBand.h.

◆ projectedForce

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

Definition at line 78 of file NudgedElasticBand.h.

◆ projectionStrat_

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

Definition at line 103 of file NudgedElasticBand.h.

◆ status

NEBStatus eonc::NudgedElasticBand::status
private

Definition at line 98 of file NudgedElasticBand.h.

◆ tangent

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

Definition at line 77 of file NudgedElasticBand.h.

◆ tangentStrat_

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

Definition at line 102 of file NudgedElasticBand.h.


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