Loading...
Searching...
No Matches
MinModeSaddleSearch.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*/
15#include "eon/EonLogger.h"
16#include "eon/EpiCenters.h"
17#include "eon/HelperFunctions.h"
19#include "eon/SaddleSearchJob.h"
20#include "eon/SafeMath.h"
21#include "eon/eonExceptions.hpp"
22
23#include <algorithm>
24#include <cmath>
25#include <format>
26#include <fstream>
27#include <memory>
28#include <stdexcept>
29#include <string>
30
31namespace eonc {
32
34private:
35 std::shared_ptr<Matter> matter;
37 std::shared_ptr<EigenmodeStrategy> minModeMethod;
38 int iteration{0};
39
40public:
42 std::shared_ptr<Matter> matterPassed,
43 std::shared_ptr<EigenmodeStrategy> minModeMethodPassed,
44 AtomMatrix modePassed, const Parameters &paramsPassed)
45 : ObjectiveFunction(paramsPassed),
46 matter{std::move(matterPassed)},
47 minModeMethod{minModeMethodPassed},
48 eigenvector{std::move(modePassed)} {}
49
50 ~MinModeObjectiveFunction() override = default;
51
52 // The lowest mode's curvature, once the mode is known and negative: the
53 // saddle direction's own scale, so the optimizer's first step needs no
54 // finite-difference probe.
55 std::optional<double> knownCurvature() const override {
56 if (iteration == 0 || !minModeMethod) {
57 return std::nullopt;
58 }
60 if (!std::isfinite(c) || c >= 0.0) {
61 return std::nullopt;
62 }
63 return -c;
64 }
65
66 VectorXd getGradient(bool fdstep = false) {
67 AtomMatrix force = matter->getForces();
68
69 if (!fdstep || iteration == 0) {
71 // Check if ImprovedDimer lost the mode
73 if (dimer && !dimer->rotationDidConverge) {
74 if (dimer->getEigenvalue() < 0.0) {
77 "[MinMode] Dimer restored to best state with C_tau={:.4f}",
78 dimer->getEigenvalue());
80 } else {
82 }
83 }
84 iteration++;
85 }
86
88 double eigenvalue = eonc::eigenmodeGetEigenvalue(*minModeMethod);
89
90 AtomMatrix proj = matDot(force, eigenvector) *
91 eonc::safemath::safe_normalized(eigenvector);
92
93 if (eigenvalue > 0.0) {
94 if (params.saddle_search_options().perp_force_ratio > 0.0) {
95 double d = params.saddle_search_options().perp_force_ratio;
96 force = d * force - (1.0 + d) * proj;
97 } else if (params.saddle_search_options().confine_positive.enabled) {
98 if (params.saddle_search_options().confine_positive.bowl_breakout) {
99 AtomMatrix forceTemp = matter->getForces();
100 const long nAtoms = matter->numberOfAtoms();
101 int nBowlActive = static_cast<int>(std::min<long>(
102 params.saddle_search_options().confine_positive.bowl_active,
103 nAtoms));
104 if (nBowlActive <= 0) {
105 force.setZero();
106 } else {
107 std::vector<int> indices_max(nBowlActive);
108
109 // Find the nBowlActive atoms with largest forces
110 for (int j = 0; j < nBowlActive; j++) {
111 double f_max = forceTemp.row(0).norm();
112 int i_max = 0;
113 for (long i = 0; i < matter->numberOfAtoms(); i++) {
114 if (f_max < forceTemp.row(i).norm()) {
115 f_max = forceTemp.row(i).norm();
116 i_max = static_cast<int>(i);
117 }
118 }
119 forceTemp.row(i_max).setZero();
120 indices_max[j] = i_max;
121 }
122 forceTemp.setZero();
123 for (int j = 0; j < nBowlActive; j++) {
124 forceTemp.row(indices_max[j]) = -proj.row(indices_max[j]);
125 }
126 force = forceTemp;
127 }
128 } else {
129 int sufficientForce = 0;
130 double minForce =
131 params.saddle_search_options().confine_positive.min_force;
132 const long maxBoostTries = std::max(
133 3 * matter->numberOfAtoms(),
134 params.saddle_search_options().confine_positive.min_active);
135 long boostTries = 0;
136 while (
137 sufficientForce <
138 params.saddle_search_options().confine_positive.min_active &&
139 boostTries < maxBoostTries) {
140 sufficientForce = 0;
141 force = matter->getForces();
142 for (long i = 0; i < matter->numberOfAtoms(); i++) {
143 for (int k = 0; k < 3; k++) {
144 if (std::abs(force(i, k)) < minForce) {
145 force(i, k) = 0;
146 } else {
147 sufficientForce++;
148 force(i, k) =
149 -params.saddle_search_options().confine_positive.boost *
150 proj(i, k);
151 }
152 }
153 }
154 minForce *=
155 params.saddle_search_options().confine_positive.scale_ratio;
156 boostTries++;
157 }
158 }
159 } else {
160 force = -proj;
161 }
162 } else {
163 force += -2.0 * proj;
164 }
165
166 VectorXd forceV = VectorXd::Map(force.data(), 3 * matter->numberOfAtoms());
167 return -forceV;
168 }
169
170 double getEnergy() { return matter->getPotentialEnergy(); }
171 void setPositions(const VectorXd &x) { matter->setPositionsV(x); }
172 VectorXd getPositions() { return matter->getPositionsV(); }
173 int degreesOfFreedom() { return 3 * matter->numberOfAtoms(); }
174 bool isConverged() {
175 return getConvergence() < params.saddle_search_options().converged_force;
176 }
177
178 double getConvergence() {
179 if (params.optimizer_options().convergence_metric == "norm") {
180 return matter->getForcesFreeV().norm();
181 } else if (params.optimizer_options().convergence_metric == "max_atom") {
182 return matter->maxForce();
183 } else if (params.optimizer_options().convergence_metric ==
184 "max_component") {
185 return matter->getForces().cwiseAbs().maxCoeff();
186 } else {
187 EONC_LOG_CRITICAL("[MinModeSaddleSearch] unknown convergence metric: {}",
188 params.optimizer_options().convergence_metric);
189 throw std::invalid_argument(
190 std::format("[MinModeSaddleSearch] unknown convergence_metric: {}",
191 params.optimizer_options().convergence_metric));
192 }
193 }
194
195 VectorXd difference(const VectorXd &a, const VectorXd &b) {
196 return matter->pbcV(a - b);
197 }
198};
199
200MinModeSaddleSearch::MinModeSaddleSearch(std::shared_ptr<Matter> matterPassed,
201 AtomMatrix modePassed,
202 double reactantEnergyPassed,
203 const Parameters &parametersPassed,
204 std::shared_ptr<Potential> potPassed)
205 : SaddleSearchMethod(potPassed, parametersPassed),
206 matter{matterPassed} {
208 params.optimizer_options().convergence_metric, "[MinModeSaddleSearch]");
209 reactantEnergy = reactantEnergyPassed;
210 mode = modePassed;
211 initialTangent_ = modePassed;
213 iteration = 0;
214
216
217 // Set reference mode for ImprovedDimer (prevents mode switching)
218 if (auto *dimer = eonc::asImprovedDimer(*minModeMethod)) {
219 VectorXd refVec = VectorXd::Map(mode.data(), 3 * matter->numberOfAtoms());
220 refVec = refVec.array() * matter->getFreeV().array();
221 dimer->setReferenceMode(refVec);
222 }
223}
224
226 return run(params.saddle_search_options().max_iterations);
227}
228
229int MinModeSaddleSearch::runRetainFrames(long max_iterations_override) {
231 climb_frames_.clear();
232 const long maxIter = max_iterations_override < 0
233 ? params.saddle_search_options().max_iterations
234 : max_iterations_override;
235 const int st = run(maxIter);
236 retain_climb_frames_ = false;
237 return st;
238}
239
240int MinModeSaddleSearch::run(long max_iterations_override) {
241 long effectiveMaxIter = max_iterations_override;
242 QUILL_LOG_DEBUG(
243 log, "Saddle point search started from reactant with energy {} eV.",
245
246 int optStatus;
247 bool firstIteration = true;
248 const char *forceLabel =
249 params.optimizer_options().convergence_metric_label.c_str();
250
251 if (params.saddle_search_options().minmode_method ==
253 QUILL_LOG_DEBUG(
254 log, "================= Using the GP Dimer Library =================");
257 QUILL_LOG_DEBUG(log, "GPR eigenvalue: {}",
260 return status;
261 }
262 if (getEigenvalue() > 0.0 && status == STATUS_GOOD) {
263 QUILL_LOG_DEBUG(log, "[MinModeSaddleSearch] eigenvalue not negative");
265 }
267 params.saddle_search_options().zero_mode_abort_curvature) {
268 QUILL_LOG_DEBUG(log, "Zero mode eigenvalue: {}",
271 }
274 } else {
275
276 if (params.saddle_search_options().minmode_method ==
278 QUILL_LOG_INFO(log,
279 "[Dimer] {:9s} {:9s} {:10s} {:18s} {:9s} "
280 "{:7s} {:6s} {:4s} {:5s}\n",
281 "Step", "Step Size", "Delta E", forceLabel, "Curvature",
282 "Torque", "Angle", "Rots", "Align");
283 } else if (params.saddle_search_options().minmode_method ==
285 QUILL_LOG_INFO(
286 log,
287 "[Lanczos] {:9s} {:9s} {:10s} {:18s} {:9s} {:10s} {:7s} {:5s}\n",
288 "Step", "Step Size", "Delta E", forceLabel, "Curvature", "Rel Change",
289 "Angle", "Iters");
290 } else if (params.saddle_search_options().minmode_method ==
292 QUILL_LOG_INFO(log,
293 "[GPRDimer] {:9s} {:9s} {:10s} {:18s} {:9s} "
294 " {:7s} {:6s} {:4s}\n",
295 "Step", "Step Size", "Delta E", forceLabel, "Curvature",
296 "Torque", "Angle", "Rots");
297 }
298
299 std::string climbLabel = "climb";
300 std::string climbDatFilename = "climb.dat";
301
302 AtomMatrix initialPosition = matter->getPositions();
303
304 auto objf = std::make_shared<MinModeObjectiveFunction>(
306 auto write_climb_frame = [&](uint64_t frameIndex, bool append,
307 double stepSize, double de, double conv,
308 double eigenval, double torque, double angle,
309 long rotations) {
311 metadata.frame_index = frameIndex;
312 metadata.energy = matter->getPotentialEnergy();
313 metadata.scalars.push_back({"step_size", stepSize});
314 metadata.scalars.push_back({"delta_e", de});
315 metadata.scalars.push_back({"convergence", conv});
316 metadata.scalars.push_back({"eigenvalue", eigenval});
317 metadata.scalars.push_back({"torque", torque});
318 metadata.scalars.push_back({"angle", angle});
319 metadata.scalars.push_back({"rotations", static_cast<double>(rotations)});
321 climb_frames_.push_back(eonc::io::matterToConFrame(*matter, &metadata));
322 }
323 if (params.debug_options().write_movies) {
324 if (!eonc::io::io_ok(
325 matter->matter2con(climbLabel, append, &metadata))) {
326 QUILL_LOG_WARNING(log, "Failed to write climb movie frame {}",
327 climbLabel);
328 }
329 eonc::helpers::saveMode(std::format("mode_{:03}.dat", frameIndex),
330 matter,
332 }
333
334 if (params.debug_options().write_deprecated_outs) {
335 std::ofstream climbDat(climbDatFilename,
336 append ? (std::ios::binary | std::ios::app)
337 : std::ios::binary);
338 if (climbDat) {
339 if (!append) {
340 climbDat << "iteration\tstep_size\tdelta_e\tconvergence"
341 "\teigenvalue\ttorque\tangle\trotations\n";
342 }
343 climbDat << std::format("{}\t{:.7e}\t{:.6f}\t{:.5e}\t{:.6f}"
344 "\t{:.6f}\t{:.4f}\t{}\n",
345 frameIndex, stepSize, de, conv, eigenval,
346 torque, angle, rotations);
347 }
348 }
349 };
350 if (params.debug_options().write_movies || retain_climb_frames_) {
351 write_climb_frame(0, false, 0.0, 0.0, objf->getConvergence(),
353 0);
354 }
355 if (params.saddle_search_options().nonnegative_displacement_abort) {
356 objf->getGradient();
358 QUILL_LOG_DEBUG(log, "Nonnegative eigenvalue: {}",
361 return status;
362 }
363 }
364
366 objf, params.optimizer_options().method, params);
367
368 while (!objf->isConverged() || iteration == 0) {
369
370 if (!firstIteration) {
371
372 if (params.saddle_search_options().nonlocal_count_abort != 0) {
374 initialPosition - matter->getPositions(),
375 params.saddle_search_options().nonlocal_distance_abort);
376 if (nm >= params.saddle_search_options().nonlocal_count_abort) {
378 break;
379 }
380 }
381
383 params.saddle_search_options().zero_mode_abort_curvature) {
384 QUILL_LOG_DEBUG(log, "Zero mode eigenvalue: {}",
387 break;
388 }
389 }
390 firstIteration = false;
391
392 if (iteration >= effectiveMaxIter) {
394 break;
395 }
396
397 AtomMatrix pos = matter->getPositions();
398
399 try {
400 if (params.saddle_search_options().confine_positive.bowl_breakout &&
402 params.optimizer_options().method == OptType::CG) {
403 optStatus = optim->step(-params.optimizer_options().max_move);
404 } else {
405 optStatus = optim->step(params.optimizer_options().max_move);
406 }
407 } catch (const eonc::DimerModeRestoredException &) {
408 QUILL_LOG_DEBUG(
409 log, "Dimer restored to best state. Checking convergence...");
410 status = objf->isConverged() ? STATUS_GOOD : STATUS_DIMER_RESTORED_BEST;
411 break;
412 } catch (const eonc::DimerModeLostException &) {
413 QUILL_LOG_WARNING(log, "Dimer lost mode completely. Aborting.");
415 break;
416 }
417
418 if (optStatus < 0) {
420 break;
421 }
422
423 double de = objf->getEnergy() - reactantEnergy;
424
425 // Melander, Laasonen, Jonsson, JCTC 11(3), 1055-1062, 2015
426 if (params.saddle_search_options().remove_rotation) {
428 }
429 double stepSize = (matter->pbc(matter->getPositions() - pos)).norm();
430
431 iteration++;
432
433 // Logging
438 double conv = objf->getConvergence();
439
440 if (params.saddle_search_options().minmode_method ==
442 QUILL_LOG_DEBUG(
443 log,
444 "[Lanczos] {:9} {:9.6f} {:10.4f} {:18.5e} {:9.4f} {:10.6f} "
445 "{:7.3f} {:5}\n",
446 iteration, stepSize, de, conv, eigenval, torque, angle, rotations);
447 } else {
448 QUILL_LOG_DEBUG(
449 log,
450 "[Dimer] {:9} {:9.7f} {:10.4f} {:18.5e} {:9.4f} {:7.3f} "
451 " {:6.3f} {:4}\n",
452 iteration, stepSize, de, conv, eigenval, torque, angle, rotations);
453 }
454
455 if (params.debug_options().write_movies || retain_climb_frames_) {
456 write_climb_frame(static_cast<uint64_t>(iteration), true, stepSize, de,
457 conv, eigenval, torque, angle, rotations);
458 }
459
460 if (params.main_options().checkpoint) {
461 if (!eonc::io::io_ok(
462 matter->matter2con("displacement_cp.con", false))) {
463 QUILL_LOG_WARNING(log, "Failed to write displacement_cp.con");
464 }
465 eonc::helpers::saveMode("mode_cp.dat", matter,
467 }
468
469 if (de > params.saddle_search_options().max_energy) {
471 break;
472 }
473
474 // Check ImprovedDimer mode convergence
475 if (auto *dimer = eonc::asImprovedDimer(*minModeMethod)) {
476 if (!dimer->rotationDidConverge) {
477 status = (dimer->getEigenvalue() < 0.0) ? STATUS_DIMER_RESTORED_BEST
480 QUILL_LOG_DEBUG(log, "Dimer restored to valid state. C_tau={:.4f}",
481 dimer->getEigenvalue());
482 }
483 break;
484 }
485 }
486 }
487
488 if (iteration == 0) {
490 }
491
492 // Never report STATUS_GOOD when the climb objective is unconverged
493 // (issue #20: unfeasible systems must not look like success).
494 const bool climbConverged = objf->isConverged();
495 const int statusBeforeGuard = status;
496 status = finalizeClimbStatus(status, climbConverged);
497 if (statusBeforeGuard == STATUS_GOOD && status != STATUS_GOOD) {
498 QUILL_LOG_WARNING(log, "[MinModeSaddleSearch] objective not converged; "
499 "refusing STATUS_GOOD");
500 }
501
502 if (getEigenvalue() > 0.0 && status == STATUS_GOOD) {
503 QUILL_LOG_DEBUG(log, "[MinModeSaddleSearch] eigenvalue not negative");
505 }
507 }
508
509 return status;
510}
511
515
519
520} // namespace eonc
Direct optimization for energy minimization.
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
#define EONC_LOG_DEBUG(...)
Definition EonLogger.h:243
#define EONC_LOG_CRITICAL(...)
Definition EonLogger.h:267
Finds transition states by finding saddle points on the potential energy surface.
static const char MINMODE_GPRDIMER[]
static const char MINMODE_DIMER[]
static const char MINMODE_LANCZOS[]
~MinModeObjectiveFunction() override=default
VectorXd getGradient(bool fdstep=false)
VectorXd difference(const VectorXd &a, const VectorXd &b)
std::shared_ptr< EigenmodeStrategy > minModeMethod
std::optional< double > knownCurvature() const override
MinModeObjectiveFunction(std::shared_ptr< Matter > matterPassed, std::shared_ptr< EigenmodeStrategy > minModeMethodPassed, AtomMatrix modePassed, const Parameters &paramsPassed)
std::shared_ptr< Matter > matter
MinModeSaddleSearch(std::shared_ptr< Matter > matterPassed, AtomMatrix modePassed, double reactantEnergyPassed, const Parameters &parametersPassed, std::shared_ptr< Potential > potPassed)
int runRetainFrames(long max_iterations_override=-1)
Like run(), but also retain climb ConFrames in memory (same stamps as write_movies climb CON).
std::vector< readcon::ConFrame > climb_frames_
std::shared_ptr< Matter > matter
static constexpr int finalizeClimbStatus(int climbStatus, bool objectiveConverged) noexcept
Issue #20 policy: never leave climb as STATUS_GOOD when the climb objective is still unconverged (unf...
std::shared_ptr< EigenmodeStrategy > minModeMethod
ObjectiveFunction(const Parameters &paramsPassed)
const Parameters & params
std::shared_ptr< Potential > pot
SaddleSearchMethod(std::shared_ptr< Potential > potPassed, const Parameters &paramsPassed)
long numAtomsMoved(const AtomMatrix v1, double cutoff)
void rotationRemove(const AtomMatrix r1, std::shared_ptr< Matter > m2)
std::unique_ptr< Optimizer > mkOptim(std::shared_ptr< ObjectiveFunction > a_objf, OptType a_otype, const Parameters &a_params)
Definition Optimizer.cpp:24
void saveMode(FILE *modeFile, std::shared_ptr< Matter > matter, AtomMatrix mode)
Write a mode; constrained axes are emitted as 0.
void requireKnownConvergenceMetric(std::string_view metric, std::string_view context)
Throws std::invalid_argument naming context when metric is unrecognized.
readcon::ConFrame matterToConFrame(Matter &m, const ConFrameMetadata *metadata)
Build a single stamped ConFrame from Matter (same builder as matter2con).
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
RAII resource manager for the ARTn C library with global synchronization.
void eigenmodeCompute(LowestEigenmode &s, std::shared_ptr< Matter > matter, AtomMatrix direction)
long eigenmodeTotalForceCalls(LowestEigenmode &s)
std::shared_ptr< LowestEigenmode > buildEigenmodeStrategy(std::shared_ptr< Matter > matter, const Parameters &params, std::shared_ptr< Potential > pot)
long eigenmodeStatsRotations(LowestEigenmode &s)
ImprovedDimer * asImprovedDimer(LowestEigenmode &s)
long eigenmodeTotalIterations(LowestEigenmode &s)
double eigenmodeGetEigenvalue(LowestEigenmode &s)
double eigenmodeStatsTorque(LowestEigenmode &s)
AtomMatrix eigenmodeGetEigenvector(LowestEigenmode &s)
double eigenmodeStatsAngle(LowestEigenmode &s)
std::optional< uint64_t > frame_index
Definition ConFileIO.h:72
std::vector< ConMetadataValue > scalars
Definition ConFileIO.h:79
std::optional< double > energy
Definition ConFileIO.h:73