Loading...
Searching...
No Matches
NEBInitialPaths.cpp
Go to the documentation of this file.
4#include "eon/Optimizer.h"
5#include "eon/Parameters.h"
6#include <cmath>
7#include <filesystem>
8#include <fstream>
9#include <memory>
10#include <span>
11#include <stdexcept>
12#include <vector>
13
14#include "eon/EonLogger.h"
15namespace fs = std::filesystem;
16
18
19// Forward declaration of ZBL setup helper to keep code clean
20std::shared_ptr<Potential> createZBLPotential() {
21 auto zbl_params = Parameters{};
23 // Strong short-range repulsion
25 // Cutoff sufficient to push overlapping atoms apart
28}
29
30std::vector<Matter> linearPath(const Matter &initImg, const Matter &finalImg,
31 const size_t nimgs) {
32 requireSameAtomCount(initImg, finalImg, "reactant and product");
33 std::vector<Matter> all_images_on_path(nimgs + 2, initImg);
34 all_images_on_path.front() = Matter(initImg);
35 all_images_on_path.back() = Matter(finalImg);
36 AtomMatrix posInitial = all_images_on_path.front().getPositions();
37 AtomMatrix posFinal = all_images_on_path.back().getPositions();
38 AtomMatrix imageSep = initImg.pbc(posFinal - posInitial) / (nimgs + 1);
39 imageSep = imageSep.array() * initImg.getFree().array();
40 for (auto it{std::next(all_images_on_path.begin())};
41 it != std::prev(all_images_on_path.end()); ++it) {
42 *it = Matter(initImg);
43 (*it).setPositions(posInitial +
44 imageSep *
45 int(std::distance(all_images_on_path.begin(), it)));
46 }
47 return all_images_on_path;
48}
49
50std::vector<Matter> filePathInit(const std::vector<fs::path> &fsrcs,
51 const Matter &refImg, const size_t nimgs) {
52 std::vector<Matter> all_images_on_path;
53 if (nimgs + 2 != fsrcs.size()) {
54 throw std::runtime_error("Error in filePathInit: Expected " +
55 std::to_string(nimgs + 2) + " files, but got " +
56 std::to_string(fsrcs.size()) + ".");
57 }
58 all_images_on_path.reserve(nimgs + 2);
59 // For all images
60 for (const auto &filePath : fsrcs) {
61 Matter img(refImg);
62 if (!eonc::io::io_ok(img.con2matter(filePath.string()))) {
63 throw std::runtime_error("failed to load NEB path frame: " +
64 filePath.string());
65 }
66 if (!all_images_on_path.empty()) {
67 requireSameAtomCount(all_images_on_path.front(), img, "path images");
68 }
69 all_images_on_path.push_back(img);
70 }
71 return all_images_on_path;
72}
73
74std::vector<fs::path> readFilePaths(const std::string &listFilePath) {
75 std::vector<fs::path> paths;
76 std::ifstream inputFile(listFilePath);
77
78 if (!inputFile.is_open()) {
79 throw std::runtime_error("Error: Could not open path list file: " +
80 listFilePath);
81 }
82
83 std::string line;
84 while (std::getline(inputFile, line)) {
85 // Skip any empty lines in the input file
86 if (!line.empty()) {
87 paths.emplace_back(line);
88 }
89 }
90
91 return paths;
92}
93
95 int natoms = m.numberOfAtoms();
96 MatrixXd d(natoms, natoms);
97 AtomMatrix pos = m.getPositions();
98 for (int i = 0; i < natoms; ++i) {
99 for (int j = 0; j < natoms; ++j) {
100 if (i == j) {
101 d(i, j) = 0.0;
102 } else {
103 d(i, j) = m.pbc(pos.row(i) - pos.row(j)).norm();
104 }
105 }
106 }
107 return d;
108}
109
110std::vector<Matter> idppPath(const Matter &initImg, const Matter &finalImg,
111 const size_t nimgs, const Parameters &params,
112 bool use_zbl) {
113
114 auto log = eonc::log::get();
115 QUILL_LOG_INFO(log, "Generating initial path using IDPP...");
116 if (use_zbl) {
117 QUILL_LOG_WARNING(
118 log, "ZBL Repulsion not implemented for iterative IDPP (idppPath). "
119 "Using standard IDPP.");
120 }
121
122 // Start with a linear interpolation to get initial Cartesian coordinates
123 std::vector<Matter> path = linearPath(initImg, finalImg, nimgs);
124
125 // Pre-calculate endpoint distance matrices
126 MatrixXd dInit = getDistanceMatrix(initImg);
127 MatrixXd dFinal = getDistanceMatrix(finalImg);
128
129 // Optimize intermediate images
130 // Note: path[0] and path[nimgs+1] are fixed endpoints
131 for (size_t i = 1; i <= nimgs; ++i) {
132
133 // Calculate the interpolation factor (Reaction Coordinate)
134 double xi = static_cast<double>(i) / (nimgs + 1);
135
136 // Linear interpolation of the distance matrix (The "Image Dependent" part)
137 MatrixXd dTarget = (1.0 - xi) * dInit + xi * dFinal;
138
139 // Create the IDPP Objective Function
140 auto idpp_objf = std::make_shared<IDPPObjectiveFunction>(
141 std::make_shared<Matter>(path[i]), params, dTarget);
142
143 // Create an Optimizer
144 // Defaults to taking the same one as optimizer
145 auto idpp_optim = eonc::helpers::create::mkOptim(
146 idpp_objf, params.neb_options().opt_method, params);
147
148 // Run the optimization
149 int status =
150 idpp_optim->run(params.neb_options().initialization.max_iterations,
152
153 // Log progress
154 double residual = idpp_objf->getConvergence();
155 QUILL_LOG_DEBUG(log,
156 "IDPP Image {:2d}/{:2d} | xi: {:.2f} | Residual: {:.4e}", i,
157 nimgs, xi, residual);
158
159 // Explicitly sync positions back to the path vector just to be safe
160 path[i].setPositionsFreeV(idpp_objf->getPositions());
161 }
162
163 QUILL_LOG_INFO(log, "IDPP path generation complete.");
164 return path;
165}
166
167std::vector<Matter> idppCollectivePath(const Matter &initImg,
168 const Matter &finalImg, size_t nimgs,
169 const Parameters &params, bool use_zbl) {
170 auto log = eonc::log::get();
171 QUILL_LOG_INFO(log, "Generating initial path using Collective IDPP-NEB...");
172
173 std::vector<Matter> path = linearPath(initImg, finalImg, nimgs);
174
175 // 1. Base Objective
176 std::shared_ptr<ObjectiveFunction> idpp_objf =
177 std::make_shared<CollectiveIDPPObjectiveFunction>(path, params);
178
179 // 2. Optional ZBL Wrapper
180 if (use_zbl) {
181 QUILL_LOG_INFO(log, "Enabling ZBL repulsive penalty for IDPP...");
182 auto zbl_pot = createZBLPotential();
183 // Wrap the IDPP objective with ZBL repulsion (weight = 1.0)
184 idpp_objf = std::make_shared<ZBLRepulsiveIDPPObjective>(idpp_objf, zbl_pot,
185 path, params, 1.0);
186 }
187
189 idpp_objf, params.neb_options().initialization.opt_method, params);
190
191 int maxSteps = params.neb_options().initialization.max_iterations;
192 int currentStep = 0;
193 int checkInterval = 40;
194
195 while (currentStep < maxSteps) {
196 optim->run(checkInterval, params.optimizer_options().max_move);
197 currentStep += checkInterval;
198
199 if (idpp_objf->isConverged()) {
200 QUILL_LOG_INFO(log,
201 "IDPP-NEB converged after {} steps. Max Residual: {:.4f}",
202 currentStep, idpp_objf->getConvergence());
203 return path;
204 }
205 }
206
207 QUILL_LOG_WARNING(log,
208 "IDPP-NEB reached max_iterations ({}) without full "
209 "convergence. Residual: {:.4f}",
210 maxSteps, idpp_objf->getConvergence());
211 return path;
212}
213
214// Helper to insert an image linearly between two others
215Matter interpolateImage(const Matter &A, const Matter &B, double fraction) {
216 requireSameAtomCount(A, B, "path images");
217 Matter newImg(A);
218 AtomMatrix posA = A.getPositions();
219 AtomMatrix posB = B.getPositions();
220 AtomMatrix diff = A.pbc(posB - posA);
221 diff = diff.array() * A.getFree().array();
222 newImg.setPositions(posA + fraction * diff);
223 return newImg;
224}
225
226std::vector<Matter> sidppPath(const Matter &initImg, const Matter &finalImg,
227 size_t target_nimgs, const Parameters &params,
228 bool use_zbl) {
229
230 auto log = eonc::log::get();
231 const auto &init = params.neb_options().initialization;
232 QUILL_LOG_INFO(log,
233 "Generating initial path using S-IDPP{} ({} images, "
234 "alpha={:.2f}, frontier_tol={:.4f})...",
235 use_zbl ? "-ZBL" : "", target_nimgs, init.sidpp_alpha,
236 init.sidpp_frontier_tol);
237
238 // 1. Start with endpoints [Reactant, Product]
239 std::vector<Matter> path;
240 path.push_back(initImg);
241 path.push_back(finalImg);
242
243 std::shared_ptr<Potential> zbl_pot = nullptr;
244 if (use_zbl) {
245 zbl_pot = createZBLPotential();
246 }
247
248 // Track frontier counts: nLeft images from reactant, nRight from product
249 int nLeft = 0;
250 int nRight = 0;
251 int nIntermediate = 0;
252 bool addToLeft = true; // Alternate L/R, starting with left
253
254 // Helper: create IDPP objective with optional ZBL wrapping
255 auto makeIDPP = [&]() -> std::shared_ptr<ObjectiveFunction> {
256 auto objf = std::make_shared<CollectiveIDPPObjectiveFunction>(path, params);
257 if (use_zbl && zbl_pot) {
258 return std::make_shared<ZBLRepulsiveIDPPObjective>(objf, zbl_pot, path,
259 params, 1.0);
260 }
261 return objf;
262 };
263
264 // Helper: relax current path on IDPP surface
265 auto relaxPath = [&](int maxSteps) -> double {
266 auto objf = makeIDPP();
267 auto optim = eonc::helpers::create::mkOptim(objf, init.opt_method, params);
268 int step = 0;
269 while (step < maxSteps) {
270 optim->run(5, init.max_move);
271 step += 5;
272 if (objf->isConverged())
273 break;
274 }
275 return objf->getConvergence();
276 };
277
278 // 2. Sequential growth loop: alternate adding images from L and R
279 while (nIntermediate < static_cast<int>(target_nimgs)) {
280
281 if (addToLeft && nIntermediate < static_cast<int>(target_nimgs)) {
282 // Add to left (reactant) frontier
283 Matter &frontier = path[nLeft];
284 Matter &next = path[nLeft + 1];
285 Matter newImg = interpolateImage(frontier, next, init.sidpp_alpha);
286 path.insert(path.begin() + nLeft + 1, newImg);
287 nLeft++;
288 nIntermediate++;
289 QUILL_LOG_DEBUG(log, "S-IDPP: +L frontier (nL={}, nR={}, total={})",
290 nLeft, nRight, nIntermediate);
291 } else if (nIntermediate < static_cast<int>(target_nimgs)) {
292 // Add to right (product) frontier
293 int rightIdx = static_cast<int>(path.size()) - 1 - nRight;
294 Matter &frontier = path[rightIdx];
295 Matter &prev = path[rightIdx - 1];
296 Matter newImg = interpolateImage(frontier, prev, init.sidpp_alpha);
297 path.insert(path.begin() + rightIdx, newImg);
298 nRight++;
299 nIntermediate++;
300 QUILL_LOG_DEBUG(log, "S-IDPP: +R frontier (nL={}, nR={}, total={})",
301 nLeft, nRight, nIntermediate);
302 }
303 addToLeft = !addToLeft; // Alternate sides
304
305 // Optimize current path on IDPP surface
306 double residual = relaxPath(init.nsteps);
307
308 // Frontier convergence gating: if not converged, keep relaxing
309 // before adding more images (up to max_iterations total)
310 if (residual > init.sidpp_frontier_tol) {
311 double residual2 = relaxPath(init.max_iterations - init.nsteps);
312 QUILL_LOG_DEBUG(log, "S-IDPP: Extended relaxation {:.4f} -> {:.4f}",
313 residual, residual2);
314 residual = residual2;
315 }
316
317 QUILL_LOG_DEBUG(log, "S-IDPP: {} images | Residual: {:.4f}", nIntermediate,
318 residual);
319 }
320
321 // 3. Reparameterize: redistribute images evenly along arc length
322 if (init.sidpp_reparam && path.size() > 3) {
323 QUILL_LOG_INFO(log, "S-IDPP: Reparameterizing {} images along arc length",
324 path.size() - 2);
325 path = resamplePath(path, target_nimgs);
326 }
327
328 // 4. Final relaxation of the complete path
329 QUILL_LOG_INFO(log, "S-IDPP: Final relaxation of full path...");
330 double finalResidual = relaxPath(init.max_iterations);
331 QUILL_LOG_INFO(log, "S-IDPP: Final residual: {:.4f}", finalResidual);
332
333 ensureDistinctAdjacentImages(path, 1.0e-6);
334 return path;
335}
336
337void ensureDistinctAdjacentImages(const std::vector<Matter> &path,
338 double min_sep) {
339 if (path.size() < 2) {
340 return;
341 }
342 if (!(min_sep > 0.0)) {
343 throw std::invalid_argument(
344 "NEB path: min adjacent image separation must be positive");
345 }
346 for (size_t i = 1; i < path.size(); ++i) {
347 const AtomMatrix diff =
348 path[i].pbc(path[i].getPositions() - path[i - 1].getPositions());
349 const double d = diff.norm();
350 if (!(d > min_sep) || !std::isfinite(d)) {
351 throw std::runtime_error(
352 "NEB path: adjacent images are degenerate (SIDPP collapse)");
353 }
354 }
355}
356
358 const AtomMatrix &P1, const AtomMatrix &T1,
359 double f) {
360 double f2 = f * f;
361 double f3 = f2 * f;
362
363 // Hermite basis functions
364 double h00 = 2 * f3 - 3 * f2 + 1;
365 double h10 = f3 - 2 * f2 + f;
366 double h01 = -2 * f3 + 3 * f2;
367 double h11 = f3 - f2;
368
369 return h00 * P0 + h10 * T0 + h01 * P1 + h11 * T1;
370}
371
372std::vector<Matter> resamplePath(const std::vector<Matter> &densePath,
373 size_t targetCount) {
374 if (densePath.size() == targetCount + 2)
375 return densePath;
376
377 size_t n = densePath.size();
378
379 // Calculate cumulative arc length along the path
380 std::vector<double> arcLength(n, 0.0);
381 for (size_t i = 1; i < n; ++i) {
382 AtomMatrix diff = densePath[i].pbc(densePath[i].getPositions() -
383 densePath[i - 1].getPositions());
384 arcLength[i] = arcLength[i - 1] + diff.norm();
385 }
386 double totalLength = arcLength.back();
387
388 // Calculate tangents for cubic interpolation
389 std::vector<AtomMatrix> tangents(n);
390 for (size_t i = 0; i < n; ++i) {
391 AtomMatrix T;
392 if (i == 0) {
393 T = densePath[i].pbc(densePath[i + 1].getPositions() -
394 densePath[i].getPositions());
395 } else if (i == n - 1) {
396 T = densePath[i].pbc(densePath[i].getPositions() -
397 densePath[i - 1].getPositions());
398 } else {
399 AtomMatrix dNext = densePath[i].pbc(densePath[i + 1].getPositions() -
400 densePath[i].getPositions());
401 AtomMatrix dPrev = densePath[i].pbc(densePath[i].getPositions() -
402 densePath[i - 1].getPositions());
403 T = 0.5 * (dNext + dPrev);
404 }
405 // Scale tangent by local segment length for proper spline parameterization
406 if (i > 0 && i < n - 1) {
407 double localScale = (arcLength[i + 1] - arcLength[i - 1]) / 2.0;
408 T = T.normalized() * localScale;
409 }
410 tangents[i] = T;
411 }
412
413 std::vector<Matter> resampled;
414 resampled.reserve(targetCount + 2);
415 resampled.push_back(densePath.front());
416
417 // Place new images at equal arc-length intervals
418 double segmentLength = totalLength / (targetCount + 1);
419
420 for (size_t i = 1; i <= targetCount; ++i) {
421 double targetArc = i * segmentLength;
422
423 // Find the segment containing this arc length
424 size_t lowIdx = 0;
425 for (size_t j = 1; j < n; ++j) {
426 if (arcLength[j] >= targetArc) {
427 lowIdx = j - 1;
428 break;
429 }
430 }
431 size_t highIdx = lowIdx + 1;
432
433 // Interpolation parameter within this segment
434 double segmentArc = arcLength[highIdx] - arcLength[lowIdx];
435 double f = (segmentArc > 1e-10)
436 ? (targetArc - arcLength[lowIdx]) / segmentArc
437 : 0.0;
438
439 Matter newImg(densePath[0]);
441 densePath[lowIdx].getPositions(), tangents[lowIdx],
442 densePath[highIdx].getPositions(), tangents[highIdx], f));
443 resampled.push_back(newImg);
444 }
445
446 resampled.push_back(densePath.back());
447 return resampled;
448}
449
450void resamplePathInPlace(std::span<std::shared_ptr<Matter>> path) {
451 size_t n = path.size();
452 if (n < 3)
453 return;
454
455 // Calculate cumulative arc length
456 std::vector<double> arcLength(n, 0.0);
457 for (size_t i = 1; i < n; ++i) {
458 AtomMatrix diff =
459 path[i]->pbc(path[i]->getPositions() - path[i - 1]->getPositions());
460 arcLength[i] = arcLength[i - 1] + diff.norm();
461 }
462 double totalLength = arcLength.back();
463 if (totalLength < 1e-12)
464 return;
465
466 // Tangents for cubic interpolation
467 std::vector<AtomMatrix> tangents(n);
468 for (size_t i = 0; i < n; ++i) {
469 AtomMatrix T;
470 if (i == 0) {
471 T = path[i]->pbc(path[i + 1]->getPositions() - path[i]->getPositions());
472 } else if (i == n - 1) {
473 T = path[i]->pbc(path[i]->getPositions() - path[i - 1]->getPositions());
474 } else {
475 AtomMatrix dN =
476 path[i]->pbc(path[i + 1]->getPositions() - path[i]->getPositions());
477 AtomMatrix dP =
478 path[i]->pbc(path[i]->getPositions() - path[i - 1]->getPositions());
479 T = 0.5 * (dN + dP);
480 }
481 if (i > 0 && i < n - 1) {
482 double localScale = (arcLength[i + 1] - arcLength[i - 1]) / 2.0;
483 T = T.normalized() * localScale;
484 }
485 tangents[i] = T;
486 }
487
488 // Store original positions for interpolation source
489 std::vector<AtomMatrix> origPos(n);
490 for (size_t i = 0; i < n; ++i)
491 origPos[i] = path[i]->getPositions();
492
493 // Redistribute interior images at equal arc-length intervals (in-place)
494 size_t nInterior = n - 2;
495 double segLen = totalLength / (nInterior + 1);
496
497 for (size_t i = 1; i <= nInterior; ++i) {
498 double targetArc = i * segLen;
499 size_t lo = 0;
500 for (size_t j = 1; j < n; ++j) {
501 if (arcLength[j] >= targetArc) {
502 lo = j - 1;
503 break;
504 }
505 }
506 size_t hi = lo + 1;
507 double sArc = arcLength[hi] - arcLength[lo];
508 double f = (sArc > 1e-10) ? (targetArc - arcLength[lo]) / sArc : 0.0;
509
510 path[i]->setPositions(cubicInterpolate(origPos[lo], tangents[lo],
511 origPos[hi], tangents[hi], f));
512 }
513}
514
515} // namespace eonc::helpers::neb_paths
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
The optimizer class is used to serve as an abstract class for all optimizers, as well as to call an o...
const AtomMatrix & getPositions() const
Definition Matter.cpp:308
void setPositions(const AtomMatrix &pos)
Definition Matter.cpp:350
AtomMatrix getFree() const
Definition Matter.cpp:704
long int numberOfAtoms() const
Definition Matter.cpp:273
AtomMatrix pbc(const AtomMatrix &diff) const
Definition Matter.cpp:810
io::IoStatus con2matter(std::string filename)
Definition Matter.h:256
const neb_options_t & neb_options() const
const optimizer_options_t & optimizer_options() const
std::unique_ptr< Optimizer > mkOptim(std::shared_ptr< ObjectiveFunction > a_objf, OptType a_otype, const Parameters &a_params)
Definition Optimizer.cpp:24
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< 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)
void resamplePathInPlace(std::span< std::shared_ptr< Matter > > path)
In-place path reparameterization for NEB shared_ptr paths.
Matter interpolateImage(const Matter &A, const Matter &B, double fraction)
std::shared_ptr< Potential > createZBLPotential()
std::vector< Matter > linearPath(const Matter &initImg, const Matter &finalImg, const size_t nimgs)
MatrixXd getDistanceMatrix(const Matter &m)
void ensureDistinctAdjacentImages(const std::vector< Matter > &path, double min_sep)
Adjacent images closer than min_sep (RMSD, PBC) are a collapsed path.
void requireSameAtomCount(const Matter &a, const Matter &b, std::string_view what)
Abort before Eigen subtracts two position matrices of different size.
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.
std::shared_ptr< Potential > makePotential(const Parameters &params)
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
quill::Logger * get() noexcept
Get or create the default "combi" logger.
Definition EonLogger.h:44
static potential_options_t & potential_options(Parameters &p)
static zbl_options_t & zbl_options(Parameters &p)
struct eonc::neb_options_t::path_initialization_t initialization