Loading...
Searching...
No Matches
Hessian.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*/
12#include "eon/Hessian.h"
13#include "eon/EonLogger.h"
14#include "eon/HelperFunctions.h"
15#include "eon/SafeMath.h"
16#include "eon/Tunneling.h"
17#include "eon/VesinNeighbors.h"
18
19#include <algorithm>
20#include <cmath>
21#include <fstream>
22#include <sstream>
23#include <stdexcept>
24#include <string>
25#include <vector>
26
27namespace eonc {
28
29namespace {
30
31// phva_atoms entries are *mobile / displaced* atoms for FD (hybrid/PHVA-class
32// active set). Intersect with non-fixed atoms in HessianJob.
33
34// Checkpoint: first line "eon_hess_ckpt <size> <next_col>", then size*size
35// doubles in row-major order matching MatrixXd storage.
36bool loadColumnCheckpoint(const std::string &path, int size, int &nextCol,
37 MatrixXd &H) {
38 std::ifstream in(path);
39 if (!in) {
40 return false;
41 }
42 std::string tag;
43 int fileSize = 0;
44 in >> tag >> fileSize >> nextCol;
45 if (!in || tag != "eon_hess_ckpt" || fileSize != size || nextCol < 0 ||
46 nextCol > size) {
47 return false;
48 }
49 H.resize(size, size);
50 for (int i = 0; i < size; ++i) {
51 for (int j = 0; j < size; ++j) {
52 double v = 0.0;
53 in >> v;
54 if (!in) {
55 return false;
56 }
57 H(i, j) = v;
58 }
59 }
60 return true;
61}
62
63bool saveColumnCheckpoint(const std::string &path, int size, int nextCol,
64 const MatrixXd &H) {
65 std::ofstream out(path);
66 if (!out) {
67 return false;
68 }
69 out << "eon_hess_ckpt " << size << " " << nextCol << "\n";
70 out.precision(17);
71 for (int i = 0; i < size; ++i) {
72 for (int j = 0; j < size; ++j) {
73 out << H(i, j) << (j + 1 == size ? '\n' : ' ');
74 }
75 }
76 return static_cast<bool>(out);
77}
78
79} // namespace
80
82 : matter{matter},
83 parameters{params} {
84 hessian.resize(0, 0);
85 freqs.resize(0);
86}
87
88MatrixXd Hessian::getHessian(Matter *matterIn, const VectorXi &atomsIn) {
89 if ((matter != matterIn) || (atoms.size() != atomsIn.size()) ||
90 (atoms != atomsIn) || (hessian.rows() == 0)) {
91 hessian.resize(0, 0);
92 matter = matterIn;
93 atoms = atomsIn;
94
95 if (!calculate()) {
96 hessian.resize(0, 0);
97 }
98 }
99 return hessian;
100}
101
102VectorXd Hessian::getFreqs(Matter *matterIn, const VectorXi &atomsIn) {
103 if ((matter != matterIn) || (atoms.size() != atomsIn.size()) ||
104 (atoms != atomsIn) || (hessian.rows() == 0)) {
105 hessian.resize(0, 0);
106 matter = matterIn;
107 atoms = atomsIn;
108
109 if (!calculate()) {
110 freqs.resize(0);
111 hessian.resize(0, 0);
112 }
113 }
114 return freqs;
115}
116
117namespace {
118
119struct MobileColoring {
120 std::vector<int> color;
121 // closed[ia] = local mobile indices in the closed cutoff neighborhood of ia
122 std::vector<std::vector<int>> closed;
123};
124
125MobileColoring buildMobileColoring(const Matter &matter, const VectorXi &atoms,
126 double cutoff) {
127 MobileColoring out;
128 const int nAtoms = static_cast<int>(matter.numberOfAtoms());
129 const int nMobile = static_cast<int>(atoms.rows());
130 if (nAtoms <= 0 || nMobile <= 0 || !(cutoff > 0.0) ||
131 !std::isfinite(cutoff)) {
132 return out;
133 }
134 const Matrix3d cell = matter.getCell();
135 if (!(std::abs(cell.determinant()) > 1e-18)) {
136 return out;
137 }
138 for (int a = 0; a < nMobile; ++a) {
139 const long idx = atoms(a);
140 if (idx < 0 || idx >= nAtoms) {
141 return {};
142 }
143 }
144
145 const AtomMatrix &pos = matter.getPositions();
148 opt.cutoff = cutoff;
149 opt.full = true;
150 opt.return_distances = false;
151 opt.return_vectors = false;
152 opt.periodic = {matter.getPeriodic(), matter.getPeriodic(),
153 matter.getPeriodic()};
154 try {
155 nl.compute(pos.data(), static_cast<std::size_t>(nAtoms), cell.data(), opt);
156 } catch (const std::exception &) {
157 return {};
158 }
159
160 std::vector<std::vector<int>> nbs(static_cast<std::size_t>(nAtoms));
161 for (std::size_t p = 0; p < nl.size(); ++p) {
162 const int i = static_cast<int>(nl.i(p));
163 const int j = static_cast<int>(nl.j(p));
164 if (i == j || i < 0 || j < 0 || i >= nAtoms || j >= nAtoms) {
165 continue;
166 }
167 nbs[static_cast<std::size_t>(i)].push_back(j);
168 }
169 for (auto &row : nbs) {
170 std::sort(row.begin(), row.end());
171 row.erase(std::unique(row.begin(), row.end()), row.end());
172 }
173
174 std::vector<int> local(static_cast<std::size_t>(nAtoms), -1);
175 for (int a = 0; a < nMobile; ++a) {
176 local[static_cast<std::size_t>(atoms(a))] = a;
177 }
178
179 out.closed.assign(static_cast<std::size_t>(nMobile), {});
180 for (int ia = 0; ia < nMobile; ++ia) {
181 auto &nbhd = out.closed[static_cast<std::size_t>(ia)];
182 nbhd.push_back(ia);
183 const int g = static_cast<int>(atoms(ia));
184 for (int nb : nbs[static_cast<std::size_t>(g)]) {
185 const int lb = local[static_cast<std::size_t>(nb)];
186 if (lb >= 0) {
187 nbhd.push_back(lb);
188 }
189 }
190 std::sort(nbhd.begin(), nbhd.end());
191 nbhd.erase(std::unique(nbhd.begin(), nbhd.end()), nbhd.end());
192 }
193
194 // Square of the cutoff graph on the mobile set: atoms whose closed
195 // neighborhoods intersect cannot move in the same finite-difference.
196 std::vector<std::vector<int>> touchers(static_cast<std::size_t>(nMobile));
197 for (int ia = 0; ia < nMobile; ++ia) {
198 for (int k : out.closed[static_cast<std::size_t>(ia)]) {
199 touchers[static_cast<std::size_t>(k)].push_back(ia);
200 }
201 }
202 std::vector<std::vector<int>> adj(static_cast<std::size_t>(nMobile));
203 for (int k = 0; k < nMobile; ++k) {
204 const auto &t = touchers[static_cast<std::size_t>(k)];
205 for (size_t a = 0; a < t.size(); ++a) {
206 for (size_t b = a + 1; b < t.size(); ++b) {
207 adj[static_cast<std::size_t>(t[a])].push_back(t[b]);
208 adj[static_cast<std::size_t>(t[b])].push_back(t[a]);
209 }
210 }
211 }
212 for (auto &row : adj) {
213 std::sort(row.begin(), row.end());
214 row.erase(std::unique(row.begin(), row.end()), row.end());
215 }
216 out.color = greedyColorCutoffGraph(adj);
217 return out;
218}
219
220void writeMassWeighted(MatrixXd &hessian, const Matter &matter,
221 const VectorXi &atoms, int col, int atomI,
222 const AtomMatrix &forceA, const AtomMatrix &forceB,
223 double denom, const std::vector<int> &owner, int ia) {
224 const int size = static_cast<int>(atoms.rows()) * 3;
225 const double massI = matter.getMass(atomI);
226 for (int j = 0; j < size; ++j) {
227 const long atomJ = atoms(j / 3);
228 double dF = 0.0;
229 if (owner[static_cast<std::size_t>(j / 3)] == ia) {
230 dF = forceA(atomJ, j % 3) - forceB(atomJ, j % 3);
231 }
232 hessian(col, j) = -dF / denom;
233 const double effMass = std::sqrt(matter.getMass(atomJ) * massI);
234 hessian(col, j) = eonc::safemath::safe_div(hessian(col, j), effMass, 0.0);
235 }
236}
237
238} // namespace
239
240std::vector<int>
241greedyColorCutoffGraph(const std::vector<std::vector<int>> &adj) {
242 const int n = static_cast<int>(adj.size());
243 std::vector<int> color(static_cast<std::size_t>(n), -1);
244 std::vector<int> used(static_cast<std::size_t>(std::max(n, 0)), 0);
245 int epoch = 0;
246 for (int v = 0; v < n; ++v) {
247 ++epoch;
248 for (int u : adj[static_cast<std::size_t>(v)]) {
249 if (u < 0 || u >= n || u == v) {
250 continue;
251 }
252 const int cu = color[static_cast<std::size_t>(u)];
253 if (cu >= 0 && cu < n) {
254 used[static_cast<std::size_t>(cu)] = epoch;
255 }
256 }
257 int c = 0;
258 while (c < n && used[static_cast<std::size_t>(c)] == epoch) {
259 ++c;
260 }
261 color[static_cast<std::size_t>(v)] = c;
262 }
263 return color;
264}
265
266std::vector<int> colorMobileCutoffGraph(const Matter &matter,
267 const VectorXi &atoms, double cutoff) {
268 return buildMobileColoring(matter, atoms, cutoff).color;
269}
270
272 int nAtoms = matter->numberOfAtoms();
273
274 int size = static_cast<int>(atoms.rows()) * 3;
275 QUILL_LOG_DEBUG(log, "[Hessian] Hessian size: {}\n", size);
276 if (size == 0) {
277 return false;
278 }
279
280 // Mobile-atom polarity: indices in `atoms` are FD-displaced DOF owners.
281 for (int a = 0; a < atoms.rows(); ++a) {
282 const long idx = atoms(a);
283 if (idx < 0 || idx >= nAtoms) {
284 QUILL_LOG_ERROR(log,
285 "[Hessian] atom index {} out of range [0, {}) at list "
286 "entry {}; aborting FD Hessian",
287 idx, nAtoms, a);
288 return false;
289 }
290 }
291
292 double dr = parameters.main_options().finiteDifference;
293 if (!(dr > 0.0) || !std::isfinite(dr)) {
294 QUILL_LOG_ERROR(log, "[Hessian] invalid finiteDifference dr={}\n", dr);
295 return false;
296 }
297
298 const FdScheme scheme = parseFdScheme(parameters.hessian_options().fd_scheme);
299 const std::string &ckptPath = parameters.hessian_options().checkpoint_path;
300
301 hessian.resize(size, size);
302 hessian.setZero();
303
304 // Net-force removal adds the same shift to every atom, so columns are
305 // no longer confined to the cutoff neighborhood. A column checkpoint is
306 // also stored one coordinate at a time.
307 bool anyFixed = false;
308 for (int i = 0; i < nAtoms; ++i) {
309 if (matter->getFixed(i)) {
310 anyFixed = true;
311 break;
312 }
313 }
314 const bool netCoupled =
315 parameters.main_options().removeNetForce && nAtoms > 1 && !anyFixed;
316 double cutoff = 0.0;
317 if (matter->getPotential()) {
318 cutoff = matter->getPotential()->finiteCutoff();
319 }
320 if (!netCoupled && ckptPath.empty() && cutoff > 0.0 &&
321 std::isfinite(cutoff)) {
322 if (calculateColored(cutoff, dr, scheme)) {
323 return true;
324 }
325 hessian.setZero();
326 }
327 // A potential that evaluates batches (calculator groups, GPU models)
328 // takes the displaced structures together; the column checkpoint stays
329 // on the one-column-at-a-time path.
330 if (ckptPath.empty() && matter->getPotential() &&
331 matter->getPotential()->supportsBatchEvaluation() && size > 1) {
332 return calculateBatched(dr, scheme);
333 }
334 return calculateSerial(dr, scheme);
335}
336
337bool Hessian::calculateBatched(double dr, FdScheme scheme) {
338 const int size = static_cast<int>(atoms.rows()) * 3;
339 const AtomMatrix pos = matter->getPositions();
340 auto pot = matter->getPotential();
341
342 Matter base(*matter);
343 const AtomMatrix force0 = base.getForces();
344 if (!force0.allFinite()) {
345 QUILL_LOG_ERROR(log, "[Hessian] non-finite forces at undisplaced geometry; "
346 "aborting FD Hessian");
347 return false;
348 }
349
350 // Stencil points per column, in the order fdForceDerivative takes them.
351 std::vector<double> steps{1.0};
352 if (scheme != FdScheme::OneSided) {
353 steps.push_back(-1.0);
354 }
355 if (scheme == FdScheme::Fourth) {
356 steps.push_back(2.0);
357 steps.push_back(-2.0);
358 }
359 const int perColumn = static_cast<int>(steps.size());
360 const long nAtoms = matter->numberOfAtoms();
361 const VectorXi nrs = matter->getAtomicNrs();
362 const Matrix3d box =
363 matter->getPeriodic() ? matter->getCell() : Matrix3d::Zero().eval();
364
365 // Columns in chunks, so memory stays bounded for large mobile sets.
366 constexpr int kChunkColumns = 32;
367 std::vector<Matter> displaced;
368 for (int c0 = 0; c0 < size; c0 += kChunkColumns) {
369 const int c1 = std::min(size, c0 + kChunkColumns);
370 const long n = static_cast<long>(c1 - c0) * perColumn;
371 displaced.assign(static_cast<size_t>(n), base);
372 std::vector<const double *> posPtr, boxPtr;
373 std::vector<const int *> nrsPtr;
374 std::vector<double *> frcPtr;
375 for (int i = c0; i < c1; ++i) {
376 for (int k = 0; k < perColumn; ++k) {
377 Matter &m = displaced[static_cast<size_t>((i - c0) * perColumn + k)];
378 AtomMatrix p = pos;
379 p(atoms(i / 3), i % 3) += steps[static_cast<size_t>(k)] * dr;
380 m.setPositions(p);
381 }
382 }
383 for (auto &m : displaced) {
384 posPtr.push_back(m.getPositions().data());
385 nrsPtr.push_back(nrs.data());
386 frcPtr.push_back(m.forcesData());
387 boxPtr.push_back(box.data());
388 }
389 std::vector<double> energies(static_cast<size_t>(n)),
390 variances(static_cast<size_t>(n));
391 pot->forceBatch(n, nAtoms, posPtr.data(), nrsPtr.data(), frcPtr.data(),
392 energies.data(), variances.data(), boxPtr.data());
393 for (long j = 0; j < n; ++j) {
394 displaced[static_cast<size_t>(j)].setComputedPotential(
395 energies[static_cast<size_t>(j)], variances[static_cast<size_t>(j)]);
396 }
397 for (int i = c0; i < c1; ++i) {
398 auto forces = [&](int k) -> const AtomMatrix & {
399 return displaced[static_cast<size_t>((i - c0) * perColumn + k)]
400 .getForces();
401 };
402 const AtomMatrix &fPlus = forces(0);
403 const AtomMatrix &fMinus = perColumn > 1 ? forces(1) : force0;
404 const AtomMatrix &fPlus2 = perColumn > 2 ? forces(2) : force0;
405 const AtomMatrix &fMinus2 = perColumn > 3 ? forces(3) : force0;
406 if (!fPlus.allFinite() || !fMinus.allFinite() || !fPlus2.allFinite() ||
407 !fMinus2.allFinite()) {
408 QUILL_LOG_ERROR(log,
409 "[Hessian] non-finite forces for FD column {}; "
410 "aborting FD Hessian",
411 i);
412 return false;
413 }
414 const AtomMatrix slope =
415 fdForceDerivative(scheme, dr, force0, fPlus, fMinus, fPlus2, fMinus2);
416 for (int j = 0; j < size; j++) {
417 const double effMass = std::sqrt(matter->getMass(atoms(j / 3)) *
418 matter->getMass(atoms(i / 3)));
419 hessian(i, j) =
420 eonc::safemath::safe_div(-slope(atoms(j / 3), j % 3), effMass, 0.0);
421 }
422 }
423 }
424 return finalizeHessian(size);
425}
426
427bool Hessian::calculateColored(double cutoff, double dr, FdScheme scheme) {
428 const int nAtoms = static_cast<int>(matter->numberOfAtoms());
429 const int nMobile = static_cast<int>(atoms.rows());
430 const int size = nMobile * 3;
431 const MobileColoring coloring = buildMobileColoring(*matter, atoms, cutoff);
432 if (static_cast<int>(coloring.color.size()) != nMobile ||
433 static_cast<int>(coloring.closed.size()) != nMobile) {
434 return false;
435 }
436
437 int nColors = 0;
438 for (int c : coloring.color) {
439 if (c < 0) {
440 return false;
441 }
442 nColors = std::max(nColors, c + 1);
443 }
444 if (nColors <= 0) {
445 return false;
446 }
447 QUILL_LOG_DEBUG(log,
448 "[Hessian] cutoff coloring: {} colors for {} mobile atoms\n",
449 nColors, nMobile);
450
451 std::vector<std::vector<int>> members(static_cast<std::size_t>(nColors));
452 for (int ia = 0; ia < nMobile; ++ia) {
453 members[static_cast<std::size_t>(
454 coloring.color[static_cast<std::size_t>(ia)])]
455 .push_back(ia);
456 }
457 std::vector<std::vector<int>> owner(
458 static_cast<std::size_t>(nColors),
459 std::vector<int>(static_cast<std::size_t>(nMobile), -1));
460 for (int c = 0; c < nColors; ++c) {
461 for (int ia : members[static_cast<std::size_t>(c)]) {
462 for (int k : coloring.closed[static_cast<std::size_t>(ia)]) {
463 int &slot =
464 owner[static_cast<std::size_t>(c)][static_cast<std::size_t>(k)];
465 if (slot >= 0 && slot != ia) {
466 return false;
467 }
468 slot = ia;
469 }
470 }
471 }
472
473 Matter matterTemp(*matter);
474 const AtomMatrix pos = matter->getPositions();
475 AtomMatrix posDisplace(nAtoms, 3);
476 AtomMatrix force0 = matterTemp.getForces();
477 if (!force0.allFinite()) {
478 QUILL_LOG_ERROR(log, "[Hessian] non-finite forces at undisplaced geometry; "
479 "aborting FD Hessian");
480 return false;
481 }
482
483 for (int dir = 0; dir < 3; ++dir) {
484 for (int c = 0; c < nColors; ++c) {
485 const auto &group = members[static_cast<std::size_t>(c)];
486 if (group.empty()) {
487 continue;
488 }
489 auto shifted = [&](double scale, const char *side) -> AtomMatrix {
490 posDisplace.setZero();
491 for (int ia : group) {
492 posDisplace(atoms(ia), dir) = scale * dr;
493 }
494 matterTemp.setPositions(pos + posDisplace);
495 AtomMatrix force = matterTemp.getForces();
496 if (!force.allFinite()) {
497 QUILL_LOG_ERROR(log,
498 "[Hessian] non-finite forces for color {} dir {} "
499 "({}); aborting FD Hessian",
500 c, dir, side);
501 return AtomMatrix();
502 }
503 return force;
504 };
505 const AtomMatrix forcePlus = shifted(1.0, "+");
506 if (forcePlus.size() == 0) {
507 return false;
508 }
509 AtomMatrix forceMinus = force0;
510 AtomMatrix forcePlus2 = force0;
511 AtomMatrix forceMinus2 = force0;
512 if (scheme != FdScheme::OneSided) {
513 forceMinus = shifted(-1.0, "-");
514 if (forceMinus.size() == 0) {
515 return false;
516 }
517 }
518 if (scheme == FdScheme::Fourth) {
519 forcePlus2 = shifted(2.0, "+2");
520 if (forcePlus2.size() == 0) {
521 return false;
522 }
523 forceMinus2 = shifted(-2.0, "-2");
524 if (forceMinus2.size() == 0) {
525 return false;
526 }
527 }
528 const AtomMatrix slope = fdForceDerivative(
529 scheme, dr, force0, forcePlus, forceMinus, forcePlus2, forceMinus2);
530 const AtomMatrix zero = AtomMatrix::Zero(nAtoms, 3);
531 for (int ia : group) {
532 const int col = ia * 3 + dir;
533 writeMassWeighted(hessian, *matter, atoms, col,
534 static_cast<int>(atoms(ia)), slope, zero, 1.0,
535 owner[static_cast<std::size_t>(c)], ia);
536 }
537 }
538 }
539 return finalizeHessian(size);
540}
541
542bool Hessian::calculateSerial(double dr, FdScheme scheme) {
543 const int nAtoms = static_cast<int>(matter->numberOfAtoms());
544 const int size = static_cast<int>(atoms.rows()) * 3;
545 const std::string &ckptPath = parameters.hessian_options().checkpoint_path;
546 const bool wantResume =
547 parameters.hessian_options().resume && !ckptPath.empty();
548
549 AtomMatrix pos = matter->getPositions();
550 AtomMatrix posDisplace(nAtoms, 3);
551 AtomMatrix posTemp(nAtoms, 3);
552 AtomMatrix forcePlus(nAtoms, 3);
553 AtomMatrix forceMinus(nAtoms, 3);
554
555 int startCol = 0;
556 if (wantResume && loadColumnCheckpoint(ckptPath, size, startCol, hessian)) {
557 QUILL_LOG_DEBUG(log, "[Hessian] resume from column {} / {}\n", startCol,
558 size);
559 } else {
560 startCol = 0;
561 hessian.setZero();
562 }
563
564 Matter matterTemp(*matter);
565 AtomMatrix force0 = matterTemp.getForces();
566 if (!force0.allFinite()) {
567 QUILL_LOG_ERROR(log, "[Hessian] non-finite forces at undisplaced geometry; "
568 "aborting FD Hessian");
569 return false;
570 }
571
572 for (int i = startCol; i < size; i++) {
573 posDisplace.setZero();
574 posDisplace(atoms(i / 3), i % 3) = dr;
575
576 posTemp = pos + posDisplace;
577 matterTemp.setPositions(posTemp);
578 forcePlus = matterTemp.getForces();
579 if (!forcePlus.allFinite()) {
580 QUILL_LOG_ERROR(log,
581 "[Hessian] non-finite forces for FD column {} (+); "
582 "aborting FD Hessian",
583 i);
584 return false;
585 }
586
587 if (scheme != FdScheme::OneSided) {
588 posTemp = pos - posDisplace;
589 matterTemp.setPositions(posTemp);
590 forceMinus = matterTemp.getForces();
591 if (!forceMinus.allFinite()) {
592 QUILL_LOG_ERROR(log,
593 "[Hessian] non-finite forces for FD column {} (-); "
594 "aborting FD Hessian",
595 i);
596 return false;
597 }
598 }
599 AtomMatrix forcePlus2 = force0;
600 AtomMatrix forceMinus2 = force0;
601 if (scheme == FdScheme::Fourth) {
602 posDisplace(atoms(i / 3), i % 3) = 2.0 * dr;
603 matterTemp.setPositions(pos + posDisplace);
604 forcePlus2 = matterTemp.getForces();
605 if (!forcePlus2.allFinite()) {
606 QUILL_LOG_ERROR(log,
607 "[Hessian] non-finite forces for FD column {} (+2); "
608 "aborting FD Hessian",
609 i);
610 return false;
611 }
612 matterTemp.setPositions(pos - posDisplace);
613 forceMinus2 = matterTemp.getForces();
614 if (!forceMinus2.allFinite()) {
615 QUILL_LOG_ERROR(log,
616 "[Hessian] non-finite forces for FD column {} (-2); "
617 "aborting FD Hessian",
618 i);
619 return false;
620 }
621 }
622 // H ≈ -dF, mass-weighted. dF is the selected real stencil.
623 const AtomMatrix slope = fdForceDerivative(
624 scheme, dr, force0, forcePlus, forceMinus, forcePlus2, forceMinus2);
625 for (int j = 0; j < size; j++) {
626 hessian(i, j) = -slope(atoms(j / 3), j % 3);
627 const double effMass = std::sqrt(matter->getMass(atoms(j / 3)) *
628 matter->getMass(atoms(i / 3)));
629 hessian(i, j) = eonc::safemath::safe_div(hessian(i, j), effMass, 0.0);
630 }
631
632 if (!ckptPath.empty()) {
633 // next column to compute after a clean interrupt
634 saveColumnCheckpoint(ckptPath, size, i + 1, hessian);
635 }
636 }
637 return finalizeHessian(size);
638}
639
641 const std::string &ckptPath = parameters.hessian_options().checkpoint_path;
642
643 // Symmetrize (FD noise breaks H=H^T; required for vib analysis)
644 for (int i = 0; i < size; i++) {
645 for (int j = 0; j < i; j++) {
646 hessian(i, j) = (hessian(i, j) + hessian(j, i)) / 2;
647 hessian(j, i) = hessian(i, j);
648 }
649 }
650
651 if (!hessian.allFinite()) {
652 QUILL_LOG_ERROR(log, "[Hessian] non-finite entries after FD assembly; "
653 "aborting eigen solve");
654 return false;
655 }
656
657 if (!parameters.main_options().quiet) {
658 QUILL_LOG_DEBUG(log, "[Hessian] writing hessian\n");
659 }
660 if (writeFile) {
661 // A previous case in the same process can still hold hessian.dat on
662 // Windows. Replace it, and try once more after removing the old file.
663 std::ofstream hessfile("hessian.dat", std::ios::out | std::ios::trunc);
664 if (!hessfile) {
665 std::remove("hessian.dat");
666 hessfile.clear();
667 hessfile.open("hessian.dat", std::ios::out | std::ios::trunc);
668 }
669 if (!hessfile) {
670 QUILL_LOG_ERROR(log, "[Hessian] failed to open hessian.dat");
671 return false;
672 }
673 hessfile << hessian;
674 hessfile.close();
675 if (!hessfile) {
676 QUILL_LOG_ERROR(log, "[Hessian] failed to write hessian.dat");
677 return false;
678 }
679 }
680
681 // Completed run: remove checkpoint so a later job does not resume stale cols
682 if (!ckptPath.empty()) {
683 std::remove(ckptPath.c_str());
684 }
685
686 double t0, t1;
687 eonc::helpers::getTime(&t0, nullptr, nullptr);
688 QUILL_LOG_DEBUG(log, "[Hessian] calculating eigen values of the hessian\n");
689 // ColMajor copy for SelfAdjointEigenSolver (eOn MatrixXd is RowMajor)
690 using ColMajorXd =
691 Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor>;
692 ColMajorXd hessianCol = hessian;
693 const bool withModes = parameters.hessian_options().write_modes;
694 Eigen::SelfAdjointEigenSolver<ColMajorXd> es(
695 hessianCol,
696 withModes ? Eigen::ComputeEigenvectors : Eigen::EigenvaluesOnly);
697 eonc::helpers::getTime(&t1, nullptr, nullptr);
698 QUILL_LOG_DEBUG(log, "[Hessian] eigenvalue problem took {:.4e} seconds\n",
699 t1 - t0);
700 if (es.info() != Eigen::Success) {
701 QUILL_LOG_ERROR(log,
702 "[Hessian] SelfAdjointEigenSolver failed (info={}); "
703 "aborting",
704 static_cast<int>(es.info()));
705 return false;
706 }
707 freqs = es.eigenvalues();
708 if (!freqs.allFinite()) {
709 QUILL_LOG_ERROR(log, "[Hessian] non-finite eigenvalues; aborting");
710 return false;
711 }
712 if (withModes) {
713 modes = es.eigenvectors();
714 } else {
715 modes.resize(0, 0);
716 }
717
718 return true;
719}
720
721VectorXd Hessian::removeZeroFreqs(const VectorXd &freqs) {
722 QUILL_LOG_DEBUG(log, "[Hessian] removing zero frequency modes");
723 int size = freqs.size();
724 if (size != 3 * matter->numberOfAtoms()) {
725 return freqs;
726 }
727 VectorXd newfreqs;
728 newfreqs.resize(size);
729 int nremoved = 0;
730 for (int i = 0; i < size; i++) {
731 if (std::abs(freqs(i)) > parameters.hessian_options().zero_freq_value) {
732 newfreqs(i - nremoved) = freqs(i);
733 } else {
734 nremoved++;
735 }
736 }
737
738 if (!trivialModeCountIsPhysical(nremoved, matter->numberOfFixedAtoms())) {
739 QUILL_LOG_WARNING(log,
740 "[Hessian] found {} trivial eigenmodes; a free cluster "
741 "has 6 (5 if linear), a periodic cell 3, and a "
742 "structure with fixed atoms none",
743 nremoved);
744 }
745 return newfreqs.head(size - nremoved);
746}
747
748std::vector<double> cartesianMode(const Matter &matter, const VectorXi &atoms,
749 const Eigen::Ref<const VectorXd> &mode) {
750 const long n = matter.numberOfAtoms();
751 std::vector<double> out(static_cast<size_t>(3 * n), 0.0);
752 if (mode.size() != 3 * atoms.size()) {
753 throw std::invalid_argument("cartesianMode: mode length is not 3 x atoms");
754 }
755 double norm2 = 0.0;
756 for (long j = 0; j < mode.size(); ++j) {
757 const long atom = atoms(j / 3);
758 const double mass = matter.getMass(atom);
759 if (!(mass > 0.0)) {
760 throw std::invalid_argument("cartesianMode: atom without a mass");
761 }
762 const double x = mode(j) / std::sqrt(mass);
763 out[static_cast<size_t>(3 * atom + j % 3)] = x;
764 norm2 += x * x;
765 }
766 if (norm2 > 0.0) {
767 const double scale = 1.0 / std::sqrt(norm2);
768 for (double &x : out) {
769 x *= scale;
770 }
771 }
772 return out;
773}
774
775bool writeNormalModes(Matter &matter, const VectorXi &atoms,
776 const VectorXd &eigenvalues, const MatrixXd &modes,
777 const std::string &path) {
778 if (modes.cols() != eigenvalues.size() || modes.rows() != 3 * atoms.size()) {
779 return false;
780 }
781 for (long k = 0; k < eigenvalues.size(); ++k) {
782 const double lambda = eigenvalues(k);
783 const double hw =
784 std::copysign(tunneling::kHbar * std::sqrt(std::abs(lambda)), lambda);
786 meta.frame_index = static_cast<uint64_t>(k);
787 meta.scalars = {{"mode_eigenvalue", lambda},
788 {"hbar_omega", hw},
789 {"wavenumber", hw * kEvToWavenumber}};
790 meta.displacements = cartesianMode(matter, atoms, modes.col(k));
791 if (!io::io_ok(matter.matter2con(path, k > 0, &meta))) {
792 return false;
793 }
794 }
795 return true;
796}
797
798bool trivialModeCountIsPhysical(long removed, long fixedAtoms) {
799 if (fixedAtoms > 0) {
800 return removed == 0;
801 }
802 return removed == 3 || removed == 5 || removed == 6;
803}
804
805} // namespace eonc
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
Eigen::Matrix< double, 3, 3, eOnStorageOrder > Matrix3d
Definition Eigen.h:35
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
bool writeFile
Definition Hessian.h:70
eonc::log::Scoped log
Definition Hessian.h:78
Matter * matter
Definition Hessian.h:64
MatrixXd modes
Definition Hessian.h:69
VectorXd freqs
Definition Hessian.h:68
Hessian(const Parameters &params, Matter *matter)
Definition Hessian.cpp:81
MatrixXd getHessian(Matter *matterIn, const VectorXi &atomsIn)
Definition Hessian.cpp:88
bool calculateSerial(double dr, FdScheme scheme)
Definition Hessian.cpp:542
VectorXd removeZeroFreqs(const VectorXd &freqs)
Definition Hessian.cpp:721
VectorXi atoms
Definition Hessian.h:72
bool calculate()
Definition Hessian.cpp:271
bool calculateBatched(double dr, FdScheme scheme)
Definition Hessian.cpp:337
const Parameters & parameters
Definition Hessian.h:65
bool finalizeHessian(int size)
Definition Hessian.cpp:640
MatrixXd hessian
Definition Hessian.h:67
bool calculateColored(double cutoff, double dr, FdScheme scheme)
Definition Hessian.cpp:427
VectorXd getFreqs(Matter *matterIn, const VectorXi &atomsIn)
Definition Hessian.cpp:102
void setPositions(const AtomMatrix &pos)
Definition Matter.cpp:350
long int numberOfAtoms() const
Definition Matter.cpp:273
double getMass(long int atom) const
Definition Matter.cpp:478
const AtomMatrix & getForces() const
Definition Matter.cpp:412
io::IoStatus matter2con(std::string filename, bool append=false, const io::ConFrameMetadata *metadata=nullptr)
Definition Matter.h:268
RAII wrapper around vesin_neighbors for Matter-style boxes (double[9] row-major 3×3 cell,...
void getTime(double *real, double *user, double *sys)
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
constexpr double safe_div(double num, double denom, double fallback=0.0)
Definition SafeMath.h:21
constexpr double kHbar
hbar in eV^0.5 amu^0.5 Angstrom, correctly rounded from the exact SI values (h = 6....
Definition Tunneling.h:37
RAII resource manager for the ARTn C library with global synchronization.
FdScheme
Real finite-difference scheme for the assembled Hessian and for Lanczos/Davidson Hessian-vector produ...
M fdForceDerivative(FdScheme scheme, double dr, const M &f0, const M &fPlus, const M &fMinus, const M &fPlus2, const M &fMinus2)
Derivative of a sampled force map along one real step of length dr.
constexpr double kEvToWavenumber
1 eV in cm^-1, e / (h c) from the exact SI values.
Definition Hessian.h:31
FdScheme parseFdScheme(std::string_view scheme)
bool writeNormalModes(Matter &matter, const VectorXi &atoms, const VectorXd &eigenvalues, const MatrixXd &modes, const std::string &path)
Writes one frame of matter per mode to path, the mode as the displacements section and mode_eigenvalu...
Definition Hessian.cpp:775
bool trivialModeCountIsPhysical(long removed, long fixedAtoms)
Whether removed zero-frequency modes are what the structure's symmetries give: 6 for a free cluster (...
Definition Hessian.cpp:798
std::vector< int > greedyColorCutoffGraph(const std::vector< std::vector< int > > &adj)
Greedy coloring of an undirected graph.
Definition Hessian.cpp:241
std::vector< int > colorMobileCutoffGraph(const Matter &matter, const VectorXi &atoms, double cutoff)
Conflict graph on atoms: two mobile atoms share an edge when their closed cutoff neighborhoods inters...
Definition Hessian.cpp:266
std::vector< double > cartesianMode(const Matter &matter, const VectorXi &atoms, const Eigen::Ref< const VectorXd > &mode)
Cartesian displacement of every atom along one mass-weighted mode over the mobile degrees of freedom ...
Definition Hessian.cpp:748
std::optional< uint64_t > frame_index
Definition ConFileIO.h:72
std::vector< ConMetadataValue > scalars
Definition ConFileIO.h:79
std::vector< double > displacements
Per-atom displacement, row-major N x 3 in Angstrom (a normal mode, a dimer direction).
Definition ConFileIO.h:85