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 <cmath>
24#include <format>
25#include <fstream>
26#include <memory>
27#include <stdexcept>
28#include <string>
29
30using namespace eonc::helpers;
31
33private:
34 std::shared_ptr<Matter> matter;
36 std::shared_ptr<EigenmodeStrategy> minModeMethod;
37 int iteration{0};
38
39public:
41 std::shared_ptr<Matter> matterPassed,
42 std::shared_ptr<EigenmodeStrategy> minModeMethodPassed,
43 AtomMatrix modePassed, const Parameters &paramsPassed)
44 : ObjectiveFunction(paramsPassed),
45 matter{std::move(matterPassed)},
46 minModeMethod{minModeMethodPassed},
47 eigenvector{std::move(modePassed)} {}
48
49 ~MinModeObjectiveFunction() override = default;
50
51 VectorXd getGradient(bool fdstep = false) {
52 AtomMatrix force = matter->getForces();
53
54 if (!fdstep || iteration == 0) {
56 // Check if ImprovedDimer lost the mode
58 if (dimer && !dimer->rotationDidConverge) {
59 if (dimer->getEigenvalue() < 0.0) {
62 "[MinMode] Dimer restored to best state with C_tau={:.4f}",
63 dimer->getEigenvalue());
65 } else {
67 }
68 }
69 iteration++;
70 }
71
73 double eigenvalue = eonc::eigenmodeGetEigenvalue(*minModeMethod);
74
75 AtomMatrix proj = matDot(force, eigenvector) *
76 eonc::safemath::safe_normalized(eigenvector);
77
78 if (eigenvalue > 0.0) {
79 if (params.saddle_search_options.perp_force_ratio > 0.0) {
80 double d = params.saddle_search_options.perp_force_ratio;
81 force = d * force - (1.0 + d) * proj;
82 } else if (params.saddle_search_options.confine_positive.enabled) {
83 if (params.saddle_search_options.confine_positive.bowl_breakout) {
84 AtomMatrix forceTemp = matter->getForces();
85 int nBowlActive =
86 params.saddle_search_options.confine_positive.bowl_active;
87 std::vector<int> indices_max(nBowlActive);
88
89 // Find the nBowlActive atoms with largest forces
90 for (int j = 0; j < nBowlActive; j++) {
91 double f_max = forceTemp.row(0).norm();
92 int i_max = 0;
93 for (long i = 0; i < matter->numberOfAtoms(); i++) {
94 if (f_max < forceTemp.row(i).norm()) {
95 f_max = forceTemp.row(i).norm();
96 i_max = static_cast<int>(i);
97 }
98 }
99 forceTemp.row(i_max).setZero();
100 indices_max[j] = i_max;
101 }
102 forceTemp.setZero();
103 for (int j = 0; j < nBowlActive; j++) {
104 forceTemp.row(indices_max[j]) = -proj.row(indices_max[j]);
105 }
106 force = forceTemp;
107 } else {
108 int sufficientForce = 0;
109 double minForce =
110 params.saddle_search_options.confine_positive.min_force;
111 while (sufficientForce <
112 params.saddle_search_options.confine_positive.min_active) {
113 sufficientForce = 0;
114 force = matter->getForces();
115 for (long i = 0; i < matter->numberOfAtoms(); i++) {
116 for (int k = 0; k < 3; k++) {
117 if (std::abs(force(i, k)) < minForce) {
118 force(i, k) = 0;
119 } else {
120 sufficientForce++;
121 force(i, k) =
122 -params.saddle_search_options.confine_positive.boost *
123 proj(i, k);
124 }
125 }
126 }
127 minForce *=
128 params.saddle_search_options.confine_positive.scale_ratio;
129 }
130 }
131 } else {
132 force = -proj;
133 }
134 } else {
135 force += -2.0 * proj;
136 }
137
138 VectorXd forceV = VectorXd::Map(force.data(), 3 * matter->numberOfAtoms());
139 return -forceV;
140 }
141
142 double getEnergy() { return matter->getPotentialEnergy(); }
143 void setPositions(const VectorXd &x) { matter->setPositionsV(x); }
144 VectorXd getPositions() { return matter->getPositionsV(); }
145 int degreesOfFreedom() { return 3 * matter->numberOfAtoms(); }
146 bool isConverged() {
147 return getConvergence() < params.saddle_search_options.converged_force;
148 }
149
150 double getConvergence() {
151 if (params.optimizer_options.convergence_metric == "norm") {
152 return matter->getForcesFreeV().norm();
153 } else if (params.optimizer_options.convergence_metric == "max_atom") {
154 return matter->maxForce();
155 } else if (params.optimizer_options.convergence_metric == "max_component") {
156 return matter->getForces().maxCoeff();
157 } else {
158 EONC_LOG_CRITICAL("[MinModeSaddleSearch] unknown convergence metric: {}",
159 params.optimizer_options.convergence_metric);
160 throw std::invalid_argument(
161 std::format("[MinModeSaddleSearch] unknown convergence_metric: {}",
162 params.optimizer_options.convergence_metric));
163 }
164 }
165
166 VectorXd difference(const VectorXd &a, const VectorXd &b) {
167 return matter->pbcV(a - b);
168 }
169};
170
171MinModeSaddleSearch::MinModeSaddleSearch(std::shared_ptr<Matter> matterPassed,
172 AtomMatrix modePassed,
173 double reactantEnergyPassed,
174 const Parameters &parametersPassed,
175 std::shared_ptr<Potential> potPassed)
176 : SaddleSearchMethod(potPassed, parametersPassed),
177 matter{matterPassed} {
179 params.optimizer_options.convergence_metric, "[MinModeSaddleSearch]");
180 reactantEnergy = reactantEnergyPassed;
181 mode = modePassed;
182 initialTangent_ = modePassed;
184 iteration = 0;
185
187
188 // Set reference mode for ImprovedDimer (prevents mode switching)
189 if (auto *dimer = eonc::asImprovedDimer(*minModeMethod)) {
190 VectorXd refVec = VectorXd::Map(mode.data(), 3 * matter->numberOfAtoms());
191 refVec = refVec.array() * matter->getFreeV().array();
192 dimer->setReferenceMode(refVec);
193 }
194}
195
197 return run(params.saddle_search_options.max_iterations);
198}
199
200int MinModeSaddleSearch::runRetainFrames(long max_iterations_override) {
202 climb_frames_.clear();
203 const long maxIter = max_iterations_override < 0
204 ? params.saddle_search_options.max_iterations
205 : max_iterations_override;
206 const int st = run(maxIter);
207 retain_climb_frames_ = false;
208 return st;
209}
210
211int MinModeSaddleSearch::run(long max_iterations_override) {
212 long effectiveMaxIter = max_iterations_override;
213 QUILL_LOG_DEBUG(
214 log, "Saddle point search started from reactant with energy {} eV.",
216
217 int optStatus;
218 bool firstIteration = true;
219 const char *forceLabel =
220 params.optimizer_options.convergence_metric_label.c_str();
221
222 if (params.saddle_search_options.minmode_method ==
224 QUILL_LOG_DEBUG(
225 log, "================= Using the GP Dimer Library =================");
228 QUILL_LOG_DEBUG(log, "GPR eigenvalue: {}",
231 }
232 if (getEigenvalue() > 0.0 && status == STATUS_GOOD) {
233 QUILL_LOG_DEBUG(log, "[MinModeSaddleSearch] eigenvalue not negative");
235 }
237 params.saddle_search_options.zero_mode_abort_curvature) {
238 QUILL_LOG_DEBUG(log, "Zero mode eigenvalue: {}",
241 }
244 } else {
245
246 if (params.saddle_search_options.minmode_method ==
248 QUILL_LOG_INFO(log,
249 "[Dimer] {:9s} {:9s} {:10s} {:18s} {:9s} "
250 "{:7s} {:6s} {:4s} {:5s}\n",
251 "Step", "Step Size", "Delta E", forceLabel, "Curvature",
252 "Torque", "Angle", "Rots", "Align");
253 } else if (params.saddle_search_options.minmode_method ==
255 QUILL_LOG_INFO(
256 log,
257 "[Lanczos] {:9s} {:9s} {:10s} {:18s} {:9s} {:10s} {:7s} {:5s}\n",
258 "Step", "Step Size", "Delta E", forceLabel, "Curvature", "Rel Change",
259 "Angle", "Iters");
260 } else if (params.saddle_search_options.minmode_method ==
262 QUILL_LOG_INFO(log,
263 "[GPRDimer] {:9s} {:9s} {:10s} {:18s} {:9s} "
264 " {:7s} {:6s} {:4s}\n",
265 "Step", "Step Size", "Delta E", forceLabel, "Curvature",
266 "Torque", "Angle", "Rots");
267 }
268
269 std::string climbLabel = "climb";
270 std::string climbDatFilename = "climb.dat";
271
272 AtomMatrix initialPosition = matter->getPositions();
273
274 auto objf = std::make_shared<MinModeObjectiveFunction>(
276 auto write_climb_frame = [&](uint64_t frameIndex, bool append,
277 double stepSize, double de, double conv,
278 double eigenval, double torque, double angle,
279 long rotations) {
281 metadata.frame_index = frameIndex;
282 metadata.energy = matter->getPotentialEnergy();
283 metadata.scalars.push_back({"step_size", stepSize});
284 metadata.scalars.push_back({"delta_e", de});
285 metadata.scalars.push_back({"convergence", conv});
286 metadata.scalars.push_back({"eigenvalue", eigenval});
287 metadata.scalars.push_back({"torque", torque});
288 metadata.scalars.push_back({"angle", angle});
289 metadata.scalars.push_back({"rotations", static_cast<double>(rotations)});
291 climb_frames_.push_back(eonc::io::matterToConFrame(*matter, &metadata));
292 }
293 if (params.debug_options.write_movies) {
294 if (!eonc::io::io_ok(
295 matter->matter2con(climbLabel, append, &metadata))) {
296 QUILL_LOG_WARNING(log, "Failed to write climb movie frame {}",
297 climbLabel);
298 }
299 }
300
301 if (params.debug_options.write_deprecated_outs) {
302 std::ofstream climbDat(climbDatFilename,
303 append ? (std::ios::binary | std::ios::app)
304 : std::ios::binary);
305 if (climbDat) {
306 if (!append) {
307 climbDat << "iteration\tstep_size\tdelta_e\tconvergence"
308 "\teigenvalue\ttorque\tangle\trotations\n";
309 }
310 climbDat << std::format("{}\t{:.7e}\t{:.6f}\t{:.5e}\t{:.6f}"
311 "\t{:.6f}\t{:.4f}\t{}\n",
312 frameIndex, stepSize, de, conv, eigenval,
313 torque, angle, rotations);
314 }
315 }
316 };
317 if (params.debug_options.write_movies || retain_climb_frames_) {
318 write_climb_frame(0, false, 0.0, 0.0, objf->getConvergence(),
320 0);
321 }
322 if (params.saddle_search_options.nonnegative_displacement_abort) {
323 objf->getGradient();
325 QUILL_LOG_DEBUG(log, "Nonnegative eigenvalue: {}",
328 }
329 }
330
332 objf, params.optimizer_options.method, params);
333
334 while (!objf->isConverged() || iteration == 0) {
335
336 if (!firstIteration) {
337
338 if (params.saddle_search_options.nonlocal_count_abort != 0) {
339 long nm = numAtomsMoved(
340 initialPosition - matter->getPositions(),
341 params.saddle_search_options.nonlocal_distance_abort);
342 if (nm >= params.saddle_search_options.nonlocal_count_abort) {
344 break;
345 }
346 }
347
349 params.saddle_search_options.zero_mode_abort_curvature) {
350 QUILL_LOG_DEBUG(log, "Zero mode eigenvalue: {}",
353 break;
354 }
355 }
356 firstIteration = false;
357
358 if (iteration >= effectiveMaxIter) {
360 break;
361 }
362
363 AtomMatrix pos = matter->getPositions();
364
365 try {
366 if (params.saddle_search_options.confine_positive.bowl_breakout &&
368 params.optimizer_options.method == OptType::CG) {
369 optStatus = optim->step(-params.optimizer_options.max_move);
370 } else {
371 optStatus = optim->step(params.optimizer_options.max_move);
372 }
373 } catch (const eonc::DimerModeRestoredException &) {
374 QUILL_LOG_DEBUG(
375 log, "Dimer restored to best state. Checking convergence...");
376 status = objf->isConverged() ? STATUS_GOOD : STATUS_DIMER_RESTORED_BEST;
377 break;
378 } catch (const eonc::DimerModeLostException &) {
379 QUILL_LOG_WARNING(log, "Dimer lost mode completely. Aborting.");
381 break;
382 }
383
384 if (optStatus < 0) {
386 break;
387 }
388
389 double de = objf->getEnergy() - reactantEnergy;
390
391 // Melander, Laasonen, Jonsson, JCTC 11(3), 1055-1062, 2015
392 if (params.saddle_search_options.remove_rotation) {
394 }
395 double stepSize = (matter->pbc(matter->getPositions() - pos)).norm();
396
397 iteration++;
398
399 // Logging
404 double conv = objf->getConvergence();
405
406 if (params.saddle_search_options.minmode_method ==
408 QUILL_LOG_DEBUG(
409 log,
410 "[Lanczos] {:9} {:9.6f} {:10.4f} {:18.5e} {:9.4f} {:10.6f} "
411 "{:7.3f} {:5}\n",
412 iteration, stepSize, de, conv, eigenval, torque, angle, rotations);
413 } else {
414 QUILL_LOG_DEBUG(
415 log,
416 "[Dimer] {:9} {:9.7f} {:10.4f} {:18.5e} {:9.4f} {:7.3f} "
417 " {:6.3f} {:4}\n",
418 iteration, stepSize, de, conv, eigenval, torque, angle, rotations);
419 }
420
421 if (params.debug_options.write_movies || retain_climb_frames_) {
422 write_climb_frame(static_cast<uint64_t>(iteration), true, stepSize, de,
423 conv, eigenval, torque, angle, rotations);
424 }
425
426 if (params.main_options.checkpoint) {
427 if (!eonc::io::io_ok(
428 matter->matter2con("displacement_cp.con", false))) {
429 QUILL_LOG_WARNING(log, "Failed to write displacement_cp.con");
430 }
431 eonc::helpers::saveMode("mode_cp.dat", matter,
433 }
434
435 if (de > params.saddle_search_options.max_energy) {
437 break;
438 }
439
440 // Check ImprovedDimer mode convergence
441 if (auto *dimer = eonc::asImprovedDimer(*minModeMethod)) {
442 if (!dimer->rotationDidConverge) {
443 status = (dimer->getEigenvalue() < 0.0) ? STATUS_DIMER_RESTORED_BEST
446 QUILL_LOG_DEBUG(log, "Dimer restored to valid state. C_tau={:.4f}",
447 dimer->getEigenvalue());
448 }
449 break;
450 }
451 }
452 }
453
454 if (iteration == 0) {
456 }
457
458 // Never report STATUS_GOOD when the climb objective is unconverged
459 // (issue #20: unfeasible systems must not look like success).
460 const bool climbConverged = objf->isConverged();
461 const int statusBeforeGuard = status;
462 status = finalizeClimbStatus(status, climbConverged);
463 if (statusBeforeGuard == STATUS_GOOD && status != STATUS_GOOD) {
464 QUILL_LOG_WARNING(log, "[MinModeSaddleSearch] objective not converged; "
465 "refusing STATUS_GOOD");
466 }
467
468 if (getEigenvalue() > 0.0 && status == STATUS_GOOD) {
469 QUILL_LOG_DEBUG(log, "[MinModeSaddleSearch] eigenvalue not negative");
471 }
472 }
473
474 return status;
475}
476
480
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:244
#define EONC_LOG_CRITICAL(...)
Definition EonLogger.h:268
Finds transition states by finding saddle points on the potential energy surface.
std::shared_ptr< Matter > matter
MinModeObjectiveFunction(std::shared_ptr< Matter > matterPassed, std::shared_ptr< EigenmodeStrategy > minModeMethodPassed, AtomMatrix modePassed, const Parameters &paramsPassed)
~MinModeObjectiveFunction() override=default
void setPositions(const VectorXd &x)
VectorXd difference(const VectorXd &a, const VectorXd &b)
VectorXd getGradient(bool fdstep=false)
std::shared_ptr< EigenmodeStrategy > minModeMethod
MinModeSaddleSearch(std::shared_ptr< Matter > matterPassed, AtomMatrix modePassed, double reactantEnergyPassed, const Parameters &parametersPassed, std::shared_ptr< Potential > potPassed)
static const char MINMODE_DIMER[]
static const char MINMODE_GPRDIMER[]
static const char MINMODE_LANCZOS[]
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)
std::unique_ptr< Optimizer > mkOptim(std::shared_ptr< ObjectiveFunction > a_objf, OptType a_otype, const Parameters &a_params)
Definition Optimizer.cpp:21
long numAtomsMoved(const AtomMatrix v1, double cutoff)
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.
void rotationRemove(const AtomMatrix r1, std::shared_ptr< Matter > m2)
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
double eigenmodeStatsAngle(EigenmodeStrategy &s)
AtomMatrix eigenmodeGetEigenvector(EigenmodeStrategy &s)
Dispatch getEigenvector() to the active variant.
ImprovedDimer * asImprovedDimer(EigenmodeStrategy &s)
Access ImprovedDimer-specific features.
double eigenmodeStatsTorque(EigenmodeStrategy &s)
std::shared_ptr< EigenmodeStrategy > buildEigenmodeStrategy(std::shared_ptr< Matter > matter, const Parameters &params, std::shared_ptr< Potential > pot)
Build the eigenmode solver from parameters.
void eigenmodeCompute(EigenmodeStrategy &s, std::shared_ptr< Matter > matter, AtomMatrix direction)
Dispatch compute() to the active variant.
double eigenmodeGetEigenvalue(EigenmodeStrategy &s)
Dispatch getEigenvalue() to the active variant.
long eigenmodeStatsRotations(EigenmodeStrategy &s)
long eigenmodeTotalForceCalls(EigenmodeStrategy &s)
Read stats from any variant (all inherit LowestEigenmode stats fields).
long eigenmodeTotalIterations(EigenmodeStrategy &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