begin
    using SciMLBase: ODEProblem, solve
    using OrdinaryDiffEqLowOrderRK: DP5
    using OrdinaryDiffEqRosenbrock: Rosenbrock23
    using OrdinaryDiffEqTsit5:Tsit5
    using Catalyst
    using VoronoiFVM: VoronoiFVM, enable_species!, enable_boundary_species!
    using VoronoiFVM: ramp, boundary_dirichlet!
    using ExtendableGrids: simplexgrid
    using GridVisualize: GridVisualize, GridVisualizer, reveal, scalarplot!, gridplot, available_kwargs
    if doplots # defined in the Appendix
      using Plots: Plots, plot, theme
      using PlotThemes
      Plots.gr()
      Plots.theme(:dark)
      GridVisualize.default_plotter!(Plots)
    end
    import PlutoUI
    using Test
    PlutoUI.TableOfContents(; depth = 4)
end

Towards Heterogeneous Catalysis

How to model and simulate heterogeneous catalysis with Catalyst.jl and VoronoiFVM.jl.

Mass action kinetics

Assume \(j=1 … M\) reversible reactions of educt (substrate) species to product species

$$ α_1^j S_1 + α_2^j S_2 + … + α_n^jS_n \underset{k_j^+}{\stackrel{k_j^-}{\longrightleftharpoons}} β_1^jS_1 + β_2^j S_2 + … + β_n^j S_n$$

or equivalently,

$$∑_{i=1}^n α_{i}^j S_i \underset{k_j^+}{\stackrel{k_j^-}{\longrightleftharpoons}} ∑_{i=1}^n β_i^j S_i $$

The rate of these reactions depend on the concentrations \([S_i]\) of the species. Within \(Catalyst.jl\), due to consistency with the derivation from stochastic approaches, the default "combinatoric rate law" is

$$ r_j=k_j^+ ∏_{i=1}^n \frac{[S_i]^{α_i^j}}{α_i^j!} - k_j^- ∏_{i=1}^n \frac{[S_i]^{β_i^j}}{β_i^j!} $$

while it appears that in most textbooks, the rate law is

$$ r_j=k_j^+ ∏_{i=1}^n [S_i]^{α_i^j} - k_j^- ∏_{i=1}^n [S_i]^{β_i^j}.$$

See the corresponding remark in the Catalyst.jl docs and the github issue. We will stick to the secobd version which can be achieved by setting combinatoric_ratelaws to false at appropriate places.

Later in the project we will see that instead of the concentrations, we need to work with so called activities.

The numbers \(σ_i^j=α_i^j-β_i^j\) are the net stoichiometric coefficients of the system.

The rate differential equations then are (TODO: check this)

$$ ∂_t [S_i] + \sum_{j=1}^M \sigma_i^jr_j = 0$$

These assume that the reactions take place in a continuously stirred tank reactor (CSTR) which means that we have complete mixing, and species concentrations are spatially constant, and we have just one concentration value for each species at a given point of time.

Example 1: A \(\longrightleftharpoons\) B

$$\begin{aligned} A& \underset{k_1^+}{\stackrel{k_1^-}{\longrightleftharpoons}} B\\ r_1&= k_1^+ [A] - k_1^- [B]\\ \partial_t[A] &= -r_1\\ \partial_t[B] &= r_1 \end{aligned} $$

Solution via plain ODE problem using OrdinaryDiffEq.jl:

Set the parameters such that the forward reaction is faster then the backward reaction:

p1 = (k_p = 1, k_m = 0.1)
(k_p = 1, k_m = 0.1)

Define an ODE function describing the right hand side of the ODE System:

function example1(du, u, p, t)
    (; k_p, k_m) = p
    r1 = k_p * u[1] - k_m * u[2]
    du[1] = -r1
    du[2] = r1
end
example1 (generic function with 1 method)

Define some initial value:

u1_ini = [1.0, 0.0]
2-element Vector{Float64}:
 1.0
 0.0

Create an solve the corresponding ODEProblem

prob1 = ODEProblem(example1, u1_ini, (0, 10), p1)
ODEProblem with uType Vector{Float64} and tType Int64. In-place: true
timespan: (0, 10)
u0: 2-element Vector{Float64}:
 1.0
 0.0
sol1 = solve(prob1, DP5())
timestampvalue1value2
10.01.00.0
20.0009990010.9990020.000998452
30.0109890.9890770.0109229
40.1108890.8956070.104393
50.4188180.6644020.335598
60.8599810.4439120.556088
71.471250.2711230.728877
82.180130.1735570.826443
92.977590.1253020.874698
103.853570.1040430.895957
114.836140.09537550.904625
125.967130.09220440.907796
137.312950.0912110.908789
148.970330.09096420.909036
1510.00.09092690.909073
doplots && plot(sol1; size = (600, 200))

Mass conservation: adding the two reaction eqauations results in

$$ \partial_t ([A]+[B]) = 0,$$

therefore \([A]+[B]\) must be constant:

all(s -> isapprox(s, sum(u1_ini)), sum(sol1; dims = 1))
true

Catalyst.@reaction_network

Catalyst.jl provides a convenient way to define a reaction network, and the resulting reaction system. So we use this to build the same system:

rn1 = @reaction_network rn1 begin
    @combinatoric_ratelaws false
    k_p, A --> B
    k_m, B --> A
end

$$\begin{align*} \mathrm{A} &\xrightleftharpoons[k_{m}]{k_{p}} \mathrm{B} \end{align*}$$

The corresponding ODE system is:

convert(ODESystem, rn1)

$$\begin{align} \frac{\mathrm{d} A\left( t \right)}{\mathrm{d}t} &= k_{m} B\left( t \right) - k_{p} A\left( t \right) \\ \frac{\mathrm{d} B\left( t \right)}{\mathrm{d}t} &= - k_{m} B\left( t \right) + k_{p} A\left( t \right) \end{align}$$

Catalyst.jl adds a new method to the ODEProblem constructor which allows to pass a reaction nerwork:

prob1n = ODEProblem(rn1, u1_ini, (0, 10.0), Dict(pairs(p1)))
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 10.0)
u0: 2-element Vector{Float64}:
 1.0
 0.0
sol1n = solve(prob1n, Rosenbrock23())
timestampA(t)B(t)
10.01.00.0
20.0001133860.9998870.000113379
30.001247250.9987540.0012464
40.008102660.9919330.00806667
50.01833760.9818460.0181539
60.03991150.9609510.0390486
70.06953730.9330540.066946
80.1171840.8900480.109952
90.1778980.8384120.161588
100.2614260.7727690.227231
...
doplots && plot(sol1n; idxs = [rn1.A, rn1.B, rn1.A + rn1.B], size = (600, 200))

Unraveling @reaction_network

Let us try to look behind the macro voodoo - this is necessary to build networks programmatically and is behind the translation from python to Julia in CatmapInterface.jl.

It is interesting if there is a "macro - less" way to define variables, parameters and species.

@variables t

$$\begin{equation} \left[ \begin{array}{c} t \\ \end{array} \right] \end{equation}$$

@parameters k_p k_m

$$\begin{equation} \left[ \begin{array}{c} k_{p} \\ k_{m} \\ \end{array} \right] \end{equation}$$

@species A(t) B(t)

$$\begin{equation} \left[ \begin{array}{c} A\left( t \right) \\ B\left( t \right) \\ \end{array} \right] \end{equation}$$

A reaction network can be combined from several reactions:

r1p = Reaction(k_p, [A], [B], [1], [1])
k_p, A --> B
r1m = Reaction(k_m, [B], [A], [1], [1])
k_m, B --> A
rn1x = complete(ReactionSystem([r1p, r1m], t; name = :example1))

$$\begin{align*} \mathrm{A} &\xrightleftharpoons[k_{m}]{k_{p}} \mathrm{B} \end{align*}$$

Once we are here, the rest remains the same.

convert(ODESystem, rn1x)

$$\begin{align} \frac{\mathrm{d} A\left( t \right)}{\mathrm{d}t} &= k_{m} B\left( t \right) - k_{p} A\left( t \right) \\ \frac{\mathrm{d} B\left( t \right)}{\mathrm{d}t} &= - k_{m} B\left( t \right) + k_{p} A\left( t \right) \end{align}$$

prob1x = ODEProblem(rn1x, u1_ini, (0, 10.0), Dict(pairs(p1)))
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 10.0)
u0: 2-element Vector{Float64}:
 1.0
 0.0
sol1x = solve(prob1x, Rosenbrock23())
timestampA(t)B(t)
10.01.00.0
20.0001133860.9998870.000113379
30.001247250.9987540.0012464
40.008102660.9919330.00806667
50.01833760.9818460.0181539
60.03991150.9609510.0390486
70.06953730.9330540.066946
80.1171840.8900480.109952
90.1778980.8384120.161588
100.2614260.7727690.227231
...
doplots && plot(sol1x; size = (600, 200))

Example 2: A + 2B \(\longrightleftharpoons\) AB_2

rn2 = @reaction_network rn2 begin
    @combinatoric_ratelaws false
    k_0A, ∅ --> A
    k_0B, ∅ --> B
    (k_1p, k_1m), A + 2B <--> AB_2
end

$$\begin{align*} \varnothing &\xrightarrow{k_{0A}} \mathrm{A} \\ \varnothing &\xrightarrow{k_{0B}} \mathrm{B} \\ \mathrm{A} + 2 \mathrm{B} &\xrightleftharpoons[k_{1m}]{k_{1p}} \mathrm{AB_{2}} \end{align*}$$

convert(ODESystem, rn2)

$$\begin{align} \frac{\mathrm{d} A\left( t \right)}{\mathrm{d}t} &= k_{0A} + k_{1m} \mathrm{AB}_{2}\left( t \right) - \left( B\left( t \right) \right)^{2} k_{1p} A\left( t \right) \\ \frac{\mathrm{d} B\left( t \right)}{\mathrm{d}t} &= k_{0B} + 2 k_{1m} \mathrm{AB}_{2}\left( t \right) - 2 \left( B\left( t \right) \right)^{2} k_{1p} A\left( t \right) \\ \frac{\mathrm{d} \mathrm{AB}_{2}\left( t \right)}{\mathrm{d}t} &= - k_{1m} \mathrm{AB}_{2}\left( t \right) + \left( B\left( t \right) \right)^{2} k_{1p} A\left( t \right) \end{align}$$

p2 = (k_0A = 0.5, k_0B = 1, k_1p = 0.1, k_1m = 1.0e-5)
(k_0A = 0.5, k_0B = 1, k_1p = 0.1, k_1m = 1.0e-5)
u2_ini = (A = 0, B = 0, AB_2 = 0)
(A = 0, B = 0, AB_2 = 0)
prob2 = ODEProblem(rn2, Dict(pairs(u2_ini)), (0, 20.0), Dict(pairs(p2)))
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 20.0)
u0: 3-element Vector{Float64}:
 0.0
 0.0
 0.0
sol2 = solve(prob2, Rosenbrock23())
timestampA(t)B(t)AB_2(t)
10.00.00.00.0
20.00015.0e-50.00016.25e-19
30.00110.000550.00111.08006e-14
40.01110.005550.01111.13501e-10
50.0584360.02921790.05843589.95852e-8
60.08245190.04122540.08245095.19335e-7
70.1417660.07087850.1417574.69768e-6
80.1787550.08936510.178731.23077e-5
90.2419570.1209370.2418744.17015e-5
100.2876190.1437260.2874518.40283e-5
...
doplots && plot(sol2; legend = :topleft, size = (600, 300))

Example 3: Catalysis for A + 2B \(\rightleftharpoons\) AB_2

The same reaction as in example 2, but now with a catalyst C.

The reaction between A and B takes places when A and B are bound (adsorbed) to the catalyst. So we have adsorption reactions, reactions at the catalyst, and desorption. The overall of free and bound catalyst needs to be constant over time.

rn3 = @reaction_network rn3 begin
    @combinatoric_ratelaws false
    k_0A, ∅ --> A
    k_0B, ∅ --> B
    (k_1p, k_1m), A + C <--> CA
    (k_2p, k_2m), B + C <--> CB
    (k_3p, k_3m), CA + 2CB <--> CAB2 + 2C
    (k_4p, k_4m), CAB2 <--> AB2 + C
end

$$\begin{align*} \varnothing &\xrightarrow{k_{0A}} \mathrm{A} \\ \varnothing &\xrightarrow{k_{0B}} \mathrm{B} \\ \mathrm{A} + \mathrm{C} &\xrightleftharpoons[k_{1m}]{k_{1p}} \mathrm{CA} \\ \mathrm{B} + \mathrm{C} &\xrightleftharpoons[k_{2m}]{k_{2p}} \mathrm{CB} \\ \mathrm{CA} + 2 \mathrm{CB} &\xrightleftharpoons[k_{3m}]{k_{3p}} \mathrm{CAB2} + 2 \mathrm{C} \\ \mathrm{CAB2} &\xrightleftharpoons[k_{4m}]{k_{4p}} \mathrm{AB2} + \mathrm{C} \end{align*}$$

convert(ODESystem, rn3)

$$\begin{align} \frac{\mathrm{d} A\left( t \right)}{\mathrm{d}t} &= k_{0A} + k_{1m} \mathrm{CA}\left( t \right) - k_{1p} A\left( t \right) C\left( t \right) \\ \frac{\mathrm{d} B\left( t \right)}{\mathrm{d}t} &= k_{0B} + k_{2m} \mathrm{CB}\left( t \right) - k_{2p} B\left( t \right) C\left( t \right) \\ \frac{\mathrm{d} C\left( t \right)}{\mathrm{d}t} &= k_{1m} \mathrm{CA}\left( t \right) + k_{2m} \mathrm{CB}\left( t \right) + k_{4p} \mathrm{CAB2}\left( t \right) - k_{1p} A\left( t \right) C\left( t \right) - k_{2p} B\left( t \right) C\left( t \right) - k_{4m} C\left( t \right) \mathrm{AB2}\left( t \right) - 2 \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) + 2 \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \\ \frac{\mathrm{d} \mathrm{CA}\left( t \right)}{\mathrm{d}t} &= - k_{1m} \mathrm{CA}\left( t \right) + k_{1p} A\left( t \right) C\left( t \right) + \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) - \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \\ \frac{\mathrm{d} \mathrm{CB}\left( t \right)}{\mathrm{d}t} &= - k_{2m} \mathrm{CB}\left( t \right) + k_{2p} B\left( t \right) C\left( t \right) + 2 \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) - 2 \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \\ \frac{\mathrm{d} \mathrm{CAB2}\left( t \right)}{\mathrm{d}t} &= - k_{4p} \mathrm{CAB2}\left( t \right) + k_{4m} C\left( t \right) \mathrm{AB2}\left( t \right) - \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) + \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \\ \frac{\mathrm{d} \mathrm{AB2}\left( t \right)}{\mathrm{d}t} &= k_{4p} \mathrm{CAB2}\left( t \right) - k_{4m} C\left( t \right) \mathrm{AB2}\left( t \right) \end{align}$$

p3 = (k_0A = 0.5, k_0B = 1,
      k_1p = 10, k_1m = 0.1,
      k_2p = 10, k_2m = 0.1,
      k_3p = 10, k_3m = 0.1,
      k_4p = 10, k_4m = 0.1)
(k_0A = 0.5, k_0B = 1, k_1p = 10, k_1m = 0.1, k_2p = 10, k_2m = 0.1, k_3p = 10, k_3m = 0.1, k_4p = 10, k_4m = 0.1)
Cini=40
40
u3_ini = (A = 0, B = 0, CA = 0, CB = 0, CAB2 = 0, AB2 = 0, C = Cini)
(A = 0, B = 0, CA = 0, CB = 0, CAB2 = 0, AB2 = 0, C = 40)
t3end = 200
200
prob3 = ODEProblem(rn3, Dict(pairs(u3_ini)), (0, t3end), Dict(pairs(p3)))
ODEProblem with uType Vector{Float64} and tType Int64. In-place: true
timespan: (0, 200)
u0: 7-element Vector{Float64}:
  0.0
  0.0
 40.0
  0.0
  0.0
  0.0
  0.0
sol3 = solve(prob3, Rosenbrock23())
timestampA(t)B(t)C(t)CA(t)CB(t)CAB2(t)AB2(t)
10.00.00.040.00.00.00.00.0
26.46784e-63.22974e-66.45949e-640.04.17882e-98.35764e-94.74654e-318.99173e-36
37.11463e-53.50726e-57.01452e-540.05.00556e-71.00111e-61.20711e-232.28831e-27
40.0001750168.45196e-50.00016903940.02.9886e-65.9772e-61.52686e-205.17114e-24
50.00033990.000158920.0003178440.01.10301e-52.20603e-51.86355e-181.10544e-21
60.0005593470.0002506380.00050127739.99992.9035e-55.807e-56.46331e-175.76826e-20
70.000853060.0003614740.00072294939.99986.50556e-50.0001301111.1845e-151.55464e-18
80.001210110.0004798250.0009596539.99960.0001252320.0002504641.26499e-142.27535e-17
90.001650390.0006043420.0012086839.99930.0002208530.0004417069.78351e-142.37856e-16
100.002167790.0007252540.0014505139.99890.0003586380.0007172775.66062e-131.79861e-15
...
doplots && plot(sol3; legend = :topleft, size = (600, 300))
ctotal = rn3.C + rn3.CA + rn3.CB + rn3.CAB2

$$\begin{equation} \mathrm{CAB2}\left( t \right) + \mathrm{CA}\left( t \right) + \mathrm{CB}\left( t \right) + C\left( t \right) \end{equation}$$

sol3[ctotal]
148-element Vector{Float64}:
 40.0
 40.0
 40.0
 39.99999999999999
 39.99999999999999
 39.99999999999999
 39.99999999999999
  ⋮
 39.99999999999902
 39.99999999999902
 39.99999999999902
 39.99999999999902
 39.99999999999902
 39.99999999999926
@test sol3[ctotal]≈ fill(Cini,length(sol3))
Test Passed

Example 4: Heterogeneous catalysis

Heterogeneous catalysis assumes that the catalytic reaction takes place at surface. This means that reacting species need to be transported towards or away from the surface, and one has to model coupled transport and surface reaction.

Here we use VoronoiFVM.jl to model transport and Catalyst.jl to create the surface reaction network.

Problem specification

Assume \(\Omega=(0,1)\) where a catalytic reaction takes place at \(x=0\). We assume that the educts A, B, and the product AB2 are bulk species transported to the domain. At \(x=1\) we set Dirichlet boundary conditions providing A,B and removing AB2.

A, B can adsorb at the catalyst at \(x=0\) and react to AB2 while adsorbed. The product desorbs and is released to the bulk. So we have

  • Mass transport in the interior of \(\Omega\):

$$\begin{aligned} \partial_t c_A + \nabla \cdot D_A \nabla c_A &=0\\ \partial_t c_B + \nabla \cdot D_B \nabla c_B &=0\\ \partial_t c_{AB2} + \nabla \cdot D_{AB2} \nabla c_{AB2} &=0 \end{aligned}$$

  • Coupled nonlinear robin boundary conditions at \(x=0\):

$$\begin{aligned} D_A\partial_n c_A + r_1 &= 0\\ D_B\partial_n c_A + r_2 &= 0\\ D_{AB2}\partial_n c_{AB2} - r_4 &= 0\\ \end{aligned}$$

  • \(r_1, r_2\) and \(r_4\) are asorption/desorption reactions:

$$\begin{aligned} r_1&=k_{1p}c_A c_C - k_{1m}c_{CA}\\ r_2&=k_{2p}c_B c_C - k_{2m}c_{CB}\\ r_4&=k_{4p}c_{AB2} - k_{4m}c_{C_C}c_{C_{AB2}}\\ \end{aligned}$$

  • The free catalyst sites C and the catalyst coverages CA, CB, CAB2 behave according to:

$$\begin{equation} \frac{\mathrm{d} C\left( t \right)}{\mathrm{d}t} = k_{1m} \mathrm{CA}\left( t \right) + k_{2m} \mathrm{CB}\left( t \right) + k_{4p} \mathrm{CAB2}\left( t \right) - k_{1p} A\left( t \right) C\left( t \right) - k_{2p} B\left( t \right) C\left( t \right) - k_{4m} C\left( t \right) \mathrm{AB2}\left( t \right) - 2 \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) + 2 \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \end{equation}$$

$$\begin{equation} \frac{\mathrm{d} \mathrm{CA}\left( t \right)}{\mathrm{d}t} = - k_{1m} \mathrm{CA}\left( t \right) + k_{1p} A\left( t \right) C\left( t \right) + \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) - \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \end{equation}$$

$$\begin{equation} \frac{\mathrm{d} \mathrm{CB}\left( t \right)}{\mathrm{d}t} = - k_{2m} \mathrm{CB}\left( t \right) + k_{2p} B\left( t \right) C\left( t \right) + 2 \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) - 2 \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \end{equation}$$

$$\begin{equation} \frac{\mathrm{d} \mathrm{CAB2}\left( t \right)}{\mathrm{d}t} = - k_{4p} \mathrm{CAB2}\left( t \right) + k_{4m} C\left( t \right) \mathrm{AB2}\left( t \right) - \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) + \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \end{equation}$$

  • Dirichlet boundary conditions at \(x=1\) :

$$\begin{aligned} c_A&=1\\ c_B&=1\\ c_{AB2}&=0 \end{aligned}$$

Finally, we set all initial concentrations to zero besides of the catalyst concenration (number of catalyst sites) \(c_C|_{t=0}=C_0=1\).

Implementation

Surface reaction network

Define a reaction network under the assumption that the supply of A and B comes from the transport and does not need to be specified.

rnv = @reaction_network rnv begin
    @combinatoric_ratelaws false
    (k_1p, k_1m), A + C <--> CA
    (k_2p, k_2m), B + C <--> CB
    (k_3p, k_3m), CA + 2CB <--> CAB2 + 2C
    (k_4p, k_4m), CAB2 <--> AB2 + C
end

$$\begin{align*} \mathrm{A} + \mathrm{C} &\xrightleftharpoons[k_{1m}]{k_{1p}} \mathrm{CA} \\ \mathrm{B} + \mathrm{C} &\xrightleftharpoons[k_{2m}]{k_{2p}} \mathrm{CB} \\ \mathrm{CA} + 2 \mathrm{CB} &\xrightleftharpoons[k_{3m}]{k_{3p}} \mathrm{CAB2} + 2 \mathrm{C} \\ \mathrm{CAB2} &\xrightleftharpoons[k_{4m}]{k_{4p}} \mathrm{AB2} + \mathrm{C} \end{align*}$$

odesys=convert(ODESystem, rnv)

$$\begin{align} \frac{\mathrm{d} A\left( t \right)}{\mathrm{d}t} &= k_{1m} \mathrm{CA}\left( t \right) - k_{1p} A\left( t \right) C\left( t \right) \\ \frac{\mathrm{d} C\left( t \right)}{\mathrm{d}t} &= k_{1m} \mathrm{CA}\left( t \right) + k_{2m} \mathrm{CB}\left( t \right) + k_{4p} \mathrm{CAB2}\left( t \right) - k_{1p} A\left( t \right) C\left( t \right) - k_{2p} B\left( t \right) C\left( t \right) - k_{4m} C\left( t \right) \mathrm{AB2}\left( t \right) - 2 \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) + 2 \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \\ \frac{\mathrm{d} \mathrm{CA}\left( t \right)}{\mathrm{d}t} &= - k_{1m} \mathrm{CA}\left( t \right) + k_{1p} A\left( t \right) C\left( t \right) + \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) - \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \\ \frac{\mathrm{d} B\left( t \right)}{\mathrm{d}t} &= k_{2m} \mathrm{CB}\left( t \right) - k_{2p} B\left( t \right) C\left( t \right) \\ \frac{\mathrm{d} \mathrm{CB}\left( t \right)}{\mathrm{d}t} &= - k_{2m} \mathrm{CB}\left( t \right) + k_{2p} B\left( t \right) C\left( t \right) + 2 \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) - 2 \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \\ \frac{\mathrm{d} \mathrm{CAB2}\left( t \right)}{\mathrm{d}t} &= - k_{4p} \mathrm{CAB2}\left( t \right) + k_{4m} C\left( t \right) \mathrm{AB2}\left( t \right) - \left( C\left( t \right) \right)^{2} k_{3m} \mathrm{CAB2}\left( t \right) + \left( \mathrm{CB}\left( t \right) \right)^{2} k_{3p} \mathrm{CA}\left( t \right) \\ \frac{\mathrm{d} \mathrm{AB2}\left( t \right)}{\mathrm{d}t} &= k_{4p} \mathrm{CAB2}\left( t \right) - k_{4m} C\left( t \right) \mathrm{AB2}\left( t \right) \end{align}$$

For coupling with VoronoiFVM we need species numbers which need to correspond to the species in our network:

begin
    smap = speciesmap(rnv)
    const iA = smap[rnv.A]
    const iB = smap[rnv.B]
    const iC = smap[rnv.C]
    const iCA = smap[rnv.CA]
    const iCB = smap[rnv.CB]
    const iCAB2 = smap[rnv.CAB2]
    const iAB2 = smap[rnv.AB2]
end;

Grid:

grid = simplexgrid(0:0.01:1)
ExtendableGrids.ExtendableGrid{Float64, Int32}
      dim =       1
   nnodes =     101
   ncells =     100
  nbfaces =       2
gridplot(grid, size=(600,100))

The grid has two boundary regions: region 1 at x=0 and region 2 at x=1.

Reaction parameters:

pcat = (k_1p = 50, k_1m = 0.1,
        k_2p = 50, k_2m = 0.1,
        k_3p = 10, k_3m = 0.1,
        k_4p = 50, k_4m = 0.1)
(k_1p = 50, k_1m = 0.1, k_2p = 50, k_2m = 0.1, k_3p = 10, k_3m = 0.1, k_4p = 50, k_4m = 0.1)

Parameters for the VoronoiFVM system:

params = (D_A = 1.0,
          D_B = 1.0,
          D_AB2 = 1.0,
          pcat = pcat)
(D_A = 1.0, D_B = 1.0, D_AB2 = 1.0, pcat = (k_1p = 50, k_1m = 0.1, k_2p = 50, k_2m = 0.1, k_3p = 10, k_3m = 0.1, k_4p = 50, k_4m = 0.1))

Initial values for the reaction network (needed only for the definition of the ODE problem)

C0 = 1.0
1.0
uv_ini = (A = 0, B = 0, CA = 0, CB = 0, CAB2 = 0, AB2 = 0, C = C0)
(A = 0, B = 0, CA = 0, CB = 0, CAB2 = 0, AB2 = 0, C = 1.0)
tvend = 200.0
200.0
const probv = ODEProblem(rnv, Dict(pairs(uv_ini)), (0, tvend), Dict(pairs(pcat)))
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 200.0)
u0: 7-element Vector{Float64}:
 0.0
 1.0
 0.0
 0.0
 0.0
 0.0
 0.0

Callback functions for VoronoiFVM

First, define flux and storage functions for the bulk process:

function storage(y, u, node, p)
    y[iA] = u[iA]
    y[iB] = u[iB]
    y[iAB2] = u[iAB2]
end
storage (generic function with 1 method)
function flux(y, u, edge, p)
    (; D_A, D_B, D_AB2) = p
    y[iA] = D_A * (u[iA, 1] - u[iA, 2])
    y[iB] = D_B * (u[iB, 1] - u[iB, 2])
    y[iAB2] = D_A * (u[iAB2, 1] - u[iAB2, 2])
end
flux (generic function with 1 method)

Storage term for the surface reaction:

function bstorage(y, u, bnode, p)
    y[iC] = u[iC]
    y[iCA] = u[iCA]
    y[iCB] = u[iCB]
    y[iCAB2] = u[iCAB2]
end
bstorage (generic function with 1 method)

Catalytic reaction. Here we use the right hand side function of the ODE problem generated above. In VoronoiFVM, reaction term are a the left hand side, so we need to multiply by -1.

Note that we need to pass the parameter record as generated for the ODE problem instead of pcat.

function catreaction(f, u, bnode, p)
    probv.f(f, u, probv.p, bnode.time)
    for i = 1:length(f)
        f[i] = -f[i]
    end
end
catreaction (generic function with 1 method)

Define the Dirichlet boundary condition at x=1 (region 2):

function bulkbc(f, u, bnode, p)
    v = ramp(bnode.time; du = (0.0, 1.0), dt = (0.0, 0.01))
    boundary_dirichlet!(f, u, bnode; species = iA, value = v, region = 2)
    boundary_dirichlet!(f, u, bnode; species = iB, value = v, region = 2)
    boundary_dirichlet!(f, u, bnode; species = iAB2, value = 0, region = 2)
end
bulkbc (generic function with 1 method)

Dispatch the boundary conditions

function breaction(f, u, bnode, p)
    if bnode.region == 1
        catreaction(f,u,bnode,p)
    else
 		bulkbc(f,u,bnode,p)
    end
end
breaction (generic function with 1 method)

Coupled transport-reaction system

Define a VoronoiFVM system from grid, params and the callback functions and enable the bulk and boundary species. unknown_storage = :sparse means that the solution is stored as a nspecies x nnodes sparse matrix in order to take into account that the surface species are non-existent in the bulk. unknown_storage = :dense would store a full matrix and solve dummy equations for the surface species values in the bulk.

begin
    sys = VoronoiFVM.System(grid; data = params,
                            flux, breaction, bstorage, storage,
                            unknown_storage = :sparse)
    enable_species!(sys, iA, [1])
    enable_species!(sys, iB, [1])
    enable_species!(sys, iAB2, [1])

    enable_boundary_species!(sys, iC, [1])
    enable_boundary_species!(sys, iCA, [1])
    enable_boundary_species!(sys, iCB, [1])
    enable_boundary_species!(sys, iCAB2, [1])
end;

Define an initial value for sys:

begin
    u0 = VoronoiFVM.unknowns(sys; inival = 0)
    u0[iC, 1] = C0
end;

Solution

Solve the time evolution

tsol = solve(sys; inival = u0, times = (1.0e-4, tvend));

t:

let
    t_plot = round(10^log_t_plot; sigdigits = 3)
    vis = GridVisualizer(;size = (600, 300), flimits = (0, 1), title = "Bulk concentrations: t=$t_plot", legend = :lt)
    sol = tsol(t_plot)
    scalarplot!(vis, grid, sol[iA, :]; color = :red, label = "A")
    scalarplot!(vis, grid, sol[iB, :]; color = :green, label = "B", clear = false)
    scalarplot!(vis, grid, sol[iAB2, :]; color = :blue, label = "AB2", clear = false)
    reveal(vis)
end
Ctotalv = tsol[iC, 1, :] + tsol[iCA, 1, :] + tsol[iCB, 1, :] + tsol[iCAB2, 1, :]
248-element Vector{Float64}:
 1.0
 1.0
 0.9999999999999999
 1.0
 1.0
 0.9999999999999999
 0.9999999999999999
 ⋮
 1.0000000000000002
 1.0000000000000002
 1.0000000000000002
 1.0000000000000002
 1.0000000000000002
 1.0000000000000002
@test Ctotalv≈ ones(length(tsol))
Test Passed
let
    vis = GridVisualizer(; size = (600, 300),
                          xlabel="t",
                         flimits = (0, 1), xlimits = (1.0e-3, tvend),
                         legend = :lt, title = "Concentrations at x=0", xscale = :log10)
    t = tsol.t
    scalarplot!(vis, t, tsol[iA, 1, :]; color = :darkred, label = "A")
    scalarplot!(vis, t, tsol[iCA, 1, :]; color = :red, label = "CA")
    scalarplot!(vis, t, tsol[iB, 1, :]; color = :darkgreen, label = "B")
    scalarplot!(vis, t, tsol[iCB, 1, :]; color = :green, label = "CB")
    scalarplot!(vis, t, tsol[iAB2, 1, :]; color = :darkblue, label = "AB2")
    scalarplot!(vis, t, tsol[iCAB2, 1, :]; color = :blue, label = "CAB2")
    scalarplot!(vis, t, tsol[iC, 1, :] / C0; color = :orange, label = "C/C0")
    scalarplot!(vis, t, Ctotalv / C0; color = :darkorange, label = "Ctot/C0")

    reveal(vis)
end

Appendix

# Some tricks helping to run the notebook during VoronoiFVM.jl CI
begin
    doplots=!haskey(ENV,"VORONOIFVM_RUNTESTS")
    import Pkg as _Pkg
    haskey(ENV, "PLUTO_PROJECT") && _Pkg.activate(ENV["PLUTO_PROJECT"])
    using Revise
end

Built with Julia 1.11.1 and