15namespace fs = std::filesystem;
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) {
43 (*it).setPositions(posInitial +
45 int(std::distance(all_images_on_path.begin(), it)));
47 return all_images_on_path;
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()) +
".");
58 all_images_on_path.reserve(nimgs + 2);
60 for (
const auto &filePath : fsrcs) {
63 throw std::runtime_error(
"failed to load NEB path frame: " +
66 if (!all_images_on_path.empty()) {
69 all_images_on_path.push_back(img);
71 return all_images_on_path;
75 std::vector<fs::path> paths;
76 std::ifstream inputFile(listFilePath);
78 if (!inputFile.is_open()) {
79 throw std::runtime_error(
"Error: Could not open path list file: " +
84 while (std::getline(inputFile, line)) {
87 paths.emplace_back(line);
98 for (
int i = 0; i < natoms; ++i) {
99 for (
int j = 0; j < natoms; ++j) {
103 d(i, j) = m.
pbc(pos.row(i) - pos.row(j)).norm();
115 QUILL_LOG_INFO(
log,
"Generating initial path using IDPP...");
118 log,
"ZBL Repulsion not implemented for iterative IDPP (idppPath). "
119 "Using standard IDPP.");
123 std::vector<Matter> path =
linearPath(initImg, finalImg, nimgs);
131 for (
size_t i = 1; i <= nimgs; ++i) {
134 double xi =
static_cast<double>(i) / (nimgs + 1);
137 MatrixXd dTarget = (1.0 - xi) * dInit + xi * dFinal;
140 auto idpp_objf = std::make_shared<IDPPObjectiveFunction>(
141 std::make_shared<Matter>(path[i]), params, dTarget);
154 double residual = idpp_objf->getConvergence();
156 "IDPP Image {:2d}/{:2d} | xi: {:.2f} | Residual: {:.4e}", i,
157 nimgs, xi, residual);
160 path[i].setPositionsFreeV(idpp_objf->getPositions());
163 QUILL_LOG_INFO(
log,
"IDPP path generation complete.");
168 const Matter &finalImg,
size_t nimgs,
171 QUILL_LOG_INFO(
log,
"Generating initial path using Collective IDPP-NEB...");
173 std::vector<Matter> path =
linearPath(initImg, finalImg, nimgs);
176 std::shared_ptr<ObjectiveFunction> idpp_objf =
177 std::make_shared<CollectiveIDPPObjectiveFunction>(path, params);
181 QUILL_LOG_INFO(
log,
"Enabling ZBL repulsive penalty for IDPP...");
184 idpp_objf = std::make_shared<ZBLRepulsiveIDPPObjective>(idpp_objf, zbl_pot,
193 int checkInterval = 40;
195 while (currentStep < maxSteps) {
197 currentStep += checkInterval;
199 if (idpp_objf->isConverged()) {
201 "IDPP-NEB converged after {} steps. Max Residual: {:.4f}",
202 currentStep, idpp_objf->getConvergence());
207 QUILL_LOG_WARNING(
log,
208 "IDPP-NEB reached max_iterations ({}) without full "
209 "convergence. Residual: {:.4f}",
210 maxSteps, idpp_objf->getConvergence());
221 diff = diff.array() * A.
getFree().array();
227 size_t target_nimgs,
const Parameters ¶ms,
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);
239 std::vector<Matter> path;
240 path.push_back(initImg);
241 path.push_back(finalImg);
243 std::shared_ptr<Potential> zbl_pot =
nullptr;
251 int nIntermediate = 0;
252 bool addToLeft =
true;
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,
265 auto relaxPath = [&](
int maxSteps) ->
double {
266 auto objf = makeIDPP();
269 while (step < maxSteps) {
270 optim->run(5, init.max_move);
272 if (objf->isConverged())
275 return objf->getConvergence();
279 while (nIntermediate <
static_cast<int>(target_nimgs)) {
281 if (addToLeft && nIntermediate <
static_cast<int>(target_nimgs)) {
283 Matter &frontier = path[nLeft];
284 Matter &next = path[nLeft + 1];
286 path.insert(path.begin() + nLeft + 1, newImg);
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)) {
293 int rightIdx =
static_cast<int>(path.size()) - 1 - nRight;
294 Matter &frontier = path[rightIdx];
295 Matter &prev = path[rightIdx - 1];
297 path.insert(path.begin() + rightIdx, newImg);
300 QUILL_LOG_DEBUG(
log,
"S-IDPP: +R frontier (nL={}, nR={}, total={})",
301 nLeft, nRight, nIntermediate);
303 addToLeft = !addToLeft;
306 double residual = relaxPath(init.nsteps);
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;
317 QUILL_LOG_DEBUG(
log,
"S-IDPP: {} images | Residual: {:.4f}", nIntermediate,
322 if (init.sidpp_reparam && path.size() > 3) {
323 QUILL_LOG_INFO(
log,
"S-IDPP: Reparameterizing {} images along arc length",
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);
339 if (path.size() < 2) {
342 if (!(min_sep > 0.0)) {
343 throw std::invalid_argument(
344 "NEB path: min adjacent image separation must be positive");
346 for (
size_t i = 1; i < path.size(); ++i) {
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)");
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;
369 return h00 * P0 + h10 * T0 + h01 * P1 + h11 * T1;
373 size_t targetCount) {
374 if (densePath.size() == targetCount + 2)
377 size_t n = densePath.size();
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();
386 double totalLength = arcLength.back();
389 std::vector<AtomMatrix> tangents(n);
390 for (
size_t i = 0; i < n; ++i) {
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());
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);
406 if (i > 0 && i < n - 1) {
407 double localScale = (arcLength[i + 1] - arcLength[i - 1]) / 2.0;
408 T = T.normalized() * localScale;
413 std::vector<Matter> resampled;
414 resampled.reserve(targetCount + 2);
415 resampled.push_back(densePath.front());
418 double segmentLength = totalLength / (targetCount + 1);
420 for (
size_t i = 1; i <= targetCount; ++i) {
421 double targetArc = i * segmentLength;
425 for (
size_t j = 1; j < n; ++j) {
426 if (arcLength[j] >= targetArc) {
431 size_t highIdx = lowIdx + 1;
434 double segmentArc = arcLength[highIdx] - arcLength[lowIdx];
435 double f = (segmentArc > 1e-10)
436 ? (targetArc - arcLength[lowIdx]) / segmentArc
439 Matter newImg(densePath[0]);
441 densePath[lowIdx].getPositions(), tangents[lowIdx],
442 densePath[highIdx].getPositions(), tangents[highIdx], f));
443 resampled.push_back(newImg);
446 resampled.push_back(densePath.back());
451 size_t n = path.size();
456 std::vector<double> arcLength(n, 0.0);
457 for (
size_t i = 1; i < n; ++i) {
459 path[i]->pbc(path[i]->getPositions() - path[i - 1]->getPositions());
460 arcLength[i] = arcLength[i - 1] + diff.norm();
462 double totalLength = arcLength.back();
463 if (totalLength < 1e-12)
467 std::vector<AtomMatrix> tangents(n);
468 for (
size_t i = 0; i < n; ++i) {
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());
476 path[i]->pbc(path[i + 1]->getPositions() - path[i]->getPositions());
478 path[i]->pbc(path[i]->getPositions() - path[i - 1]->getPositions());
481 if (i > 0 && i < n - 1) {
482 double localScale = (arcLength[i + 1] - arcLength[i - 1]) / 2.0;
483 T = T.normalized() * localScale;
489 std::vector<AtomMatrix> origPos(n);
490 for (
size_t i = 0; i < n; ++i)
491 origPos[i] = path[i]->getPositions();
494 size_t nInterior = n - 2;
495 double segLen = totalLength / (nInterior + 1);
497 for (
size_t i = 1; i <= nInterior; ++i) {
498 double targetArc = i * segLen;
500 for (
size_t j = 1; j < n; ++j) {
501 if (arcLength[j] >= targetArc) {
507 double sArc = arcLength[hi] - arcLength[lo];
508 double f = (sArc > 1e-10) ? (targetArc - arcLength[lo]) / sArc : 0.0;
511 origPos[hi], tangents[hi], f));
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
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
void setPositions(const AtomMatrix &pos)
AtomMatrix getFree() const
long int numberOfAtoms() const
AtomMatrix pbc(const AtomMatrix &diff) const
io::IoStatus con2matter(std::string filename)
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)
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 ¶ms, 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 ¶ms, bool use_zbl)
std::vector< Matter > idppCollectivePath(const Matter &initImg, const Matter &finalImg, size_t nimgs, const Parameters ¶ms, 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 ¶ms)
constexpr bool io_ok(IoStatus s) noexcept
quill::Logger * get() noexcept
Get or create the default "combi" logger.
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