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_Timer.H>
22
23// Our includes
24#include <CD_Random.H>
25#include <CD_KMCSolver.H>
26#include <CD_LaPackUtils.H>
27#include <CD_NamespaceHeader.H>
28
29template <typename R, typename State, typename T>
31{
32 CH_TIME("KMCSolver::KMCSolver");
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>
39{
40 CH_TIME("KMCSolver::KMCSolver");
41
42 this->define(a_reactions);
43}
44
45template <typename R, typename State, typename T>
47{
48 CH_TIME("KMCSolver::~KMCSolver");
49}
50
51template <typename R, typename State, typename T>
52inline void
54{
55 CH_TIME("KMCSolver::define");
56
57 m_reactions = a_reactions;
58
59 // Default settings. These are equivalent to ALWAYS using tau-leaping.
60 this->setSolverParameters(0, 0, 100, std::numeric_limits<Real>::max(), 0.0, 1.E-6);
61}
62
63template <typename R, typename State, typename T>
64inline void
66 const T a_numSSA,
67 const T a_maxIter,
68 const Real a_eps,
69 const Real a_SSAlim,
70 const Real a_exitTol) noexcept
71{
72 CH_TIME("KMCSolver::setSolverParameters");
73
74 m_Ncrit = a_numCrit;
75 m_numSSA = a_numSSA;
76 m_maxIter = a_maxIter;
77 m_eps = a_eps;
78 m_SSAlim = a_SSAlim;
79 m_exitTol = a_exitTol;
80}
81
82template <typename R, typename State, typename T>
85{
86 CH_TIME("KMCSolver::getNu(State)");
87
89
90 for (int j = 0; j < a_reactions.size(); j++) {
92
93 // Linearize a trivial state
94 State state = a_state;
95
96 std::vector<T> preState = state.linearOut();
97 for (auto& x : preState) {
98 x = static_cast<T>(0);
99 }
100 state.linearIn(preState);
101
102 // Advance with exactly one reaction.
103 a_reactions[j]->advanceState(state, static_cast<T>(1));
104
105 std::vector<T> postState = state.linearOut();
106
107 // Compute the state change vector.
108 nu.resize(preState.size());
109 for (int i = 0; i < preState.size(); i++) {
110 nu[i] = postState[i] - preState[i];
111 }
112 }
113
114 return ret;
115}
116
117template <typename R, typename State, typename T>
120{
121 CH_TIME("KMCSolver::propensities(State)");
122
123 return this->propensities(a_state, m_reactions);
124}
125
126template <typename R, typename State, typename T>
129{
130 CH_TIME("KMCSolver::propensities(State, ReactionList)");
131
133
134 const size_t numReactions = a_reactions.size();
135
136 for (size_t i = 0; i < numReactions; i++) {
137 A[i] = a_reactions[i]->propensity(a_state);
138 }
139
140 return A;
141}
142
143template <typename R, typename State, typename T>
144inline Real
146{
147 CH_TIME("KMCSolver::totalPropensity(State)");
148
149 return this->totalPropensity(a_state, m_reactions);
150}
151
152template <typename R, typename State, typename T>
153inline Real
155{
156 CH_TIME("KMCSolver::totalPropensity(State, ReactionList)");
157
158 Real A = 0.0;
159
160 const size_t numReactions = a_reactions.size();
161
162 for (size_t i = 0; i < numReactions; i++) {
163 A += a_reactions[i]->propensity(a_state);
164 }
165
166 return A;
167}
168
169template <typename R, typename State, typename T>
172{
173 CH_TIME("KMCSolver::partitionReactions(State)");
174
175 return this->partitionReactions(a_state, m_reactions);
176}
177
178template <typename R, typename State, typename T>
181{
182 CH_TIME("KMCSolver::partitionReactions(State, ReactionList)");
183
186
187 const size_t numReactions = a_reactions.size();
188
189 // Each reaction lands in exactly one of the two lists, so reserving the full count up front avoids reallocations
190 // during the emplace_back calls below.
193
194 for (size_t i = 0; i < numReactions; i++) {
195 const T Lj = a_reactions[i]->computeCriticalNumberOfReactions(a_state);
196
197 if (Lj < m_Ncrit) {
198 criticalReactions.emplace_back(a_reactions[i]);
199 }
200 else {
201 nonCriticalReactions.emplace_back(a_reactions[i]);
202 }
203 }
204
206}
207
208template <typename R, typename State, typename T>
209inline Real
211
212{
213 return this->getCriticalTimeStep(a_state, m_reactions);
214}
215
216template <typename R, typename State, typename T>
217inline Real
219 const ReactionList& a_criticalReactions) const noexcept
220{
221 CH_TIME("KMCSolver::getCriticalTimeStep");
222
223 // TLDR: This computes the time until the firing of the next critical reaction.
224
225 Real dt = std::numeric_limits<Real>::max();
226
227 if (a_criticalReactions.size() > 0) {
228 // Add numeric_limits<Real>::min to A and u to avoid division by zero.
229 const Real A = std::numeric_limits<Real>::min() + this->totalPropensity(a_state, a_criticalReactions);
230
231 dt = this->getCriticalTimeStep(A);
232 }
233
234 return dt;
235}
236
237template <typename R, typename State, typename T>
238inline Real
240{
241 CH_TIME("KineticMonteCarlo::getCriticalTimeStep(std::vector<Real>)");
242
243 // To avoid division by zero later on.
244 Real A = std::numeric_limits<Real>::min();
245
246 for (const auto& p : a_propensities) {
247 A += p;
248 }
249
250 return this->getCriticalTimeStep(A);
251}
252
253template <typename R, typename State, typename T>
254inline Real
256{
257 CH_TIME("KineticMonteCarlo::getCriticalTimeStep(Real)");
258
259 const Real u = std::numeric_limits<Real>::min() + Random::getUniformReal01();
260
261 return log(1.0 / u) / a_totalPropensity;
262}
263
264template <typename R, typename State, typename T>
265inline Real
267{
268 CH_TIME("KMCSolver::getNonCriticalTimeStep(State");
269
270 const auto& partitionedReactions = this->partitionReactions(a_state, m_reactions);
271
272 return this->getNonCriticalTimeStep(a_state, partitionedReactions.second);
273}
274
275template <typename R, typename State, typename T>
276inline Real
278{
279 CH_TIME("KMCSolver::getNonCriticalTimeStep(State, ReactionList");
280
281 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
282
283 return this->getNonCriticalTimeStep(a_state, a_reactions, propensities);
284}
285
286template <typename R, typename State, typename T>
287inline Real
290 const std::vector<Real>& a_nonCriticalPropensities) const noexcept
291{
292 CH_TIME("KMCSolver::getNonCriticalTimeStep(State, ReactionList, std::vector<Real>");
293
295
296 constexpr Real one = 1.0;
297
298 Real dt = std::numeric_limits<Real>::max();
299
300 const size_t numReactions = a_nonCriticalReactions.size();
301
302 if (numReactions > 0) {
303
304 // 1. Gather a list of all reactants involved in the non-critical reactions.
305 auto allReactants = a_nonCriticalReactions[0]->getReactants();
306 for (size_t i = 1; i < numReactions; i++) {
307 const auto& curReactants = a_nonCriticalReactions[i]->getReactants();
308
309 allReactants.insert(allReactants.end(), curReactants.begin(), curReactants.end());
310 }
311
312 // Only do unique reactants. Sorting before the unique() collapses ALL duplicates (std::list::unique only removes
313 // consecutive equal elements), so each distinct reactant is processed exactly once. This does not change the result:
314 // each reactant's contribution is independent and only feeds into a min-reduction.
315 allReactants.sort();
316 allReactants.unique();
317
318 // 2. Iterate through all reactants and compute deviations.
319 for (const auto& reactant : allReactants) {
320
321 // Xi is the population of the current reactant. It might seem weird that we are indexing this
322 // through the reactions rather then through the state. Which it is, but the reason for that is
323 // that it is the REACTION that determines how we index the state population. This is simply a
324 // design choice that permits the user to apply different type of reactions without changing
325 // the underlying state.
326 const T Xi = a_nonCriticalReactions[0]->population(reactant, a_state);
327
328 if (Xi > (T)0) {
329 Real mu = 0.0;
330 Real sigma2 = 0.0;
331
332 // Set gi to 1 for now. A more complex version would parse this through an input parameter
333 // where the user has inspected the highest-order-reaction.
334 constexpr Real gi = 1.0;
335
336 for (size_t i = 0; i < numReactions; i++) {
337 const Real& p = a_nonCriticalPropensities[i];
338 const auto& muIJ = a_nonCriticalReactions[i]->getStateChange(reactant);
339
340 mu += muIJ * p;
341 sigma2 += muIJ * muIJ * p;
342 }
343
344 Real dt1 = std::numeric_limits<Real>::max();
345 Real dt2 = std::numeric_limits<Real>::max();
346
347 const Real f = std::max(m_eps * Xi / gi, one);
348
349 mu = std::abs(mu);
351
352 if (mu > std::numeric_limits<Real>::min()) {
353 dt1 = f / mu;
354 }
355 if (sigma2 > std::numeric_limits<Real>::min()) {
356 dt2 = (f * f) / sigma2;
357 }
358
359 dt = std::min(dt, std::min(dt1, dt2));
360 }
361 }
362 }
363
364 return dt;
365}
366
367template <typename R, typename State, typename T>
368inline Real
372 const Real a_epsilon) const noexcept
373{
374 CH_TIME("KMCSolver::computeDt(State, ReactionList, std::vector<Real>, Real");
375
376 CH_assert(a_reactions.size() == a_propensities.size());
377
378 constexpr Real one = 1.0;
379
380 Real dt = std::numeric_limits<Real>::max();
381
382 const size_t numReactions = a_reactions.size();
383
384 if (numReactions > 0) {
385
386 // 1. Gather a list of all reactants involved in the input reactions
387 auto allReactants = a_reactions[0]->getReactants();
388 for (size_t i = 1; i < numReactions; i++) {
389 const auto& curReactants = a_reactions[i]->getReactants();
390
391 allReactants.insert(allReactants.end(), curReactants.begin(), curReactants.end());
392 }
393
394 // Only do unique reactants. Sorting before the unique() collapses ALL duplicates (std::list::unique only removes
395 // consecutive equal elements), so each distinct reactant is processed exactly once. This does not change the result:
396 // each reactant's contribution is independent and only feeds into a min-reduction.
397 allReactants.sort();
398 allReactants.unique();
399
400 // 2. Iterate through all reactants and compute deviations.
401 for (const auto& reactant : allReactants) {
402
403 // Xi is the population of the current reactant. It might seem weird that we are indexing this
404 // through the reactions rather then through the state. Which it is, but the reason for that is
405 // that it is the REACTION that determines how we index the state population. This is simply a
406 // design choice that permits the user to apply different type of reactions without changing
407 // the underlying state.
408 const T Xi = R::population(reactant, a_state);
409
410 if (Xi > (T)0) {
411 Real mu = 0.0;
412
413 // Set gi to 1 for now. A more complex version would parse this through an input parameter
414 // where the user has inspected the highest-order-reaction.
415 constexpr Real gi = 1.0;
416
417 for (size_t i = 0; i < numReactions; i++) {
418 const Real& p = a_propensities[i];
419 const auto& muIJ = a_reactions[i]->getStateChange(reactant);
420
421 mu += muIJ * p;
422 }
423
424 const Real f = std::max(a_epsilon * Xi / gi, one);
425
426 mu = std::abs(mu);
427
428 if (mu > std::numeric_limits<Real>::min()) {
429 dt = std::min(dt, f / mu);
430 }
431 }
432 }
433 }
434
435 return dt;
436}
437
438template <typename R, typename State, typename T>
439inline void
441{
442 CH_TIME("KMCSolver::stepSSA(State, Real)");
443
444 this->stepSSA(a_state, m_reactions);
445}
446
447template <typename R, typename State, typename T>
448inline void
450{
451 CH_TIME("KMCSolver::stepSSA(State, ReactionList, Real)");
452
453 if (a_reactions.size() > 0) {
454
455 // Compute all propensities.
456 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
457
458 this->stepSSA(a_state, a_reactions, propensities);
459 }
460}
461
462template <typename R, typename State, typename T>
463inline void
466 const std::vector<Real>& a_propensities) const noexcept
467{
468 CH_TIME("KMCSolver::stepSSA(State, ReactionList, std::vector<Real>)");
469
470 CH_assert(a_reactions.size() == a_propensities.size());
471
472 const size_t numReactions = a_reactions.size();
473
474 if (numReactions > 0) {
475 constexpr T one = (T)1;
476
477 // Determine the reaction type as per Gillespie algorithm.
478 Real A = 0.0;
479 for (size_t i = 0; i < numReactions; i++) {
480 A += a_propensities[i];
481 }
482
484
485 size_t r = numReactions - 1;
486
487 Real sumProp = 0.0;
488 for (size_t i = 0; i + 1 < numReactions; i++) {
490
491 if (sumProp >= u * A) {
492 r = i;
493
494 break;
495 }
496 }
497
498 CH_assert(r < a_reactions.size());
499
500 // Advance by one reaction.
501 a_reactions[r]->advanceState(a_state, one);
502 }
503}
504
505template <typename R, typename State, typename T>
506inline void
508{
509 CH_TIME("KMCSolver::advanceSSA(State, Real)");
510
511 this->advanceSSA(a_state, m_reactions, a_dt);
512}
513
514template <typename R, typename State, typename T>
515inline void
517{
518 CH_TIME("KMCSolver::advanceSSA(State, ReactionList, Real)");
519
520 const size_t numReactions = a_reactions.size();
521
522 if (numReactions > 0) {
523
524 // Simulated time within the SSA.
525 Real curDt = 0.0;
526
527 while (curDt <= a_dt) {
528
529 // Compute the propensities and get the time to the next reaction.
530 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
531
532 const Real nextDt = this->getCriticalTimeStep(propensities);
533
534 // Fire one reaction if occurs within a_dt.
535 if (curDt + nextDt <= a_dt) {
536 this->stepSSA(a_state, a_reactions, propensities);
537 }
538
539 curDt += nextDt;
540 }
541 }
542}
543
544template <typename R, typename State, typename T>
545inline void
547{
548 CH_TIME("KMCSolver::stepExplicitEuler(State, Real)");
549
550 this->stepExplicitEuler(a_state, m_reactions, a_dt);
551}
552
553template <typename R, typename State, typename T>
554inline void
557 const Real a_dt) const noexcept
558{
559 CH_TIME("KMCSolver::stepExplicitEuler(State, ReactionList, std::vector<Real>, Real)");
560
561 CH_assert(a_dt > 0.0);
562
563 if (a_reactions.size() > 0) {
564 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
565
566 for (size_t i = 0; i < a_reactions.size(); i++) {
567
568 // Number of reactions is always an integer -- draw from a Poisson distribution in long long. I'm just
569 // using a large integer type to avoid potential overflows.
570 const T numReactions = (T)Random::getPoisson<long long>(propensities[i] * a_dt);
571
572 a_reactions[i]->advanceState(a_state, numReactions);
573 }
574 }
575}
576
577template <typename R, typename State, typename T>
578inline void
580{
581 CH_TIME("KMCSolver::stepMidpoint(State&, const Real)");
582
583 this->stepMidpoint(a_state, m_reactions, a_dt);
584}
585
586template <typename R, typename State, typename T>
587inline void
589{
590 CH_TIMERS("KMCSolver::stepMidpoint(State&, const ReactionList&, const Real)");
591 CH_TIMER("KMCSolver::stepTauMidPoint::first_loop", t1);
592 CH_TIMER("KMCSolver::stepTauMidPoint::second_loop", t2);
593
594 const int numReactions = a_reactions.size();
595
596 if (numReactions > 0) {
597
598 std::vector<Real> propensities = this->propensities(a_state, a_reactions);
599
600 State Xdagger = a_state;
601
602 CH_START(t1);
603 for (size_t i = 0; i < numReactions; i++) {
604 // TLDR: Predict a midpoint state -- unfortunately this means that as a_dt->0 we end up with plain
605 // tau-leaping. I don't know of a way to fix this without introducing double fluctuations (yet).
606 a_reactions[i]->advanceState(Xdagger, (T)std::round(0.5 * propensities[i] * a_dt));
607 }
608 CH_STOP(t1);
609
610 propensities = this->propensities(Xdagger, a_reactions);
611
612 CH_START(t2);
613 for (size_t i = 0; i < numReactions; i++) {
614 const T curReactions = (T)Random::getPoisson<long long>(propensities[i] * a_dt);
615
616 a_reactions[i]->advanceState(a_state, curReactions);
617 }
618 CH_STOP(t2);
619 }
620}
621
622template <typename R, typename State, typename T>
623inline void
625{
626 CH_TIME("KMCSolver::stepPRC(State&, const Real)");
627
628 this->stepPRC(a_state, m_reactions, a_dt);
629}
630
631template <typename R, typename State, typename T>
632inline void
634{
635 CH_TIMERS("KMCSolver::stepPRC(State&, const ReactionList&, const Real)");
636 CH_TIMER("KMCSolver::stepPRC::first_loop", t1);
637 CH_TIMER("KMCSolver::stepPRC::second_loop", t2);
638
639 const int numReactions = a_reactions.size();
640
641 if (numReactions > 0) {
642
643 std::vector<Real> aj = this->propensities(a_state, a_reactions);
644
645 const std::vector<Real> ak = aj;
646
647 CH_START(t1);
648 for (int j = 0; j < numReactions; j++) {
649 for (int k = 0; k < numReactions; k++) {
650 State x = a_state;
651
652 a_reactions[k]->advanceState(x, (T)1);
653
654 const Real etajk = a_reactions[j]->propensity(x) - ak[j];
655
656 aj[j] += 0.5 * a_dt * ak[k] * etajk;
657 }
658 }
659 CH_STOP(t1);
660
661 CH_START(t2);
662 for (size_t i = 0; i < numReactions; i++) {
663 const T nr = (T)Random::getPoisson<long long>(aj[i] * a_dt);
664
665 a_reactions[i]->advanceState(a_state, nr);
666 }
667 CH_STOP(t2);
668 }
669}
670
671template <typename R, typename State, typename T>
672inline void
674{
675 CH_TIME("KMCSolver::stepImplicitEuler(State, a_dt)");
676
677 this->stepImplicitEuler(a_state, m_reactions, a_dt);
678}
679
680template <typename R, typename State, typename T>
681inline void
684 const Real a_dt) const noexcept
685{
686 CH_TIME("KMCSolver::stepImplicitEuler(State, ReactionList, a_dt)");
687
688 // TLDR: The implicit Euler tau-leaping scheme is equivalent to the solution of
689 //
690 // F(X) = X - (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)]) - dt*sum_j nu_j a_j(X)
691 // = X - c - dt*sum_j nu_j * a_j(X),
692 // = 0
693 //
694 // where c = (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)]) is a constant term throughout the Newton iterations. This term is presampled
695 // before the Newton iterations begin.
696
697 // Linearize input state onto something understandable to LAPACK
698 const std::vector<T> inputState = a_state.linearOut();
699 const std::vector<std::vector<T>> nu = this->getNu(a_state, a_reactions);
700 const std::vector<Real> ajX = this->propensities(a_state, a_reactions);
701
702 // Number of equations and number of reactions.
703 const int N = inputState.size();
704 const int M = a_reactions.size();
705
706 // Lambda that computes the constant term c and the initial guess.
707 auto compConstantTerm = [&](double* C, double* X, const State& state, const Real a_dt) -> void {
708 // TLDR: To compute the constant term we perform a Poisson sampling and then linearize the output
709 // state.
710
712
714
715 const std::vector<T> eulerOut = explicitEulerState.linearOut();
716
717 // Make c = (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)])
718 for (int i = 0; i < N; i++) {
719 C[i] = 1.0 * eulerOut[i];
720 X[i] = 1.0 * eulerOut[i];
721 }
722
723 // Subtract the mean.
724 for (int j = 0; j < M; j++) {
725 const std::vector<T>& nuj = nu[j];
726
727 for (int i = 0; i < N; i++) {
728 C[i] -= nuj[i] * ajX[j] * a_dt;
729 }
730 }
731 };
732
733 // Lambda that computes the each equation.
734 auto computeF = [&](double* F, const double* Xit, const double* C) -> void {
735 // Compute the propensities for the Xit state. This requires us to round to the nearest integer.
736 State stateXit = a_state;
737
739 for (int i = 0; i < N; i++) {
740 linState[i] = static_cast<T>(llround(Xit[i]));
741 }
742
743 stateXit.linearIn(linState);
744
745 const std::vector<Real> ajXit = this->propensities(stateXit);
746
747 // Compute Xit - c - dt * sum_j nu_j * aj(round(Xit))
748 for (int i = 0; i < N; i++) {
749 F[i] = Xit[i] - C[i];
750 }
751
752 for (int j = 0; j < M; j++) {
753 const std::vector<T>& nuj = nu[j];
754
755 for (int i = 0; i < N; i++) {
756 F[i] -= a_dt * nuj[i] * ajXit[j];
757 }
758 }
759 };
760
761 // Compute the max-norm
762 auto computeNorm = [&](double* F, const double* X, const double* C) -> Real {
763 computeF(F, X, C);
764
765 Real norm = 0.0;
766
767 for (int i = 0; i < N; i++) {
768 norm = std::max(norm, std::abs(F[i]));
769 }
770
771 return norm;
772 };
773
774 // Allocate memory for the Jacobian (J), the Newton increment (X), and F(X) (F). Also include the constant term from the Poisson sampling (c)
775 std::vector<double> J(static_cast<size_t>(N * N));
776 std::vector<double> X(static_cast<size_t>(N));
777 std::vector<double> F(static_cast<size_t>(N));
778 std::vector<double> C(static_cast<size_t>(N));
779
780 // Temporary storage used for computing the Jacobian matrix.
781 std::vector<double> X1(static_cast<size_t>(N));
782 std::vector<double> X2(static_cast<size_t>(N));
783 std::vector<double> F2(static_cast<size_t>(N));
784
785 // Things that are required by LAPACK
786 int INFO = 0;
787 int NRHS = 1;
788 std::vector<int> IPIV(static_cast<size_t>(N));
789
790 // Compute the constant term.
791 compConstantTerm(C.data(), X.data(), a_state, a_dt);
792
793 // Compute the max-norm of F(0) to use as an exit criterion.
794 for (int i = 0; i < N; i++) {
795 X1[i] = 0.0;
796 }
797
798 const Real initNorm = computeNorm(F.data(), X1.data(), C.data());
799
800 bool converged = true;
801
802 for (int k = 0; k < m_maxIter; k++) {
803
804 // Compute the residual F(X) at the current iterate. It is the unperturbed column used in every finite-difference
805 // Jacobian column (it does not depend on the perturbation direction j) and is also the right-hand side for the
806 // linear solve below, so it is computed once per Newton iteration rather than N+1 times.
807 computeF(F.data(), X.data(), C.data());
808
809 // Numerically computed the Jacobian using finite differences. The Jacobian is given by
810 // Jij = dF_i/dx_j
811 for (int j = 0; j < N; j++) {
812
813 for (int s = 0; s < N; s++) {
814 X2[s] = X[s];
815 }
816
817 X2[j] += std::max(0.01 * X[j], 1.0);
818
819 computeF(F2.data(), X2.data(), C.data());
820
821 for (int i = 0; i < N; i++) {
822 J[i + j * N] = (F2[i] - F[i]) / (X2[j] - X[j]);
823 }
824 }
825
826 // Solve J*dX = F, but note that the true system is J*dX = -F, so we invert the
827 // dX vector below.
828 dgesv_((int*)&N, &NRHS, J.data(), (int*)&N, IPIV.data(), F.data(), (int*)&N, &INFO);
829
830 if (INFO != 0) {
831#if 1 // Could not solve
832 const std::string err = "KMCSolver<R, State, T>::stepImplicitEuler -- could not solve A*x = b";
833
834 pout() << err << endl;
835#endif
836 converged = false;
837
838 break;
839 }
840 else {
841 // Increment and move on to next iteration if necessary. Note that F = -dX as per the comment above.
842 for (int i = 0; i < N; i++) {
843 X[i] = X[i] - F[i];
844 }
845 }
846
847 // Recompute the norm and exit if necessary.
848 const Real norm = computeNorm(F.data(), X.data(), C.data());
849
850 if (norm / initNorm < m_exitTol) {
851 break;
852 }
853 }
854
855 // Turn X into an integer state.
857
858 if (converged) {
859 for (int i = 0; i < N; i++) {
860 outputState[i] = static_cast<T>(llround(X[i]));
861 }
862 }
863 else {
864 // Set X to (what is hopefully) an invalid state and rely on step rejection.
865 for (int i = 0; i < N; i++) {
866 outputState[i] = static_cast<T>(-1);
867 }
868 }
869
870 a_state.linearIn(outputState);
871}
872
873template <typename R, typename State, typename T>
874inline void
876 const Real& a_dt,
877 const KMCLeapPropagator& a_leapPropagator) const noexcept
878{
879 CH_TIME("KMCSolver::advanceTau(State, ReactionList, Real, KMCLeapPropagator)");
880
881 this->advanceTau(a_state, m_reactions, a_dt, a_leapPropagator);
882}
883
884template <typename R, typename State, typename T>
885inline void
888 const Real& a_dt,
889 const KMCLeapPropagator& a_leapPropagator) const noexcept
890{
891 CH_TIME("KMCSolver::advanceTau(State, ReactionList, Real, KMCLeapPropagator)");
892
893 if (a_reactions.size() > 0) {
894 Real curTime = 0.0;
895 Real curDt = a_dt;
896
897 while (curTime < a_dt) {
898
899 // Try big time step first.
900 curDt = a_dt - curTime;
901
902 bool valid = false;
903
904 // Substepping so we end up with a valid state.
905 while (!valid) {
906
907 State state = a_state;
908
909 // Do a tau-leaping step.
910 switch (a_leapPropagator) {
912 this->stepExplicitEuler(state, a_reactions, curDt);
913
914 break;
915 }
917 this->stepMidpoint(state, a_reactions, curDt);
918
919 break;
920 }
922 this->stepPRC(state, a_reactions, curDt);
923
924 break;
925 }
927 this->stepImplicitEuler(state, a_reactions, curDt);
928
929 break;
930 }
931 default: {
932 break;
933 }
934 }
935
936 // If this was a valid step, accept it. Else reduce dt.
937 valid = state.isValidState();
938
939 if (valid) {
940 a_state = state;
941
942 curTime += curDt;
943 }
944 else {
945 curDt *= 0.5;
946 }
947 }
948 }
949 }
950}
951
952template <typename R, typename State, typename T>
953inline void
955 const Real a_dt,
956 const KMCLeapPropagator& a_leapPropagator) const noexcept
957{
958 CH_TIME("KMCSolver::advanceHybrid(State, Real, KMCLeapPropagator)");
959
960 this->advanceHybrid(a_state, m_reactions, a_dt, a_leapPropagator);
961}
962
963template <typename R, typename State, typename T>
964inline void
967 const Real a_dt,
968 const KMCLeapPropagator& a_leapPropagator) const noexcept
969{
970 CH_TIME("KMCSolver::advanceHybrid(State, ReactionList, Real, KMCLeapPropagator)");
971
972 switch (a_leapPropagator) {
974 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
975 this->stepExplicitEuler(s, r, dt);
976 });
977
978 break;
979 }
981 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
982 this->stepMidpoint(s, r, dt);
983 });
984
985 break;
986 }
988 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
989 this->stepPRC(s, r, dt);
990 });
991
992 break;
993 }
995 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
996 this->stepImplicitEuler(s, r, dt);
997 });
998
999 break;
1000 }
1001 default: {
1002 MayDay::Error("KMCSolver::advanceHybrid - unknown leap propagator requested");
1003 }
1004 }
1005}
1006
1007template <typename R, typename State, typename T>
1008inline void
1010 State& a_state,
1012 const Real a_dt,
1013 const std::function<void(State&, const ReactionList& a_reactions, const Real a_dt)>& a_propagator) const noexcept
1014{
1015 CH_TIME("KMCSolver::advanceHybrid(State, ReactionList, Real, std::function)");
1016
1017 constexpr T one = (T)1;
1018
1019 // Simulated time within the advancement algorithm.
1020 Real curTime = 0.0;
1021
1022 // Outer loop is for reactive substepping over a_dt.
1023 while (curTime < a_dt) {
1024
1025 // Partition reactions into critical and non-critical reactions and compute the critical and non-critical time steps.
1027
1030
1031 const std::vector<Real> propensitiesCrit = this->propensities(a_state, criticalReactions);
1033
1034 Real dtCrit = this->getCriticalTimeStep(propensitiesCrit);
1035 Real dtNonCrit = this->getNonCriticalTimeStep(a_state, nonCriticalReactions, propensitiesNonCrit);
1036
1037 // Try the various advancement algorithms; the loop is for step rejection in case we end up with an invalid state, e.g.
1038 // a state with a negative number of particles.
1039 bool validStep = false;
1040
1041 while (!validStep) {
1042
1043 // Do a backup of the advancement state to operate on. This is necessary because the tau-leaping
1044 // algorithms advance the state but we may need to reject those steps if we end up with a thermodynamically
1045 // invalid state.
1046 State state = a_state;
1047
1048 // Compute the time step to be used.
1049 const Real curDt = std::min(a_dt - curTime, std::min(dtCrit, dtNonCrit));
1050
1051 // Are we only doing non-critical reactions?
1052 const bool nonCriticalOnly = (dtNonCrit < dtCrit) || (criticalReactions.size() == 0) ||
1053 (dtCrit > (a_dt - curTime));
1054
1055 // Decide whether tau-leaping is efficient enough, or whether we should fall back to an exact SSA advancement
1056 // over the WHOLE reaction set. This decision is made independently of whether a critical reaction is pending --
1057 // in low-activity regimes exact SSA is both cheaper and more accurate than tau-leaping, so it must be available
1058 // in the critical branch as well. SSA is only used when the user has permitted at least one SSA step
1059 // (m_numSSA > 0); otherwise we always tau-leap, which also avoids a no-progress infinite loop when m_numSSA == 0.
1060 const Real A = this->totalPropensity(state, a_reactions);
1061 const bool useSSA = (m_numSSA >= one) && (A * curDt < m_SSAlim);
1062
1063 if (useSSA) {
1064 // TLDR: Tau-leaping is inefficient here so we switch to an SSA-based algorithm for the WHOLE reaction set.
1065
1066 // Number of SSA steps taken and simulated time within the SSA. We will compute until either dtSSA >= curDt or
1067 // we've exceeded the maximum number of SSA steps that the user has permitted (m_numSSA).
1068 Real dtSSA = 0.0;
1069 T numSSA = 0;
1070
1071 while (dtSSA < curDt && numSSA < m_numSSA) {
1072
1073 // Recompute propensities for the full reaction set and advance everything using the SSA.
1074 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
1075
1076 Real Asum = 0.0;
1077 for (size_t i = 0; i < propensities.size(); i++) {
1078 Asum += propensities[i];
1079 }
1080
1081 // Compute the time to the next reaction.
1082 const Real dtReact = this->getCriticalTimeStep(Asum);
1083
1084 if (dtSSA + dtReact < curDt) {
1085 this->stepSSA(a_state, a_reactions, propensities);
1086
1087 dtSSA += dtReact;
1088 numSSA += one;
1089 }
1090 else {
1091
1092 // Next reaction occurred outside the substep -- break out of the loop.
1093 dtSSA = curDt;
1094 }
1095 }
1096
1097 validStep = true;
1098 curTime += dtSSA;
1099 }
1100 else if (nonCriticalOnly) {
1101 // Do the tau-leaping over the non-critical reactions only.
1103
1104 // Check if we need to reject the state and rather try again with a smaller non-critical time step.
1105 validStep = state.isValidState();
1106
1107 if (validStep) {
1108 a_state = state;
1109
1110 curTime += curDt;
1111 }
1112 else {
1113 dtNonCrit *= 0.5;
1114 }
1115 }
1116 else {
1117 // TLDR: Here, one critical reaction fires. We tau-leap the non-critical reactions over curDt and fire exactly
1118 // one critical reaction using the SSA. The non-critical tau-leap is performed FIRST, while the state is
1119 // still at the start of the step, so that its propensities are evaluated at the start-of-step state. The
1120 // single critical firing is then applied as a fixed stoichiometric increment -- its reaction was already
1121 // selected from the start-of-step propensities (propensitiesCrit), so applying it last is exact. This
1122 // keeps both contributions consistent with the start-of-step state, as required by the
1123 // Cao-Gillespie-Petzold scheme.
1125
1126 this->stepSSA(state, criticalReactions, propensitiesCrit);
1127
1128 // Check if we need to reject the state and rather try again with a smaller non-critical time step.
1129 validStep = state.isValidState();
1130
1131 if (validStep) {
1132 a_state = state;
1133
1134 curTime += curDt;
1135 }
1136 else {
1137 dtNonCrit *= 0.5;
1138 }
1139 }
1140 }
1141 }
1142}
1143
1144#include <CD_NamespaceFooter.H>
1145
1146#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:63
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:65
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:369
virtual ~KMCSolver() noexcept
Destructor.
Definition CD_KMCSolverImplem.H:46
void define(const ReactionList &a_reactions) noexcept
Define function. Sets the reactions.
Definition CD_KMCSolverImplem.H:53
KMCSolver() noexcept
Default constructor. Must subsequently call define.
Definition CD_KMCSolverImplem.H:30
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:84
Real getCriticalTimeStep(const State &a_state) const noexcept
Get the time to the next critical reaction.
Definition CD_KMCSolverImplem.H:210
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:507
void stepImplicitEuler(State &a_state, Real a_dt) const noexcept
Perform one implicit Euler tau-leaping step using ALL reactions.
Definition CD_KMCSolverImplem.H:673
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:954
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:624
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:579
std::pair< ReactionList, ReactionList > partitionReactions(const State &a_state) const noexcept
Partition reactions into critical and non-critical reactions.
Definition CD_KMCSolverImplem.H:171
void stepSSA(State &a_state) const noexcept
Perform a single SSA step.
Definition CD_KMCSolverImplem.H:440
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:875
Real getNonCriticalTimeStep(const State &a_state) const noexcept
Get the non-critical time step.
Definition CD_KMCSolverImplem.H:266
Real totalPropensity(const State &a_state) const noexcept
Compute the total propensity for ALL reactions.
Definition CD_KMCSolverImplem.H:145
void stepExplicitEuler(State &a_state, Real a_dt) const noexcept
Perform one plain tau-leaping step using ALL reactions.
Definition CD_KMCSolverImplem.H:546
std::vector< Real > propensities(const State &a_state) const noexcept
Compute propensities for ALL reactions.
Definition CD_KMCSolverImplem.H:119
static Real getUniformReal01()
Get a uniform real number on the interval [0,1].
Definition CD_RandomImplem.H:156
Base class for a tracer particle solver. This solver can advance particles in a pre-defined velocity ...
Definition CD_TracerParticleSolver.H:38
TracerParticleSolver()
Default constructor.
Definition CD_TracerParticleSolverImplem.H:26