Loading...
Searching...
No Matches
eonc::helpers::neb_paths Namespace Reference

Functions

std::shared_ptr< PotentialcreateZBLPotential ()
std::vector< MatterlinearPath (const Matter &initImg, const Matter &finalImg, const size_t nimgs)
std::vector< MatterfilePathInit (const std::vector< fs::path > &fsrcs, const Matter &refImg, const size_t nimgs)
std::vector< fs::path > readFilePaths (const std::string &listFilePath)
 Reads a file where each line contains a path to another file.
MatrixXd getDistanceMatrix (const Matter &m)
std::vector< MatteridppPath (const Matter &initImg, const Matter &finalImg, const size_t nimgs, const Parameters &params, bool use_zbl)
std::vector< MatteridppCollectivePath (const Matter &initImg, const Matter &finalImg, size_t nimgs, const Parameters &params, bool use_zbl)
Matter interpolateImage (const Matter &A, const Matter &B, double fraction)
std::vector< MattersidppPath (const Matter &initImg, const Matter &finalImg, size_t target_nimgs, const Parameters &params, bool use_zbl)
AtomMatrix cubicInterpolate (const AtomMatrix &P0, const AtomMatrix &T0, const AtomMatrix &P1, const AtomMatrix &T1, double f)
 Interpolates positions using a cubic Hermite spline.
std::vector< MatterresamplePath (const std::vector< Matter > &densePath, size_t targetCount)
void resamplePathInPlace (std::span< std::shared_ptr< Matter > > path)
 In-place path reparameterization for NEB shared_ptr paths.

Function Documentation

◆ createZBLPotential()

std::shared_ptr< Potential > eonc::helpers::neb_paths::createZBLPotential ( )

Definition at line 18 of file NEBInitialPaths.cpp.

18 {
19 auto zbl_params = Parameters{};
21 // Strong short-range repulsion
22 zbl_params.zbl_options.cut_inner = 0.5;
23 // Cutoff sufficient to push overlapping atoms apart
24 zbl_params.zbl_options.cut_global = 3.0;
26}
struct eonc::Parameters::potential_options_t potential_options
std::shared_ptr< Potential > makePotential(const Parameters &params)

◆ cubicInterpolate()

AtomMatrix eonc::helpers::neb_paths::cubicInterpolate ( const AtomMatrix & P0,
const AtomMatrix & T0,
const AtomMatrix & P1,
const AtomMatrix & T1,
double f )

Interpolates positions using a cubic Hermite spline.

Parameters
P0Starting positions
T0Tangent at P0 (scaled by segment length)
P1Ending positions
T1Tangent at P1 (scaled by segment length)
fFraction between 0 and 1

Definition at line 330 of file NEBInitialPaths.cpp.

332 {
333 double f2 = f * f;
334 double f3 = f2 * f;
335
336 // Hermite basis functions
337 double h00 = 2 * f3 - 3 * f2 + 1;
338 double h10 = f3 - 2 * f2 + f;
339 double h01 = -2 * f3 + 3 * f2;
340 double h11 = f3 - f2;
341
342 return h00 * P0 + h10 * T0 + h01 * P1 + h11 * T1;
343}

◆ filePathInit()

std::vector< Matter > eonc::helpers::neb_paths::filePathInit ( const std::vector< fs::path > & fsrcs,
const Matter & refImg,
const size_t nimgs )

Definition at line 47 of file NEBInitialPaths.cpp.

48 {
49 std::vector<Matter> all_images_on_path;
50 if (nimgs + 2 != fsrcs.size()) {
51 throw std::runtime_error("Error in filePathInit: Expected " +
52 std::to_string(nimgs + 2) + " files, but got " +
53 std::to_string(fsrcs.size()) + ".");
54 }
55 all_images_on_path.reserve(nimgs + 2);
56 // For all images
57 for (const auto &filePath : fsrcs) {
58 Matter img(refImg);
59 if (!eonc::io::io_ok(img.con2matter(filePath.string()))) {
60 throw std::runtime_error("failed to load NEB path frame: " +
61 filePath.string());
62 }
63 all_images_on_path.push_back(img);
64 }
65 return all_images_on_path;
66}
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38

◆ getDistanceMatrix()

MatrixXd eonc::helpers::neb_paths::getDistanceMatrix ( const Matter & m)

Definition at line 88 of file NEBInitialPaths.cpp.

88 {
89 int natoms = m.numberOfAtoms();
90 MatrixXd d(natoms, natoms);
91 AtomMatrix pos = m.getPositions();
92 for (int i = 0; i < natoms; ++i) {
93 for (int j = 0; j < natoms; ++j) {
94 if (i == j) {
95 d(i, j) = 0.0;
96 } else {
97 d(i, j) = m.pbc(pos.row(i) - pos.row(j)).norm();
98 }
99 }
100 }
101 return d;
102}
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
AtomMatrix pbc(const AtomMatrix &diff) const
Definition Matter.h:163
long int numberOfAtoms() const
Definition Matter.cpp:209
const AtomMatrix & getPositions() const
Definition Matter.cpp:236

◆ idppCollectivePath()

std::vector< Matter > eonc::helpers::neb_paths::idppCollectivePath ( const Matter & initImg,
const Matter & finalImg,
size_t nimgs,
const Parameters & params,
bool use_zbl )

Definition at line 162 of file NEBInitialPaths.cpp.

164 {
165 auto log = eonc::log::get();
166 QUILL_LOG_INFO(log, "Generating initial path using Collective IDPP-NEB...");
167
168 std::vector<Matter> path = linearPath(initImg, finalImg, nimgs);
169
170 // 1. Base Objective
171 std::shared_ptr<ObjectiveFunction> idpp_objf =
172 std::make_shared<CollectiveIDPPObjectiveFunction>(path, params);
173
174 // 2. Optional ZBL Wrapper
175 if (use_zbl) {
176 QUILL_LOG_INFO(log, "Enabling ZBL repulsive penalty for IDPP...");
177 auto zbl_pot = createZBLPotential();
178 // Wrap the IDPP objective with ZBL repulsion (weight = 1.0)
179 idpp_objf = std::make_shared<ZBLRepulsiveIDPPObjective>(idpp_objf, zbl_pot,
180 path, params, 1.0);
181 }
182
184 idpp_objf, params.neb_options.initialization.opt_method, params);
185
186 int maxSteps = params.neb_options.initialization.max_iterations;
187 int currentStep = 0;
188 int checkInterval = 40;
189
190 while (currentStep < maxSteps) {
191 optim->run(checkInterval, params.optimizer_options.max_move);
192 currentStep += checkInterval;
193
194 if (idpp_objf->isConverged()) {
195 QUILL_LOG_INFO(log,
196 "IDPP-NEB converged after {} steps. Max Residual: {:.4f}",
197 currentStep, idpp_objf->getConvergence());
198 return path;
199 }
200 }
201
202 QUILL_LOG_WARNING(log,
203 "IDPP-NEB reached max_iterations ({}) without full "
204 "convergence. Residual: {:.4f}",
205 maxSteps, idpp_objf->getConvergence());
206 return path;
207}
struct eonc::Parameters::optimizer_options_t optimizer_options
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::shared_ptr< Potential > createZBLPotential()
std::vector< Matter > linearPath(const Matter &initImg, const Matter &finalImg, const size_t nimgs)
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

◆ idppPath()

std::vector< Matter > eonc::helpers::neb_paths::idppPath ( const Matter & initImg,
const Matter & finalImg,
const size_t nimgs,
const Parameters & params,
bool use_zbl )

Definition at line 104 of file NEBInitialPaths.cpp.

106 {
107
108 auto log = eonc::log::get();
109 QUILL_LOG_INFO(log, "Generating initial path using IDPP...");
110 if (use_zbl) {
111 QUILL_LOG_WARNING(
112 log, "ZBL Repulsion not implemented for iterative IDPP (idppPath). "
113 "Using standard IDPP.");
114 }
115
116 // Start with a linear interpolation to get initial Cartesian coordinates
117 std::vector<Matter> path = linearPath(initImg, finalImg, nimgs);
118
119 // Pre-calculate endpoint distance matrices
120 MatrixXd dInit = getDistanceMatrix(initImg);
121 MatrixXd dFinal = getDistanceMatrix(finalImg);
122
123 // Optimize intermediate images
124 // Note: path[0] and path[nimgs+1] are fixed endpoints
125 for (size_t i = 1; i <= nimgs; ++i) {
126
127 // Calculate the interpolation factor (Reaction Coordinate)
128 double xi = static_cast<double>(i) / (nimgs + 1);
129
130 // Linear interpolation of the distance matrix (The "Image Dependent" part)
131 MatrixXd dTarget = (1.0 - xi) * dInit + xi * dFinal;
132
133 // Create the IDPP Objective Function
134 auto idpp_objf = std::make_shared<IDPPObjectiveFunction>(
135 std::make_shared<Matter>(path[i]), params, dTarget);
136
137 // Create an Optimizer
138 // Defaults to taking the same one as optimizer
139 auto idpp_optim = eonc::helpers::create::mkOptim(
140 idpp_objf, params.neb_options.opt_method, params);
141
142 // Run the optimization
143 int status =
144 idpp_optim->run(params.neb_options.initialization.max_iterations,
146
147 // Log progress
148 double residual = idpp_objf->getConvergence();
149 QUILL_LOG_DEBUG(log,
150 "IDPP Image {:2d}/{:2d} | xi: {:.2f} | Residual: {:.4e}", i,
151 nimgs, xi, residual);
152
153 // Explicitly sync positions back to the path vector just to be safe
154 path[i].setPositions(AtomMatrix::Map(idpp_objf->getPositions().data(),
155 path[i].numberOfAtoms(), 3));
156 }
157
158 QUILL_LOG_INFO(log, "IDPP path generation complete.");
159 return path;
160}
MatrixXd getDistanceMatrix(const Matter &m)

◆ interpolateImage()

Matter eonc::helpers::neb_paths::interpolateImage ( const Matter & A,
const Matter & B,
double fraction )

Definition at line 210 of file NEBInitialPaths.cpp.

210 {
211 Matter newImg(A);
212 AtomMatrix posA = A.getPositions();
213 AtomMatrix posB = B.getPositions();
214 // Use PBC-aware interpolation
215 AtomMatrix diff = A.pbc(posB - posA);
216 newImg.setPositions(posA + fraction * diff);
217 return newImg;
218}

◆ linearPath()

std::vector< Matter > eonc::helpers::neb_paths::linearPath ( const Matter & initImg,
const Matter & finalImg,
const size_t nimgs )

Definition at line 28 of file NEBInitialPaths.cpp.

29 {
30 std::vector<Matter> all_images_on_path(nimgs + 2, initImg);
31 all_images_on_path.front() = Matter(initImg);
32 all_images_on_path.back() = Matter(finalImg);
33 AtomMatrix posInitial = all_images_on_path.front().getPositions();
34 AtomMatrix posFinal = all_images_on_path.back().getPositions();
35 AtomMatrix imageSep = initImg.pbc(posFinal - posInitial) / (nimgs + 1);
36 // Only the ones which are not the front and back
37 for (auto it{std::next(all_images_on_path.begin())};
38 it != std::prev(all_images_on_path.end()); ++it) {
39 *it = Matter(initImg);
40 (*it).setPositions(posInitial +
41 imageSep *
42 int(std::distance(all_images_on_path.begin(), it)));
43 }
44 return all_images_on_path;
45}

◆ readFilePaths()

std::vector< std::filesystem::path > eonc::helpers::neb_paths::readFilePaths ( const std::string & listFilePath)

Reads a file where each line contains a path to another file.

Parameters
listFilePathThe path to the file containing the list of file paths.
Returns
A vector of filesystem paths. Returns an empty vector if the file cannot be opened.

Definition at line 68 of file NEBInitialPaths.cpp.

68 {
69 std::vector<fs::path> paths;
70 std::ifstream inputFile(listFilePath);
71
72 if (!inputFile.is_open()) {
73 throw std::runtime_error("Error: Could not open path list file: " +
74 listFilePath);
75 }
76
77 std::string line;
78 while (std::getline(inputFile, line)) {
79 // Skip any empty lines in the input file
80 if (!line.empty()) {
81 paths.emplace_back(line);
82 }
83 }
84
85 return paths;
86}

◆ resamplePath()

std::vector< Matter > eonc::helpers::neb_paths::resamplePath ( const std::vector< Matter > & densePath,
size_t targetCount )

Definition at line 345 of file NEBInitialPaths.cpp.

346 {
347 if (densePath.size() == targetCount + 2)
348 return densePath;
349
350 size_t n = densePath.size();
351
352 // Calculate cumulative arc length along the path
353 std::vector<double> arcLength(n, 0.0);
354 for (size_t i = 1; i < n; ++i) {
355 AtomMatrix diff = densePath[i].pbc(densePath[i].getPositions() -
356 densePath[i - 1].getPositions());
357 arcLength[i] = arcLength[i - 1] + diff.norm();
358 }
359 double totalLength = arcLength.back();
360
361 // Calculate tangents for cubic interpolation
362 std::vector<AtomMatrix> tangents(n);
363 for (size_t i = 0; i < n; ++i) {
364 AtomMatrix T;
365 if (i == 0) {
366 T = densePath[i].pbc(densePath[i + 1].getPositions() -
367 densePath[i].getPositions());
368 } else if (i == n - 1) {
369 T = densePath[i].pbc(densePath[i].getPositions() -
370 densePath[i - 1].getPositions());
371 } else {
372 AtomMatrix dNext = densePath[i].pbc(densePath[i + 1].getPositions() -
373 densePath[i].getPositions());
374 AtomMatrix dPrev = densePath[i].pbc(densePath[i].getPositions() -
375 densePath[i - 1].getPositions());
376 T = 0.5 * (dNext + dPrev);
377 }
378 // Scale tangent by local segment length for proper spline parameterization
379 if (i > 0 && i < n - 1) {
380 double localScale = (arcLength[i + 1] - arcLength[i - 1]) / 2.0;
381 T = T.normalized() * localScale;
382 }
383 tangents[i] = T;
384 }
385
386 std::vector<Matter> resampled;
387 resampled.reserve(targetCount + 2);
388 resampled.push_back(densePath.front());
389
390 // Place new images at equal arc-length intervals
391 double segmentLength = totalLength / (targetCount + 1);
392
393 for (size_t i = 1; i <= targetCount; ++i) {
394 double targetArc = i * segmentLength;
395
396 // Find the segment containing this arc length
397 size_t lowIdx = 0;
398 for (size_t j = 1; j < n; ++j) {
399 if (arcLength[j] >= targetArc) {
400 lowIdx = j - 1;
401 break;
402 }
403 }
404 size_t highIdx = lowIdx + 1;
405
406 // Interpolation parameter within this segment
407 double segmentArc = arcLength[highIdx] - arcLength[lowIdx];
408 double f = (segmentArc > 1e-10)
409 ? (targetArc - arcLength[lowIdx]) / segmentArc
410 : 0.0;
411
412 Matter newImg(densePath[0]);
413 newImg.setPositions(cubicInterpolate(
414 densePath[lowIdx].getPositions(), tangents[lowIdx],
415 densePath[highIdx].getPositions(), tangents[highIdx], f));
416 resampled.push_back(newImg);
417 }
418
419 resampled.push_back(densePath.back());
420 return resampled;
421}
AtomMatrix cubicInterpolate(const AtomMatrix &P0, const AtomMatrix &T0, const AtomMatrix &P1, const AtomMatrix &T1, double f)
Interpolates positions using a cubic Hermite spline.

◆ resamplePathInPlace()

void eonc::helpers::neb_paths::resamplePathInPlace ( std::span< std::shared_ptr< Matter > > path)

In-place path reparameterization for NEB shared_ptr paths.

Redistributes interior images at equal arc-length intervals using cubic Hermite interpolation, without allocating new Matter objects.

Parameters
pathThe FULL path including endpoints. path[0] and path[n-1] are treated as fixed endpoints and are never modified. Interior images path[1] through path[n-2] are repositioned. Pass the entire NEB path vector, not a sub-span of intermediate images.

Definition at line 423 of file NEBInitialPaths.cpp.

423 {
424 size_t n = path.size();
425 if (n < 3)
426 return;
427
428 // Calculate cumulative arc length
429 std::vector<double> arcLength(n, 0.0);
430 for (size_t i = 1; i < n; ++i) {
431 AtomMatrix diff =
432 path[i]->pbc(path[i]->getPositions() - path[i - 1]->getPositions());
433 arcLength[i] = arcLength[i - 1] + diff.norm();
434 }
435 double totalLength = arcLength.back();
436 if (totalLength < 1e-12)
437 return;
438
439 // Tangents for cubic interpolation
440 std::vector<AtomMatrix> tangents(n);
441 for (size_t i = 0; i < n; ++i) {
442 AtomMatrix T;
443 if (i == 0) {
444 T = path[i]->pbc(path[i + 1]->getPositions() - path[i]->getPositions());
445 } else if (i == n - 1) {
446 T = path[i]->pbc(path[i]->getPositions() - path[i - 1]->getPositions());
447 } else {
448 AtomMatrix dN =
449 path[i]->pbc(path[i + 1]->getPositions() - path[i]->getPositions());
450 AtomMatrix dP =
451 path[i]->pbc(path[i]->getPositions() - path[i - 1]->getPositions());
452 T = 0.5 * (dN + dP);
453 }
454 if (i > 0 && i < n - 1) {
455 double localScale = (arcLength[i + 1] - arcLength[i - 1]) / 2.0;
456 T = T.normalized() * localScale;
457 }
458 tangents[i] = T;
459 }
460
461 // Store original positions for interpolation source
462 std::vector<AtomMatrix> origPos(n);
463 for (size_t i = 0; i < n; ++i)
464 origPos[i] = path[i]->getPositions();
465
466 // Redistribute interior images at equal arc-length intervals (in-place)
467 size_t nInterior = n - 2;
468 double segLen = totalLength / (nInterior + 1);
469
470 for (size_t i = 1; i <= nInterior; ++i) {
471 double targetArc = i * segLen;
472 size_t lo = 0;
473 for (size_t j = 1; j < n; ++j) {
474 if (arcLength[j] >= targetArc) {
475 lo = j - 1;
476 break;
477 }
478 }
479 size_t hi = lo + 1;
480 double sArc = arcLength[hi] - arcLength[lo];
481 double f = (sArc > 1e-10) ? (targetArc - arcLength[lo]) / sArc : 0.0;
482
483 path[i]->setPositions(cubicInterpolate(origPos[lo], tangents[lo],
484 origPos[hi], tangents[hi], f));
485 }
486}

◆ sidppPath()

std::vector< Matter > eonc::helpers::neb_paths::sidppPath ( const Matter & initImg,
const Matter & finalImg,
size_t target_nimgs,
const Parameters & params,
bool use_zbl )

Definition at line 220 of file NEBInitialPaths.cpp.

222 {
223
224 auto log = eonc::log::get();
225 const auto &init = params.neb_options.initialization;
226 QUILL_LOG_INFO(log,
227 "Generating initial path using S-IDPP{} ({} images, "
228 "alpha={:.2f}, frontier_tol={:.4f})...",
229 use_zbl ? "-ZBL" : "", target_nimgs, init.sidpp_alpha,
230 init.sidpp_frontier_tol);
231
232 // 1. Start with endpoints [Reactant, Product]
233 std::vector<Matter> path;
234 path.push_back(initImg);
235 path.push_back(finalImg);
236
237 std::shared_ptr<Potential> zbl_pot = nullptr;
238 if (use_zbl) {
239 zbl_pot = createZBLPotential();
240 }
241
242 // Track frontier counts: nLeft images from reactant, nRight from product
243 int nLeft = 0;
244 int nRight = 0;
245 int nIntermediate = 0;
246 bool addToLeft = true; // Alternate L/R, starting with left
247
248 // Helper: create IDPP objective with optional ZBL wrapping
249 auto makeIDPP = [&]() -> std::shared_ptr<ObjectiveFunction> {
250 auto objf = std::make_shared<CollectiveIDPPObjectiveFunction>(path, params);
251 if (use_zbl && zbl_pot) {
252 return std::make_shared<ZBLRepulsiveIDPPObjective>(objf, zbl_pot, path,
253 params, 1.0);
254 }
255 return objf;
256 };
257
258 // Helper: relax current path on IDPP surface
259 auto relaxPath = [&](int maxSteps) -> double {
260 auto objf = makeIDPP();
261 auto optim = eonc::helpers::create::mkOptim(objf, init.opt_method, params);
262 int step = 0;
263 while (step < maxSteps) {
264 optim->run(5, init.max_move);
265 step += 5;
266 if (objf->isConverged())
267 break;
268 }
269 return objf->getConvergence();
270 };
271
272 // 2. Sequential growth loop: alternate adding images from L and R
273 while (nIntermediate < static_cast<int>(target_nimgs)) {
274
275 if (addToLeft && nIntermediate < static_cast<int>(target_nimgs)) {
276 // Add to left (reactant) frontier
277 Matter &frontier = path[nLeft];
278 Matter &next = path[nLeft + 1];
279 Matter newImg = interpolateImage(frontier, next, init.sidpp_alpha);
280 path.insert(path.begin() + nLeft + 1, newImg);
281 nLeft++;
282 nIntermediate++;
283 QUILL_LOG_DEBUG(log, "S-IDPP: +L frontier (nL={}, nR={}, total={})",
284 nLeft, nRight, nIntermediate);
285 } else if (nIntermediate < static_cast<int>(target_nimgs)) {
286 // Add to right (product) frontier
287 int rightIdx = static_cast<int>(path.size()) - 1 - nRight;
288 Matter &frontier = path[rightIdx];
289 Matter &prev = path[rightIdx - 1];
290 Matter newImg = interpolateImage(frontier, prev, init.sidpp_alpha);
291 path.insert(path.begin() + rightIdx, newImg);
292 nRight++;
293 nIntermediate++;
294 QUILL_LOG_DEBUG(log, "S-IDPP: +R frontier (nL={}, nR={}, total={})",
295 nLeft, nRight, nIntermediate);
296 }
297 addToLeft = !addToLeft; // Alternate sides
298
299 // Optimize current path on IDPP surface
300 double residual = relaxPath(init.nsteps);
301
302 // Frontier convergence gating: if not converged, keep relaxing
303 // before adding more images (up to max_iterations total)
304 if (residual > init.sidpp_frontier_tol) {
305 double residual2 = relaxPath(init.max_iterations - init.nsteps);
306 QUILL_LOG_DEBUG(log, "S-IDPP: Extended relaxation {:.4f} -> {:.4f}",
307 residual, residual2);
308 residual = residual2;
309 }
310
311 QUILL_LOG_DEBUG(log, "S-IDPP: {} images | Residual: {:.4f}", nIntermediate,
312 residual);
313 }
314
315 // 3. Reparameterize: redistribute images evenly along arc length
316 if (init.sidpp_reparam && path.size() > 3) {
317 QUILL_LOG_INFO(log, "S-IDPP: Reparameterizing {} images along arc length",
318 path.size() - 2);
319 path = resamplePath(path, target_nimgs);
320 }
321
322 // 4. Final relaxation of the complete path
323 QUILL_LOG_INFO(log, "S-IDPP: Final relaxation of full path...");
324 double finalResidual = relaxPath(init.max_iterations);
325 QUILL_LOG_INFO(log, "S-IDPP: Final residual: {:.4f}", finalResidual);
326
327 return path;
328}
std::vector< Matter > resamplePath(const std::vector< Matter > &densePath, size_t targetCount)
Matter interpolateImage(const Matter &A, const Matter &B, double fraction)