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 return this->getNonCriticalTimeStep(a_state, partitionedReactions.second);
245}
246
247template <typename R, typename State, typename T>
248inline Real
249KMCSolver<R, State, T>::getNonCriticalTimeStep(const State& a_state, const ReactionList& a_reactions) const noexcept
250{
251 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
252
253 return this->getNonCriticalTimeStep(a_state, a_reactions, propensities);
254}
255
256template <typename R, typename State, typename T>
257inline void
259{
260 // Size the scratch from the largest reactant index the reactions refer to. Species above that are
261 // not reactants of any of these reactions and can never be reduced over below.
262 size_t numSpecies = 0;
263
264 for (const auto& reaction : a_reactions) {
265 for (const size_t reactant : reaction->getReactants()) {
266 numSpecies = std::max(numSpecies, reactant + 1);
267 }
268 }
269
270 if (m_seenScratch.size() < numSpecies) {
271 m_seenScratch.resize(numSpecies, 0);
272 m_muScratch.resize(numSpecies, 0.0);
273 m_sigmaScratch.resize(numSpecies, 0.0);
274 }
275
276 // Collect the distinct reactants. The stamps make this a single pass with no sort: a reactant is
277 // appended the first time it is seen and skipped afterwards.
278 m_reactantScratch.clear();
279
280 for (const auto& reaction : a_reactions) {
281 for (const size_t reactant : reaction->getReactants()) {
282 if (!m_seenScratch[reactant]) {
283 m_seenScratch[reactant] = 1;
284
285 m_reactantScratch.push_back(reactant);
286 }
287 }
288 }
289
290 // Leave the stamps clear for the next call.
291 for (const size_t reactant : m_reactantScratch) {
292 m_seenScratch[reactant] = 0;
293 }
294}
295
296template <typename R, typename State, typename T>
297inline Real
299 const ReactionList& a_nonCriticalReactions,
300 const std::vector<Real>& a_nonCriticalPropensities) const noexcept
301{
302 CH_assert(a_nonCriticalReactions.size() == a_nonCriticalPropensities.size());
303
304 constexpr Real one = 1.0;
305
306 Real dt = std::numeric_limits<Real>::max();
307
308 const size_t numReactions = a_nonCriticalReactions.size();
309
310 if (numReactions > 0) {
311
312 // 1. Gather the distinct reactants involved in the non-critical reactions.
313 this->gatherDistinctReactants(a_nonCriticalReactions);
314
315 for (const size_t reactant : m_reactantScratch) {
316 m_muScratch[reactant] = 0.0;
317 m_sigmaScratch[reactant] = 0.0;
318 }
319
320 // 2. Accumulate the expected change and variance per reactant. The loops are ordered reaction-
321 // major so that each reaction's state change is fetched once per reactant from its dense table
322 // rather than through a map lookup; the sums are the same as reactant-major.
323 for (size_t i = 0; i < numReactions; i++) {
324 const Real& p = a_nonCriticalPropensities[i];
325
326 for (const size_t reactant : m_reactantScratch) {
327 const auto muIJ = a_nonCriticalReactions[i]->getStateChange(reactant);
328
329 m_muScratch[reactant] += muIJ * p;
330 m_sigmaScratch[reactant] += muIJ * muIJ * p;
331 }
332 }
333
334 // 3. Reduce over the reactants.
335 for (const size_t reactant : m_reactantScratch) {
336
337 // Xi is the population of the current reactant. It might seem weird that we are indexing this
338 // through the reactions rather then through the state. Which it is, but the reason for that is
339 // that it is the REACTION that determines how we index the state population. This is simply a
340 // design choice that permits the user to apply different type of reactions without changing
341 // the underlying state.
342 const T Xi = a_nonCriticalReactions[0]->population(reactant, a_state);
343
344 if (Xi > (T)0) {
345
346 // Set gi to 1 for now. A more complex version would parse this through an input parameter
347 // where the user has inspected the highest-order-reaction.
348 constexpr Real gi = 1.0;
349
350 Real dt1 = std::numeric_limits<Real>::max();
351 Real dt2 = std::numeric_limits<Real>::max();
352
353 const Real f = std::max(m_eps * Xi / gi, one);
354
355 const Real mu = std::abs(m_muScratch[reactant]);
356 const Real sigma2 = std::abs(m_sigmaScratch[reactant]);
357
358 if (mu > std::numeric_limits<Real>::min()) {
359 dt1 = f / mu;
360 }
361 if (sigma2 > std::numeric_limits<Real>::min()) {
362 dt2 = (f * f) / sigma2;
363 }
364
365 dt = std::min(dt, std::min(dt1, dt2));
366 }
367 }
368 }
369
370 return dt;
371}
372
373template <typename R, typename State, typename T>
374inline Real
376 const ReactionList& a_reactions,
377 const std::vector<Real>& a_propensities,
378 const Real a_epsilon) const noexcept
379{
380 CH_assert(a_reactions.size() == a_propensities.size());
381
382 constexpr Real one = 1.0;
383
384 Real dt = std::numeric_limits<Real>::max();
385
386 const size_t numReactions = a_reactions.size();
387
388 if (numReactions > 0) {
389
390 // 1. Gather the distinct reactants involved in the input reactions.
391 this->gatherDistinctReactants(a_reactions);
392
393 for (const size_t reactant : m_reactantScratch) {
394 m_muScratch[reactant] = 0.0;
395 }
396
397 // 2. Accumulate the expected change per reactant. The loops are ordered reaction-major so that
398 // each reaction's state change is fetched once per reactant from its dense table rather than
399 // through a map lookup; the sums are the same as reactant-major.
400 for (size_t i = 0; i < numReactions; i++) {
401 const Real& p = a_propensities[i];
402
403 for (const size_t reactant : m_reactantScratch) {
404 const auto muIJ = a_reactions[i]->getStateChange(reactant);
405
406 m_muScratch[reactant] += muIJ * p;
407 }
408 }
409
410 // 3. Reduce over the reactants.
411 for (const size_t reactant : m_reactantScratch) {
412
413 // Xi is the population of the current reactant. It might seem weird that we are indexing this
414 // through the reactions rather then through the state. Which it is, but the reason for that is
415 // that it is the REACTION that determines how we index the state population. This is simply a
416 // design choice that permits the user to apply different type of reactions without changing
417 // the underlying state.
418 const T Xi = R::population(reactant, a_state);
419
420 if (Xi > (T)0) {
421
422 // Set gi to 1 for now. A more complex version would parse this through an input parameter
423 // where the user has inspected the highest-order-reaction.
424 constexpr Real gi = 1.0;
425
426 const Real f = std::max(a_epsilon * Xi / gi, one);
427 const Real mu = std::abs(m_muScratch[reactant]);
428
429 if (mu > std::numeric_limits<Real>::min()) {
430 dt = std::min(dt, f / mu);
431 }
432 }
433 }
434 }
435
436 return dt;
437}
438
439template <typename R, typename State, typename T>
440inline void
441KMCSolver<R, State, T>::stepSSA(State& a_state) const noexcept
442{
443 this->stepSSA(a_state, m_reactions);
444}
445
446template <typename R, typename State, typename T>
447inline void
448KMCSolver<R, State, T>::stepSSA(State& a_state, const ReactionList& a_reactions) const noexcept
449{
450 if (a_reactions.size() > 0) {
451
452 // Compute all propensities.
453 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
454
455 this->stepSSA(a_state, a_reactions, propensities);
456 }
457}
458
459template <typename R, typename State, typename T>
460inline void
462 const ReactionList& a_reactions,
463 const std::vector<Real>& a_propensities) const noexcept
464{
465 CH_assert(a_reactions.size() == a_propensities.size());
466
467 const size_t numReactions = a_reactions.size();
468
469 if (numReactions > 0) {
470 constexpr T one = (T)1;
471
472 // Determine the reaction type as per Gillespie algorithm.
473 Real A = 0.0;
474 for (size_t i = 0; i < numReactions; i++) {
475 A += a_propensities[i];
476 }
477
478 const Real u = Random::getUniformReal01();
479
480 size_t r = numReactions - 1;
481
482 Real sumProp = 0.0;
483 for (size_t i = 0; i + 1 < numReactions; i++) {
484 sumProp += a_propensities[i];
485
486 if (sumProp >= u * A) {
487 r = i;
488
489 break;
490 }
491 }
492
493 CH_assert(r < a_reactions.size());
494
495 // Advance by one reaction.
496 a_reactions[r]->advanceState(a_state, one);
497 }
498}
499
500template <typename R, typename State, typename T>
501inline void
502KMCSolver<R, State, T>::advanceSSA(State& a_state, const Real a_dt) const noexcept
503{
504 this->advanceSSA(a_state, m_reactions, a_dt);
505}
506
507template <typename R, typename State, typename T>
508inline void
509KMCSolver<R, State, T>::advanceSSA(State& a_state, const ReactionList& a_reactions, const Real a_dt) const noexcept
510{
511 const size_t numReactions = a_reactions.size();
512
513 if (numReactions > 0) {
514
515 // Simulated time within the SSA.
516 Real curDt = 0.0;
517
518 while (curDt <= a_dt) {
519
520 // Compute the propensities and get the time to the next reaction.
521 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
522
523 const Real nextDt = this->getCriticalTimeStep(propensities);
524
525 // Fire one reaction if occurs within a_dt.
526 if (curDt + nextDt <= a_dt) {
527 this->stepSSA(a_state, a_reactions, propensities);
528 }
529
530 curDt += nextDt;
531 }
532 }
533}
534
535template <typename R, typename State, typename T>
536inline void
537KMCSolver<R, State, T>::stepExplicitEuler(State& a_state, const Real a_dt) const noexcept
538{
539 this->stepExplicitEuler(a_state, m_reactions, a_dt);
540}
541
542template <typename R, typename State, typename T>
543inline void
545 const ReactionList& a_reactions,
546 const Real a_dt) const noexcept
547{
548 CH_assert(a_dt > 0.0);
549
550 if (a_reactions.size() > 0) {
551 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
552
553 for (size_t i = 0; i < a_reactions.size(); i++) {
554
555 // Number of reactions is always an integer -- draw from a Poisson distribution in long long. I'm just
556 // using a large integer type to avoid potential overflows.
557 const T numReactions = (T)Random::getPoisson<long long>(propensities[i] * a_dt);
558
559 a_reactions[i]->advanceState(a_state, numReactions);
560 }
561 }
562}
563
564template <typename R, typename State, typename T>
565inline void
566KMCSolver<R, State, T>::stepMidpoint(State& a_state, const Real a_dt) const noexcept
567{
568 this->stepMidpoint(a_state, m_reactions, a_dt);
569}
570
571template <typename R, typename State, typename T>
572inline void
573KMCSolver<R, State, T>::stepMidpoint(State& a_state, const ReactionList& a_reactions, const Real a_dt) const noexcept
574{
575 const int numReactions = a_reactions.size();
576
577 if (numReactions > 0) {
578
579 std::vector<Real> propensities = this->propensities(a_state, a_reactions);
580
581 State Xdagger = a_state;
582
583 for (size_t i = 0; i < numReactions; i++) {
584 // TLDR: Predict a midpoint state -- unfortunately this means that as a_dt->0 we end up with plain
585 // tau-leaping. I don't know of a way to fix this without introducing double fluctuations (yet).
586 a_reactions[i]->advanceState(Xdagger, (T)std::round(0.5 * propensities[i] * a_dt));
587 }
588
589 propensities = this->propensities(Xdagger, a_reactions);
590
591 for (size_t i = 0; i < numReactions; i++) {
592 const T curReactions = (T)Random::getPoisson<long long>(propensities[i] * a_dt);
593
594 a_reactions[i]->advanceState(a_state, curReactions);
595 }
596 }
597}
598
599template <typename R, typename State, typename T>
600inline void
601KMCSolver<R, State, T>::stepPRC(State& a_state, const Real a_dt) const noexcept
602{
603 this->stepPRC(a_state, m_reactions, a_dt);
604}
605
606template <typename R, typename State, typename T>
607inline void
608KMCSolver<R, State, T>::stepPRC(State& a_state, const ReactionList& a_reactions, const Real a_dt) const noexcept
609{
610 const int numReactions = a_reactions.size();
611
612 if (numReactions > 0) {
613
614 std::vector<Real> aj = this->propensities(a_state, a_reactions);
615
616 const std::vector<Real> ak = aj;
617
618 for (int j = 0; j < numReactions; j++) {
619 for (int k = 0; k < numReactions; k++) {
620 State x = a_state;
621
622 a_reactions[k]->advanceState(x, (T)1);
623
624 const Real etajk = a_reactions[j]->propensity(x) - ak[j];
625
626 aj[j] += 0.5 * a_dt * ak[k] * etajk;
627 }
628 }
629
630 for (size_t i = 0; i < numReactions; i++) {
631 const T nr = (T)Random::getPoisson<long long>(aj[i] * a_dt);
632
633 a_reactions[i]->advanceState(a_state, nr);
634 }
635 }
636}
637
638template <typename R, typename State, typename T>
639inline void
640KMCSolver<R, State, T>::stepImplicitEuler(State& a_state, const Real a_dt) const noexcept
641{
642 this->stepImplicitEuler(a_state, m_reactions, a_dt);
643}
644
645template <typename R, typename State, typename T>
646inline void
648 const ReactionList& a_reactions,
649 const Real a_dt) const noexcept
650{
651 // TLDR: The implicit Euler tau-leaping scheme is equivalent to the solution of
652 //
653 // F(X) = X - (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)]) - dt*sum_j nu_j a_j(X)
654 // = X - c - dt*sum_j nu_j * a_j(X),
655 // = 0
656 //
657 // where c = (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)]) is a constant term throughout the Newton iterations. This
658 // term is presampled before the Newton iterations begin.
659
660 // Linearize input state onto something understandable to LAPACK
661 const std::vector<T> inputState = a_state.linearOut();
662 const std::vector<std::vector<T>> nu = this->getNu(a_state, a_reactions);
663 const std::vector<Real> ajX = this->propensities(a_state, a_reactions);
664
665 // Number of equations and number of reactions.
666 const int N = inputState.size();
667 const int M = a_reactions.size();
668
669 // Lambda that computes the constant term c and the initial guess.
670 auto compConstantTerm = [&](double* C, double* X, const State& state, const Real a_dt) -> void {
671 // TLDR: To compute the constant term we perform a Poisson sampling and then linearize the output
672 // state.
673
674 State explicitEulerState = state;
675
676 this->advanceTau(explicitEulerState, a_reactions, a_dt, KMCLeapPropagator::ExplicitEuler);
677
678 const std::vector<T> eulerOut = explicitEulerState.linearOut();
679
680 // Make c = (x + sum_j (nu_j * (P(a(x)*dt) - a(x)*dt)])
681 for (int i = 0; i < N; i++) {
682 C[i] = 1.0 * eulerOut[i];
683 X[i] = 1.0 * eulerOut[i];
684 }
685
686 // Subtract the mean.
687 for (int j = 0; j < M; j++) {
688 const std::vector<T>& nuj = nu[j];
689
690 for (int i = 0; i < N; i++) {
691 C[i] -= nuj[i] * ajX[j] * a_dt;
692 }
693 }
694 };
695
696 // Lambda that computes the each equation.
697 auto computeF = [&](double* F, const double* Xit, const double* C) -> void {
698 // Compute the propensities for the Xit state. This requires us to round to the nearest integer.
699 State stateXit = a_state;
700
701 std::vector<T> linState(N);
702 for (int i = 0; i < N; i++) {
703 linState[i] = static_cast<T>(llround(Xit[i]));
704 }
705
706 stateXit.linearIn(linState);
707
708 const std::vector<Real> ajXit = this->propensities(stateXit);
709
710 // Compute Xit - c - dt * sum_j nu_j * aj(round(Xit))
711 for (int i = 0; i < N; i++) {
712 F[i] = Xit[i] - C[i];
713 }
714
715 for (int j = 0; j < M; j++) {
716 const std::vector<T>& nuj = nu[j];
717
718 for (int i = 0; i < N; i++) {
719 F[i] -= a_dt * nuj[i] * ajXit[j];
720 }
721 }
722 };
723
724 // Compute the max-norm
725 auto computeNorm = [&](double* F, const double* X, const double* C) -> Real {
726 computeF(F, X, C);
727
728 Real norm = 0.0;
729
730 for (int i = 0; i < N; i++) {
731 norm = std::max(norm, std::abs(F[i]));
732 }
733
734 return norm;
735 };
736
737 // Allocate memory for the Jacobian (J), the Newton increment (X), and F(X) (F). Also include the constant term from
738 // the Poisson sampling (c)
739 std::vector<double> J(static_cast<size_t>(N * N));
740 std::vector<double> X(static_cast<size_t>(N));
741 std::vector<double> F(static_cast<size_t>(N));
742 std::vector<double> C(static_cast<size_t>(N));
743
744 // Temporary storage used for computing the Jacobian matrix.
745 std::vector<double> X1(static_cast<size_t>(N));
746 std::vector<double> X2(static_cast<size_t>(N));
747 std::vector<double> F2(static_cast<size_t>(N));
748
749 // Things that are required by LAPACK
750 int INFO = 0;
751 int NRHS = 1;
752 std::vector<int> IPIV(static_cast<size_t>(N));
753
754 // Compute the constant term.
755 compConstantTerm(C.data(), X.data(), a_state, a_dt);
756
757 // Compute the max-norm of F(0) to use as an exit criterion.
758 for (int i = 0; i < N; i++) {
759 X1[i] = 0.0;
760 }
761
762 const Real initNorm = computeNorm(F.data(), X1.data(), C.data());
763
764 bool converged = true;
765
766 for (int k = 0; k < m_maxIter; k++) {
767
768 // Compute the residual F(X) at the current iterate. It is the unperturbed column used in every finite-difference
769 // Jacobian column (it does not depend on the perturbation direction j) and is also the right-hand side for the
770 // linear solve below, so it is computed once per Newton iteration rather than N+1 times.
771 computeF(F.data(), X.data(), C.data());
772
773 // Numerically computed the Jacobian using finite differences. The Jacobian is given by
774 // Jij = dF_i/dx_j
775 for (int j = 0; j < N; j++) {
776
777 for (int s = 0; s < N; s++) {
778 X2[s] = X[s];
779 }
780
781 X2[j] += std::max(0.01 * X[j], 1.0);
782
783 computeF(F2.data(), X2.data(), C.data());
784
785 for (int i = 0; i < N; i++) {
786 J[i + j * N] = (F2[i] - F[i]) / (X2[j] - X[j]);
787 }
788 }
789
790 // Solve J*dX = F, but note that the true system is J*dX = -F, so we invert the
791 // dX vector below.
792 dgesv_((int*)&N, &NRHS, J.data(), (int*)&N, IPIV.data(), F.data(), (int*)&N, &INFO);
793
794 if (INFO != 0) {
795#if 1 // Could not solve
796 const std::string err = "KMCSolver<R, State, T>::stepImplicitEuler -- could not solve A*x = b";
797
798 pout() << err << endl;
799#endif
800 converged = false;
801
802 break;
803 }
804 else {
805 // Increment and move on to next iteration if necessary. Note that F = -dX as per the comment above.
806 for (int i = 0; i < N; i++) {
807 X[i] = X[i] - F[i];
808 }
809 }
810
811 // Recompute the norm and exit if necessary.
812 const Real norm = computeNorm(F.data(), X.data(), C.data());
813
814 if (norm / initNorm < m_exitTol) {
815 break;
816 }
817 }
818
819 // Turn X into an integer state.
820 std::vector<T> outputState(N);
821
822 if (converged) {
823 for (int i = 0; i < N; i++) {
824 outputState[i] = static_cast<T>(llround(X[i]));
825 }
826 }
827 else {
828 // Set X to (what is hopefully) an invalid state and rely on step rejection.
829 for (int i = 0; i < N; i++) {
830 outputState[i] = static_cast<T>(-1);
831 }
832 }
833
834 a_state.linearIn(outputState);
835}
836
837template <typename R, typename State, typename T>
838inline void
840 const Real& a_dt,
841 const KMCLeapPropagator& a_leapPropagator) const noexcept
842{
843 this->advanceTau(a_state, m_reactions, a_dt, a_leapPropagator);
844}
845
846template <typename R, typename State, typename T>
847inline void
849 const ReactionList& a_reactions,
850 const Real& a_dt,
851 const KMCLeapPropagator& a_leapPropagator) const noexcept
852{
853 if (a_reactions.size() > 0) {
854 Real curTime = 0.0;
855
856 while (curTime < a_dt) {
857
858 // Bound the substep by the tau-leaping condition tau = eps * X_i / |sum_j nu_ij * a_j|, as advanceHybrid
859 // does for its non-critical reactions. The propensities are recomputed each substep because the state
860 // changes.
861 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
862
863 const Real dtLeap = this->getNonCriticalTimeStep(a_state, a_reactions, propensities);
864
865 Real curDt = std::min(a_dt - curTime, dtLeap);
866
867 bool valid = false;
868
869 // Substepping so we end up with a valid state.
870 while (!valid) {
871
872 State state = a_state;
873
874 // Do a tau-leaping step.
875 switch (a_leapPropagator) {
877 this->stepExplicitEuler(state, a_reactions, curDt);
878
879 break;
880 }
882 this->stepMidpoint(state, a_reactions, curDt);
883
884 break;
885 }
887 this->stepPRC(state, a_reactions, curDt);
888
889 break;
890 }
892 this->stepImplicitEuler(state, a_reactions, curDt);
893
894 break;
895 }
896 default: {
897 break;
898 }
899 }
900
901 // If this was a valid step, accept it. Else reduce dt.
902 valid = state.isValidState();
903
904 if (valid) {
905 a_state = state;
906
907 curTime += curDt;
908 }
909 else {
910 curDt *= 0.5;
911 }
912 }
913 }
914 }
915}
916
917template <typename R, typename State, typename T>
918inline void
920 const Real a_dt,
921 const KMCLeapPropagator& a_leapPropagator) const noexcept
922{
923 this->advanceHybrid(a_state, m_reactions, a_dt, a_leapPropagator);
924}
925
926template <typename R, typename State, typename T>
927inline void
929 const ReactionList& a_reactions,
930 const Real a_dt,
931 const KMCLeapPropagator& a_leapPropagator) const noexcept
932{
933 switch (a_leapPropagator) {
935 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
936 this->stepExplicitEuler(s, r, dt);
937 });
938
939 break;
940 }
942 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
943 this->stepMidpoint(s, r, dt);
944 });
945
946 break;
947 }
949 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
950 this->stepPRC(s, r, dt);
951 });
952
953 break;
954 }
956 this->advanceHybrid(a_state, a_reactions, a_dt, [this](State& s, const ReactionList& r, const Real dt) {
957 this->stepImplicitEuler(s, r, dt);
958 });
959
960 break;
961 }
962 default: {
963 MayDay::Error("KMCSolver::advanceHybrid - unknown leap propagator requested");
964 }
965 }
966}
967
968template <typename R, typename State, typename T>
969inline void
971 State& a_state,
972 const ReactionList& a_reactions,
973 const Real a_dt,
974 const std::function<void(State&, const ReactionList& a_reactions, const Real a_dt)>& a_propagator) const noexcept
975{
976 constexpr T one = (T)1;
977
978 // Simulated time within the advancement algorithm.
979 Real curTime = 0.0;
980
981 // Outer loop is for reactive substepping over a_dt.
982 while (curTime < a_dt) {
983
984 // Partition reactions into critical and non-critical reactions and compute the critical and non-critical time
985 // steps.
986 const std::pair<ReactionList, ReactionList> partitionedReactions = this->partitionReactions(a_state, a_reactions);
987
988 const ReactionList& criticalReactions = partitionedReactions.first;
989 const ReactionList& nonCriticalReactions = partitionedReactions.second;
990
991 const std::vector<Real> propensitiesCrit = this->propensities(a_state, criticalReactions);
992 const std::vector<Real> propensitiesNonCrit = this->propensities(a_state, nonCriticalReactions);
993
994 Real dtCrit = this->getCriticalTimeStep(propensitiesCrit);
995 Real dtNonCrit = this->getNonCriticalTimeStep(a_state, nonCriticalReactions, propensitiesNonCrit);
996
997 // Try the various advancement algorithms; the loop is for step rejection in case we end up with an invalid state,
998 // e.g. a state with a negative number of particles.
999 bool validStep = false;
1000
1001 while (!validStep) {
1002
1003 // Do a backup of the advancement state to operate on. This is necessary because the tau-leaping
1004 // algorithms advance the state but we may need to reject those steps if we end up with a thermodynamically
1005 // invalid state.
1006 State state = a_state;
1007
1008 // Compute the time step to be used.
1009 const Real curDt = std::min(a_dt - curTime, std::min(dtCrit, dtNonCrit));
1010
1011 // Are we only doing non-critical reactions?
1012 const bool nonCriticalOnly = (dtNonCrit < dtCrit) || (criticalReactions.size() == 0) ||
1013 (dtCrit > (a_dt - curTime));
1014
1015 // Decide whether tau-leaping is efficient enough, or whether we should fall back to an exact SSA advancement
1016 // over the WHOLE reaction set. This decision is made independently of whether a critical reaction is pending --
1017 // in low-activity regimes exact SSA is both cheaper and more accurate than tau-leaping, so it must be available
1018 // in the critical branch as well. SSA is only used when the user has permitted at least one SSA step
1019 // (m_numSSA > 0); otherwise we always tau-leap, which also avoids a no-progress infinite loop when m_numSSA == 0.
1020 const Real A = this->totalPropensity(state, a_reactions);
1021 const bool useSSA = (m_numSSA >= one) && (A * curDt < m_SSAlim);
1022
1023 if (useSSA) {
1024 // TLDR: Tau-leaping is inefficient here so we switch to an SSA-based algorithm for the WHOLE reaction set.
1025
1026 // Number of SSA steps taken and simulated time within the SSA. We will compute until either dtSSA >= curDt or
1027 // we've exceeded the maximum number of SSA steps that the user has permitted (m_numSSA).
1028 Real dtSSA = 0.0;
1029 T numSSA = 0;
1030
1031 while (dtSSA < curDt && numSSA < m_numSSA) {
1032
1033 // Recompute propensities for the full reaction set and advance everything using the SSA.
1034 const std::vector<Real> propensities = this->propensities(a_state, a_reactions);
1035
1036 Real Asum = 0.0;
1037 for (size_t i = 0; i < propensities.size(); i++) {
1038 Asum += propensities[i];
1039 }
1040
1041 // Compute the time to the next reaction.
1042 const Real dtReact = this->getCriticalTimeStep(Asum);
1043
1044 if (dtSSA + dtReact < curDt) {
1045 this->stepSSA(a_state, a_reactions, propensities);
1046
1047 dtSSA += dtReact;
1048 numSSA += one;
1049 }
1050 else {
1051
1052 // Next reaction occurred outside the substep -- break out of the loop.
1053 dtSSA = curDt;
1054 }
1055 }
1056
1057 validStep = true;
1058 curTime += dtSSA;
1059 }
1060 else if (nonCriticalOnly) {
1061 // Do the tau-leaping over the non-critical reactions only.
1062 a_propagator(state, nonCriticalReactions, curDt);
1063
1064 // Check if we need to reject the state and rather try again with a smaller non-critical time step.
1065 validStep = state.isValidState();
1066
1067 if (validStep) {
1068 a_state = state;
1069
1070 curTime += curDt;
1071 }
1072 else {
1073 dtNonCrit *= 0.5;
1074 }
1075 }
1076 else {
1077 // TLDR: Here, one critical reaction fires. We tau-leap the non-critical reactions over curDt and fire exactly
1078 // one critical reaction using the SSA. The non-critical tau-leap is performed FIRST, while the state is
1079 // still at the start of the step, so that its propensities are evaluated at the start-of-step state. The
1080 // single critical firing is then applied as a fixed stoichiometric increment -- its reaction was already
1081 // selected from the start-of-step propensities (propensitiesCrit), so applying it last is exact. This
1082 // keeps both contributions consistent with the start-of-step state, as required by the
1083 // Cao-Gillespie-Petzold scheme.
1084 a_propagator(state, nonCriticalReactions, curDt);
1085
1086 this->stepSSA(state, criticalReactions, propensitiesCrit);
1087
1088 // Check if we need to reject the state and rather try again with a smaller non-critical time step.
1089 validStep = state.isValidState();
1090
1091 if (validStep) {
1092 a_state = state;
1093
1094 curTime += curDt;
1095 }
1096 else {
1097 dtNonCrit *= 0.5;
1098 }
1099 }
1100 }
1101 }
1102}
1103
1104#include <CD_NamespaceFooter.H>
1105
1106#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:375
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:502
void gatherDistinctReactants(const ReactionList &a_reactions) const noexcept
Fill m_reactantScratch with the distinct reactants of the input reactions.
Definition CD_KMCSolverImplem.H:258
void stepImplicitEuler(State &a_state, Real a_dt) const noexcept
Perform one implicit Euler tau-leaping step using ALL reactions.
Definition CD_KMCSolverImplem.H:640
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:919
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:601
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:566
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:441
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:839
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:537
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:156