Loading...
Searching...
No Matches
eonc::NudgedElasticBandJob Class Reference

#include <NudgedElasticBandJob.h>

Inheritance diagram for eonc::NudgedElasticBandJob:

Public Member Functions

 NudgedElasticBandJob (std::unique_ptr< Parameters > parameters, Runtime &rt)
 ~NudgedElasticBandJob (void)=default
std::vector< std::string > run (void)
 Virtual run; used solely for dynamic dispatch.
Public Member Functions inherited from eonc::Job
void adoptRuntime (std::unique_ptr< Runtime > rt)
 Take ownership of a Runtime previously passed as Runtime&.
 Job (std::unique_ptr< Parameters > parameters, Runtime &rt)
 Borrow: caller keeps Runtime alive (CLI stack / Python Session).
 Job (std::unique_ptr< Parameters > parameters, std::unique_ptr< Runtime > rt)
 Own a Runtime (one-shot makeJob / rvalue).
 Job (std::unique_ptr< Parameters > parameters)
 Own a default-constructed Runtime.
 Job (std::shared_ptr< Potential > potPassed, const Parameters &parameters)
virtual ~Job ()=default
JobType getType ()
PotRegistry & pots () noexcept
void releasePotential ()
 Drop the Potential so on_destroyed is recorded before Runtime dies.

Private Member Functions

void printEndState (NudgedElasticBand::NEBStatus status)
void saveData (NudgedElasticBand::NEBStatus status, NudgedElasticBand *neb)

Private Attributes

std::vector< std::string > returnFiles
size_t fCallsNEB
eonc::log::Scoped m_log

Additional Inherited Members

Protected Attributes inherited from eonc::Job
JobType jtype
Parameters params
std::unique_ptr< Runtime > owned_runtime_
 Non-null when this Job owns the composition root (one-shot makeJob).
Runtime * runtime_
 Always valid: either owned_runtime_.get() or a caller-owned Runtime.
std::shared_ptr< Potential > pot

Detailed Description

Definition at line 22 of file NudgedElasticBandJob.h.

Constructor & Destructor Documentation

◆ NudgedElasticBandJob()

eonc::NudgedElasticBandJob::NudgedElasticBandJob ( std::unique_ptr< Parameters > parameters,
Runtime & rt )
inline

Definition at line 25 of file NudgedElasticBandJob.h.

26 : Job(std::move(parameters), rt),
27 fCallsNEB{0} {}
Job(std::unique_ptr< Parameters > parameters, Runtime &rt)
Borrow: caller keeps Runtime alive (CLI stack / Python Session).
Definition Job.h:76

◆ ~NudgedElasticBandJob()

eonc::NudgedElasticBandJob::~NudgedElasticBandJob ( void )
default

Member Function Documentation

◆ printEndState()

void eonc::NudgedElasticBandJob::printEndState ( NudgedElasticBand::NEBStatus status)
private

Definition at line 314 of file NudgedElasticBandJob.cpp.

314 {
315 QUILL_LOG_DEBUG(m_log, "Final state: ");
317 QUILL_LOG_DEBUG(m_log, "Nudged elastic band, successful.");
319 QUILL_LOG_DEBUG(m_log, "Nudged elastic band, too many iterations.");
320 else
321 QUILL_LOG_WARNING(m_log, "Unknown status: {}!", static_cast<int>(status));
322 return;
323}

◆ run()

std::vector< std::string > eonc::NudgedElasticBandJob::run ( void )
virtual

Virtual run; used solely for dynamic dispatch.

Implements eonc::Job.

Definition at line 29 of file NudgedElasticBandJob.cpp.

29 {
31 size_t f1;
32
33 std::string reactantFilename = eonc::helpers::getRelevantFile("reactant.con");
34 std::string productFilename = eonc::helpers::getRelevantFile("product.con");
35
36 // TS interpolation: only when initializer is FILE (explicit path loading).
37 // Previously, any ts.con in CWD would silently override SIDPP/IDPP paths.
38 bool tsInterpolate = false;
39 std::shared_ptr<Matter> transitionState = nullptr;
40 if (params.neb_options().initialization.method == NEBInit::FILE &&
41 std::filesystem::exists("ts.con")) {
42 std::string transitionStateFilename =
44 transitionState = std::make_shared<Matter>(pot, params);
45 tsInterpolate = true;
46 if (!eonc::io::io_ok(
47 transitionState->con2matter(transitionStateFilename))) {
48 EONC_LOG_CRITICAL("Failed to load {}", transitionStateFilename);
49 throw std::runtime_error("failed to load " + transitionStateFilename);
50 }
51 }
52
53 auto initial = std::make_shared<Matter>(pot, params);
54 auto final_state = std::make_shared<Matter>(pot, params);
55
56 // When initializer=file and initial_path_in is set, endpoints are the first
57 // and last frames of that list — do not require reactant.con / product.con
58 // (issue #278; buggy endpoint files were still loaded and could crash).
59 const bool usePathEndpoints =
60 params.neb_options().initialization.method == NEBInit::FILE &&
61 !params.neb_options().initialization.input_path.empty();
62
63 if (usePathEndpoints) {
64 const auto file_paths = eonc::helpers::neb_paths::readFilePaths(
65 params.neb_options().initialization.input_path);
66 if (file_paths.size() < 2) {
67 throw std::runtime_error(
68 "NEB initial_path_in must list at least two frames "
69 "(reactant and product endpoints)");
70 }
71 if (!eonc::io::io_ok(initial->con2matter(file_paths.front().string()))) {
72 EONC_LOG_CRITICAL("Failed to load NEB path reactant frame {}",
73 file_paths.front().string());
74 throw std::runtime_error("failed to load NEB path reactant frame");
75 }
76 if (!eonc::io::io_ok(final_state->con2matter(file_paths.back().string()))) {
77 EONC_LOG_CRITICAL("Failed to load NEB path product frame {}",
78 file_paths.back().string());
79 throw std::runtime_error("failed to load NEB path product frame");
80 }
81 QUILL_LOG_INFO(m_log,
82 "NEB endpoints from initial path list (skipped {} / {})",
83 reactantFilename, productFilename);
84 } else {
85 if (!eonc::io::io_ok(initial->con2matter(reactantFilename))) {
86 EONC_LOG_CRITICAL("Failed to load {}", reactantFilename);
87 throw std::runtime_error("failed to load reactant.con");
88 }
89 if (!eonc::io::io_ok(final_state->con2matter(productFilename))) {
90 EONC_LOG_CRITICAL("Failed to load {}", productFilename);
91 throw std::runtime_error("failed to load product.con");
92 }
93 }
94
96 "reactant and product");
97 if (tsInterpolate) {
98 eonc::helpers::neb_paths::requireSameAtomCount(*initial, *transitionState,
99 "reactant and ts.con");
100 }
101
102 // Endpoint minimization logic:
103 // - If params.neb_options().endpoints.minimize is false: never minimize
104 // endpoints.
105 // - If params.neb_options().endpoints.minimize is true and
106 // params.neb_options().initialization.input_path is empty: minimize
107 // endpoints.
108 // - If params.nebMinimEP is true and params.nebIpath is NOT empty:
109 // -> minimize endpoints only if params.nebMinimEPIpath is true.
110 // Log what decision was made so users can see behavior.
111 bool shouldMinimizeEndpoints = false;
112 if (!params.neb_options().endpoints.minimize) {
113 QUILL_LOG_DEBUG(m_log,
114 "minimize_endpoints == false: not minimizing endpoints.");
115 shouldMinimizeEndpoints = false;
116 } else {
117 if (params.neb_options().initialization.input_path.empty()) {
118 QUILL_LOG_DEBUG(m_log, "minimize_endpoints == true and nebIpath "
119 "empty: minimizing endpoints.");
120 shouldMinimizeEndpoints = true;
121 } else {
122 // nebIpath provided: only minimize if neb_options.endpoints.use_path_file
123 // explicitly allowed.
124 if (params.neb_options().endpoints.use_path_file) {
125 QUILL_LOG_DEBUG(
126 m_log,
127 "minimize_endpoints == true and nebIpath provided, but "
128 "minimize_endpoints_for_ipath == true: minimizing endpoints.");
129 shouldMinimizeEndpoints = true;
130 } else {
131 QUILL_LOG_DEBUG(
132 m_log,
133 "minimize_endpoints == true but nebIpath provided and "
134 "minimize_endpoints_for_ipath == false: not minimizing endpoints.");
135 shouldMinimizeEndpoints = false;
136 }
137 }
138 }
139
140 if (shouldMinimizeEndpoints) {
141 QUILL_LOG_DEBUG(m_log, "Minimizing reactant");
142 initial->relax(false, params.debug_options().write_movies,
143 params.main_options().checkpoint, "react_neb", "react_neb");
144 QUILL_LOG_DEBUG(m_log, "Minimized reactant");
145 QUILL_LOG_DEBUG(m_log, "Minimizing product");
146 final_state->relax(false, params.debug_options().write_movies,
147 params.main_options().checkpoint, "prod_neb",
148 "prod_neb");
149 }
150
151 auto neb =
152 std::make_unique<NudgedElasticBand>(initial, final_state, params, pot);
153
154 if (tsInterpolate) {
155 AtomMatrix reactantToTS = transitionState->pbc(
156 transitionState->getPositions() - initial->getPositions());
157 AtomMatrix TSToProduct = transitionState->pbc(
158 final_state->getPositions() - transitionState->getPositions());
159 for (int image = 1; image <= neb->numImages; image++) {
160 int mid = neb->numImages / 2 + 1;
161 if (image < mid) {
162 double frac = static_cast<double>(image) / static_cast<double>(mid);
163 neb->path[image]->setPositions(initial->getPositions() +
164 frac * reactantToTS);
165 } else if (image > mid) {
166 double frac = static_cast<double>(image - mid) /
167 static_cast<double>(neb->numImages - mid + 1);
168 neb->path[image]->setPositions(transitionState->getPositions() +
169 frac * TSToProduct);
170 } else if (image == mid) {
171 neb->path[image]->setPositions(transitionState->getPositions());
172 }
173 }
174 }
175
177 status = neb->compute();
179
181 neb->printImageData();
182 neb->findExtrema();
183 }
184
185 printEndState(status);
186 saveData(status, neb.get());
187
188 return returnFiles;
189}
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
#define EONC_LOG_CRITICAL(...)
Definition EonLogger.h:267
std::shared_ptr< Potential > pot
Definition Job.h:63
Parameters params
Definition Job.h:58
void printEndState(NudgedElasticBand::NEBStatus status)
std::vector< std::string > returnFiles
void saveData(NudgedElasticBand::NEBStatus status, NudgedElasticBand *neb)
static PotRegistry & get() noexcept
Process-lifetime singleton.
size_t total_force_calls() const noexcept
void requireSameAtomCount(const Matter &a, const Matter &b, std::string_view what)
Abort before Eigen subtracts two position matrices of different size.
std::vector< fs::path > readFilePaths(const std::string &listFilePath)
Reads a file where each line contains a path to another file.
std::string getRelevantFile(std::string filename)
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38

◆ saveData()

void eonc::NudgedElasticBandJob::saveData ( NudgedElasticBand::NEBStatus status,
NudgedElasticBand * neb )
private

Definition at line 191 of file NudgedElasticBandJob.cpp.

192 {
193 std::string resultsFilename("results.dat");
194 returnFiles.push_back(resultsFilename);
195
196 {
201 params.potential_options().potential,
202 PotRegistry::get().total_force_calls(), true, neb->reactantEnergy);
203 env.job_type = "neb";
204 env.extras.emplace_back("force_calls_neb", static_cast<double>(fCallsNEB));
205 env.extras.emplace_back("energy_reference", neb->reactantEnergy);
206 env.extras.emplace_back("number_of_images",
207 static_cast<double>(neb->numImages));
208 env.writeResultsDat(resultsFilename);
209 std::ofstream out(resultsFilename, std::ios::binary | std::ios::app);
210 if (!out) {
211 QUILL_LOG_ERROR(m_log, "Failed to reopen {} for image keys",
212 resultsFilename);
213 return;
214 }
215
216 for (long i = 0; i <= neb->numImages + 1; i++) {
217 out << std::format(
218 "{:f} image{}_energy\n",
219 neb->path[i]->getPotentialEnergy() - neb->reactantEnergy, i);
220 out << std::format("{:f} image{}_force\n",
221 neb->path[i]->getForces().norm(), i);
222 double proj_norm = (i >= 1 && i <= neb->numImages)
223 ? neb->projectedForce[i]->norm()
224 : 0.0;
225 out << std::format("{:f} image{}_projected_force\n", proj_norm, i);
226 }
227
228 long safeNumExtrema = std::min(
229 neb->numExtrema, static_cast<long>(neb->extremumPosition.size()));
230 out << std::format("{} number_of_extrema\n", safeNumExtrema);
231 for (long i = 0; i < safeNumExtrema; i++) {
232 out << std::format("{:f} extremum{}_position\n", neb->extremumPosition[i],
233 i);
234 out << std::format("{:f} extremum{}_energy\n", neb->extremumEnergy[i], i);
235 }
236 }
237
238 // Save the Full NEB Path
239 std::string nebFilename("neb.con");
240 returnFiles.push_back(nebFilename);
242 neb->path, neb->tangent, neb->eigenmode_solvers, neb->numImages,
243 params.debug_options().estimate_neb_eigenvalues, nebFilename,
244 std::nullopt, neb->reactantEnergy))) {
245 QUILL_LOG_ERROR(m_log, "Failed to write {}", nebFilename);
246 }
247
248 // Save Discrete Saddle Point (Highest Energy Image)
249 std::string spFilename("sp.con");
250 if (!eonc::io::io_ok(
251 neb->path[neb->maxEnergyImage]->matter2con(spFilename))) {
252 QUILL_LOG_ERROR(m_log, "Failed to write {}", spFilename);
253 }
254 returnFiles.push_back(spFilename);
255
256 // Setup Dimer Configurations for Each Spline Peak
257 if (params.neb_options().mmf_peaks.enabled && neb->numExtrema > 0) {
258 int peakCount = 0;
259 for (long i = 0; i < neb->numExtrema; i++) {
260 // Filter 1: Only look at maxima (negative curvature)
261 // Filter 2: Energy threshold (e.g., peak must be > 0.05 eV above
262 // reactant)
263 double relativeEnergy = neb->extremumEnergy[i] - neb->reactantEnergy;
264
265 if (neb->extremumCurvature[i] < 0 &&
266 relativeEnergy > params.neb_options().mmf_peaks.tolerance) {
267 double posFraction = neb->extremumPosition[i];
268 int leftIdx = static_cast<int>(std::floor(posFraction));
269 double f = posFraction - leftIdx;
270
271 if (leftIdx < 0 || leftIdx >= neb->numImages + 1)
272 continue;
273
274 // 1. Write Interpolated Position (.con)
276 *neb->path[leftIdx], *neb->path[leftIdx + 1], f);
277 std::string peakPosFile = std::format("peak{:02d}_pos.con", peakCount);
278 if (!eonc::io::io_ok(peakPos.matter2con(peakPosFile))) {
279 QUILL_LOG_ERROR(m_log, "Failed to write {}", peakPosFile);
280 }
281 returnFiles.push_back(peakPosFile);
282
283 // 2. Write Interpolated Tangent as standard mode.dat
285 neb->path, neb->tangent, neb->numImages, posFraction);
286
287 std::string peakModeFile =
288 std::format("peak{:02d}_mode.dat", peakCount);
289 {
290 std::ofstream modeOut(peakModeFile);
291 if (modeOut) {
292 for (long row = 0; row < peakMode.rows(); ++row) {
293 modeOut << std::format("{:.17g} {:.17g} {:.17g}\n",
294 peakMode(row, 0), peakMode(row, 1),
295 peakMode(row, 2));
296 }
297 returnFiles.push_back(peakModeFile);
298 }
299 }
300
301 QUILL_LOG_INFO(
302 m_log,
303 "Generated MMF peak {:02d} at position {:.3f} (Energy: {:.3f} eV)",
304 peakCount, posFraction, relativeEnergy);
305 peakCount++;
306 }
307 }
308 }
309
310 returnFiles.push_back("neb.dat");
311 neb->printImageData(true, std::numeric_limits<size_t>::max());
312}
Matter interpolateImage(const Matter &A, const Matter &B, double fraction)
AtomMatrix interpolatedPeakMode(const std::vector< std::shared_ptr< Matter > > &path, const std::vector< std::shared_ptr< AtomMatrix > > &tangent, long numImages, double posFraction)
Unit tangent at a fractional image index.
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)
Write a NEB band as a multi-frame .con via readcon ConFrameBuilder::clone().
static JobResultEnvelope fromMinimization(RunStatus status, PotType pot, std::uint64_t fcalls, bool hasE, double energy)
Definition JobResult.h:101

Member Data Documentation

◆ fCallsNEB

size_t eonc::NudgedElasticBandJob::fCallsNEB
private

Definition at line 38 of file NudgedElasticBandJob.h.

◆ m_log

eonc::log::Scoped eonc::NudgedElasticBandJob::m_log
private

Definition at line 39 of file NudgedElasticBandJob.h.

◆ returnFiles

std::vector<std::string> eonc::NudgedElasticBandJob::returnFiles
private

Definition at line 37 of file NudgedElasticBandJob.h.


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