20namespace fs = std::filesystem;
27 const double n = v.norm();
36storedOrEndpointTangent(
const std::vector<std::shared_ptr<Matter>> &path,
37 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
38 long numImages,
long imageIndex) {
40 if (imageIndex == 0) {
41 tang = path[0]->pbc(path[1]->getPositions() - path[0]->getPositions());
42 normalizeOrZero(tang);
45 if (imageIndex == numImages + 1) {
46 tang = path[numImages]->pbc(path[numImages + 1]->getPositions() -
47 path[numImages]->getPositions());
48 normalizeOrZero(tang);
51 return *tangent[
static_cast<size_t>(imageIndex)];
55double referenceOrFirst(
const std::vector<std::shared_ptr<Matter>> &path,
56 double referenceEnergy) {
57 return std::isnan(referenceEnergy) ? path[0]->getPotentialEnergy()
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) {
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());
75 tang = *tangent[imageIndex];
77 normalizeOrZero(tang);
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);
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);
89 metadata.
neb_band =
static_cast<uint64_t
>(*bandIndex);
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});
95 if (estimateEigenvalues && imageIndex >= 0 &&
96 imageIndex <
static_cast<long>(eigenmode_solvers.size()) &&
97 eigenmode_solvers[imageIndex]) {
101 {
"lowest_eigenvalue",
112 const std::vector<std::shared_ptr<AtomMatrix>> &tangent,
119 std::vector<double> a(numImages + 1), b(numImages + 1), c(numImages + 1),
121 double F1, F2, U1, U2, dist;
123 for (
long i = 0; i <= numImages; i++) {
124 dist = path[i]->distanceTo(*path[i + 1]);
127 path[i]->pbc(path[1]->getPositions() - path[0]->getPositions());
128 normalizeOrZero(tangentEndpoint);
129 F1 =
matDot(path[i]->getForces(), tangentEndpoint) * dist;
131 F1 =
matDot(path[i]->getForces(), *tangent[i]) * dist;
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;
139 F2 =
matDot(path[i + 1]->getForces(), *tangent[i + 1]) * dist;
141 U1 = path[i]->getPotentialEnergy();
142 U2 = path[i + 1]->getPotentialEnergy();
145 c[i] = 3. * (U2 - U1) + 2. * F1 + F2;
146 d[i] = -2. * (U2 - U1) - (F1 + F2);
150 result.
positions.resize(2 * (numImages + 1));
151 result.
energies.resize(2 * (numImages + 1));
152 result.
curvatures.resize(2 * (numImages + 1));
154 double discriminant, f;
156 for (
long i = 0; i <= numImages; i++) {
157 discriminant = c[i] * c[i] - 3.0 * b[i] * d[i];
158 if (discriminant >= 0) {
162 if ((d[i] == 0) && (c[i] != 0)) {
163 f = (-b[i] / (2. * c[i]));
166 else if (d[i] != 0) {
167 f = -(c[i] + std::sqrt(discriminant)) / (3. * d[i]);
169 if ((f >= 0) && (f <= 1)) {
172 ((d[i] * f + c[i]) * f + b[i]) * f + a[i];
179 if (d[i] != 0 && discriminant != 0.0) {
180 f = -(c[i] - std::sqrt(discriminant)) / (3. * d[i]);
184 if ((f >= 0) && (f <= 1)) {
187 ((d[i] * f + c[i]) * f + b[i]) * f + a[i];
195 QUILL_LOG_DEBUG(
log,
"Energy reference: {}", path[0]->getPotentialEnergy());
196 for (
long i = 0; i < result.
numExtrema; i++) {
198 log,
"extrema #{} at image position {} with energy {} and curvature {}",
200 result.
energies[i] - path[0]->getPotentialEnergy(),
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);
217 const double f = posFraction -
static_cast<double>(leftIdx);
219 (1.0 - f) * storedOrEndpointTangent(path, tangent, numImages, leftIdx) +
220 f * storedOrEndpointTangent(path, tangent, numImages, leftIdx + 1);
221 normalizeOrZero(mode);
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,
232 double dist, distTotal = 0;
234 path[0]->pbc(path[1]->getPositions() - path[0]->getPositions());
236 path[numImages + 1]->getPositions() - path[numImages]->getPositions());
239 if (estimateEigenvalues) {
240 header = std::format(
"{:>3s} {:>12s} {:>12s} {:>12s} {:>12s}",
"img",
241 "rxn_coord",
"energy",
"f_para",
"eigval");
243 header = std::format(
"{:>3s} {:>12s} {:>12s} {:>12s}",
"img",
"rxn_coord",
247 normalizeOrZero(tangentStart);
248 normalizeOrZero(tangentEnd);
250 std::ofstream fileLogger;
252 std::string neb_dat_fs;
253 if (idx == std::numeric_limits<size_t>::max()) {
254 neb_dat_fs =
"neb.dat";
256 neb_dat_fs = std::format(
"neb_{:03}.dat", idx);
258 if (fs::exists(neb_dat_fs)) {
259 fs::remove(neb_dat_fs);
261 fileLogger.open(neb_dat_fs);
262 if (fileLogger.is_open()) {
263 fileLogger << header <<
"\n";
266 const double energy_reactant = referenceOrFirst(path, referenceEnergy);
268 for (
long i = 0; i <= numImages + 1; i++) {
271 }
else if (i == numImages + 1) {
278 dist = path[i]->distanceTo(*path[i - 1]);
282 double relative_energy = path[i]->getPotentialEnergy() - energy_reactant;
283 double parallel_force =
matDot(path[i]->getForces(), tang);
285 if (estimateEigenvalues) {
287 double lowest_eigenvalue =
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);
294 QUILL_LOG_DEBUG(
log,
"{:>3} {:>12.6f} {:>12.6f} {:>12.6f} {:>12.6f}", i,
295 distTotal, relative_energy, parallel_force,
299 if (fileLogger.is_open()) {
300 fileLogger << std::format(
"{:>3} {:>12.6f} {:>12.6f} {:>12.6f}\n", i,
301 distTotal, relative_energy, parallel_force);
303 QUILL_LOG_DEBUG(
log,
"{:>3} {:>12.6f} {:>12.6f} {:>12.6f}", i,
304 distTotal, relative_energy, parallel_force);
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) {
321 double distTotal = 0.0;
322 std::vector<eonc::io::ConFrameMetadata> metas;
323 metas.reserve(nframes);
330 for (
long i = 0; i <= numImages + 1; i++) {
332 distTotal += path[i]->distanceTo(*path[i - 1]);
337 }
catch (
const std::invalid_argument &) {
342 metas.push_back(neb_frame_metadata(path, tangent, eigenmode_solvers,
343 numImages, estimateEigenvalues, i,
344 distTotal, bandIndex, referenceEnergy));
346 metas.back().scalars.push_back({
"reaction_coordinate_mw", distMw});
352 if (haveMw && !metas.empty()) {
354 std::vector<std::shared_ptr<Matter>> ends(
355 path.begin(), path.begin() +
static_cast<std::ptrdiff_t
>(nframes));
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 &) {
369 std::vector<std::shared_ptr<Matter>> band(
370 path.begin(), path.begin() +
static_cast<std::ptrdiff_t
>(nframes));
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) {
382 estimateEigenvalues, bandIndex, referenceEnergy);
383 if (frames.empty()) {
double matDot(const AtomMatrix &a, const AtomMatrix &b)
SIMD-optimized dot product for contiguous Eigen matrices.
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
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).
quill::Logger * get() noexcept
Get or create the default "combi" logger.
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.
void eigenmodeCompute(LowestEigenmode &s, std::shared_ptr< Matter > matter, AtomMatrix direction)
double eigenmodeGetEigenvalue(LowestEigenmode &s)
RAII helper for class-scoped logging.
std::vector< double > energies
std::vector< double > curvatures
std::vector< double > positions