votca 2026-dev
Loading...
Searching...
No Matches
lifetimesimulation.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 <stdexcept>
24
25// Third party includes
26#include <Eigen/IterativeLinearSolvers>
27#include <Eigen/SparseLU>
28
29// Local VOTCA includes
32
33namespace votca {
34namespace xtp {
35
36namespace {
37
38using DecaySums = LifetimeSimulation::DecaySums;
39
40class LifetimeObserver : public KMCEngine::Observer {
41 public:
42 LifetimeObserver(const ToFGraph& graph, Index run, bool keep,
43 Eigen::VectorXd& occupation, Eigen::VectorXd& decays,
44 std::vector<LifetimeSimulation::Record>& records,
45 Index energy_hops,
46 std::vector<LifetimeSimulation::EnergySample>* energy)
47 : graph_(graph),
48 run_(run),
49 keep_(keep),
50 occupation_(occupation),
51 decays_(decays),
52 records_(records),
53 energy_hops_(energy_hops),
54 energy_(energy) {}
55
56 void Inject(Index carrier, Index site, double t) override {
57 if (carrier >= Index(site_.size())) {
58 Index n = carrier + 1;
59 site_.resize(n, -1);
60 entry_.resize(n, 0.0);
61 injected_.resize(n, 0.0);
62 hops_.resize(n, 0);
63 displacement_.resize(n, Eigen::Vector3d::Zero());
64 }
65 site_[carrier] = site;
66 entry_[carrier] = t;
67 injected_[carrier] = t;
68 }
69
70 void Hop(Index carrier, Index from, Index to, const Eigen::Vector3d& dR,
71 double t) override {
72 occupation_[from] += t - entry_[carrier];
73 entry_[carrier] = t;
74 site_[carrier] = to;
75 displacement_[carrier] += dR;
76 ++hops_[carrier];
77 ++total_hops_;
78 if (energy_ != nullptr && energy_hops_ > 0 &&
79 total_hops_ % energy_hops_ == 0) {
80 double e = 0.0;
81 Index n = 0;
82 for (Index s : site_) {
83 if (s >= 0) {
84 e += graph_.getSite(s).energy;
85 ++n;
86 }
87 }
88 energy_->push_back({t, total_hops_, n > 0 ? e / double(n) : 0.0});
89 }
90 }
91
92 void Decay(Index carrier, Index site, double t) override {
93 occupation_[site] += t - entry_[carrier];
94 decays_[site] += 1.0;
95 double lifetime = t - injected_[carrier];
96 const Eigen::Vector3d& R = displacement_[carrier];
97 double r2 = R.squaredNorm();
98 sums_.decays++;
99 sums_.lifetime += lifetime;
100 sums_.lifetime2 += lifetime * lifetime;
101 sums_.distance += std::sqrt(r2);
102 sums_.distance2 += r2;
103 sums_.distance4 += r2 * r2;
104 sums_.components += R.cwiseAbs2();
105 sums_.hops += double(hops_[carrier]);
106 if (keep_) {
107 LifetimeSimulation::Record r;
108 r.run = run_;
109 r.carrier = carrier;
110 r.time = t;
111 r.lifetime = lifetime;
112 r.hops = hops_[carrier];
113 r.site = site;
114 r.displacement = R;
115 records_.push_back(r);
116 }
117 site_[carrier] = -1;
118 }
119
120 void Finish(double end) {
121 for (Index c = 0; c < Index(site_.size()); ++c) {
122 if (site_[c] >= 0) {
123 occupation_[site_[c]] += end - entry_[c];
124 }
125 }
126 }
127
128 const DecaySums& Sums() const { return sums_; }
129
130 private:
131 const ToFGraph& graph_;
132 Index run_;
133 bool keep_;
134 Eigen::VectorXd& occupation_;
135 Eigen::VectorXd& decays_;
136 std::vector<LifetimeSimulation::Record>& records_;
137 Index energy_hops_;
138 std::vector<LifetimeSimulation::EnergySample>* energy_;
139 std::vector<Index> site_;
140 std::vector<double> entry_;
141 std::vector<double> injected_;
142 std::vector<Index> hops_;
143 std::vector<Eigen::Vector3d> displacement_;
144 Index total_hops_ = 0;
145 DecaySums sums_;
146};
147
148} // namespace
149
151 std::vector<double> decay_rates,
152 const Settings& settings)
153 : graph_(graph),
154 decay_rates_(std::move(decay_rates)),
155 settings_(settings),
156 own_rates_(std::make_unique<StaticRates>(graph)),
157 own_strategy_(std::make_unique<ExponentialWaitingTime>()) {
159}
160
162 std::vector<double> decay_rates,
163 const Settings& settings,
164 const TimeDependentRates& rates,
165 WaitingTimeStrategy& strategy)
166 : graph_(graph), decay_rates_(std::move(decay_rates)), settings_(settings) {
167 if (&rates.Graph() != &graph_) {
168 throw std::runtime_error(
169 "LifetimeSimulation: the rates belong to a different graph");
170 }
171 Setup(strategy, rates);
172}
173
175 const TimeDependentRates& rates) {
176 Check();
177 strategy.Prepare(rates);
178 engine_ = std::make_unique<KMCEngine>(graph_, strategy);
180 decay.rates = &decay_rates_;
181 decay.replace = true;
182 engine_->setDecay(decay);
183}
184
186 if (Index(decay_rates_.size()) != graph_.NumberOfSites()) {
187 throw std::runtime_error(
188 "LifetimeSimulation: need one decay rate per site");
189 }
190 for (Index i = 0; i < graph_.NumberOfSites(); ++i) {
191 double k = decay_rates_[i];
192 if (!graph_.getSite(i).ignored && !(k > 0.0 && std::isfinite(k))) {
193 throw std::runtime_error("LifetimeSimulation: the decay rate of site " +
194 std::to_string(i) +
195 " must be positive and finite");
196 }
197 }
198 if (settings_.runs < 1 || settings_.decays < settings_.runs) {
199 throw std::runtime_error(
200 "LifetimeSimulation: need at least one run and one decay per run");
201 }
202 if (settings_.carriers < 1 ||
203 settings_.carriers > Index(graph_.SourceSites().size())) {
204 throw std::runtime_error(
205 "LifetimeSimulation: the number of carriers must be between 1 and "
206 "the number of injection sites (" +
207 std::to_string(graph_.SourceSites().size()) + ")");
208 }
209 if (settings_.energy_hops < 0) {
210 throw std::runtime_error("LifetimeSimulation: energy_hops < 0");
211 }
212}
213
215 Index run, Index decays, std::vector<EnergySample>* energy) const {
216 const Index n = graph_.NumberOfSites();
217 RunData data;
218 data.occupation = Eigen::VectorXd::Zero(n);
219 data.decays = Eigen::VectorXd::Zero(n);
220 LifetimeObserver observer(graph_, run, settings_.keep_records,
221 data.occupation, data.decays, data.records,
222 settings_.energy_hops, energy);
223 KMCEngine::Random random(settings_.seed, std::uint64_t(run));
224 KMCEngine::Limits limits;
225 limits.max_decays = decays;
226 limits.max_wall_time = settings_.max_wall_time;
227 std::vector<double> injection(settings_.carriers, 0.0);
228 data.status = engine_->Run(injection, random, observer, limits);
229 observer.Finish(data.status.end_time);
230 data.sums = observer.Sums();
231 return data;
232}
233
235 const Index runs = settings_.runs;
236 const Index n = graph_.NumberOfSites();
237 Result result;
238 result.decay_fraction = Eigen::VectorXd::Zero(n);
239 result.occupation = Eigen::VectorXd::Zero(n);
240 DecaySums total;
241
242 // blocks of runs in parallel; reduction in run order (thread independent)
243 const Index block = 64;
244 std::vector<RunData> data(block);
245 for (Index start = 0; start < runs; start += block) {
246 Index count = std::min(block, runs - start);
247#pragma omp parallel for schedule(dynamic)
248 for (Index k = 0; k < count; ++k) {
249 Index run = start + k;
250 Index decays =
251 settings_.decays / runs + (run < settings_.decays % runs ? 1 : 0);
252 data[k] = SingleRun(run, decays, run == 0 ? &result.energy : nullptr);
253 }
254 for (Index k = 0; k < count; ++k) {
255 const RunData& d = data[k];
256 result.occupation += d.occupation;
257 result.decay_fraction += d.decays;
258 result.simulated_time += d.status.end_time;
259 result.hop_events += d.status.hops;
260 result.null_events += d.status.null_events;
262 ++result.wall_time_stops;
263 }
264 const DecaySums& s = d.sums;
265 total.decays += s.decays;
266 total.lifetime += s.lifetime;
267 total.lifetime2 += s.lifetime2;
268 total.distance += s.distance;
269 total.distance2 += s.distance2;
270 total.distance4 += s.distance4;
271 total.components += s.components;
272 total.hops += s.hops;
273 result.records.insert(result.records.end(), d.records.begin(),
274 d.records.end());
275 }
276 }
277 if (total.decays == 0) {
278 throw std::runtime_error("LifetimeSimulation: no carrier decayed");
279 }
280 double N = double(total.decays);
281 result.decays = total.decays;
282 auto error = [N](double sum, double sum2) {
283 if (N < 2.0) {
284 return 0.0;
285 }
286 double mean = sum / N;
287 double var = std::max(sum2 / N - mean * mean, 0.0) * N / (N - 1.0);
288 return std::sqrt(var / N);
289 };
290 result.lifetime = total.lifetime / N;
291 result.lifetime_error = error(total.lifetime, total.lifetime2);
292 result.mean_distance = total.distance / N;
293 result.mean_distance_error = error(total.distance, total.distance2);
294 result.squared_displacement = total.distance2 / N;
295 result.squared_displacement_error = error(total.distance2, total.distance4);
296 result.squared_components = total.components / N;
297 result.hops = total.hops / N;
298 result.decay_fraction /= N;
299 if (result.simulated_time > 0.0) {
300 result.occupation /= result.simulated_time;
301 }
302 return result;
303}
304
306 const ToFGraph& graph, const std::vector<double>& decay_rates,
307 Index direct_limit, double tolerance) {
308 const Index n = graph.NumberOfSites();
309 if (Index(decay_rates.size()) != n) {
310 throw std::runtime_error("LifetimeSimulation::Exact: one rate per site");
311 }
312 using SpMat = Eigen::SparseMatrix<double>;
313 std::vector<Eigen::Triplet<double>> triplets;
314 triplets.reserve(graph.NumberOfEdges() + n);
315 Eigen::MatrixXd b = Eigen::MatrixXd::Zero(n, 3);
316 for (Index i = 0; i < n; ++i) {
317 double diag = decay_rates[i] + graph.EscapeRate(i);
318 if (!(diag > 0.0)) {
319 if (graph.getSite(i).ignored) {
320 diag = 1.0; // never visited
321 } else {
322 throw std::runtime_error("LifetimeSimulation::Exact: site " +
323 std::to_string(i) +
324 " neither decays nor hops");
325 }
326 }
327 triplets.emplace_back(i, i, diag);
328 for (const ToFGraph::Edge& e : graph.OutEdges(i)) {
329 triplets.emplace_back(i, e.to, -e.rate);
330 b.row(i) += e.rate * e.dR.transpose();
331 }
332 }
333 SpMat A(n, n);
334 A.setFromTriplets(triplets.begin(), triplets.end());
335 A.makeCompressed();
336 SpMat At = A.transpose();
337 At.makeCompressed();
338
339 ExactResult result;
340 result.converged = true;
341 auto solve = [&](const SpMat& M, const Eigen::MatrixXd& rhs) {
342 Eigen::MatrixXd x(rhs.rows(), rhs.cols());
343 if (n <= direct_limit) {
344 result.solver = "sparse LU";
345 Eigen::SparseLU<SpMat, Eigen::COLAMDOrdering<int>> lu;
346 lu.compute(M);
347 if (lu.info() != Eigen::Success) {
348 throw std::runtime_error(
349 "LifetimeSimulation::Exact: LU factorization failed");
350 }
351 x = lu.solve(rhs);
352 } else {
353 result.solver = "BiCGSTAB with ILUT";
354 Eigen::BiCGSTAB<SpMat, Eigen::IncompleteLUT<double>> bicg;
355 bicg.setTolerance(tolerance);
356 bicg.setMaxIterations(100000);
357 bicg.compute(M); // M outlives bicg
358 for (Index c = 0; c < rhs.cols(); ++c) {
359 // scaled right-hand side (see ScaledSolve); a zero one gives zero
360 bool solved = false;
361 x.col(c) = ScaledSolve(bicg, Eigen::VectorXd(rhs.col(c)), &solved);
362 if (solved && bicg.iterations() >= bicg.maxIterations()) {
363 result.converged = false;
364 }
365 }
366 }
367 for (Index c = 0; c < rhs.cols(); ++c) {
368 double scale = (M * x.col(c)).cwiseAbs().maxCoeff() +
369 rhs.col(c).cwiseAbs().maxCoeff();
370 double err = (M * x.col(c) - rhs.col(c)).cwiseAbs().maxCoeff() /
371 std::max(scale, std::numeric_limits<double>::min());
372 result.backward_error = std::max(result.backward_error, err);
373 }
374 return x;
375 };
376
377 // first moments: lifetime and mean displacement
378 Eigen::MatrixXd rhs1(n, 4);
379 rhs1.col(0).setOnes();
380 rhs1.rightCols(3) = b;
381 Eigen::MatrixXd x1 = solve(A, rhs1);
382 Eigen::VectorXd tau = x1.col(0);
383 Eigen::MatrixXd m = x1.rightCols(3);
384 // second moments per component
385 Eigen::MatrixXd rhs2 = Eigen::MatrixXd::Zero(n, 3);
386 for (Index i = 0; i < n; ++i) {
387 for (const ToFGraph::Edge& e : graph.OutEdges(i)) {
388 rhs2.row(i) +=
389 e.rate * (e.dR.cwiseAbs2().transpose() +
390 2.0 * e.dR.transpose().cwiseProduct(m.row(e.to)));
391 }
392 }
393 Eigen::MatrixXd s2 = solve(A, rhs2);
394 // start distribution: uniform on the injection sites
395 const std::vector<Index>& sources = graph.SourceSites();
396 Eigen::VectorXd p0 = Eigen::VectorXd::Zero(n);
397 for (Index s : sources) {
398 p0[s] = 1.0 / double(sources.size());
399 }
400 Eigen::VectorXd g = solve(At, p0);
401
402 result.lifetime = p0.dot(tau);
403 result.squared_components = (p0.transpose() * s2).transpose();
404 result.squared_displacement = result.squared_components.sum();
405 Eigen::VectorXd k = Eigen::Map<const Eigen::VectorXd>(decay_rates.data(), n);
406 result.decay_probability = k.cwiseProduct(g);
407 result.occupation = g / g.sum();
408 result.converged = result.converged && result.backward_error < 1e-10;
409 return result;
410}
411
412} // namespace xtp
413} // namespace votca
Time-homogeneous KMC: waiting time Exp(escape rate), destination with probability w_e / escape rate....
std::unique_ptr< TimeDependentRates > own_rates_
std::unique_ptr< KMCEngine > engine_
std::unique_ptr< WaitingTimeStrategy > own_strategy_
static ExactResult Exact(const ToFGraph &graph, const std::vector< double > &decay_rates, Index direct_limit=20000, double tolerance=1e-12)
RunData SingleRun(Index run, Index decays, std::vector< EnergySample > *energy) const
void Setup(WaitingTimeStrategy &strategy, const TimeDependentRates &rates)
LifetimeSimulation(const ToFGraph &graph, std::vector< double > decay_rates, const Settings &settings)
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
const std::vector< Index > & SourceSites() const
source sites from which a sink can be reached
Definition tofgraph.h:166
Index NumberOfSites() const
Definition tofgraph.h:149
Index NumberOfEdges() const
Definition tofgraph.h:157
EdgeRange OutEdges(Index i) const
Definition tofgraph.h:153
double EscapeRate(Index i) const
sum of the rates of all out-edges
Definition tofgraph.h:163
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
const std::vector< double > * rates
Definition kmcengine.h:99
bool replace
inject a new carrier at the time of every decay
Definition kmcengine.h:101
double max_wall_time
wall-clock time of the run, s (checked every 4096 events)
Definition kmcengine.h:110
Index max_decays
stop after this many decays
Definition kmcengine.h:112
Eigen::VectorXd occupation
expected time per site per carrier, normalized by the lifetime
Eigen::VectorXd occupation
occupation probability per site (sum: number of carriers)
Eigen::VectorXd decay_fraction
fraction of the decays per site
bool ignored
segment type in Options::ignore_types, no edges
Definition tofgraph.h:122