votca 2026-dev
Loading...
Searching...
No Matches
bulksimulation.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20// Standard includes
21#include <algorithm>
22#include <cmath>
23#include <functional>
24#include <limits>
25#include <stdexcept>
26
27// Third party includes
28#include <Eigen/IterativeLinearSolvers>
29#include <Eigen/SparseLU>
30
31// Local VOTCA includes
34
35namespace votca {
36namespace xtp {
37
38namespace {
39
43class BulkObserver : public KMCEngine::Observer {
44 public:
45 BulkObserver(const ToFGraph& graph, Index carriers, double start,
46 Index blocks, double tau0, Eigen::VectorXd& occupation,
47 double sample_interval, Index sample_hops,
48 std::vector<BulkSimulation::Sample>* samples)
49 : graph_(graph),
50 position_(carriers, Eigen::Vector3d::Zero()),
51 site_(carriers, -1),
52 entry_(carriers, 0.0),
53 start_(start),
54 blocks_(blocks),
55 tau_(tau0),
56 occupation_(occupation),
57 sample_interval_(sample_interval),
58 sample_hops_(sample_hops),
59 samples_(samples) {}
60
61 void Inject(Index carrier, Index site, double t) override {
62 Advance(t);
63 position_[carrier] = graph_.getSite(site).pos;
64 site_[carrier] = site;
65 entry_[carrier] = t;
66 }
67
68 void Hop(Index carrier, Index from, Index to, const Eigen::Vector3d& dR,
69 double t) override {
70 Advance(t);
71 Occupy(from, entry_[carrier], t);
72 position_[carrier] += dR;
73 site_[carrier] = to;
74 entry_[carrier] = t;
75 ++hops_;
76 if (samples_ != nullptr && sample_hops_ > 0 && hops_ % sample_hops_ == 0) {
77 Record(t);
78 }
79 }
80
82 void Finish(double end) {
83 Advance(end);
84 for (Index c = 0; c < Index(site_.size()); ++c) {
85 if (site_[c] >= 0) {
86 Occupy(site_[c], entry_[c], end);
87 }
88 }
89 }
90
91 const std::vector<std::vector<Eigen::Vector3d>>& Boundaries() const {
92 return boundaries_;
93 }
94 double Tau() const { return tau_; }
95 const std::vector<Eigen::Vector3d>& Positions() const { return position_; }
96
97 private:
98 const ToFGraph& graph_;
99 std::vector<Eigen::Vector3d> position_;
100 std::vector<Index> site_;
101 std::vector<double> entry_;
102 double start_;
103 Index blocks_;
104 double tau_;
105 Eigen::VectorXd& occupation_;
106 double sample_interval_;
107 Index sample_hops_;
108 std::vector<BulkSimulation::Sample>* samples_;
109 std::vector<std::vector<Eigen::Vector3d>> boundaries_;
110 Index next_sample_ = 0;
111 Index hops_ = 0;
112
113 void Occupy(Index site, double from, double to) {
114 from = std::max(from, start_);
115 if (to > from) {
116 occupation_[site] += to - from;
117 }
118 }
119
122 void Advance(double t) {
123 while (start_ + double(boundaries_.size()) * tau_ < t) {
124 if (Index(boundaries_.size()) == 2 * blocks_ + 1) {
125 // keep the boundaries at even multiples of tau
126 std::vector<std::vector<Eigen::Vector3d>> kept;
127 kept.reserve(blocks_ + 1);
128 for (Index j = 0; j < Index(boundaries_.size()); j += 2) {
129 kept.push_back(std::move(boundaries_[j]));
130 }
131 boundaries_ = std::move(kept);
132 tau_ *= 2.0;
133 continue;
134 }
135 boundaries_.push_back(position_);
136 }
137 if (samples_ == nullptr || !(sample_interval_ > 0.0)) {
138 return;
139 }
140 while (double(next_sample_) * sample_interval_ < t) {
141 Record(double(next_sample_) * sample_interval_);
142 ++next_sample_;
143 }
144 }
145
146 void Record(double t) {
147 BulkSimulation::Sample s;
148 s.time = t;
149 s.hops = hops_;
150 s.positions = position_;
151 double energy = 0.0;
152 Index n = 0;
153 for (Index site : site_) {
154 if (site >= 0) {
155 energy += graph_.getSite(site).energy;
156 ++n;
157 }
158 }
159 s.energy = (n > 0) ? energy / double(n) : 0.0;
160 samples_->push_back(std::move(s));
161 }
162};
163
164} // namespace
165
167 : graph_(graph),
168 settings_(settings),
169 own_rates_(std::make_unique<StaticRates>(graph)),
170 own_strategy_(std::make_unique<ExponentialWaitingTime>()) {
171 Check();
172 own_strategy_->Prepare(*own_rates_);
173 engine_ = std::make_unique<KMCEngine>(graph_, *own_strategy_);
174}
175
177 const TimeDependentRates& rates,
178 WaitingTimeStrategy& strategy)
179 : graph_(graph), settings_(settings) {
180 Check();
181 if (&rates.Graph() != &graph_) {
182 throw std::runtime_error(
183 "BulkSimulation: the rates belong to a different graph");
184 }
185 strategy.Prepare(rates);
186 engine_ = std::make_unique<KMCEngine>(graph_, strategy);
187}
188
190 if (!graph_.isBulk()) {
191 throw std::runtime_error(
192 "BulkSimulation: needs a bulk graph (ToFGraph::BulkOptions), not a ToF "
193 "graph with sinks");
194 }
195 if (settings_.runs < 1 || settings_.blocks < 2) {
196 throw std::runtime_error(
197 "BulkSimulation: need at least one run and two blocks");
198 }
199 if (settings_.carriers < 1 ||
200 settings_.carriers > Index(graph_.SourceSites().size())) {
201 throw std::runtime_error(
202 "BulkSimulation: the number of carriers must be between 1 and the "
203 "number of injection sites (" +
204 std::to_string(graph_.SourceSites().size()) + ")");
205 }
206 if (settings_.equilibration < 0.0 || settings_.sample_interval < 0.0 ||
207 settings_.sample_hops < 0) {
208 throw std::runtime_error(
209 "BulkSimulation: equilibration and sampling interval must not be "
210 "negative");
211 }
212 const KMCEngine::Limits& l = settings_.limits;
213 if (l.max_time == std::numeric_limits<double>::infinity() &&
214 l.max_events == std::numeric_limits<Index>::max()) {
215 throw std::runtime_error(
216 "BulkSimulation: a limit on the simulated time or on the number of "
217 "events is required");
218 }
219 if (settings_.equilibration >= l.max_time) {
220 throw std::runtime_error(
221 "BulkSimulation: the equilibration time is not shorter than the run");
222 }
223}
224
226 Index run, Eigen::VectorXd& occupation,
227 std::vector<Sample>* trajectory) const {
228 const Index n = settings_.carriers;
229 const KMCEngine::Limits& limits = settings_.limits;
230 double max_escape = 0.0;
231 for (Index i = 0; i < graph_.NumberOfSites(); ++i) {
232 max_escape = std::max(max_escape, graph_.EscapeRate(i));
233 }
234 // any small start works, tau is doubled as needed
235 double tau0 = (max_escape > 0.0) ? 1e-3 / max_escape : 1e-18;
236 BulkObserver observer(graph_, n, settings_.equilibration, settings_.blocks,
237 tau0, occupation, settings_.sample_interval,
238 settings_.sample_hops, trajectory);
239 KMCEngine::Random random(settings_.seed, std::uint64_t(run));
240 std::vector<double> injection(n, 0.0);
241
242 RunData data;
243 data.status = engine_->Run(injection, random, observer, limits);
244 // the state is constant after the last event: if the run stopped at the
245 // time limit, or because nobody could move, the window extends to it
246 const KMCEngine::RunStatus& st = data.status;
247 bool stopped_early = st.reached_wall_time || st.events >= limits.max_events;
248 double end = (!stopped_early && std::isfinite(limits.max_time))
249 ? limits.max_time
250 : st.end_time;
251 observer.Finish(end);
252 data.start = settings_.equilibration;
253 data.end = end;
254 double window = end - data.start;
255 if (!(window > 0.0)) {
256 occupation.setZero();
257 return data; // the run ended before the equilibration time
258 }
259 occupation /= window;
260
261 const auto& b = observer.Boundaries();
262 data.displacement.resize(n);
263 for (Index c = 0; c < n; ++c) {
264 data.displacement[c] = observer.Positions()[c] - b.front()[c];
265 data.velocity += data.displacement[c];
266 }
267 data.velocity /= double(n) * window;
268
269 Index blocks = Index(b.size()) - 1;
270 data.blocks = blocks;
271 data.block_length = observer.Tau();
272 if (blocks < 2) {
273 return data;
274 }
275 const double tau = observer.Tau();
276 Eigen::Vector3d mean = Eigen::Vector3d::Zero();
277 for (Index k = 0; k < blocks; ++k) {
278 for (Index c = 0; c < n; ++c) {
279 mean += b[k + 1][c] - b[k][c];
280 }
281 }
282 double samples = double(blocks * n);
283 mean /= samples;
284 Eigen::Matrix3d cov = Eigen::Matrix3d::Zero();
285 Eigen::Matrix3d block_cov = Eigen::Matrix3d::Zero();
286 for (Index k = 0; k < blocks; ++k) {
287 Eigen::Vector3d per_block = Eigen::Vector3d::Zero();
288 for (Index c = 0; c < n; ++c) {
289 Eigen::Vector3d d = b[k + 1][c] - b[k][c] - mean;
290 cov += d * d.transpose();
291 per_block += d;
292 }
293 per_block /= double(n) * tau; // velocity of the block minus the mean
294 block_cov += per_block * per_block.transpose();
295 }
296 if (samples > 1.0) {
297 data.diffusion = cov / ((samples - 1.0) * 2.0 * tau);
298 }
299 data.velocity_covariance = block_cov / (double(blocks - 1) * double(blocks));
300 return data;
301}
302
304 const Index runs = settings_.runs;
305 const Index sites = graph_.NumberOfSites();
306 Result result;
307 result.runs.resize(runs);
308 result.occupation = Eigen::VectorXd::Zero(sites);
309
310 // blocks of runs in parallel; reduction in run order (thread independent)
311 const Index block = 64;
312 std::vector<Eigen::VectorXd> run_occupation(block);
313 for (Index start = 0; start < runs; start += block) {
314 Index count = std::min(block, runs - start);
315#pragma omp parallel for schedule(dynamic)
316 for (Index k = 0; k < count; ++k) {
317 run_occupation[k] = Eigen::VectorXd::Zero(sites);
318 Index run = start + k;
319 result.runs[run] = SingleRun(run, run_occupation[k],
320 run == 0 ? &result.trajectory : nullptr);
321 }
322 for (Index k = 0; k < count; ++k) {
323 result.occupation += run_occupation[k];
324 }
325 }
326 result.occupation /= double(runs);
327
328 Index valid = 0;
329 for (const RunData& r : result.runs) {
330 result.hops += r.status.hops;
331 result.null_events += r.status.null_events;
333 ++result.wall_time_stops;
334 }
335 if (r.end > r.start) {
336 result.velocity += r.velocity;
337 result.diffusion += r.diffusion;
338 ++valid;
339 }
340 }
341 if (valid == 0) {
342 throw std::runtime_error(
343 "BulkSimulation: no run lasted longer than the equilibration time");
344 }
345 result.velocity /= double(valid);
346 result.diffusion /= double(valid);
347 if (valid == 1) {
348 for (const RunData& r : result.runs) {
349 if (r.end > r.start) {
350 result.velocity_error =
351 r.velocity_covariance.diagonal().cwiseMax(0.0).cwiseSqrt();
352 }
353 }
354 return result;
355 }
356 Eigen::Vector3d var_v = Eigen::Vector3d::Zero();
357 Eigen::Matrix3d var_d = Eigen::Matrix3d::Zero();
358 for (const RunData& r : result.runs) {
359 if (r.end > r.start) {
360 var_v += (r.velocity - result.velocity).cwiseAbs2();
361 var_d += (r.diffusion - result.diffusion).cwiseAbs2();
362 }
363 }
364 double norm = double(valid) * double(valid - 1);
365 result.velocity_error = (var_v / norm).cwiseSqrt();
366 result.diffusion_error = (var_d / norm).cwiseSqrt();
367 return result;
368}
369
370namespace {
373std::vector<Index> Components(const ToFGraph& g, Index& count) {
374 const Index n = g.NumberOfSites();
375 std::vector<Index> index(n, -1), low(n, 0), comp(n, -1);
376 std::vector<char> on_stack(n, 0);
377 std::vector<Index> stack;
378 std::vector<std::pair<Index, Index>> call; // (site, next edge)
379 Index counter = 0;
380 count = 0;
381 for (Index root = 0; root < n; ++root) {
382 if (index[root] >= 0) {
383 continue;
384 }
385 call.emplace_back(root, g.EdgeOffset(root));
386 index[root] = low[root] = counter++;
387 stack.push_back(root);
388 on_stack[root] = 1;
389 while (!call.empty()) {
390 Index v = call.back().first;
391 Index& e = call.back().second;
392 if (e < g.EdgeOffset(v + 1)) {
393 const ToFGraph::Edge& edge = g.getEdge(e++);
394 if (!(edge.rate > 0.0)) {
395 continue;
396 }
397 Index w = edge.to;
398 if (index[w] < 0) {
399 index[w] = low[w] = counter++;
400 stack.push_back(w);
401 on_stack[w] = 1;
402 call.emplace_back(w, g.EdgeOffset(w));
403 } else if (on_stack[w]) {
404 low[v] = std::min(low[v], index[w]);
405 }
406 continue;
407 }
408 if (low[v] == index[v]) {
409 Index w;
410 do {
411 w = stack.back();
412 stack.pop_back();
413 on_stack[w] = 0;
414 comp[w] = count;
415 } while (w != v);
416 ++count;
417 }
418 call.pop_back();
419 if (!call.empty()) {
420 Index u = call.back().first;
421 low[u] = std::min(low[u], low[v]);
422 }
423 }
424 }
425 return comp;
426}
427} // namespace
428
430 Index direct_limit,
431 double tolerance) {
432 ExactResult result;
433 const Index n = graph.NumberOfSites();
434 result.occupation = Eigen::VectorXd::Zero(n);
435 Index ncomp = 0;
436 std::vector<Index> comp = Components(graph, ncomp);
437 // closed classes: no edge with positive rate leaves the component; ignored
438 // sites are isolated and never visited, so they do not count
439 std::vector<char> closed(ncomp, 1);
440 std::vector<char> relevant(ncomp, 0);
441 for (Index i = 0; i < n; ++i) {
442 if (!graph.getSite(i).ignored) {
443 relevant[comp[i]] = 1;
444 }
445 for (const ToFGraph::Edge& e : graph.OutEdges(i)) {
446 if (e.rate > 0.0 && comp[e.to] != comp[i]) {
447 closed[comp[i]] = 0;
448 }
449 }
450 }
451 std::vector<Index> classes;
452 for (Index c = 0; c < ncomp; ++c) {
453 if (closed[c] && relevant[c]) {
454 classes.push_back(c);
455 }
456 }
457 if (classes.size() != 1) {
458 result.message = std::to_string(classes.size()) +
459 " closed classes of sites (e.g. isolated sites or "
460 "clusters without connection): the stationary "
461 "distribution depends on the start";
462 return result;
463 }
464 // local numbering of the closed class
465 std::vector<Index> local(n, -1);
466 std::vector<Index> sites;
467 for (Index i = 0; i < n; ++i) {
468 if (comp[i] == classes[0]) {
469 local[i] = Index(sites.size());
470 sites.push_back(i);
471 }
472 }
473 const Index m = Index(sites.size());
474 result.sites = m;
475 if (m == 1) {
476 result.available = true;
477 result.converged = true;
478 result.solver = "trivial";
479 result.occupation[sites[0]] = 1.0;
480 return result;
481 }
482
483 using SpMat = Eigen::SparseMatrix<double>;
484 // generator rows (from): L_ii = -W_i, L_ij = w_ij
485 std::vector<Eigen::Triplet<double>> tl;
486 Eigen::MatrixXd b = Eigen::MatrixXd::Zero(m, 3);
487 for (Index a = 0; a < m; ++a) {
488 Index i = sites[a];
489 double out = 0.0;
490 for (const ToFGraph::Edge& e : graph.OutEdges(i)) {
491 if (e.rate > 0.0) {
492 tl.emplace_back(a, local[e.to], e.rate);
493 out += e.rate;
494 b.row(a) += e.rate * e.dR.transpose();
495 }
496 }
497 tl.emplace_back(a, a, -out);
498 }
499 SpMat L(m, m);
500 L.setFromTriplets(tl.begin(), tl.end());
501 double scale = 0.0;
502 for (Index a = 0; a < m; ++a) {
503 scale = std::max(scale, -L.coeff(a, a));
504 }
505
506 result.converged = true;
507 auto solve = [&](SpMat M, const Eigen::MatrixXd& rhs) {
508 M.makeCompressed();
509 Eigen::MatrixXd x(rhs.rows(), rhs.cols());
510 if (m <= direct_limit) {
511 result.solver = "sparse LU";
512 Eigen::SparseLU<SpMat, Eigen::COLAMDOrdering<int>> lu;
513 lu.compute(M);
514 if (lu.info() != Eigen::Success) {
515 throw std::runtime_error("BulkSimulation::Exact: LU failed");
516 }
517 x = lu.solve(rhs);
518 } else {
519 result.solver = "BiCGSTAB with ILUT";
520 Eigen::BiCGSTAB<SpMat, Eigen::IncompleteLUT<double>> bicg;
521 bicg.setTolerance(tolerance);
522 bicg.setMaxIterations(100000);
523 bicg.compute(M); // M outlives bicg
524 for (Index c = 0; c < rhs.cols(); ++c) {
525 // scaled right-hand side (see ScaledSolve); a zero one gives zero
526 bool solved = false;
527 x.col(c) = ScaledSolve(bicg, Eigen::VectorXd(rhs.col(c)), &solved);
528 if (solved && bicg.iterations() >= bicg.maxIterations()) {
529 result.converged = false;
530 }
531 }
532 }
533 for (Index c = 0; c < rhs.cols(); ++c) {
534 Eigen::VectorXd r = M * x.col(c) - rhs.col(c);
535 double denom = (M * x.col(c)).cwiseAbs().maxCoeff() +
536 rhs.col(c).cwiseAbs().maxCoeff();
537 if (denom > 0.0) {
538 result.backward_error =
539 std::max(result.backward_error, r.cwiseAbs().maxCoeff() / denom);
540 }
541 }
542 return x;
543 };
544
545 // One equation of L^T pi = 0 and of L chi = v - b is dependent; it is
546 // replaced by fixing the value at one site ("pin"), which keeps the
547 // matrices sparse (a normalization or gauge row would be dense and fill
548 // the LU and ILUT factors). The pin is the site of lowest energy, where pi
549 // is large, so that the other values are not huge. Afterwards pi is
550 // normalized and chi is shifted to the gauge pi^T chi = 0 (L 1 = 0).
551 Index pin = 0;
552 for (Index a = 1; a < m; ++a) {
553 if (graph.getSite(sites[a]).energy < graph.getSite(sites[pin]).energy) {
554 pin = a;
555 }
556 }
557 auto pinned = [pin](SpMat M) {
558 M.prune([pin](Index row, Index, double) { return row != pin; });
559 M.coeffRef(pin, pin) = 1.0;
560 return M;
561 };
562 SpMat A = pinned(SpMat(L.transpose()) / scale);
563 Eigen::MatrixXd e0 = Eigen::MatrixXd::Zero(m, 1);
564 e0(pin, 0) = 1.0;
565 Eigen::VectorXd pi = solve(A, e0).col(0);
566 pi /= pi.sum();
567 Eigen::Vector3d v = (pi.transpose() * b).transpose();
568 SpMat B = pinned(L / scale);
569 Eigen::MatrixXd rhs = (Eigen::VectorXd::Ones(m) * v.transpose() - b) / scale;
570 rhs.row(pin).setZero();
571 Eigen::MatrixXd chi = solve(B, rhs);
572 for (Index c = 0; c < 3; ++c) {
573 chi.col(c).array() -= pi.dot(chi.col(c));
574 }
575
576 Eigen::Matrix3d D = Eigen::Matrix3d::Zero();
577 for (Index a = 0; a < m; ++a) {
578 for (const ToFGraph::Edge& e : graph.OutEdges(sites[a])) {
579 if (e.rate > 0.0) {
580 Eigen::Vector3d g =
581 e.dR + (chi.row(local[e.to]) - chi.row(a)).transpose();
582 D += 0.5 * pi(a) * e.rate * g * g.transpose();
583 }
584 }
585 }
586 for (Index a = 0; a < m; ++a) {
587 result.occupation[sites[a]] = pi(a);
588 }
589 result.velocity = v;
590 result.diffusion = D;
591 result.available = true;
592 result.converged = result.converged && result.backward_error < 1e-10;
593 return result;
594}
595
596} // namespace xtp
597} // namespace votca
std::unique_ptr< WaitingTimeStrategy > own_strategy_
static ExactResult Exact(const ToFGraph &graph, Index direct_limit=20000, double tolerance=1e-12)
direct sparse LU up to direct_limit sites, BiCGSTAB with ILUT above
std::unique_ptr< KMCEngine > engine_
RunData SingleRun(Index run, Eigen::VectorXd &occupation, std::vector< Sample > *trajectory) const
BulkSimulation(const ToFGraph &graph, const Settings &settings)
std::unique_ptr< TimeDependentRates > own_rates_
Time-homogeneous KMC: waiting time Exp(escape rate), destination with probability w_e / escape rate....
the rates stored in the ToFGraph, constant in time
Definition kmcrates.h:80
Hop rates of the edges of a ToFGraph as functions of time.
Definition kmcrates.h:52
const ToFGraph & Graph() const
Definition kmcrates.h:72
Hopping graph for time-of-flight (ToF) transport.
Definition tofgraph.h:77
const Site & getSite(Index i) const
Definition tofgraph.h:151
Index NumberOfSites() const
Definition tofgraph.h:149
const Edge & getEdge(Index e) const
Definition tofgraph.h:161
EdgeRange OutEdges(Index i) const
Definition tofgraph.h:153
Index EdgeOffset(Index i) const
Definition tofgraph.h:160
How a carrier's next hop is drawn: the waiting time and the destination. The pluggable part of the KM...
virtual void Prepare(const TimeDependentRates &rates)=0
STL namespace.
Charge transport classes.
Definition ERIs.h:28
Eigen::VectorXd ScaledSolve(const IterativeSolver &solver, const Eigen::VectorXd &b, bool *solved=nullptr)
Solves A x = b with an Eigen iterative solver for the right-hand side scaled to a largest entry of 1.
Definition scaledsolve.h:44
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
Eigen::Matrix3d diffusion_error
standard error over runs, zero for one run
Eigen::VectorXd occupation
occupation probability per site, averaged over runs
Eigen::Matrix3d velocity_covariance
covariance of the block increments of the velocity estimate
double start
observation window [start, end]
std::vector< Eigen::Vector3d > displacement
displacement of each carrier over the window
Index max_events
candidate events (hops and null events)
Definition kmcengine.h:108
double max_time
simulated time, s
Definition kmcengine.h:106
Index events
candidate events processed
Definition kmcengine.h:135
double energy
site energy of the carrier
Definition tofgraph.h:118
bool ignored
segment type in Options::ignore_types, no edges
Definition tofgraph.h:122