Loading...
Searching...
No Matches
ImprovedDimer.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// An implementation of Johannes Kaestner and Paul Sherwood's improved dimer.
13// An attempt to keep to the variable names in their 2008 paper has been made.
14
15#include "eon/ImprovedDimer.h"
17#include "eon/HelperFunctions.h"
18#include "eon/LowestEigenmode.h"
19#include "eon/PotCapabilities.h"
20#include "eon/SafeMath.h"
21#include "eon/eonExceptions.hpp"
22
23#include <cmath>
24#include <exception>
25#include <thread>
26
27#include "eon/EonLogger.h"
28
29namespace eonc {
30
31ImprovedDimer::ImprovedDimer(std::shared_ptr<Matter> matter,
32 const Parameters &params,
33 std::shared_ptr<Potential> pot)
35 // Each dimer image gets its own potential for lock-free parallel evaluation
36 // A clone keeps the caller's potential; makePotential rebuilds from the
37 // configuration and is the fallback for backends that cannot clone.
38 std::shared_ptr<Potential> x1Pot = pot;
39 if (pot->needsPerImageInstance() && params.main_options().parallel) {
40 auto cloned = pot->clonePotential();
41 x1Pot = cloned ? cloned : eonc::helpers::makePotential(params);
42 }
43 x0 = std::make_shared<Matter>(pot, params);
44 x1 = std::make_shared<Matter>(x1Pot, params);
45 // Matter's copy assignment copies the potential too; keep x1's own
46 // instance so the two images can be evaluated at the same time.
47 *x0 = *matter;
48 *x1 = *matter;
49 x1->setPotential(x1Pot);
50 tau.resize(3 * matter->numberOfAtoms());
51 tau.setZero();
53
54 if (params.dimer_options().opt_method == OptType::CG) {
55 init_cg = true;
56 }
57}
58
59void ImprovedDimer::setReferenceMode(const VectorXd &ref) {
61 if (fixedReferenceMode.norm() > 1e-10) {
62 eonc::safemath::safe_normalize_inplace(fixedReferenceMode);
63 }
64 hasFixedReference = true;
65}
66
68
69void ImprovedDimer::compute(std::shared_ptr<Matter> matter,
70 AtomMatrix initialDirectionAtomMatrix) {
71
72 VectorXd initialDirection = VectorXd::Map(initialDirectionAtomMatrix.data(),
73 3 * matter->numberOfAtoms());
74 tau = initialDirection.array() * matter->getFreeV().array();
77 if (tau.norm() > 1e-10) {
78 eonc::safemath::safe_normalize_inplace(tau);
79 } else {
80 // Fallback if the tangent was zero on free atoms (unlikely but safe)
81 tau.setRandom();
82 tau = tau.array() * matter->getFreeV().array();
83 eonc::safemath::safe_normalize_inplace(tau);
84 }
85
86 // Track the best (most negative) curvature and corresponding mode
87 double bestNegativeCurvature = std::numeric_limits<double>::max();
88 VectorXd bestTau = tau;
89 VectorXd bestX0Positions;
90 VectorXd bestG0, bestG1;
91
92 // Reference mode tracking for OCINEB mode-switching prevention
93 VectorXd referenceMode = hasFixedReference ? fixedReferenceMode : tau;
94
95 {
96 // Keep x1's per-image potential across the copy (see the constructor).
97 auto x1Pot = x1->getPotential();
98 *x0 = *matter;
99 *x1 = *matter;
100 x1->setPotential(x1Pot);
101 }
102 VectorXd x0_r = x0->getPositionsV();
103 bestX0Positions = x0_r;
104
105 double delta = params.main_options().finiteDifference;
106 x1->setPositionsV(x0_r + delta * tau);
107
108 // x0 and x1 in one call when both need one, so two calculator groups
109 // take one each; a cached x0 costs nothing.
110 {
111 Matter *const ends[] = {x0.get(), x1.get()};
113 }
114
115 // If we stepped into a high-energy wall, flip the tangent immediately
116 if (x1->getPotentialEnergy() - x0->getPotentialEnergy() > 10.0 * delta) {
117 tau = -tau;
118 x1->setPositionsV(x0_r + delta * tau);
119 QUILL_LOG_DEBUG(
120 log, "[IDimer] Initial tangent flipped due to high energy wall.");
121 }
122
123 // Optional: LOR / Lanczos / Davidson rotation backends (enum dispatch).
124 if (auto alt = runAlternativeRotation(
125 params.dimer_options().rotation_backend, matter, params, pot,
126 AtomMatrix::Map(tau.data(), matter->numberOfAtoms(), 3),
127 static_cast<quill::Logger *>(log))) {
128 C_tau = alt->eigenvalue;
129 tau = VectorXd::Map(alt->eigenvector.data(), 3 * matter->numberOfAtoms());
130 totalForceCalls += alt->forceCalls;
131 statsRotations = alt->rotations;
132 tau = tau.array() * matter->getFreeV().array();
133 eonc::safemath::safe_normalize_inplace(tau);
134 x0_r = matter->getPositionsV();
135 x0->setPositionsV(x0_r);
136 x1->setPositionsV(x0_r + delta * tau);
137 *matter = *x0;
138 rotationDidConverge = alt->converged;
140 return;
141 }
142
143 if (params.dimer_options().opt_method == OptType::LBFGS) {
144 s.clear();
145 y.clear();
146 rho.clear();
147 init_lbfgs = true;
148 }
149 if (params.dimer_options().opt_method == OptType::CG) {
150 init_cg = true;
151 }
152
153 VectorXd x1_rp, x1_r, tau_prime, tau_Old, g1_prime;
154 double phi_tol =
155 eonc::helpers::pi * (params.dimer_options().converged_angle / 180.0);
156 double phi_prime = 0.0;
157 double phi_min = 0.0;
158
159 statsRotations = 0;
160
161 // Use x1's potential for the trial rotation image (consistent with per-image)
162 auto x1p = std::make_shared<Matter>(x1->getPotential(), params);
163
164 // Melander, Laasonen, Jonsson, JCTC 11(3), 1055-1062, 2015
165 if (params.dimer_options().remove_rotation) {
167 AtomMatrix::Map(x0_r.data(), x0->numberOfAtoms(), 3), x1);
168 x1_r = x1->getPositionsV();
169 tau = x1->pbcV(x1_r - x0_r);
170 eonc::safemath::safe_normalize_inplace(tau);
171 x1_r = x0_r + tau * delta;
172 x1->setPositionsV(x1_r);
173 }
174
175 // Calculate gradients on x0 and x1.
176 // Prefer batched evaluation when the potential supports it (single
177 // model.forward() call for both replicas, e.g. MetatomicPotential on GPU).
178 // Else fall back to thread-parallel when the potential is thread-safe or
179 // wants per-image instances. Otherwise sequential.
180 VectorXd g0, g1;
181 // Two threads may share an instance only when it is thread safe; a
182 // per-image potential needs x0 and x1 to hold distinct instances.
183 bool canParallel = eonc::potAllowsSharedInstance(*pot) ||
184 (pot->needsPerImageInstance() &&
185 x0->getPotential().get() != x1->getPotential().get());
186 if (pot->supportsBatchEvaluation()) {
187 long n = x0->numberOfAtoms();
188 bool x0dirty = x0->needsForceUpdate();
189 bool x1dirty = x1->needsForceUpdate();
190
191 if (x0dirty && x1dirty) {
192 auto nrs0 = x0->getAtomicNrs();
193 auto nrs1 = x1->getAtomicNrs();
194 auto box0 = x0->getCell();
195 auto box1 = x1->getCell();
196 const double *posVec[] = {x0->getPositions().data(),
197 x1->getPositions().data()};
198 const int *nrsVec[] = {nrs0.data(), nrs1.data()};
199 double *frcVec[] = {x0->forcesData(), x1->forcesData()};
200 double energies[2], vars[2];
201 const double *boxVec[] = {box0.data(), box1.data()};
202 pot->forceBatch(2, n, posVec, nrsVec, frcVec, energies, vars, boxVec);
203 x0->setComputedPotential(energies[0], vars[0]);
204 x1->setComputedPotential(energies[1], vars[1]);
205 } else if (x1dirty) {
206 auto nrs = x1->getAtomicNrs();
207 auto box = x1->getCell();
208 const double *posVec[] = {x1->getPositions().data()};
209 const int *nrsVec[] = {nrs.data()};
210 double *frcVec[] = {x1->forcesData()};
211 double energies[1], vars[1];
212 const double *boxVec[] = {box.data()};
213 pot->forceBatch(1, n, posVec, nrsVec, frcVec, energies, vars, boxVec);
214 x1->setComputedPotential(energies[0], vars[0]);
215 } else if (x0dirty) {
216 x0->getForcesRaw(); // through computePotential
217 }
218 g0 = -x0->getForcesV();
219 g1 = -x1->getForcesV();
220 } else if (params.main_options().parallel && canParallel) {
221 // std::thread instead of std::jthread (Apple Clang libc++). Guard so an
222 // exception from the foreground call still joins t0 before rethrow.
223 // An exception may not leave a thread function (std::terminate), so t0
224 // hands its error back and the caller rethrows after the join.
225 std::exception_ptr t0Error;
226 std::thread t0([&] {
227 try {
228 g0 = -x0->getForcesV();
229 } catch (...) {
230 t0Error = std::current_exception();
231 }
232 });
233 try {
234 g1 = -x1->getForcesV();
235 } catch (...) {
236 t0.join();
237 throw;
238 }
239 t0.join();
240 if (t0Error)
241 std::rethrow_exception(t0Error);
242 } else {
243 g0 = -x0->getForcesV();
244 g1 = -x1->getForcesV();
245 }
246
247 bestG0 = g0;
248 bestG1 = g1;
249
250 positions.clear();
251 gradients.clear();
252 positions.push_back(x0->getPositionsV());
253 positions.push_back(x1->getPositionsV());
254 gradients.push_back(g0);
255 gradients.push_back(g1);
256
257 do { // Rotation loop: converge phi or hit max rotations
258
259 // Rotational force, F_R
260 F_R = -2.0 * (g1 - g0) + 2.0 * ((g1 - g0).dot(tau)) * tau;
261 statsTorque = eonc::safemath::safe_div(F_R.norm(), delta * 2.0, 0.0);
262
263 // Determine step direction theta via selected optimizer
264 if (params.dimer_options().opt_method == OptType::SD) {
265 theta = eonc::safemath::safe_normalized(F_R);
266
267 } else if (params.dimer_options().opt_method == OptType::CG) {
268 if (init_cg) {
269 init_cg = false;
270 gamma = 0.0;
271 } else {
272 a = std::abs(F_R.dot(F_R_Old));
273 b = F_R_Old.squaredNorm();
274 gamma = (a < 0.5 * b)
276 : 0.0;
277 }
278
279 theta = (gamma == 0.0) ? F_R : F_R + thetaOld * gamma;
280 theta -= theta.dot(tau) * tau;
281 thetaOld = theta;
282 if (theta.norm() < eonc::safemath::eps) {
283 theta = F_R - F_R.dot(tau) * tau;
284 }
285 eonc::safemath::safe_normalize_inplace(theta);
286 F_R_Old = F_R;
287
288 } else if (params.dimer_options().opt_method == OptType::LBFGS) {
289 if (!init_lbfgs) {
290 VectorXd s0 = tau - tau_Old;
291 s.push_back(s0);
292 VectorXd y0 =
293 eonc::safemath::safe_div(1.0, delta, 0.0) * (F_R_Old - F_R);
294 y.push_back(y0);
295 rho.push_back(eonc::safemath::safe_recip(s0.dot(y0), 0.0));
296 } else {
297 init_lbfgs = false;
298 }
299
300 double H0 = 1.0 / 60.0;
301 size_t loopmax = s.size();
302 std::vector<double> alpha(loopmax);
303
304 VectorXd q = -F_R;
305 for (long i = static_cast<long>(loopmax) - 1; i >= 0; i--) {
306 alpha[i] = rho[i] * s[i].dot(q);
307 q -= alpha[i] * y[i];
308 }
309 VectorXd z = H0 * q;
310 for (size_t i = 0; i < loopmax; i++) {
311 double bv = rho[i] * y[i].dot(z);
312 z += s[i] * (alpha[i] - bv);
313 }
314
315 double vd = std::clamp(-eonc::safemath::safe_normalized(z).dot(
316 eonc::safemath::safe_normalized(F_R)),
317 -1.0, 1.0);
318 double angle =
320
321 if (angle > 87.0) {
322 s.clear();
323 y.clear();
324 rho.clear();
325 z = -F_R;
326 }
327
328 theta = -eonc::safemath::safe_normalized(z);
329 theta -= theta.dot(tau) * tau;
330 eonc::safemath::safe_normalize_inplace(theta);
331
332 thetaOld = theta;
333 F_R_Old = F_R;
334 tau_Old = tau;
335 }
336
337 // Curvature along tau
338 C_tau = eonc::safemath::safe_div((g1 - g0).dot(tau), delta, 0.0);
339
340 // Track best negative curvature for mode restoration
341 if (C_tau < bestNegativeCurvature) {
342 bestNegativeCurvature = C_tau;
343 bestTau = tau;
344 bestX0Positions = x0->getPositionsV();
345 bestG0 = g0;
346 bestG1 = g1;
348 }
349
350 // Estimate optimum rotation angle
351 double d_C_tau_d_phi =
352 2.0 * eonc::safemath::safe_div((g1 - g0).dot(theta), delta, 0.0);
353 phi_prime = -0.5 * eonc::safemath::safe_atan_ratio(
354 d_C_tau_d_phi, 2.0 * std::abs(C_tau), 0.0);
355 statsAngle = phi_prime * (180.0 / eonc::helpers::pi);
356
357 double alignment = std::abs(tau.dot(referenceMode));
358
359 if (std::abs(phi_prime) > phi_tol) {
360 double b1 = 0.5 * d_C_tau_d_phi;
361
362 // Trial rotation to phi_prime
363 x0_r = x0->getPositionsV();
364 tau_prime = tau * std::cos(phi_prime) + theta * std::sin(phi_prime);
365 tau_prime = eonc::safemath::safe_normalized(tau_prime);
366 x1_rp = x0_r + tau_prime * delta;
367
368 *x1p = *x1;
369 x1p->setPositionsV(x1_rp);
370 g1_prime = -x1p->getForcesV();
371
372 positions.push_back(x1_rp);
373 gradients.push_back(g1_prime);
374
375 double C_tau_prime =
376 eonc::safemath::safe_div((g1_prime - g0).dot(tau_prime), delta, 0.0);
377
378 // Optimal rotation angle via Fourier interpolation
379 double a1 = eonc::safemath::safe_div(
380 C_tau - C_tau_prime + b1 * std::sin(2.0 * phi_prime),
381 1.0 - std::cos(2.0 * phi_prime), 0.0);
382 double a0 = 2.0 * (C_tau - a1);
383 phi_min = 0.5 * eonc::safemath::safe_atan_ratio(b1, a1, 0.0);
384
385 double C_tau_min = 0.5 * a0 + a1 * std::cos(2.0 * phi_min) +
386 b1 * std::sin(2.0 * phi_min);
387
388 // If curvature is being maximized, push over pi/2
389 if (C_tau_min > C_tau) {
390 phi_min += eonc::helpers::pi * 0.5;
391 C_tau_min = 0.5 * a0 + a1 * std::cos(2.0 * phi_min) +
392 b1 * std::sin(2.0 * phi_min);
393 }
394
395 // Keep phi_min in [-pi/2, pi/2] for accurate LBFGS
396 if (phi_min > eonc::helpers::pi * 0.5) {
397 phi_min -= eonc::helpers::pi;
398 }
399 statsAngle = phi_min * (180.0 / eonc::helpers::pi);
400
401 // Apply optimal rotation
402 tau = tau * std::cos(phi_min) + theta * std::sin(phi_min);
403 tau = eonc::safemath::safe_normalized(tau);
404 x1_r = x0_r + tau * delta;
405
406 // Melander, Laasonen, Jonsson, JCTC 11(3), 1055-1062, 2015
407 if (params.dimer_options().remove_rotation) {
408 x1->setPositionsV(x1_r);
410 AtomMatrix::Map(x0_r.data(), x0->numberOfAtoms(), 3), x1);
411 x1_r = x1->getPositionsV();
412 tau = x1_r - x0_r;
413 eonc::safemath::safe_normalize_inplace(tau);
414 x1_r = x0_r + tau * delta;
415 }
416
417 x1->setPositionsV(x1_r);
418 C_tau = C_tau_min;
419
420 // Interpolate g1 at phi_min from g1 and g1_prime (saves one force call)
421 double sin_pp = std::sin(phi_prime);
422 g1 = g1 * eonc::safemath::safe_div(std::sin(phi_prime - phi_min), sin_pp,
423 0.0) +
424 g1_prime * eonc::safemath::safe_div(std::sin(phi_min), sin_pp, 0.0) +
425 g0 * (1.0 - std::cos(phi_min) -
426 std::sin(phi_min) * std::tan(phi_prime * 0.5));
427
428 statsTorque = eonc::safemath::safe_div(F_R.norm(), 2.0 * delta, 0.0);
429 statsRotations += 1;
430 QUILL_LOG_INFO(
431 log,
432 "[IDimerRot] ----- --------- ---------- ------------------ "
433 "{:9.4f} {:7.3f} {:6.3f} {:4} {:5.3f}",
435 } else {
436 QUILL_LOG_INFO(
437 log,
438 "[IDimerRot] ----- --------- ---------- ------------------ "
439 "{:9.4f} {:7.3f} ------ ---- {:5.3f}",
440 C_tau, F_R.norm() / delta, alignment);
441 }
442
443 // Check for mode loss (OCINEB dimer refinement)
444 if (alignment < params.neb_options().climbing_image.ocineb.angle_tol &&
445 params.neb_options().climbing_image.ocineb.use_mmf) {
446 QUILL_LOG_WARNING(
447 log, "Terminating dimer due to lost mode (align {:.3f}).", alignment);
448 rotationDidConverge = false;
449
450 if (bestNegativeCurvature < 0.0) {
451 // Restore the best negative curvature state
452 C_tau = bestNegativeCurvature;
453 tau = bestTau;
454 x0->setPositionsV(bestX0Positions);
455 x1->setPositionsV(bestX0Positions + delta * bestTau);
456 *matter = *x0;
457 QUILL_LOG_DEBUG(
458 log, "Restored best negative curvature state: C_tau={:.4f}", C_tau);
460 } else {
461 QUILL_LOG_WARNING(
462 log, "Never found negative curvature. Final C_tau: {:.4f}", C_tau);
464 }
465 }
466
467 } while (std::abs(phi_prime) > std::abs(phi_tol) &&
468 std::abs(phi_min) > std::abs(phi_tol) &&
469 statsRotations < params.dimer_options().rotations_max);
470}
471
473
475 return AtomMatrix::Map(tau.data(), x0->numberOfAtoms(), 3);
476}
477
478} // namespace eonc
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
ImprovedDimer(std::shared_ptr< Matter > matter, const Parameters &params, std::shared_ptr< Potential > pot)
std::shared_ptr< Matter > x1
void compute(std::shared_ptr< Matter > matter, AtomMatrix initialDirection) override
std::vector< VectorXd > s
VectorXd fixedReferenceMode
void setReferenceMode(const VectorXd &ref)
std::vector< VectorXd > positions
std::vector< VectorXd > gradients
std::shared_ptr< Matter > x0
std::vector< VectorXd > y
eonc::log::Scoped log
std::vector< double > rho
double getEigenvalue() override
AtomMatrix getEigenvector() override
const Parameters & params
std::shared_ptr< Potential > pot
LowestEigenmode(std::shared_ptr< Potential > potPassed, const Parameters &parameters)
void rotationRemove(const AtomMatrix r1, std::shared_ptr< Matter > m2)
constexpr double pi
constexpr double safe_div(double num, double denom, double fallback=0.0)
Definition SafeMath.h:21
double safe_acos(double x)
Definition SafeMath.h:34
constexpr double eps
Definition SafeMath.h:19
constexpr double safe_recip(double x, double fallback=0.0)
Definition SafeMath.h:29
double safe_atan_ratio(double num, double denom, double fallback=0.0)
Definition SafeMath.h:42
RAII resource manager for the ARTn C library with global synchronization.
void evaluateTogether(Potential &pot, std::span< Matter *const > systems)
Evaluates every system that needs a force update.
Definition Matter.cpp:847
bool potAllowsSharedInstance(const P &p) noexcept
std::optional< DimerRotationResult > runAlternativeRotation(DimerRotationBackend backend, const std::shared_ptr< Matter > &matter, const Parameters &params, const std::shared_ptr< Potential > &pot, const AtomMatrix &initialDirection, quill::Logger *log=nullptr)
Run Lanczos, Davidson, or LOR.