chombo-discharge
Loading...
Searching...
No Matches
CD_KMCSolverImplem.H
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2021-2026 SINTEF Energy Research
3 *
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
13#ifndef CD_KMCSOLVERIMPLEM_H
14#define CD_KMCSOLVERIMPLEM_H
15
16// Std includes
17#include <limits>
18#include <unordered_set>
19
20// Chombo includes
21#include <CH_assert.H>
22#include <MayDay.H>
23#include <parstream.H>
24
25// Our includes
26#include <CD_Random.H>
27#include <CD_KMCSolver.H>
28#include <CD_LaPackUtils.H>
29#include <CD_NamespaceHeader.H>
30
31template <typename R, typename State, typename T>
33{
34 this->setSolverParameters(0, 0, 100, std::numeric_limits<Real>::max(), 0.0, 1.E-6);
35}
36
37template <typename R, typename State, typename T>
38inline KMCSolver<R, State, T>::KMCSolver(const ReactionList& a_reactions) noexcept
39{
40 this->define(a_reactions);
41}
42
43template <typename R, typename State, typename T>
46
47template <typename R, typename State, typename T>
48inline void
49KMCSolver<R, State, T>::define(const ReactionList& a_reactions) noexcept
50{
51 m_reactions = a_reactions;
52
53 // Default settings. These are equivalent to ALWAYS using tau-leaping.
54 this->setSolverParameters(0, 0, 100, std::numeric_limits<Real>::max(), 0.0, 1.E-6);
55}
56
57template <typename R, typename State, typename T>
58inline void
60 const T a_numSSA,
61 const T a_maxIter,
62 const Real a_eps,
63 const Real a_SSAlim,
64 const Real a_exitTol) noexcept
65{
66 m_Ncrit = a_numCrit;
67 m_numSSA = a_numSSA;
68 m_maxIter = a_maxIter;
69 m_eps = a_eps;
70 m_SSAlim = a_SSAlim;
71 m_exitTol = a_exitTol;
72}
73
74template <typename R, typename State, typename T>
75inline std::vector<std::vector<T>>
76KMCSolver<R, State, T>::getNu(const State& a_state, const ReactionList& a_reactions) const noexcept
77{
78 std::vector<std::vector<T>> ret(a_reactions.size());
79
80 for (int j = 0; j < a_reactions.size(); j++) {
81 std::vector<T>& nu = ret[j];
82
83 // Linearize a trivial state
84 State state = a_state;
85
86 std::vector<T> preState = state.linearOut();
87 for (auto& x : preState) {
88 x = static_cast<T>(0);
89 }
90 state.linearIn(preState);
91
92 // Advance with exactly one reaction.
93 a_reactions[j]->advanceState(state, static_cast<T>(1));
94
95 std::vector<T> postState = state.linearOut();
96
97 // Compute the state change vector.
98 nu.resize(preState.size());
99 for (int i = 0; i < preState.size(); i++) {
100 nu[i] = postState[i] - preState[i];
101 }
102 }
103
104 return ret;
105}
106
107template <typename R, typename State, typename T>
108inline std::vector<Real>
109KMCSolver<R, State, T>::propensities(const State& a_state) const noexcept
110{
111 return this->propensities(a_state, m_reactions);
112}
113
114template <typename R, typename State, typename T>
115inline std::vector<Real>
116KMCSolver<R, State, T>::propensities(const State& a_state, const ReactionList& a_reactions) const noexcept
117{
118 std::vector<Real> A(a_reactions.size());
119
120 const size_t numReactions = a_reactions.size();
121
122 for (size_t i = 0; i < numReactions; i++) {
123 A[i] = a_reactions[i]->propensity(a_state);
124 }
125
126 return A;
127}
128
129template <typename R, typename State, typename T>
130inline Real
131KMCSolver<R, State, T>::totalPropensity(const State& a_state) const noexcept
132{
133 return this->totalPropensity(a_state, m_reactions);
134}
135
136template <typename R, typename State, typename T>
137inline Real
138KMCSolver<R, State, T>::totalPropensity(const State& a_state, const ReactionList& a_reactions) const noexcept
139{
140 Real A = 0.0;
141
142 const size_t numReactions = a_reactions.size();
143
144 for (size_t i = 0; i < numReactions; i++) {
145 A += a_reactions[i]->propensity(a_state);
146 }
147
148 return A;
149}
150
151template <typename R, typename State, typename T>
152inline std::pair<typename KMCSolver<R, State, T>::ReactionList, typename KMCSolver<R, State, T>::ReactionList>
153KMCSolver<R, State, T>::partitionReactions(const State& a_state) const noexcept
154{
155 return this->partitionReactions(a_state, m_reactions);
156}
157
158template <typename R, typename State, typename T>
159inline std::pair<typename KMCSolver<R, State, T>::ReactionList, typename KMCSolver<R, State, T>::ReactionList>
160KMCSolver<R, State, T>::partitionReactions(const State& a_state, const ReactionList& a_reactions) const noexcept
161{
162 ReactionList criticalReactions;
163 ReactionList nonCriticalReactions;
164
165 const size_t numReactions = a_reactions.size();
166
167 // Each reaction lands in exactly one of the two lists, so reserving the full count up front avoids reallocations
168 // during the emplace_back calls below.
169 criticalReactions.reserve(numReactions);
170 nonCriticalReactions.reserve(numReactions);
171
172 for (size_t i = 0; i < numReactions; i++) {
173 const T Lj = a_reactions[i]->computeCriticalNumberOfReactions(a_state);
174
175 if (Lj < m_Ncrit) {
176 criticalReactions.emplace_back(a_reactions[i]);
177 }
178 else {
179 nonCriticalReactions.emplace_back(a_reactions[i]);
180 }
181 }
182
183 // Move rather than copy: this runs once per substep per grid cell, and copying the two lists would
184 // duplicate both allocations and bump the refcount of every reaction twice more.
185 return std::make_pair(std::move(criticalReactions), std::move(nonCriticalReactions));
186}
187
188template <typename R, typename State, typename T>
189inline Real
190KMCSolver<R, State, T>::getCriticalTimeStep(const State& a_state) const noexcept
191
192{
193 return this->getCriticalTimeStep(a_state, m_reactions);
194}
195
196template <typename R, typename State, typename T>
197inline Real
199 const ReactionList& a_criticalReactions) const noexcept
200{
201 // TLDR: This computes the time until the firing of the next critical reaction.
202
203 Real dt = std::numeric_limits<Real>::max();
204
205 if (a_criticalReactions.size() > 0) {
206 // Add numeric_limits<Real>::min to A and u to avoid division by zero.
207 const Real A = std::numeric_limits<Real>::min() + this->totalPropensity(a_state, a_criticalReactions);
208
209 dt = this->getCriticalTimeStep(A);
210 }
211
212 return dt;
213}
214
215template <typename R, typename State, typename T>
216inline Real
217KMCSolver<R, State, T>::getCriticalTimeStep(const std::vector<Real>& a_propensities) const noexcept
218{
219 // To avoid division by zero later on.
220 Real A = std::numeric_limits<Real>::min();
221
222 for (const auto& p : a_propensities) {
223 A += p;
224 }
225
226 return this->getCriticalTimeStep(A);
227}
228
229template <typename R, typename State, typename T>
230inline Real
231KMCSolver<R, State, T>::getCriticalTimeStep(const Real& a_totalPropensity) const noexcept
232{
233 const Real u = std::numeric_limits<Real>::min() + Random::getUniformReal01();
234
235 return log(1.0 / u) / a_totalPropensity;
236}
237
238template <typename R, typename State, typename T>
239inline Real
240KMCSolver<R, State, T>::getNonCriticalTimeStep(const State& a_state) const noexcept
241{
242 const auto& partitionedReactions = this->partitionReactions(a_state, m_reactions);
243
244 const std::vector<Real> propensities = this->propensities(a_state, partitionedReactions.second);
245
246 return this->getNonCriticalTimeStep(a_state, m_reactions, partitionedReactions.second, propensities);
247}
248
249template <typename R, typename State, typename T>
250inline Real
251KMCSolver<R, State, T>::getNonCriticalTimeStep(const State& a_state, const ReactionList& a_reactions) const noexcept
252{
253 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
254
255 return this->getNonCriticalTimeStep(a_state, a_reactions, a_reactions, propensities);
256}
257
258template <typename R, typename State, typename T>
259inline void
261{
262 // Size the scratch from the largest reactant index the reactions refer to. Species above that are
263 // not reactants of any of these reactions and can never be reduced over below.
264 size_t numSpecies = 0;
265
266 for (const auto& reaction : a_reactions) {
267 for (const size_t reactant : reaction->getReactants()) {
268 numSpecies = std::max(numSpecies, reactant + 1);
269 }
270 }
271
272 if (m_seenScratch.size() < numSpecies) {
273 m_seenScratch.resize(numSpecies, 0);
274 m_muScratch.resize(numSpecies, 0.0);
275 m_sigmaScratch.resize(numSpecies, 0.0);
276 m_lossScratch.resize(numSpecies, 0.0);
277 }
278
279 // Collect the distinct reactants. The stamps make this a single pass with no sort: a reactant is
280 // appended the first time it is seen and skipped afterwards.
281 m_reactantScratch.clear();
282
283 for (const auto& reaction : a_reactions) {
284 for (const size_t reactant : reaction->getReactants()) {
285 if (!m_seenScratch[reactant]) {
286 m_seenScratch[reactant] = 1;
287
288 m_reactantScratch.push_back(reactant);
289 }
290 }
291 }
292
293 // Leave the stamps clear for the next call.
294 for (const size_t reactant : m_reactantScratch) {
295 m_seenScratch[reactant] = 0;
296 }
297}
298
299template <typename R, typename State, typename T>
300inline Real
302 const ReactionList& a_reactions,
303 const ReactionList& a_nonCriticalReactions,
304 const std::vector<Real>& a_nonCriticalPropensities) const noexcept
305{
306 CH_assert(a_nonCriticalReactions.size() == a_nonCriticalPropensities.size());
307 CH_assert(a_nonCriticalReactions.size() <= a_reactions.size());
308
309 constexpr Real one = 1.0;
310
311 Real dt = std::numeric_limits<Real>::max();
312
313 const size_t numReactions = a_nonCriticalReactions.size();
314
315 if (numReactions > 0) {
316
317 // 1. Gather the distinct reactants of ALL reactions. A reactant of a critical reaction that the non-critical
318 // reactions produce must be bounded too, since the critical propensities are frozen during the leap and must
319 // not drift more than the leap condition allows.
320 this->gatherDistinctReactants(a_reactions);
321
322 for (const size_t reactant : m_reactantScratch) {
323 m_muScratch[reactant] = 0.0;
324 m_sigmaScratch[reactant] = 0.0;
325 m_lossScratch[reactant] = 0.0;
326 }
327
328 // 2. Accumulate the expected net change, its variance, and the expected consumption per reactant.
329 // The consumption only collects the reactions that remove the reactant, so it is the loss side
330 // of the net change without the production that may cancel it. The loops are ordered reaction-
331 // major so that each reaction's state change is fetched once per reactant from its dense table
332 // rather than through a map lookup; the sums are the same as reactant-major.
333 for (size_t i = 0; i < numReactions; i++) {
334 const Real& p = a_nonCriticalPropensities[i];
335
336 for (const size_t reactant : m_reactantScratch) {
337 const auto muIJ = a_nonCriticalReactions[i]->getStateChange(reactant);
338
339 m_muScratch[reactant] += muIJ * p;
340 m_sigmaScratch[reactant] += muIJ * muIJ * p;
341
342 if (muIJ < 0) {
343 m_lossScratch[reactant] -= muIJ * p;
344 }
345 }
346 }
347
348 // 3. Reduce over the reactants.
349 for (const size_t reactant : m_reactantScratch) {
350
351 // Xi is the population of the current reactant. It might seem weird that we are indexing this
352 // through the reactions rather then through the state. Which it is, but the reason for that is
353 // that it is the REACTION that determines how we index the state population. This is simply a
354 // design choice that permits the user to apply different type of reactions without changing
355 // the underlying state.
356 const T Xi = a_nonCriticalReactions[0]->population(reactant, a_state);
357
358 // Set gi to 1 for now. A more complex version would parse this through an input parameter
359 // where the user has inspected the highest-order-reaction.
360 constexpr Real gi = 1.0;
361
362 Real dt1 = std::numeric_limits<Real>::max();
363 Real dt2 = std::numeric_limits<Real>::max();
364 Real dt3 = std::numeric_limits<Real>::max();
365
366 // The floor of one applies at every population, so a reactant at zero is bounded to one
367 // expected net change rather than left unbounded. This is what lets the critical reactions
368 // respond to a species that the leap produces from nothing.
369 const Real f = std::max(m_eps * Xi / gi, one);
370
371 const Real mu = std::abs(m_muScratch[reactant]);
372 const Real sigma2 = std::abs(m_sigmaScratch[reactant]);
373 const Real loss = m_lossScratch[reactant];
374
375 // Leap condition on the mean and variance of the net change.
376 if (mu > std::numeric_limits<Real>::min()) {
377 dt1 = f / mu;
378 }
379 if (sigma2 > std::numeric_limits<Real>::min()) {
380 dt2 = (f * f) / sigma2;
381 }
382
383 // Bound on the gross consumption: the reactions that remove this reactant may not, between
384 // them, be expected to fire more than f times. This is what keeps the Poisson draws of a fast
385 // reaction below the population it draws on when a producing reaction hides its loss from mu.
386 if (loss > std::numeric_limits<Real>::min()) {
387 dt3 = f / loss;
388 }
389
390 dt = std::min(dt, std::min(dt1, std::min(dt2, dt3)));
391 }
392 }
393
394 return dt;
395}
396
397template <typename R, typename State, typename T>
398inline Real
400 const ReactionList& a_reactions,
401 const std::vector<Real>& a_propensities,
402 const Real a_epsilon) const noexcept
403{
404 CH_assert(a_reactions.size() == a_propensities.size());
405
406 constexpr Real one = 1.0;
407
408 Real dt = std::numeric_limits<Real>::max();
409
410 const size_t numReactions = a_reactions.size();
411
412 if (numReactions > 0) {
413
414 // 1. Gather the distinct reactants involved in the input reactions.
415 this->gatherDistinctReactants(a_reactions);
416
417 for (const size_t reactant : m_reactantScratch) {
418 m_muScratch[reactant] = 0.0;
419 }
420
421 // 2. Accumulate the expected change per reactant. The loops are ordered reaction-major so that
422 // each reaction's state change is fetched once per reactant from its dense table rather than
423 // through a map lookup; the sums are the same as reactant-major.
424 for (size_t i = 0; i < numReactions; i++) {
425 const Real& p = a_propensities[i];
426
427 for (const size_t reactant : m_reactantScratch) {
428 const auto muIJ = a_reactions[i]->getStateChange(reactant);
429
430 m_muScratch[reactant] += muIJ * p;
431 }
432 }
433
434 // 3. Reduce over the reactants.
435 for (const size_t reactant : m_reactantScratch) {
436
437 // Xi is the population of the current reactant. It might seem weird that we are indexing this
438 // through the reactions rather then through the state. Which it is, but the reason for that is
439 // that it is the REACTION that determines how we index the state population. This is simply a
440 // design choice that permits the user to apply different type of reactions without changing
441 // the underlying state.
442 const T Xi = R::population(reactant, a_state);
443
444 if (Xi > (T)0) {
445
446 // Set gi to 1 for now. A more complex version would parse this through an input parameter
447 // where the user has inspected the highest-order-reaction.
448 constexpr Real gi = 1.0;
449
450 const Real f = std::max(a_epsilon * Xi / gi, one);
451 const Real mu = std::abs(m_muScratch[reactant]);
452
453 if (mu > std::numeric_limits<Real>::min()) {
454 dt = std::min(dt, f / mu);
455 }
456 }
457 }
458 }
459
460 return dt;
461}
462
463template <typename R, typename State, typename T>
464inline void
465KMCSolver<R, State, T>::stepSSA(State& a_state) const noexcept
466{
467 this->stepSSA(a_state, m_reactions);
468}
469
470template <typename R, typename State, typename T>
471inline void
472KMCSolver<R, State, T>::stepSSA(State& a_state, const ReactionList& a_reactions) const noexcept
473{
474 if (a_reactions.size() > 0) {
475
476 // Compute all propensities.
477 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
478
479 this->stepSSA(a_state, a_reactions, propensities);
480 }
481}
482
483template <typename R, typename State, typename T>
484inline void
486 const ReactionList& a_reactions,
487 const std::vector<Real>& a_propensities) const noexcept
488{
489 CH_assert(a_reactions.size() == a_propensities.size());
490
491 const size_t numReactions = a_reactions.size();
492
493 if (numReactions > 0) {
494 constexpr T one = (T)1;
495
496 // Determine the reaction type as per Gillespie algorithm.
497 Real A = 0.0;
498 for (size_t i = 0; i < numReactions; i++) {
499 A += a_propensities[i];
500 }
501
502 const Real u = Random::getUniformReal01();
503
504 size_t r = numReactions - 1;
505
506 Real sumProp = 0.0;
507 for (size_t i = 0; i + 1 < numReactions; i++) {
508 sumProp += a_propensities[i];
509
510 if (sumProp >= u * A) {
511 r = i;
512
513 break;
514 }
515 }
516
517 CH_assert(r < a_reactions.size());
518
519 // Advance by one reaction.
520 a_reactions[r]->advanceState(a_state, one);
521 }
522}
523
524template <typename R, typename State, typename T>
525inline void
526KMCSolver<R, State, T>::advanceSSA(State& a_state, const Real a_dt) const noexcept
527{
528 this->advanceSSA(a_state, m_reactions, a_dt);
529}
530
531template <typename R, typename State, typename T>
532inline void
533KMCSolver<R, State, T>::advanceSSA(State& a_state, const ReactionList& a_reactions, const Real a_dt) const noexcept
534{
535 const size_t numReactions = a_reactions.size();
536
537 if (numReactions > 0) {
538
539 // Simulated time within the SSA.
540 Real curDt = 0.0;
541
542 while (curDt <= a_dt) {
543
544 // Compute the propensities and get the time to the next reaction.
545 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
546
547 const Real nextDt = this->getCriticalTimeStep(propensities);
548
549 // Fire one reaction if occurs within a_dt.
550 if (curDt + nextDt <= a_dt) {
551 this->stepSSA(a_state, a_reactions, propensities);
552 }
553
554 curDt += nextDt;
555 }
556 }
557}
558
559template <typename R, typename State, typename T>
560inline void
561KMCSolver<R, State, T>::stepExplicitEuler(State& a_state, const Real a_dt) const noexcept
562{
563 this->stepExplicitEuler(a_state, m_reactions, a_dt);
564}
565
566template <typename R, typename State, typename T>
567inline void
569 const ReactionList& a_reactions,
570 const Real a_dt) const noexcept
571{
572 CH_assert(a_dt > 0.0);
573
574 if (a_reactions.size() > 0) {
575 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
576
577 for (size_t i = 0; i < a_reactions.size(); i++) {
578
579 // Number of reactions is always an integer -- draw from a Poisson distribution in long long. I'm just
580 // using a large integer type to avoid potential overflows.
581 const T numReactions = (T)Random::getPoisson<long long>(propensities[i] * a_dt);
582
583 a_reactions[i]->advanceState(a_state, numReactions);
584 }
585 }
586}
587
588template <typename R, typename State, typename T>
589inline void
590KMCSolver<R, State, T>::stepMidpoint(State& a_state, const Real a_dt) const noexcept
591{
592 this->stepMidpoint(a_state, m_reactions, a_dt);
593}
594
595template <typename R, typename State, typename T>
596inline void
597KMCSolver<R, State, T>::stepMidpoint(State& a_state, const ReactionList& a_reactions, const Real a_dt) const noexcept
598{
599 const int numReactions = a_reactions.size();
600
601 if (numReactions > 0) {
602
603 std::vector<Real> propensities = this->propensities(a_state, a_reactions);
604
605 State Xdagger = a_state;
606
607 for (size_t i = 0; i < numReactions; i++) {
608 // TLDR: Predict a midpoint state -- unfortunately this means that as a_dt->0 we end up with plain
609 // tau-leaping. I don't know of a way to fix this without introducing double fluctuations (yet).
610 a_reactions[i]->advanceState(Xdagger, (T)std::round(0.5 * propensities[i] * a_dt));
611 }
612
613 propensities = this->propensities(Xdagger, a_reactions);
614
615 for (size_t i = 0; i < numReactions; i++) {
616 const T curReactions = (T)Random::getPoisson<long long>(propensities[i] * a_dt);
617
618 a_reactions[i]->advanceState(a_state, curReactions);
619 }
620 }
621}
622
623template <typename R, typename State, typename T>
624inline void
625KMCSolver<R, State, T>::stepPRC(State& a_state, const Real a_dt) const noexcept
626{
627 this->stepPRC(a_state, m_reactions, a_dt);
628}
629
630template <typename R, typename State, typename T>
631inline void
632KMCSolver<R, State, T>::stepPRC(State& a_state, const ReactionList& a_reactions, const Real a_dt) const noexcept
633{
634 const int numReactions = a_reactions.size();
635
636 if (numReactions > 0) {
637
638 std::vector<Real> aj = this->propensities(a_state, a_reactions);
639
640 const std::vector<Real> ak = aj;
641
642 for (int j = 0; j < numReactions; j++) {
643 for (int k = 0; k < numReactions; k++) {
644 State x = a_state;
645
646 a_reactions[k]->advanceState(x, (T)1);
647
648 const Real etajk = a_reactions[j]->propensity(x) - ak[j];
649
650 aj[j] += 0.5 * a_dt * ak[k] * etajk;
651 }
652 }
653
654 for (size_t i = 0; i < numReactions; i++) {
655 const T nr = (T)Random::getPoisson<long long>(aj[i] * a_dt);
656
657 a_reactions[i]->advanceState(a_state, nr);
658 }
659 }
660}
661
662template <typename R, typename State, typename T>
663inline void
664KMCSolver<R, State, T>::stepImplicitEuler(State& a_state, const Real a_dt) const noexcept
665{
666 this->stepImplicitEuler(a_state, m_reactions, a_dt);
667}
668
669template <typename R, typename State, typename T>
670inline void
672 const ReactionList& a_reactions,
673 const Real a_dt) const noexcept
674{
675 // TLDR: The implicit Euler tau-leaping scheme is equivalent to the solution of
676 //
677 // F(X) = X - (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)]) - dt*sum_j nu_j a_j(X)
678 // = X - c - dt*sum_j nu_j * a_j(X),
679 // = 0
680 //
681 // where c = (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)]) is a constant term throughout the Newton iterations. This
682 // term is presampled before the Newton iterations begin.
683
684 // Linearize input state onto something understandable to LAPACK
685 const std::vector<T> inputState = a_state.linearOut();
686 const std::vector<std::vector<T>> nu = this->getNu(a_state, a_reactions);
687 const std::vector<Real> ajX = this->propensities(a_state, a_reactions);
688
689 // Number of equations and number of reactions.
690 const int N = inputState.size();
691 const int M = a_reactions.size();
692
693 // Lambda that computes the constant term c and the initial guess.
694 auto compConstantTerm = [&](double* C, double* X, const State& state, const Real a_dt) -> void {
695 // TLDR: To compute the constant term we perform a Poisson sampling and then linearize the output
696 // state.
697
698 State explicitEulerState = state;
699
700 this->advanceTau(explicitEulerState, a_reactions, a_dt, KMCLeapPropagator::ExplicitEuler);
701
702 const std::vector<T> eulerOut = explicitEulerState.linearOut();
703
704 // Make c = (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)])
705 for (int i = 0; i < N; i++) {
706 C[i] = 1.0 * eulerOut[i];
707 X[i] = 1.0 * eulerOut[i];
708 }
709
710 // Subtract the mean.
711 for (int j = 0; j < M; j++) {
712 const std::vector<T>& nuj = nu[j];
713
714 for (int i = 0; i < N; i++) {
715 C[i] -= nuj[i] * ajX[j] * a_dt;
716 }
717 }
718 };
719
720 // Lambda that computes the each equation.
721 auto computeF = [&](double* F, const double* Xit, const double* C) -> void {
722 // Compute the propensities for the Xit state. This requires us to round to the nearest integer.
723 State stateXit = a_state;
724
725 std::vector<T> linState(N);
726 for (int i = 0; i < N; i++) {
727 linState[i] = static_cast<T>(llround(Xit[i]));
728 }
729
730 stateXit.linearIn(linState);
731
732 const std::vector<Real> ajXit = this->propensities(stateXit);
733
734 // Compute Xit - c - dt * sum_j nu_j * aj(round(Xit))
735 for (int i = 0; i < N; i++) {
736 F[i] = Xit[i] - C[i];
737 }
738
739 for (int j = 0; j < M; j++) {
740 const std::vector<T>& nuj = nu[j];
741
742 for (int i = 0; i < N; i++) {
743 F[i] -= a_dt * nuj[i] * ajXit[j];
744 }
745 }
746 };
747
748 // Compute the max-norm
749 auto computeNorm = [&](double* F, const double* X, const double* C) -> Real {
750 computeF(F, X, C);
751
752 Real norm = 0.0;
753
754 for (int i = 0; i < N; i++) {
755 norm = std::max(norm, std::abs(F[i]));
756 }
757
758 return norm;
759 };
760
761 // Allocate memory for the Jacobian (J), the Newton increment (X), and F(X) (F). Also include the constant term from
762 // the Poisson sampling (c)
763 std::vector<double> J(static_cast<size_t>(N * N));
764 std::vector<double> X(static_cast<size_t>(N));
765 std::vector<double> F(static_cast<size_t>(N));
766 std::vector<double> C(static_cast<size_t>(N));
767
768 // Temporary storage used for computing the Jacobian matrix.
769 std::vector<double> X1(static_cast<size_t>(N));
770 std::vector<double> X2(static_cast<size_t>(N));
771 std::vector<double> F2(static_cast<size_t>(N));
772
773 // Things that are required by LAPACK
774 int INFO = 0;
775 int NRHS = 1;
776 std::vector<int> IPIV(static_cast<size_t>(N));
777
778 // Compute the constant term.
779 compConstantTerm(C.data(), X.data(), a_state, a_dt);
780
781 // Compute the max-norm of F(0) to use as an exit criterion.
782 for (int i = 0; i < N; i++) {
783 X1[i] = 0.0;
784 }
785
786 const Real initNorm = computeNorm(F.data(), X1.data(), C.data());
787
788 bool converged = true;
789
790 for (int k = 0; k < m_maxIter; k++) {
791
792 // Compute the residual F(X) at the current iterate. It is the unperturbed column used in every finite-difference
793 // Jacobian column (it does not depend on the perturbation direction j) and is also the right-hand side for the
794 // linear solve below, so it is computed once per Newton iteration rather than N+1 times.
795 computeF(F.data(), X.data(), C.data());
796
797 // Numerically computed the Jacobian using finite differences. The Jacobian is given by
798 // Jij = dF_i/dx_j
799 for (int j = 0; j < N; j++) {
800
801 for (int s = 0; s < N; s++) {
802 X2[s] = X[s];
803 }
804
805 X2[j] += std::max(0.01 * X[j], 1.0);
806
807 computeF(F2.data(), X2.data(), C.data());
808
809 for (int i = 0; i < N; i++) {
810 J[i + j * N] = (F2[i] - F[i]) / (X2[j] - X[j]);
811 }
812 }
813
814 // Solve J*dX = F, but note that the true system is J*dX = -F, so we invert the
815 // dX vector below.
816 dgesv_((int*)&N, &NRHS, J.data(), (int*)&N, IPIV.data(), F.data(), (int*)&N, &INFO);
817
818 if (INFO != 0) {
819#if 1 // Could not solve
820 const std::string err = "KMCSolver<R, State, T>::stepImplicitEuler -- could not solve A*x = b";
821
822 pout() << err << endl;
823#endif
824 converged = false;
825
826 break;
827 }
828 else {
829 // Increment and move on to next iteration if necessary. Note that F = -dX as per the comment above.
830 for (int i = 0; i < N; i++) {
831 X[i] = X[i] - F[i];
832 }
833 }
834
835 // Recompute the norm and exit if necessary.
836 const Real norm = computeNorm(F.data(), X.data(), C.data());
837
838 if (norm / initNorm < m_exitTol) {
839 break;
840 }
841 }
842
843 // Turn X into an integer state.
844 std::vector<T> outputState(N);
845
846 if (converged) {
847 for (int i = 0; i < N; i++) {
848 outputState[i] = static_cast<T>(llround(X[i]));
849 }
850 }
851 else {
852 // Set X to (what is hopefully) an invalid state and rely on step rejection.
853 for (int i = 0; i < N; i++) {
854 outputState[i] = static_cast<T>(-1);
855 }
856 }
857
858 a_state.linearIn(outputState);
859}
860
861template <typename R, typename State, typename T>
862inline void
864 const Real& a_dt,
865 const KMCLeapPropagator& a_leapPropagator) const noexcept
866{
867 this->advanceTau(a_state, m_reactions, a_dt, a_leapPropagator);
868}
869
870template <typename R, typename State, typename T>
871inline void
873 const ReactionList& a_reactions,
874 const Real& a_dt,
875 const KMCLeapPropagator& a_leapPropagator) const noexcept
876{
877 if (a_reactions.size() > 0) {
878 Real curTime = 0.0;
879
880 while (curTime < a_dt) {
881
882 // Bound the substep by the tau-leaping condition tau = eps * X_i / |sum_j nu_ij * a_j|, as advanceHybrid
883 // does for its non-critical reactions. The propensities are recomputed each substep because the state
884 // changes.
885 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
886
887 const Real dtLeap = this->getNonCriticalTimeStep(a_state, a_reactions, a_reactions, propensities);
888
889 Real curDt = std::min(a_dt - curTime, dtLeap);
890
891 bool valid = false;
892
893 // Substepping so we end up with a valid state.
894 while (!valid) {
895
896 State state = a_state;
897
898 // Do a tau-leaping step.
899 switch (a_leapPropagator) {
901 this->stepExplicitEuler(state, a_reactions, curDt);
902
903 break;
904 }
906 this->stepMidpoint(state, a_reactions, curDt);
907
908 break;
909 }
911 this->stepPRC(state, a_reactions, curDt);
912
913 break;
914 }
916 this->stepImplicitEuler(state, a_reactions, curDt);
917
918 break;
919 }
920 default: {
921 break;
922 }
923 }
924
925 // If this was a valid step, accept it. Else reduce dt.
926 valid = state.isValidState();
927
928 if (valid) {
929 a_state = state;
930
931 curTime += curDt;
932 }
933 else {
934 curDt *= 0.5;
935 }
936 }
937 }
938 }
939}
940
941template <typename R, typename State, typename T>
942inline void
944 const Real a_dt,
945 const KMCLeapPropagator& a_leapPropagator) const noexcept
946{
947 this->advanceHybrid(a_state, m_reactions, a_dt, a_leapPropagator);
948}
949
950template <typename R, typename State, typename T>
951inline void
953 const ReactionList& a_reactions,
954 const Real a_dt,
955 const KMCLeapPropagator& a_leapPropagator) const noexcept
956{
957 switch (a_leapPropagator) {
959 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
960 this->stepExplicitEuler(s, r, dt);
961 });
962
963 break;
964 }
966 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
967 this->stepMidpoint(s, r, dt);
968 });
969
970 break;
971 }
973 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
974 this->stepPRC(s, r, dt);
975 });
976
977 break;
978 }
980 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
981 this->stepImplicitEuler(s, r, dt);
982 });
983
984 break;
985 }
986 default: {
987 MayDay::Error("KMCSolver::advanceHybrid - unknown leap propagator requested");
988 }
989 }
990}
991
992template <typename R, typename State, typename T>
993inline void
995 State& a_state,
996 const ReactionList& a_reactions,
997 const Real a_dt,
998 const std::function<void(State&, const ReactionList& a_reactions, const Real a_dt)>& a_propagator) const noexcept
999{
1000 constexpr T one = (T)1;
1001
1002 // Simulated time within the advancement algorithm.
1003 Real curTime = 0.0;
1004
1005 // Outer loop is for reactive substepping over a_dt.
1006 while (curTime < a_dt) {
1007
1008 // Partition reactions into critical and non-critical reactions. The propensities are those of the state at the
1009 // start of the substep: the critical firing (if any) is selected from them, and the non-critical leap starts from
1010 // the same state.
1011 const std::pair<ReactionList, ReactionList> partitionedReactions = this->partitionReactions(a_state, a_reactions);
1012
1013 const ReactionList& criticalReactions = partitionedReactions.first;
1014 const ReactionList& nonCriticalReactions = partitionedReactions.second;
1015
1016 const std::vector<Real> propensitiesCrit = this->propensities(a_state, criticalReactions);
1017 const std::vector<Real> propensitiesNonCrit = this->propensities(a_state, nonCriticalReactions);
1018
1019 // Leap candidate from the tau-leaping condition on the non-critical reactions. Halved on every rejected step.
1020 Real dtNonCrit = this->getNonCriticalTimeStep(a_state, a_reactions, nonCriticalReactions, propensitiesNonCrit);
1021
1022 // Total propensity of the whole reaction set, which decides between tau-leaping and the SSA below.
1023 const Real A = this->totalPropensity(a_state, a_reactions);
1024
1025 // The loop is for step rejection in case we end up with an invalid state, e.g. a state with a negative number of
1026 // particles.
1027 bool validStep = false;
1028
1029 while (!validStep) {
1030
1031 // The leap candidate, bounded by the remaining time. It is a function of the state only, so the choice between
1032 // the SSA and tau-leaping below is made without reference to any random draw.
1033 const Real dtLeap = std::min(a_dt - curTime, dtNonCrit);
1034
1035 // Fall back to an exact SSA advancement of the WHOLE reaction set when the leap is expected to carry fewer than
1036 // m_SSAlim firings; in that limit the SSA is both cheaper and more accurate than tau-leaping. This is only done
1037 // when the user has permitted at least one SSA step (m_numSSA > 0); otherwise we always tau-leap, which also
1038 // avoids a no-progress infinite loop when m_numSSA == 0.
1039 const bool useSSA = (m_numSSA >= one) && (A * dtLeap < m_SSAlim);
1040
1041 if (useSSA) {
1042 // Advance with the SSA until either dtLeap has elapsed or m_numSSA reactions have fired. A firing whose
1043 // waiting time reaches beyond dtLeap is not applied; the state is advanced to dtLeap and the waiting time is
1044 // drawn again in the next substep, which is exact because the waiting time is memoryless.
1045 Real dtSSA = 0.0;
1046 T numSSA = 0;
1047
1048 while (dtSSA < dtLeap && numSSA < m_numSSA) {
1049
1050 // Recompute propensities for the full reaction set and advance everything using the SSA.
1051 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
1052
1053 const Real dtReact = this->getCriticalTimeStep(propensities);
1054
1055 if (dtSSA + dtReact < dtLeap) {
1056 this->stepSSA(a_state, a_reactions, propensities);
1057
1058 dtSSA += dtReact;
1059 numSSA += one;
1060 }
1061 else {
1062 dtSSA = dtLeap;
1063 }
1064 }
1065
1066 validStep = true;
1067 curTime += dtSSA;
1068 }
1069 else {
1070 // Waiting time to the next critical firing. Drawn only now, after the SSA decision, and drawn again on every
1071 // retry, so that every draw is either used or discarded together with the step it belonged to.
1072 const Real dtCrit = (criticalReactions.size() > 0) ? this->getCriticalTimeStep(propensitiesCrit)
1073 : std::numeric_limits<Real>::max();
1074
1075 // One critical reaction fires if it is due before the leap candidate runs out; the substep then ends at the
1076 // firing.
1077 const bool fireCritical = (dtCrit < dtLeap);
1078
1079 const Real curDt = fireCritical ? dtCrit : dtLeap;
1080
1081 // Operate on a copy so that the step can be rejected.
1082 State state = a_state;
1083
1084 // Tau-leap the non-critical reactions over curDt. This is done FIRST, while the state is still at the start of
1085 // the substep, so that the leap propensities are evaluated at the start-of-substep state. The single critical
1086 // firing is then applied as a fixed stoichiometric increment; its reaction is selected from the
1087 // start-of-substep propensities (propensitiesCrit), so applying it last is exact. Both contributions are thus
1088 // consistent with the start-of-substep state, as required by the Cao-Gillespie-Petzold scheme.
1089 a_propagator(state, nonCriticalReactions, curDt);
1090
1091 if (fireCritical) {
1092 this->stepSSA(state, criticalReactions, propensitiesCrit);
1093 }
1094
1095 // Check if we need to reject the state and rather try again with a smaller leap candidate.
1096 validStep = state.isValidState();
1097
1098 if (validStep) {
1099 a_state = state;
1100
1101 curTime += curDt;
1102 }
1103 else {
1104 dtNonCrit *= 0.5;
1105 }
1106 }
1107 }
1108 }
1109}
1110
1111#include <CD_NamespaceFooter.H>
1112
1113#endif
Class for running Kinetic Monte Carlo functionality.
KMCLeapPropagator
Supported propagators for hybrid tau leaping.
Definition CD_KMCSolver.H:35
@ ImplicitEuler
Implicit Euler tau leaping.
@ Midpoint
Gillespie's midpoint method.
@ ExplicitEuler
Regular tau leaping.
@ PRC
Hu and Li's Poisson random correction method.
Interface to some LaPack routines.
File containing some useful static methods related to random number generation.
std::vector< std::shared_ptr< const R > > ReactionList
Alias for the list of reactions.
Definition CD_KMCSolver.H:66
void setSolverParameters(T a_numCrit, T a_numSSA, T a_maxIter, Real a_eps, Real a_SSAlim, Real a_exitTol) noexcept
Set solver parameters.
Definition CD_KMCSolverImplem.H:59
Real computeDt(const State &a_state, const ReactionList &a_reactions, const std::vector< Real > &a_propensities, Real a_epsilon) const noexcept
Compute a time step using the leap condition on the mean value.
Definition CD_KMCSolverImplem.H:399
virtual ~KMCSolver() noexcept
Destructor.
Definition CD_KMCSolverImplem.H:44
void define(const ReactionList &a_reactions) noexcept
Define function. Sets the reactions.
Definition CD_KMCSolverImplem.H:49
KMCSolver() noexcept
Default constructor. Must subsequently call define.
Definition CD_KMCSolverImplem.H:32
std::vector< std::vector< T > > getNu(const State &a_state, const ReactionList &a_reactions) const noexcept
Compute the state vector changes for all reactions.
Definition CD_KMCSolverImplem.H:76
Real getCriticalTimeStep(const State &a_state) const noexcept
Get the time to the next critical reaction.
Definition CD_KMCSolverImplem.H:190
void advanceSSA(State &a_state, Real a_dt) const noexcept
Advance with the SSA over the input time. This can end up using substepping.
Definition CD_KMCSolverImplem.H:526
void gatherDistinctReactants(const ReactionList &a_reactions) const noexcept
Fill m_reactantScratch with the distinct reactants of the input reactions.
Definition CD_KMCSolverImplem.H:260
void stepImplicitEuler(State &a_state, Real a_dt) const noexcept
Perform one implicit Euler tau-leaping step using ALL reactions.
Definition CD_KMCSolverImplem.H:664
void advanceHybrid(State &a_state, Real a_dt, const KMCLeapPropagator &a_leapPropagator=KMCLeapPropagator::ExplicitEuler) const noexcept
Advance using Cao et. al. hybrid algorithm over the input time. This can end up using substepping.
Definition CD_KMCSolverImplem.H:943
void stepPRC(State &a_state, Real a_dt) const noexcept
Perform one leaping step using the PRC method for ALL reactions.
Definition CD_KMCSolverImplem.H:625
void stepMidpoint(State &a_state, Real a_dt) const noexcept
Perform one leaping step using the midpoint method for ALL reactions.
Definition CD_KMCSolverImplem.H:590
std::pair< ReactionList, ReactionList > partitionReactions(const State &a_state) const noexcept
Partition reactions into critical and non-critical reactions.
Definition CD_KMCSolverImplem.H:153
void stepSSA(State &a_state) const noexcept
Perform a single SSA step.
Definition CD_KMCSolverImplem.H:465
void advanceTau(State &a_state, const Real &a_dt, const KMCLeapPropagator &a_leapPropagator=KMCLeapPropagator::ExplicitEuler) const noexcept
Advance using a specified tau-leaping algorithm.
Definition CD_KMCSolverImplem.H:863
Real getNonCriticalTimeStep(const State &a_state) const noexcept
Get the non-critical time step.
Definition CD_KMCSolverImplem.H:240
Real totalPropensity(const State &a_state) const noexcept
Compute the total propensity for ALL reactions.
Definition CD_KMCSolverImplem.H:131
void stepExplicitEuler(State &a_state, Real a_dt) const noexcept
Perform one plain tau-leaping step using ALL reactions.
Definition CD_KMCSolverImplem.H:561
std::vector< Real > propensities(const State &a_state) const noexcept
Compute propensities for ALL reactions.
Definition CD_KMCSolverImplem.H:109
static Real getUniformReal01()
Get a uniform real number on the interval [0,1].
Definition CD_RandomImplem.H:158