Loading...
Searching...
No Matches
NEBOcinebController.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/EonLogger.h"
16#include "eon/eonExceptions.hpp"
17#include <algorithm>
18#include <cmath>
19
20namespace eonc::neb {
21
24 auto &ci = params.neb_options.climbing_image;
25 auto &r = ci.ocineb;
26 return Config{
27 r.use_mmf,
28 r.trigger_force,
29 r.trigger_factor,
30 r.max_steps,
31 r.ci_stability_count,
32 r.angle_tol,
34 };
35}
36
39
40void OCINEBController::initBaseline(double baseline_force) {
41 baseline_force_ = baseline_force;
42 current_threshold_ = baseline_force_ * cfg_.trigger_factor;
43}
44
45bool OCINEBController::shouldTrigger(double convForce, bool ci_active,
46 long climbingImage, long numImages,
47 int ciStabilityCounter) const {
48 if (!cfg_.use_mmf || !ci_active)
49 return false;
50 if (climbingImage <= 0 || climbingImage > numImages)
51 return false;
52 if (ciStabilityCounter <= static_cast<int>(cfg_.ci_stability_count))
53 return false;
54 if (convForce <= cfg_.force_tolerance)
55 return false;
56 return (convForce < current_threshold_ || convForce < cfg_.trigger_force);
57}
58
59void OCINEBController::updateStability(long climbingImage) {
60 if (climbingImage == previousClimbingImage_) {
62 } else {
64 previousClimbingImage_ = climbingImage;
65 has_cached_mode_ = false;
66 }
67}
68
70 double convForce) {
71 auto *log = eonc::log::get();
72
73 QUILL_LOG_DEBUG(log,
74 "Triggering MMF. Force: {:.4f}, Threshold: {:.4f} "
75 "({:.2f}x baseline)",
76 convForce, current_threshold_,
78
79 // Save climbing image state before MMF
80 AtomMatrix savedPositions = neb.path[neb.climbingImage]->getPositions();
81
82 double alignment = 0.0;
83 int mmfResult = runDimer(neb, alignment);
84
85 // Always update forces after MMF
86 neb.movedAfterForceCall = true;
87 neb.updateForces();
88 double newForce = neb.convergenceForce();
89
90 // Check convergence
91 if (newForce < cfg_.force_tolerance) {
92 QUILL_LOG_DEBUG(log, "NEB converged after MMF. Force: {:.4f}", newForce);
93 return {newForce, true, false};
94 }
95
96 bool mmfHelped = (newForce < convForce) && mmfResult != -2;
97
98 if (mmfHelped) {
99 updateThresholdSuccess(convForce, newForce);
100 QUILL_LOG_DEBUG(log,
101 "MMF helped (status={}). Force: {:.4f} -> {:.4f} "
102 "({:.2f}x baseline). New threshold: {:.4f}",
103 mmfResult, convForce, newForce, newForce / baseline_force_,
105 } else {
106 // On positive curvature (status=-2), the CI is at a minimum, not a
107 // saddle. Restore to pre-MMF position to prevent catastrophic force
108 // explosion. For other failures (alignment loss, force increase),
109 // let the NEB recover naturally -- the CI position may still be
110 // closer to the saddle than before.
111 if (mmfResult == -2) {
112 neb.path[neb.climbingImage]->setPositions(savedPositions);
113 neb.movedAfterForceCall = true;
114 has_cached_mode_ = false;
115 newForce = convForce;
116 }
117 updateThresholdBackoff(alignment);
118 QUILL_LOG_DEBUG(
119 log,
120 "MMF backoff (status={}). Force: {:.4f} -> {:.4f}, "
121 "Alignment: {:.3f}. {}New threshold: {:.4f} ({:.2f}x baseline)",
122 mmfResult, convForce, newForce, alignment,
123 mmfResult == -2 ? "Restored CI. " : "", current_threshold_,
125 }
126
127 bool shouldReset =
128 (savedPositions - neb.path[neb.climbingImage]->getPositions()).norm() >
129 neb.params.optimizer_options.max_move *
130 neb.params.neb_options.image_count;
131
132 if (shouldReset) {
133 QUILL_LOG_DEBUG(log, "Resetting optimization history.");
134 }
135
137
138 return {newForce, false, shouldReset};
139}
140
142 double &alignment) {
143 auto *log = eonc::log::get();
144 alignment = 0.0;
145
146 if (neb.climbingImage <= 0 || neb.climbingImage > neb.numImages) {
147 QUILL_LOG_WARNING(log, "Invalid climbing image for MMF: {}",
148 neb.climbingImage);
149 return -1;
150 }
151
152 AtomMatrix initialMode;
153 if (has_cached_mode_) {
154 initialMode = cached_mode_;
155 } else {
156 initialMode = *neb.tangent[neb.climbingImage];
157 }
158 double tangentNorm = initialMode.norm();
159 if (tangentNorm < 1e-8) {
160 QUILL_LOG_WARNING(log, "Tangent too small for MMF initialization");
161 return -1;
162 }
163 initialMode /= tangentNorm;
164
165 auto tempMinModeSearch = std::make_shared<MinModeSaddleSearch>(
166 neb.path[neb.climbingImage], initialMode,
167 neb.path[neb.climbingImage]->getPotentialEnergy(), neb.params, neb.pot);
168
169 int minModeStatus;
170 try {
171 minModeStatus = tempMinModeSearch->run(cfg_.max_steps);
172 } catch (const eonc::DimerModeRestoredException &) {
174 QUILL_LOG_DEBUG(log, "MMF: Dimer restored to best state");
175 } catch (const eonc::DimerModeLostException &) {
177 QUILL_LOG_WARNING(log, "Dimer lost mode during MMF refinement");
178 }
179
180 mmf_iterations_used_ += tempMinModeSearch->iteration;
181
182 double eigenvalue = tempMinModeSearch->getEigenvalue();
183 if (eigenvalue > 0.0) {
184 QUILL_LOG_WARNING(log,
185 "MMF skipped: Positive curvature detected (eig={:.4f}).",
186 eigenvalue);
187 return -2;
188 }
189
190 AtomMatrix finalModeMatrix = tempMinModeSearch->getEigenvector();
191 VectorXd finalMode = VectorXd::Map(finalModeMatrix.data(), 3 * neb.atoms);
192 VectorXd currentTangent =
193 VectorXd::Map(neb.tangent[neb.climbingImage]->data(), 3 * neb.atoms);
194 alignment = std::abs(finalMode.normalized().dot(currentTangent.normalized()));
195
196 if (minModeStatus == MinModeSaddleSearch::STATUS_GOOD ||
198 if (alignment < cfg_.angle_tol) {
199 QUILL_LOG_WARNING(
200 log,
201 "MMF converged/restored but mode drifted (alignment={:.3f} < {:.3f})",
202 alignment, cfg_.angle_tol);
203 return -1;
204 }
205 cached_mode_ = finalModeMatrix;
206 has_cached_mode_ = true;
207 return 0;
208 } else if (minModeStatus == MinModeSaddleSearch::STATUS_BAD_MAX_ITERATIONS) {
209 return 1;
210 } else {
211 QUILL_LOG_WARNING(log, "MMF failed. Mode-tangent alignment: {:.3f}",
212 alignment);
213 return -1;
214 }
215}
216
218 double newForce) {
219 current_threshold_ = newForce * (0.5 + 0.4 * (newForce / convForce));
220 double max_threshold = baseline_force_ * cfg_.trigger_factor;
221 current_threshold_ = std::min(current_threshold_, max_threshold);
222}
223
225 double alpha = std::clamp(alignment, 0.0, 1.0);
226 double penalty_factor = 0.5 + 0.5 * alpha;
227 current_threshold_ = baseline_force_ * cfg_.trigger_factor * penalty_factor;
228 // Lower bound on the MMF trigger threshold. Scaled by force_tolerance so
229 // the MMF gate never collapses to zero when the NEB is already near
230 // convergence, but also capped by the trigger_factor envelope so a
231 // loose force_tolerance cannot push min_threshold above the cap and
232 // starve MMF activation.
233 double min_threshold = std::min(cfg_.force_tolerance * 2.0,
234 baseline_force_ * cfg_.trigger_factor);
235 current_threshold_ = std::max(current_threshold_, min_threshold);
236}
237
238} // namespace eonc::neb
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
struct eonc::Parameters::neb_options_t neb_options
bool shouldTrigger(double convForce, bool ci_active, long climbingImage, long numImages, int ciStabilityCounter) const
static Config fromParams(const Parameters &params)
void updateThresholdSuccess(double convForce, double newForce)
void updateStability(long climbingImage)
MMFResult run(eonc::NudgedElasticBand &neb, double convForce)
int runDimer(eonc::NudgedElasticBand &neb, double &alignment)
void initBaseline(double baseline_force)
void updateThresholdBackoff(double alignment)
quill::Logger * get() noexcept
Get or create the default "combi" logger.
Definition EonLogger.h:44
struct eonc::Parameters::neb_options_t::climbing_image_options_t::hybrid_dimer_t ocineb
struct eonc::Parameters::neb_options_t::climbing_image_options_t climbing_image