Loading...
Searching...
No Matches
DynamicsSaddleSearch.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/BondBoost.h"
14#include "eon/Dynamics.h"
18#include "eon/SafeMath.h"
19
20#include <algorithm>
21#include <cmath>
22#include <filesystem>
23#include <limits>
24#include <stdexcept>
25
26namespace eonc {
27
29 std::vector<std::shared_ptr<Matter>> mdSnapshots;
30 std::vector<double> mdTimes;
31 QUILL_LOG_DEBUG(log, "Starting dynamics NEB saddle search");
32
33 if (std::filesystem::exists("masses.dat")) {
34 QUILL_LOG_DEBUG(log, "Found mass weights file");
35 Eigen::VectorXd masses =
36 eonc::helpers::loadMasses("masses.dat", saddle->numberOfAtoms());
37 saddle->setMasses(masses);
38 QUILL_LOG_DEBUG(log, "Applied mass weights");
39 } else {
40 QUILL_LOG_DEBUG(log, "No mass weights file found");
41 }
42
43 Dynamics dyn(saddle.get(), params);
44 QUILL_LOG_DEBUG(
45 log, "Initializing velocities from Maxwell-Boltzmann distribution");
46 dyn.setTemperature(params.saddle_search_options().dynamics.temperature);
48
49 const double dt = params.dynamics_options().time_step;
50 if (!(dt > 0.0)) {
51 throw std::invalid_argument(
52 "DynamicsSaddleSearch: time_step must be positive");
53 }
54 int dephaseSteps = static_cast<int>(
55 std::floor(params.parallel_replica_options().dephase_time / dt + 0.5));
56
57 while (true) {
58
59 QUILL_LOG_DEBUG(log, "Dephasing: {} steps", dephaseSteps);
60 // always start from the initial configuration
61 *saddle = *reactant;
63
64 // Dephase MD trajectory
65 for (int step = 1; step <= dephaseSteps; step++) {
66 dyn.oneStep(step);
67 }
68
69 // Check to see if a transition occured
70 Matter min(pot, params);
71 min = *saddle;
72 min.relax();
73
74 if (min.compare(*reactant)) {
75 QUILL_LOG_DEBUG(log, "Dephasing successful");
76 break;
77 } else {
78 QUILL_LOG_DEBUG(log, "Transition occured during dephasing; Restarting");
79 dephaseSteps /= 2;
80 if (dephaseSteps < 1)
81 dephaseSteps = 1;
82 }
83 }
84
85 BondBoost bondBoost(saddle.get(), params);
86 // setBiasPotential does not own this object. Clear it before bondBoost
87 // leaves the stack, including on the early returns below.
88 struct BiasGuard {
89 Matter *matter{nullptr};
90 ~BiasGuard() {
91 if (matter == nullptr) {
92 return;
93 }
94 matter->setBiasPotential(nullptr);
95 matter->setBiasForces(AtomMatrix::Zero(matter->numberOfAtoms(), 3));
96 }
97 } biasGuard;
98 if (params.hyperdynamics_options().bias_potential ==
100 QUILL_LOG_DEBUG(log, "Initializing Bond Boost");
101 bondBoost.initialize();
102 saddle->setBiasPotential(&bondBoost);
103 biasGuard.matter = saddle.get();
104 }
105
106 int checkInterval = static_cast<int>(
107 params.saddle_search_options().dynamics.state_check_interval /
108 params.dynamics_options().time_step +
109 0.5);
110 // A zero or sub-step interval floors to 0. step % 0 is undefined, and a
111 // state check shorter than one dynamics step still has to run.
112 if (checkInterval < 1) {
113 checkInterval = 1;
114 }
115 int recordInterval =
116 static_cast<int>(params.saddle_search_options().dynamics.record_interval /
117 params.dynamics_options().time_step +
118 0.5);
119
120 if (params.debug_options().write_movies) {
121 if (!eonc::io::io_ok(saddle->matter2con("dynamics", false))) {
122 QUILL_LOG_WARNING(log, "Failed to write dynamics movie header");
123 }
124 }
125
126 for (int step = 1; step <= params.dynamics_options().steps; step++) {
127 if (params.hyperdynamics_options().bias_potential ==
129 // oneStep() calls getAccelerations(), and therefore boost(), more
130 // than once. Advance the rmd_time counter once per MD step.
131 bondBoost.advance();
132 }
133 dyn.oneStep(step);
134
135 if (recordInterval != 0 && step % recordInterval == 0) {
136 QUILL_LOG_DEBUG(log, "recording configuration at step {} time {:.3f}",
137 step,
138 step * params.dynamics_options().time_step *
139 params.constants().timeUnit);
140 // BUG FIX: was sharing ownership with saddle instead of copying
141 auto snapshot = std::make_shared<Matter>(*saddle);
142 mdSnapshots.push_back(snapshot);
143 mdTimes.push_back(step * params.dynamics_options().time_step);
144 }
145
146 if (params.debug_options().write_movies) {
147 if (!eonc::io::io_ok(saddle->matter2con("dynamics", true))) {
148 QUILL_LOG_WARNING(log, "Failed to append dynamics movie frame");
149 }
150 }
151
152 if (step % checkInterval == 0) {
153 QUILL_LOG_DEBUG(log, "Minimizing trajectory, step {}", step);
154
155 product = std::make_shared<Matter>(*saddle);
156 product->relax(false, false);
157
158 if (!product->compare(*reactant)) {
159 QUILL_LOG_DEBUG(log, "Found new state");
160 // A record interval that rounds to 0 stores nothing. refineTransition
161 // then has no frame, and subscript 0 is outside the vector.
162 int image = -1;
163 if (!mdSnapshots.empty()) {
164 image = refineTransition(mdSnapshots, product);
165 }
166 if (image < 0 || static_cast<size_t>(image) >= mdSnapshots.size() ||
167 static_cast<size_t>(image) >= mdTimes.size()) {
168 QUILL_LOG_DEBUG(log,
169 "No MD snapshots; using the detecting configuration");
170 time = step * params.dynamics_options().time_step;
171 } else {
172 *saddle = *mdSnapshots[static_cast<size_t>(image)];
173 QUILL_LOG_DEBUG(log, "Found transition at snapshot image {}", image);
174 for (int ii = 0; ii < static_cast<int>(mdTimes.size()); ii++) {
175 QUILL_LOG_DEBUG(log, "MDTimes[{}] = {:.3f}", ii,
176 mdTimes[ii] * params.constants().timeUnit);
177 }
178 // Subtract half the record interval to avoid systematic bias
179 time = mdTimes[static_cast<size_t>(image)] -
180 params.saddle_search_options().dynamics.record_interval / 2.0;
181 // Half a record interval before the leaving frame is the unbiased
182 // crossing, and it must not fall before the preceding reactant frame.
183 if (image > 0 && time < mdTimes[static_cast<size_t>(image - 1)]) {
184 time = mdTimes[static_cast<size_t>(image - 1)];
185 }
186 }
187 QUILL_LOG_DEBUG(log, "Transition time {:.2f} fs",
188 time * params.constants().timeUnit);
189
191
192 if (!params.saddle_search_options().dynamics.linear_interpolation) {
193 QUILL_LOG_DEBUG(
194 log, "Interpolating initial band through MD transition state");
195 AtomMatrix reactantToSaddle =
196 saddle->pbc(saddle->getPositions() - reactant->getPositions());
197 AtomMatrix saddleToProduct =
198 saddle->pbc(product->getPositions() - saddle->getPositions());
199 QUILL_LOG_DEBUG(log, "Initial band saved to neb_initial_band.con");
200 if (!eonc::io::io_ok(
201 neb.path[0]->matter2con("neb_initial_band.con", false))) {
202 QUILL_LOG_WARNING(log, "Failed to write neb_initial_band.con");
203 }
204 int mid = neb.numImages / 2 + 1;
205 for (int img = 1; img <= neb.numImages; img++) {
206 if (img < mid) {
207 double frac = static_cast<double>(img) / static_cast<double>(mid);
208 neb.path[img]->setPositions(reactant->getPositions() +
209 frac * reactantToSaddle);
210 } else if (img > mid) {
211 double frac = static_cast<double>(img - mid) /
212 static_cast<double>(neb.numImages - mid + 1);
213 neb.path[img]->setPositions(saddle->getPositions() +
214 frac * saddleToProduct);
215 } else {
216 neb.path[img]->setPositions(saddle->getPositions());
217 }
218 if (!eonc::io::io_ok(
219 neb.path[img]->matter2con("neb_initial_band.con", true))) {
220 QUILL_LOG_WARNING(log, "Failed to append neb_initial_band frame");
221 }
222 }
223 if (!eonc::io::io_ok(neb.path[neb.numImages + 1]->matter2con(
224 "neb_initial_band.con", true))) {
225 QUILL_LOG_WARNING(log,
226 "Failed to append neb_initial_band endpoint");
227 }
228 } else {
229 QUILL_LOG_DEBUG(
230 log, "Linear interpolation between minima used for initial band");
231 if (!eonc::io::io_ok(
232 neb.path[0]->matter2con("neb_initial_band.con", false))) {
233 QUILL_LOG_WARNING(log, "Failed to write neb_initial_band.con");
234 }
235 for (int j = 1; j <= neb.numImages + 1; j++) {
236 if (!eonc::io::io_ok(
237 neb.path[j]->matter2con("neb_initial_band.con", true))) {
238 QUILL_LOG_WARNING(log, "Failed to append neb_initial_band frame");
239 }
240 }
241 }
242
243 AtomMatrix mode;
244 if (params.neb_options().max_iterations > 0) {
245 auto minModeMethod =
247
248 neb.compute();
249 neb.printImageData(true);
250 int extremumImage = -1;
251 int jExt = 0;
252 for (jExt = 0; jExt < neb.numExtrema; jExt++) {
253 if (neb.extremumCurvature[jExt] <
254 params.saddle_search_options().dynamics.max_init_curvature) {
255 extremumImage =
256 static_cast<int>(std::floor(neb.extremumPosition[jExt]));
257 *saddle = *neb.path[extremumImage];
258 double interpDist = neb.extremumPosition[jExt] -
259 static_cast<double>(extremumImage);
260 AtomMatrix bandDir =
261 saddle->pbc(neb.path[extremumImage + 1]->getPositions() -
262 neb.path[extremumImage]->getPositions());
263 saddle->setPositions(interpDist * bandDir +
264 saddle->getPositions());
265 mode = saddle->pbc(neb.path[extremumImage + 1]->getPositions() -
266 saddle->getPositions());
267 eonc::safemath::safe_normalize_inplace(mode);
268 eonc::eigenmodeCompute(*minModeMethod, saddle, mode);
269 double ev = eonc::eigenmodeGetEigenvalue(*minModeMethod);
270 QUILL_LOG_DEBUG(log, "extrema #{} has eigenvalue {:.8f}",
271 jExt + 1, ev);
272
273 if (ev < 0) {
274 QUILL_LOG_DEBUG(
275 log, "chose image {} (extrema #{}) as extremum image",
276 extremumImage, jExt + 1);
277 break;
278 } else {
279 extremumImage = -1;
280 }
281 }
282 }
283
284 if (extremumImage != -1) {
285 *saddle = *neb.path[extremumImage];
286 double interpDist =
287 neb.extremumPosition[jExt] - static_cast<double>(extremumImage);
288 QUILL_LOG_DEBUG(log, "interpDistance {}", interpDist);
289 AtomMatrix bandDir =
290 saddle->pbc(neb.path[extremumImage + 1]->getPositions() -
291 neb.path[extremumImage]->getPositions());
292 saddle->setPositions(interpDist * bandDir + saddle->getPositions());
293 mode = saddle->pbc(neb.path[extremumImage + 1]->getPositions() -
294 saddle->getPositions());
295 eonc::safemath::safe_normalize_inplace(mode);
296 } else {
297 QUILL_LOG_DEBUG(
298 log, "no maxima found, using max energy non-endpoint image");
299 double maxEnergy = -std::numeric_limits<double>::infinity();
300 for (int img = 1; img <= neb.numImages; img++) {
301 double U = neb.path[img]->getPotentialEnergy();
302 if (U > maxEnergy) {
303 maxEnergy = U;
304 *saddle = *neb.path[img];
305 mode = saddle->pbc(neb.path[img + 1]->getPositions() -
306 saddle->getPositions());
307 eonc::safemath::safe_normalize_inplace(mode);
308 }
309 }
310 if (maxEnergy <= reactant->getPotentialEnergy()) {
311 QUILL_LOG_DEBUG(log, "warning: no barrier found");
313 }
314 }
315 } else {
316 // No NEB iterations: the middle image of the initial band is the
317 // guess, and the dimer still needs an n-atom tangent. An empty
318 // mode replaces the dimer direction and then indexes every atom.
319 neb.maxEnergyImage = neb.numImages / 2 + 1;
320 const int img = static_cast<int>(neb.maxEnergyImage);
321 const int last = static_cast<int>(neb.path.size()) - 1;
322 const int from = std::clamp(img, 0, last);
323 const int to = std::clamp(img + 1, 0, last);
324 mode = saddle->pbc(neb.path[to]->getPositions() -
325 neb.path[from]->getPositions());
326 if (mode.norm() > 0.0) {
327 mode.normalize();
328 } else {
329 mode = AtomMatrix::Zero(saddle->numberOfAtoms(), 3);
330 if (mode.rows() > 0) {
331 mode(0, 0) = 1.0;
332 }
333 }
334 }
335
336 QUILL_LOG_DEBUG(
337 log, "Initial saddle guess saved to saddle_initial_guess.con");
338 if (!eonc::io::io_ok(saddle->matter2con("saddle_initial_guess.con"))) {
339 QUILL_LOG_WARNING(log, "Failed to write saddle_initial_guess.con");
340 }
342 saddle, mode, reactant->getPotentialEnergy(), params, pot);
343 int minModeStatus = search.run();
344
345 if (minModeStatus != MinModeSaddleSearch::STATUS_GOOD) {
346 QUILL_LOG_DEBUG(log, "error in min mode saddle search");
347 return minModeStatus;
348 }
349
350 eigenvalue = search.getEigenvalue();
351 eigenvector = search.getEigenvector();
352 QUILL_LOG_DEBUG(log, "eigenvalue: {:.3f}", eigenvalue);
353
354 double barrier =
355 saddle->getPotentialEnergy() - reactant->getPotentialEnergy();
356 QUILL_LOG_DEBUG(log, "found barrier of {:.3f}", barrier);
357 mdSnapshots.clear();
358 mdTimes.clear();
360 } else {
361 QUILL_LOG_DEBUG(log, "Still in original state");
362 mdTimes.clear();
363 mdSnapshots.clear();
364 }
365 }
366 }
367
368 mdSnapshots.clear();
369 time = params.dynamics_options().steps * params.dynamics_options().time_step;
371}
372
375 const std::vector<std::shared_ptr<Matter>> &snapshots,
376 std::shared_ptr<Matter> prod) {
377 int lo = 0;
378 int hi = static_cast<int>(snapshots.size()) - 1;
379 // One frame is index 0. Zero frames yield -1, which is not a subscript.
380 if (hi <= 0) {
381 return hi;
382 }
383
384 QUILL_LOG_DEBUG(log, "refining transition time");
385
386 while ((hi - lo) > 1) {
387 int mid = lo + (hi - lo) / 2;
388 QUILL_LOG_DEBUG(log, "minimizing image {}", mid);
389 Matter snap(pot, params);
390 snap = *snapshots[mid];
391 snap.relax(false);
392
393 if (snap.compare(*reactant)) {
394 QUILL_LOG_DEBUG(log, "image {} minimizes to reactant", mid);
395 lo = mid;
396 } else {
397 QUILL_LOG_DEBUG(log, "image {} minimizes to product", mid);
398 *prod = snap;
399 hi = mid;
400 }
401 }
402
403 // Adjacent brackets make (lo + hi) / 2 equal lo, the snapshot that
404 // still minimizes to the reactant. The transition is the higher index.
405 if (hi < 0) {
406 return 0;
407 }
408 return hi;
409}
410
412
414
415} // namespace eonc
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
Functionality relying on the conjugate gradients algorithm.
Definition BondBoost.h:25
void advance()
Advance the equilibration / boost schedule by one MD step.
int refineTransition(const std::vector< std::shared_ptr< Matter > > &snapshots, std::shared_ptr< Matter > product)
Binary search through MD snapshots to find the transition point.
std::shared_ptr< Matter > saddle
std::shared_ptr< Matter > reactant
std::shared_ptr< Matter > product
void setTemperature(double temperature)
Definition Dynamics.cpp:53
void setThermalVelocity()
Definition Dynamics.cpp:247
void oneStep(int stepNumber=-1)
Definition Dynamics.cpp:57
static const char BOND_BOOST[]
Definition BondBoost.h:75
void setBiasPotential(BondBoost *bondBoost)
Definition Matter.cpp:394
bool relax(bool quiet=false, bool writeMovie=false, bool checkpoint=false, std::string prefixMovie=std::string(), std::string prefixCheckpoint=std::string(), bool retainMovieFrames=false)
Definition Matter.cpp:334
bool compare(const Matter &matter, bool indistinguishable=false)
Definition Matter.cpp:184
long int numberOfAtoms() const
Definition Matter.cpp:273
void setBiasForces(const AtomMatrix &bf)
Definition Matter.cpp:406
std::shared_ptr< Potential > pot
VectorXd loadMasses(std::string filename, int nAtoms)
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)
std::shared_ptr< LowestEigenmode > buildEigenmodeStrategy(std::shared_ptr< Matter > matter, const Parameters &params, std::shared_ptr< Potential > pot)
double eigenmodeGetEigenvalue(LowestEigenmode &s)