Loading...
Searching...
No Matches
NEBSplineExtrema.cpp
Go to the documentation of this file.
1/*
2** This file is part of eOn.
3**
4** SPDX-License-Identifier: BSD-3-Clause
5**
6** Copyright (c) 2010--present, eOn Development Team
7** All rights reserved.
8**
9** Repo:
10** https://github.com/TheochemUI/eOn
11*/
13#include "eon/ConFileIO.h"
14#include "eon/Tunneling.h"
15#include <cmath>
16#include <format>
17#include <fstream>
18#include <limits>
19
20namespace fs = std::filesystem;
21
22namespace eonc::neb {
23
24namespace {
25
26void normalizeOrZero(AtomMatrix &v) {
27 const double n = v.norm();
28 if (n > 1e-10) {
29 v /= n;
30 }
31}
32
33// Stored endpoint tangents are left at zero. The band direction there is the
34// normalized minimum-image step to the neighboring image.
36storedOrEndpointTangent(const std::vector<std::shared_ptr<Matter>> &path,
37 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
38 long numImages, long imageIndex) {
39 AtomMatrix tang;
40 if (imageIndex == 0) {
41 tang = path[0]->pbc(path[1]->getPositions() - path[0]->getPositions());
42 normalizeOrZero(tang);
43 return tang;
44 }
45 if (imageIndex == numImages + 1) {
46 tang = path[numImages]->pbc(path[numImages + 1]->getPositions() -
47 path[numImages]->getPositions());
48 normalizeOrZero(tang);
49 return tang;
50 }
51 return *tangent[static_cast<size_t>(imageIndex)];
52}
53
54// NaN means path[0]; see pathToConFrames.
55double referenceOrFirst(const std::vector<std::shared_ptr<Matter>> &path,
56 double referenceEnergy) {
57 return std::isnan(referenceEnergy) ? path[0]->getPotentialEnergy()
58 : referenceEnergy;
59}
60
61eonc::io::ConFrameMetadata neb_frame_metadata(
62 const std::vector<std::shared_ptr<Matter>> &path,
63 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
64 const std::vector<std::shared_ptr<EigenmodeStrategy>> &eigenmode_solvers,
65 long numImages, bool estimateEigenvalues, long imageIndex,
66 double reactionCoordinate, std::optional<size_t> bandIndex,
67 double referenceEnergy) {
68 AtomMatrix tang;
69 if (imageIndex == 0) {
70 tang = path[0]->pbc(path[1]->getPositions() - path[0]->getPositions());
71 } else if (imageIndex == numImages + 1) {
72 tang = path[numImages]->pbc(path[numImages + 1]->getPositions() -
73 path[numImages]->getPositions());
74 } else {
75 tang = *tangent[imageIndex];
76 }
77 normalizeOrZero(tang);
78
79 const double reference_energy = referenceOrFirst(path, referenceEnergy);
80 const double absolute_energy = path[imageIndex]->getPotentialEnergy();
81 const double relative_energy = absolute_energy - reference_energy;
82 const double parallel_force = matDot(path[imageIndex]->getForces(), tang);
83
84 eonc::io::ConFrameMetadata metadata;
85 metadata.frame_index = static_cast<uint64_t>(imageIndex);
86 metadata.energy = absolute_energy;
87 metadata.neb_bead = static_cast<uint64_t>(imageIndex);
88 if (bandIndex) {
89 metadata.neb_band = static_cast<uint64_t>(*bandIndex);
90 }
91 metadata.scalars.push_back({"reaction_coordinate", reactionCoordinate});
92 metadata.scalars.push_back({"relative_energy", relative_energy});
93 metadata.scalars.push_back({"parallel_force", parallel_force});
94
95 if (estimateEigenvalues && imageIndex >= 0 &&
96 imageIndex < static_cast<long>(eigenmode_solvers.size()) &&
97 eigenmode_solvers[imageIndex]) {
98 eonc::eigenmodeCompute(*eigenmode_solvers[imageIndex], path[imageIndex],
99 tang);
100 metadata.scalars.push_back(
101 {"lowest_eigenvalue",
102 eonc::eigenmodeGetEigenvalue(*eigenmode_solvers[imageIndex])});
103 }
104
105 return metadata;
106}
107
108} // namespace
109
111findSplineExtrema(const std::vector<std::shared_ptr<Matter>> &path,
112 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
113 long numImages) {
114
115 auto *log = eonc::log::get();
116
117 // Calculate cubic parameters for each interval
118 AtomMatrix tangentEndpoint;
119 std::vector<double> a(numImages + 1), b(numImages + 1), c(numImages + 1),
120 d(numImages + 1);
121 double F1, F2, U1, U2, dist;
122
123 for (long i = 0; i <= numImages; i++) {
124 dist = path[i]->distanceTo(*path[i + 1]);
125 if (i == 0) {
126 tangentEndpoint =
127 path[i]->pbc(path[1]->getPositions() - path[0]->getPositions());
128 normalizeOrZero(tangentEndpoint);
129 F1 = matDot(path[i]->getForces(), tangentEndpoint) * dist;
130 } else {
131 F1 = matDot(path[i]->getForces(), *tangent[i]) * dist;
132 }
133 if (i == numImages) {
134 tangentEndpoint = path[i + 1]->pbc(path[numImages + 1]->getPositions() -
135 path[numImages]->getPositions());
136 normalizeOrZero(tangentEndpoint);
137 F2 = matDot(path[i + 1]->getForces(), tangentEndpoint) * dist;
138 } else {
139 F2 = matDot(path[i + 1]->getForces(), *tangent[i + 1]) * dist;
140 }
141 U1 = path[i]->getPotentialEnergy();
142 U2 = path[i + 1]->getPotentialEnergy();
143 a[i] = U1;
144 b[i] = -F1;
145 c[i] = 3. * (U2 - U1) + 2. * F1 + F2;
146 d[i] = -2. * (U2 - U1) - (F1 + F2);
147 }
148
149 ExtremaResult result;
150 result.positions.resize(2 * (numImages + 1));
151 result.energies.resize(2 * (numImages + 1));
152 result.curvatures.resize(2 * (numImages + 1));
153
154 double discriminant, f;
155
156 for (long i = 0; i <= numImages; i++) {
157 discriminant = c[i] * c[i] - 3.0 * b[i] * d[i];
158 if (discriminant >= 0) {
159 f = -1;
160
161 // Quadratic case
162 if ((d[i] == 0) && (c[i] != 0)) {
163 f = (-b[i] / (2. * c[i]));
164 }
165 // Cubic case 1
166 else if (d[i] != 0) {
167 f = -(c[i] + std::sqrt(discriminant)) / (3. * d[i]);
168 }
169 if ((f >= 0) && (f <= 1)) {
170 result.positions[result.numExtrema] = i + f;
171 result.energies[result.numExtrema] =
172 ((d[i] * f + c[i]) * f + b[i]) * f + a[i]; // Horner's method
173 result.curvatures[result.numExtrema] = 6.0 * d[i] * f + 2 * c[i];
174 result.numExtrema++;
175 }
176 // Cubic case 2. A zero cubic coefficient leaves f at the quadratic
177 // root, and a zero discriminant is that same cubic root. Storing
178 // either again doubles one stationary point.
179 if (d[i] != 0 && discriminant != 0.0) {
180 f = -(c[i] - std::sqrt(discriminant)) / (3. * d[i]);
181 } else {
182 f = -1;
183 }
184 if ((f >= 0) && (f <= 1)) {
185 result.positions[result.numExtrema] = i + f;
186 result.energies[result.numExtrema] =
187 ((d[i] * f + c[i]) * f + b[i]) * f + a[i]; // Horner's method
188 result.curvatures[result.numExtrema] = 6 * d[i] * f + 2 * c[i];
189 result.numExtrema++;
190 }
191 }
192 }
193
194 QUILL_LOG_DEBUG(log, "Found {} extrema", result.numExtrema);
195 QUILL_LOG_DEBUG(log, "Energy reference: {}", path[0]->getPotentialEnergy());
196 for (long i = 0; i < result.numExtrema; i++) {
197 QUILL_LOG_DEBUG(
198 log, "extrema #{} at image position {} with energy {} and curvature {}",
199 i + 1, result.positions[i],
200 result.energies[i] - path[0]->getPotentialEnergy(),
201 result.curvatures[i]);
202 }
203
204 return result;
205}
206
208interpolatedPeakMode(const std::vector<std::shared_ptr<Matter>> &path,
209 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
210 long numImages, double posFraction) {
211 const int nat = path.empty() ? 0 : path[0]->numberOfAtoms();
212 const auto leftIdx = static_cast<long>(std::floor(posFraction));
213 if (path.size() < 2 || leftIdx < 0 || leftIdx >= numImages + 1 ||
214 leftIdx + 1 >= static_cast<long>(path.size())) {
215 return AtomMatrix::Zero(nat, 3);
216 }
217 const double f = posFraction - static_cast<double>(leftIdx);
218 AtomMatrix mode =
219 (1.0 - f) * storedOrEndpointTangent(path, tangent, numImages, leftIdx) +
220 f * storedOrEndpointTangent(path, tangent, numImages, leftIdx + 1);
221 normalizeOrZero(mode);
222 return mode;
223}
224
226 const std::vector<std::shared_ptr<Matter>> &path,
227 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
228 const std::vector<std::shared_ptr<EigenmodeStrategy>> &eigenmode_solvers,
229 long numImages, bool estimateEigenvalues, bool writeToFile, size_t idx,
230 eonc::log::Scoped log, double referenceEnergy) {
231
232 double dist, distTotal = 0;
233 AtomMatrix tangentStart =
234 path[0]->pbc(path[1]->getPositions() - path[0]->getPositions());
235 AtomMatrix tangentEnd = path[numImages]->pbc(
236 path[numImages + 1]->getPositions() - path[numImages]->getPositions());
237 AtomMatrix tang;
238 std::string header;
239 if (estimateEigenvalues) {
240 header = std::format("{:>3s} {:>12s} {:>12s} {:>12s} {:>12s}", "img",
241 "rxn_coord", "energy", "f_para", "eigval");
242 } else {
243 header = std::format("{:>3s} {:>12s} {:>12s} {:>12s}", "img", "rxn_coord",
244 "energy", "f_para");
245 }
246
247 normalizeOrZero(tangentStart);
248 normalizeOrZero(tangentEnd);
249
250 std::ofstream fileLogger;
251 if (writeToFile) {
252 std::string neb_dat_fs;
253 if (idx == std::numeric_limits<size_t>::max()) {
254 neb_dat_fs = "neb.dat";
255 } else {
256 neb_dat_fs = std::format("neb_{:03}.dat", idx);
257 }
258 if (fs::exists(neb_dat_fs)) {
259 fs::remove(neb_dat_fs);
260 }
261 fileLogger.open(neb_dat_fs);
262 if (fileLogger.is_open()) {
263 fileLogger << header << "\n";
264 }
265 }
266 const double energy_reactant = referenceOrFirst(path, referenceEnergy);
267
268 for (long i = 0; i <= numImages + 1; i++) {
269 if (i == 0) {
270 tang = tangentStart;
271 } else if (i == numImages + 1) {
272 tang = tangentEnd;
273 } else {
274 tang = *tangent[i];
275 }
276
277 if (i > 0) {
278 dist = path[i]->distanceTo(*path[i - 1]);
279 distTotal += dist;
280 }
281
282 double relative_energy = path[i]->getPotentialEnergy() - energy_reactant;
283 double parallel_force = matDot(path[i]->getForces(), tang);
284
285 if (estimateEigenvalues) {
286 eonc::eigenmodeCompute(*eigenmode_solvers[i], path[i], tang);
287 double lowest_eigenvalue =
288 eonc::eigenmodeGetEigenvalue(*eigenmode_solvers[i]);
289 if (fileLogger.is_open()) {
290 fileLogger << std::format(
291 "{:>3} {:>12.6f} {:>12.6f} {:>12.6f} {:>12.6f}\n", i, distTotal,
292 relative_energy, parallel_force, lowest_eigenvalue);
293 } else {
294 QUILL_LOG_DEBUG(log, "{:>3} {:>12.6f} {:>12.6f} {:>12.6f} {:>12.6f}", i,
295 distTotal, relative_energy, parallel_force,
296 lowest_eigenvalue);
297 }
298 } else {
299 if (fileLogger.is_open()) {
300 fileLogger << std::format("{:>3} {:>12.6f} {:>12.6f} {:>12.6f}\n", i,
301 distTotal, relative_energy, parallel_force);
302 } else {
303 QUILL_LOG_DEBUG(log, "{:>3} {:>12.6f} {:>12.6f} {:>12.6f}", i,
304 distTotal, relative_energy, parallel_force);
305 }
306 }
307 }
308}
309
310std::vector<readcon::ConFrame> pathToConFrames(
311 const std::vector<std::shared_ptr<Matter>> &path,
312 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
313 const std::vector<std::shared_ptr<EigenmodeStrategy>> &eigenmode_solvers,
314 long numImages, bool estimateEigenvalues, std::optional<size_t> bandIndex,
315 double referenceEnergy) {
316 const size_t nframes = static_cast<size_t>(numImages) + 2;
317 if (path.size() < nframes) {
318 return {};
319 }
320
321 double distTotal = 0.0;
322 std::vector<eonc::io::ConFrameMetadata> metas;
323 metas.reserve(nframes);
324
325 // The mass-weighted arc length rides beside the Cartesian one: it is the
326 // coordinate a tunnelling integral along the band runs over. A structure
327 // without masses leaves it out rather than failing the write.
328 double distMw = 0.0;
329 bool haveMw = true;
330 for (long i = 0; i <= numImages + 1; i++) {
331 if (i > 0) {
332 distTotal += path[i]->distanceTo(*path[i - 1]);
333 if (haveMw) {
334 try {
335 distMw +=
336 eonc::tunneling::massWeightedDistance(*path[i - 1], *path[i]);
337 } catch (const std::invalid_argument &) {
338 haveMw = false;
339 }
340 }
341 }
342 metas.push_back(neb_frame_metadata(path, tangent, eigenmode_solvers,
343 numImages, estimateEigenvalues, i,
344 distTotal, bandIndex, referenceEnergy));
345 if (haveMw) {
346 metas.back().scalars.push_back({"reaction_coordinate_mw", distMw});
347 }
348 }
349 // The band's one-dimensional tunnelling estimate goes on its first frame:
350 // the well frequencies, the WKB action and the splitting a two-level-system
351 // screen reads. Left out when an image has no mass or an end well is flat.
352 if (haveMw && !metas.empty()) {
353 try {
354 std::vector<std::shared_ptr<Matter>> ends(
355 path.begin(), path.begin() + static_cast<std::ptrdiff_t>(nframes));
356 const auto split = eonc::tunneling::bandSplitting(
357 ends, referenceOrFirst(path, referenceEnergy));
358 auto &head = metas.front().scalars;
359 head.push_back({"hbar_omega_reactant", split.hwReactant});
360 head.push_back({"hbar_omega_product", split.hwProduct});
361 head.push_back({"tunnel_action", split.action});
362 head.push_back({"tunnel_splitting", split.delta0});
363 head.push_back({"tls_energy", split.tlsEnergy()});
364 head.push_back({"tunnel_deep_wells", split.deepWells ? 1.0 : 0.0});
365 } catch (const std::invalid_argument &) {
366 }
367 }
368
369 std::vector<std::shared_ptr<Matter>> band(
370 path.begin(), path.begin() + static_cast<std::ptrdiff_t>(nframes));
371 return eonc::io::buildNebPathFrames(band, metas);
372}
373
375 const std::vector<std::shared_ptr<Matter>> &path,
376 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
377 const std::vector<std::shared_ptr<EigenmodeStrategy>> &eigenmode_solvers,
378 long numImages, bool estimateEigenvalues, std::string filename,
379 std::optional<size_t> bandIndex, double referenceEnergy) {
380 auto frames =
381 pathToConFrames(path, tangent, eigenmode_solvers, numImages,
382 estimateEigenvalues, bandIndex, referenceEnergy);
383 if (frames.empty()) {
385 }
386 return eonc::io::writeConFrames(std::move(filename), frames);
387}
388
389} // namespace eonc::neb
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
IoStatus writeConFrames(std::string filename, const std::vector< readcon::ConFrame > &frames)
Write already-built ConFrames to a multi-frame .con (temp or durable).
std::vector< readcon::ConFrame > buildNebPathFrames(const std::vector< std::shared_ptr< Matter > > &path, const std::vector< ConFrameMetadata > &metadata_per_image)
Build NEB band ConFrames without writing (clone builder path of writeNebPath).
IoStatus
Structured I/O result for the client surface (nanobind-friendly).
Definition ConFileIO.h:29
quill::Logger * get() noexcept
Get or create the default "combi" logger.
Definition EonLogger.h:44
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, double referenceEnergy)
Build stamped ConFrames for a NEB band (same metadata as writePathCon).
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.
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, double referenceEnergy)
Print NEB image data to log and optionally to file.
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().
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.
Splitting bandSplitting(const std::vector< std::shared_ptr< Matter > > &band, double referenceEnergy)
The splitting of a converged band, with the well frequencies from the band's curvature at each end.
double massWeightedDistance(const Matter &a, const Matter &b)
sqrt(sum_i m_i |b_i - a_i|^2) under the minimum image of a's cell.
Definition Tunneling.cpp:34
void eigenmodeCompute(LowestEigenmode &s, std::shared_ptr< Matter > matter, AtomMatrix direction)
double eigenmodeGetEigenvalue(LowestEigenmode &s)
std::optional< uint64_t > frame_index
Definition ConFileIO.h:72
std::optional< uint64_t > neb_band
Definition ConFileIO.h:77
std::vector< ConMetadataValue > scalars
Definition ConFileIO.h:79
std::optional< double > energy
Definition ConFileIO.h:73
std::optional< uint64_t > neb_bead
Definition ConFileIO.h:76
RAII helper for class-scoped logging.
Definition EonLogger.h:171
std::vector< double > energies
std::vector< double > curvatures
std::vector< double > positions