Loading...
Searching...
No Matches
PIQTST.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#include "eon/PIQTST.h"
13
14#include <algorithm>
15#include <cmath>
16#include <limits>
17#include <numbers>
18#include <stdexcept>
19#include <string>
20
21namespace eonc::piqtst {
22
23namespace {
24
26MatrixXd trapezoidWeights(const std::vector<Plane> &planes) {
27 const long n = static_cast<long>(planes.size());
28 MatrixXd w = MatrixXd::Zero(n, n);
29 for (long j = 1; j < n; ++j) {
30 w.row(j) = w.row(j - 1);
31 const double h =
32 planes[static_cast<size_t>(j)].s - planes[static_cast<size_t>(j - 1)].s;
33 w(j, j - 1) += 0.5 * h;
34 w(j, j) += 0.5 * h;
35 }
36 return w;
37}
38
39VectorXd forceErrors(const std::vector<Plane> &planes) {
40 VectorXd e(static_cast<long>(planes.size()));
41 for (size_t i = 0; i < planes.size(); ++i) {
42 e(static_cast<long>(i)) = planes[i].meanForceError;
43 }
44 return e;
45}
46
47double propagated(const VectorXd &gradient, const VectorXd &errors) {
48 return std::sqrt(gradient.cwiseProduct(errors).squaredNorm());
49}
50
54struct Axes {
55 VectorXd a;
56 VectorXd b;
57};
58
59Axes axes(const Coordinate &c) {
60 const long dof = 3 * c.atoms;
61 if (c.atoms < 1 || static_cast<long>(c.masses.size()) != c.atoms ||
62 static_cast<long>(c.free.size()) != dof || c.reference.size() != dof ||
63 c.direction.size() != dof) {
64 throw std::invalid_argument("piqtst: coordinate sizes do not match");
65 }
66 Axes out{VectorXd::Zero(dof), VectorXd::Zero(dof)};
67 double nn = 0.0;
68 for (long i = 0; i < dof; ++i) {
69 if (!c.free[static_cast<size_t>(i)]) {
70 continue;
71 }
72 const double sm = std::sqrt(c.masses[static_cast<size_t>(i / 3)]);
73 out.a(i) = sm * c.direction(i);
74 out.b(i) = c.direction(i) / sm;
75 nn += c.direction(i) * c.direction(i);
76 }
77 if (std::abs(nn - 1.0) > 1e-8) {
78 throw std::invalid_argument(
79 "piqtst: the direction must be a unit vector on the free coordinates");
80 }
81 return out;
82}
83
84} // namespace
85
86void integrate(std::vector<Plane> &planes) {
87 if (planes.empty()) {
88 return;
89 }
90 const MatrixXd w = trapezoidWeights(planes);
91 VectorXd f(static_cast<long>(planes.size()));
92 for (size_t i = 0; i < planes.size(); ++i) {
93 f(static_cast<long>(i)) = planes[i].meanForce;
94 }
95 const VectorXd errors = forceErrors(planes);
96 const VectorXd values = w * f;
97 for (size_t j = 0; j < planes.size(); ++j) {
98 planes[j].freeEnergy = values(static_cast<long>(j));
99 planes[j].freeEnergyError =
100 propagated(w.row(static_cast<long>(j)).transpose(), errors);
101 }
102}
103
104std::vector<Plane> scan(Potential &pot, const Coordinate &c,
105 const ScanOptions &o) {
106 const long dof = 3 * c.atoms;
107 const Axes ax = axes(c);
108 const VectorXd &a = ax.a;
109 const VectorXd &b = ax.b;
110 if (o.planes.size() < 2) {
111 throw std::invalid_argument("piqtst: at least two planes are needed");
112 }
113 for (size_t j = 1; j < o.planes.size(); ++j) {
114 if (!(o.planes[j] > o.planes[j - 1])) {
115 throw std::invalid_argument("piqtst: plane positions must ascend");
116 }
117 }
118 if (o.equilibration < 0 || o.production < 2 || o.blocks < 2 ||
119 o.production < o.blocks) {
120 throw std::invalid_argument(
121 "piqtst: sampling needs at least as many steps as blocks, and two "
122 "blocks");
123 }
124 const double aNorm = a.norm();
125 auto seedAt = [&](double s) -> VectorXd {
126 if (o.seed) {
127 VectorXd x = o.seed(s);
128 if (x.size() != dof) {
129 throw std::invalid_argument("piqtst: a seed has the wrong length");
130 }
131 return x;
132 }
133 return c.reference + s * b;
134 };
135
137 const long beads = o.ring.beads;
138 const long blockSize = o.production / o.blocks;
139 std::vector<Plane> out;
140 out.reserve(o.planes.size());
141 for (size_t j = 0; j < o.planes.size(); ++j) {
142 const double s = o.planes[j];
143 const VectorXd origin = c.reference + s * b;
144 const VectorXd target = seedAt(s);
145 if (j == 0) {
146 ring.setAllBeads(target.data());
147 } else {
148 const VectorXd shift = target - ring.centroid();
149 std::vector<VectorXd> moved = ring.beads();
150 for (auto &q : moved) {
151 q += shift;
152 }
153 ring.setBeads(moved);
154 }
155 ring.setHyperplane(a, origin);
156
157 const long batches0 = ring.batches();
158 for (long step = 0; step < o.equilibration; ++step) {
159 ring.step(pot, c.box, false);
160 }
161 Plane plane;
162 plane.s = s;
163 plane.centroid = VectorXd::Zero(dof);
164 VectorXd spread2 = VectorXd::Zero(dof);
165 double sum = 0.0;
166 double blockSum = 0.0;
167 std::vector<double> blockMeans;
168 for (long step = 0; step < o.production; ++step) {
169 ring.resetAverages();
170 ring.step(pot, c.box, true);
171 const double fn = ring.meanForce();
172 sum += fn;
173 blockSum += fn;
174 if ((step + 1) % blockSize == 0 &&
175 static_cast<long>(blockMeans.size()) < o.blocks) {
176 blockMeans.push_back(blockSum / static_cast<double>(blockSize));
177 blockSum = 0.0;
178 }
179 const VectorXd centroid = ring.centroid();
180 plane.centroid += centroid;
181 for (const auto &q : ring.beads()) {
182 spread2 += (q - centroid).cwiseAbs2();
183 }
184 }
185 const double steps = static_cast<double>(o.production);
186 plane.centroid /= steps;
187 spread2 /= steps * static_cast<double>(beads);
188 plane.spread.resize(static_cast<size_t>(dof));
189 for (long i = 0; i < dof; ++i) {
190 plane.spread[static_cast<size_t>(i)] = std::sqrt(spread2(i));
191 }
192 const double mean = sum / steps;
193 double blockMean = 0.0;
194 for (const double m : blockMeans) {
195 blockMean += m;
196 }
197 const double nb = static_cast<double>(blockMeans.size());
198 blockMean /= nb;
199 double var = 0.0;
200 for (const double m : blockMeans) {
201 var += (m - blockMean) * (m - blockMean);
202 }
203 var /= nb * (nb - 1.0);
204 plane.meanForce = -mean / aNorm;
205 plane.meanForceError = std::sqrt(var) / aNorm;
206 plane.batches = ring.batches() - batches0;
207 out.push_back(std::move(plane));
208 }
209 integrate(out);
210 return out;
211}
212
213Rate rate(const std::vector<Plane> &planes, double beta) {
214 const long n = static_cast<long>(planes.size());
215 if (n < 2) {
216 throw std::invalid_argument("piqtst: the rate needs two planes");
217 }
218 if (!(beta > 0.0)) {
219 throw std::invalid_argument("piqtst: the rate needs a positive beta");
220 }
221 const MatrixXd w = trapezoidWeights(planes);
222 const VectorXd errors = forceErrors(planes);
223 Rate r;
224 double fMin = std::numeric_limits<double>::infinity();
225 for (long j = 0; j < n; ++j) {
226 const double f = planes[static_cast<size_t>(j)].freeEnergy;
227 if (f < fMin) {
228 fMin = f;
229 r.reactant = j;
230 }
231 }
232 const long top = n - 1;
233 const double fTop = planes[static_cast<size_t>(top)].freeEnergy;
234 r.barrier = fTop - fMin;
235 r.barrierError =
236 propagated((w.row(top) - w.row(r.reactant)).transpose(), errors);
237 r.firstPlaneHeight = beta * (planes.front().freeEnergy - fMin);
238
239 // Z = int exp(-beta F) ds by the trapezoid rule, referenced to fMin.
240 VectorXd weight(n);
241 double z = 0.0;
242 for (long j = 0; j < n; ++j) {
243 const double left = j > 0 ? planes[static_cast<size_t>(j)].s -
244 planes[static_cast<size_t>(j - 1)].s
245 : 0.0;
246 const double right = j + 1 < n ? planes[static_cast<size_t>(j + 1)].s -
247 planes[static_cast<size_t>(j)].s
248 : 0.0;
249 weight(j) =
250 0.5 * (left + right) *
251 std::exp(-beta * (planes[static_cast<size_t>(j)].freeEnergy - fMin));
252 z += weight(j);
253 }
254 weight /= z;
255 r.logRate = std::log(0.5 * std::sqrt(2.0 / (std::numbers::pi * beta))) -
256 beta * (fTop - fMin) - std::log(z);
257 const VectorXd gradient =
258 -beta * w.row(top).transpose() + beta * (w.transpose() * weight);
259 r.logRateError = propagated(gradient, errors);
260 return r;
261}
262
264 const RecrossingOptions &o) {
265 const long dof = 3 * c.atoms;
266 const Axes ax = axes(c);
267 if (o.parents < 2 || o.children < 1 || o.spacing < 1 || o.steps < 4 ||
268 o.equilibration < 0) {
269 throw std::invalid_argument(
270 "piqtst: recrossing needs two parents, one child, a positive "
271 "spacing and four steps");
272 }
273 const VectorXd origin = c.reference + o.s * ax.b;
274 VectorXd start = origin;
275 if (o.seed) {
276 start = o.seed(o.s);
277 if (start.size() != dof) {
278 throw std::invalid_argument("piqtst: a seed has the wrong length");
279 }
280 }
282 o.ring);
283 parent.setAllBeads(start.data());
284 parent.setHyperplane(ax.a, origin);
285 for (long step = 0; step < o.equilibration; ++step) {
286 parent.step(pot, c.box, false);
287 }
288
289 // The children draw every normal mode from the free-ring Boltzmann
290 // distribution at beta / P, which PILE's initial momenta are, and carry
291 // their own stream so parent and child noise stay independent.
292 pathintegral::Options childOptions = o.ring;
294 childOptions.gleFile.clear();
295 childOptions.seed = o.ring.seed + 0x9E3779B97F4A7C15ULL;
297 childOptions);
298
299 const long n = o.steps + 1;
300 // Per parent: sum over children of sdot(0) h(s(t) - s*) at each time,
301 // and of sdot(0) h(sdot(0)).
302 std::vector<VectorXd> numerator(static_cast<size_t>(o.parents),
303 VectorXd::Zero(n));
304 std::vector<double> denominator(static_cast<size_t>(o.parents), 0.0);
305 Recrossing out;
306 for (long p = 0; p < o.parents; ++p) {
307 for (long step = 0; step < o.spacing; ++step) {
308 parent.step(pot, c.box, false);
309 }
310 const std::vector<VectorXd> beads = parent.beads();
311 VectorXd &num = numerator[static_cast<size_t>(p)];
312 double &den = denominator[static_cast<size_t>(p)];
313 for (long k = 0; k < o.children; ++k) {
314 child.setBeads(beads);
315 child.thermalMomenta();
316 std::vector<VectorXd> reversed = child.momenta();
317 const double forward = ax.a.dot(child.centroidVelocity());
318 for (int sign = 0; sign < 2; ++sign) {
319 if (sign == 1) {
320 for (auto &v : reversed) {
321 v = -v;
322 }
323 child.setBeads(beads);
324 child.setMomenta(reversed);
325 }
326 const double sdot = sign == 0 ? forward : -forward;
327 const double flux = sdot > 0.0 ? sdot : 0.0;
328 den += flux;
329 num(0) += flux;
330 for (long i = 1; i < n; ++i) {
331 child.nveStep(pot, c.box);
332 const double s = ax.a.dot(child.centroid() - c.reference);
333 if (s > o.s) {
334 num(i) += sdot;
335 }
336 }
337 ++out.trajectories;
338 }
339 }
340 }
341
342 VectorXd total = VectorXd::Zero(n);
343 double totalDen = 0.0;
344 for (long p = 0; p < o.parents; ++p) {
345 total += numerator[static_cast<size_t>(p)];
346 totalDen += denominator[static_cast<size_t>(p)];
347 }
348 if (!(totalDen > 0.0)) {
349 throw std::runtime_error("piqtst: no child left the plane forward");
350 }
351 out.time.resize(static_cast<size_t>(n));
352 out.kappa.resize(static_cast<size_t>(n));
353 for (long i = 0; i < n; ++i) {
354 out.time[static_cast<size_t>(i)] = static_cast<double>(i) * o.ring.dt;
355 out.kappa[static_cast<size_t>(i)] = total(i) / totalDen;
356 }
357
358 // Plateau numerators per parent, averaged over the last quarter, and the
359 // leave-one-parent-out ratios.
360 const long first = n - 1 - o.steps / 4;
361 const double span = static_cast<double>(n - first);
362 std::vector<double> plateauNum(static_cast<size_t>(o.parents), 0.0);
363 double sumNum = 0.0;
364 for (long p = 0; p < o.parents; ++p) {
365 plateauNum[static_cast<size_t>(p)] =
366 numerator[static_cast<size_t>(p)].tail(n - first).sum() / span;
367 sumNum += plateauNum[static_cast<size_t>(p)];
368 }
369 out.plateau = sumNum / totalDen;
370 const double np = static_cast<double>(o.parents);
371 std::vector<double> leaveOut(static_cast<size_t>(o.parents), 0.0);
372 double meanLeave = 0.0;
373 for (long p = 0; p < o.parents; ++p) {
374 leaveOut[static_cast<size_t>(p)] =
375 (sumNum - plateauNum[static_cast<size_t>(p)]) /
376 (totalDen - denominator[static_cast<size_t>(p)]);
377 meanLeave += leaveOut[static_cast<size_t>(p)];
378 }
379 meanLeave /= np;
380 double var = 0.0;
381 for (const double v : leaveOut) {
382 var += (v - meanLeave) * (v - meanLeave);
383 }
384 out.plateauError = std::sqrt((np - 1.0) / np * var);
385 out.batches = parent.batches() + child.batches();
386 return out;
387}
388
389} // namespace eonc::piqtst
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
Ring-polymer NVT step.
void setAllBeads(const double *q)
void setMomenta(const std::vector< VectorXd > &momenta)
One momentum vector of length 3 * nAtoms per bead; fixed coordinates are zeroed.
void nveStep(Potential &pot, const double *box)
Thermostat-free RPMD step: velocity Verlet with the free ring propagated exactly in normal modes.
void setBeads(const std::vector< VectorXd > &beads)
One position vector of length 3 * nAtoms per bead.
void setHyperplane(const VectorXd &normal, const VectorXd &origin)
Hold n · (q_centroid - origin) = 0.
void step(Potential &pot, const double *box, bool record)
const std::vector< VectorXd > & momenta() const
const std::vector< VectorXd > & beads() const
Recrossing recrossing(Potential &pot, const Coordinate &c, const RecrossingOptions &o)
Bennett-Chandler transmission at s*: parents sampled with the centroid held on the plane,...
Definition PIQTST.cpp:263
std::vector< Plane > scan(Potential &pot, const Coordinate &c, const ScanOptions &o)
Samples one ring per plane and integrates the mean force.
Definition PIQTST.cpp:104
void integrate(std::vector< Plane > &planes)
Trapezoid integral of the mean forces, F(s_0) = 0, with errors from independent planes.
Definition PIQTST.cpp:86
Rate rate(const std::vector< Plane > &planes, double beta)
k = (1/2) sqrt(2 / (pi beta)) exp(-beta F(s*)) / int_{s_0}^{s*} exp(-beta F(s)) ds,...
Definition PIQTST.cpp:213
std::string gleFile
Normal-mode GLE matrices.
The coordinate s = n .
Definition PIQTST.h:42
std::vector< char > free
Definition PIQTST.h:46
std::vector< int > numbers
Definition PIQTST.h:45
std::vector< double > masses
Definition PIQTST.h:44
const double * box
Definition PIQTST.h:49
double meanForceError
Definition PIQTST.h:73
std::vector< double > spread
Root-mean-square bead displacement from the centroid per Cartesian coordinate, Angstrom,...
Definition PIQTST.h:82
VectorXd centroid
Production average of the centroid, Cartesian.
Definition PIQTST.h:79
double meanForce
dF/ds = -<n .
Definition PIQTST.h:72
long reactant
Index of the plane with the lowest F, the reactant.
Definition PIQTST.h:98
double barrierError
Definition PIQTST.h:101
double firstPlaneHeight
beta (F(s_0) - F(reactant)): the reactant integral is cut at s_0, so a small value means the first pl...
Definition PIQTST.h:108
double logRate
ln k with k in inverse eOn time units (sqrt(amu Angstrom^2 / eV)), and its standard error.
Definition PIQTST.h:104
double barrier
F(s*) - F(reactant), eV, and its error.
Definition PIQTST.h:100
double logRateError
Definition PIQTST.h:105
long parents
Parent configurations, each this many thermostatted steps after the last.
Definition PIQTST.h:124
long equilibration
Thermostatted steps on the plane before the first parent.
Definition PIQTST.h:121
std::function< VectorXd(double)> seed
Cartesian centroid to start the parent ring at.
Definition PIQTST.h:135
long steps
Unconstrained, thermostat-free steps per child of ring.dt.
Definition PIQTST.h:129
long children
Momentum draws per parent; each runs forward and reversed.
Definition PIQTST.h:127
double s
The dividing plane s*, amu^0.5 Angstrom.
Definition PIQTST.h:119
pathintegral::Options ring
The parents' ring and thermostat.
Definition PIQTST.h:132
std::vector< double > time
t = step * ring.dt, from 0 to steps * ring.dt, and kappa(t) = <sdot(0) h(s(t) - s*)> / <sdot(0) h(sdo...
Definition PIQTST.h:143
double plateau
Mean of kappa(t) over the last quarter of the times, and its jackknife standard error over parents.
Definition PIQTST.h:147
std::vector< double > kappa
Definition PIQTST.h:144
std::function< VectorXd(double)> seed
Cartesian centroid to start the ring at on the plane at s.
Definition PIQTST.h:65
std::vector< double > planes
Plane positions in amu^0.5 Angstrom, ascending.
Definition PIQTST.h:55
long blocks
Equal blocks of the production run for the standard error.
Definition PIQTST.h:59
pathintegral::Options ring
Beads, temperature, units (kB in eV / K, hbar in eV time units), time step and thermostat of the ring...
Definition PIQTST.h:62