Linear Reaction-Diffusion Equations

Next, we write a specialised solver for solving linear reaction-diffusion equations. What we produce in this section can also be accessed in FiniteVolumeMethod.LinearReactionDiffusionEquation.

Mathematical Details

To start, let's give the mathematical details. The problems we will be solving take the form

\[\pdv{u}{t} = \div\left[D(\vb x)\grad u\right] + f(\vb x)u.\]

We want to turn this into an equation of the form $\mathrm d\vb u/\mathrm dt = \vb A\vb u + \vb b$ as usual. This takes the same form as our diffusion equation example, except with the extra $f(\vb x)u$ term, which just adds an exta $f(\vb x)$ term to the diagonal of $\vb A$. See the previois sections for further mathematical details.

Implementation

Let us now implement the solver. For constructing $\vb A$, we can use FiniteVolumeMethod.triangle_contributions! as in the previous sections, but we will need an extra function to add $f(\vb x)$ to the appropriate diagonals. We can also reuse apply_dirichlet_conditions!, apply_dudt_conditions, and boundary_edge_contributions! from the diffusion equation example. Here is our implementation.

using FiniteVolumeMethod, SparseArrays, OrdinaryDiffEq, LinearAlgebra
const FVM = FiniteVolumeMethod
function linear_source_contributions!(A, mesh, conditions, source_function, source_parameters)
    for i in each_solid_vertex(mesh.triangulation)
        if !FVM.has_condition(conditions, i)
            x, y = get_point(mesh, i)
            A[i, i] += source_function(x, y, source_parameters)
        end
    end
end
function linear_reaction_diffusion_equation(mesh::FVMGeometry,
    BCs::BoundaryConditions,
    ICs::InternalConditions=InternalConditions();
    diffusion_function,
    diffusion_parameters=nothing,
    source_function,
    source_parameters=nothing,
    initial_condition,
    initial_time=0.0,
    final_time)
    conditions = Conditions(mesh, BCs, ICs)
    n = DelaunayTriangulation.num_solid_vertices(mesh.triangulation)
    Afull = zeros(n + 1, n + 1)
    A = @views Afull[begin:end-1, begin:end-1]
    b = @views Afull[begin:end-1, end]
    _ic = vcat(initial_condition, 1)
    FVM.triangle_contributions!(A, mesh, conditions, diffusion_function, diffusion_parameters)
    FVM.boundary_edge_contributions!(A, b, mesh, conditions, diffusion_function, diffusion_parameters)
    linear_source_contributions!(A, mesh, conditions, source_function, source_parameters)
    FVM.apply_dudt_conditions!(b, mesh, conditions)
    FVM.apply_dirichlet_conditions!(_ic, mesh, conditions)
    FVM.fix_missing_vertices!(A, b, mesh)
    Af = sparse(Afull)
    prob = ODEProblem(MatrixOperator(Af), _ic, (initial_time, final_time))
    return prob
end
linear_reaction_diffusion_equation (generic function with 2 methods)

If you go and look back at the diffusion_equation function from the diffusion equation example, you will see that this is essentially the same function except we now have linear_source_contributions! and source_function and source_parameters arguments.

Let's now test this function. We consider the problem

\[\pdv{T}{t} = \div\left[10^{-3}x^2y\grad T\right] + (x-1)(y-1)T, \quad \vb x \in [0,1]^2,\]

with $\grad T \vdot\vu n = 1$ on the boundary.

using DelaunayTriangulation
tri = triangulate_rectangle(0, 1, 0, 1, 150, 150, single_boundary=true)
mesh = FVMGeometry(tri)
BCs = BoundaryConditions(mesh, (x, y, t, u, p) -> one(x), Neumann)
diffusion_function = (x, y, p) -> p.D * x^2 * y
diffusion_parameters = (D=1e-3,)
source_function = (x, y, p) -> (x - 1) * (y - 1)
initial_condition = [x^2 + y^2 for (x, y) in DelaunayTriangulation.each_point(tri)]
final_time = 8.0
prob = linear_reaction_diffusion_equation(mesh, BCs;
    diffusion_function, diffusion_parameters,
    source_function, initial_condition, final_time)
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 8.0)
u0: 22501-element Vector{Float64}:
 0.0
 4.504301608035674e-5
 0.00018017206432142696
 0.00040538714472321063
 0.0007206882572857079
 ⋮
 1.9601369307688843
 1.9733345344804287
 1.986622224224134
 2.0
 1.0
sol = solve(prob, Tsit5(); saveat=2)
retcode: Success
Interpolation: 1st order linear
t: 5-element Vector{Float64}:
 0.0
 2.0
 4.0
 6.0
 8.0
u: 5-element Vector{Vector{Float64}}:
 [0.0, 4.504301608035674e-5, 0.00018017206432142696, 0.00040538714472321063, 0.0007206882572857079, 0.0011260754020089186, 0.0016215485788928425, 0.0022071077879374803, 0.0028827530291428314, 0.0036484843025088956  …  1.8955002026935723, 1.9082473762443133, 1.921084635827215, 1.9340119814422772, 1.9470294130895003, 1.9601369307688843, 1.9733345344804287, 1.986622224224134, 2.0, 1.0]
 [5.392972715241139e-10, 0.0003283969313676682, 0.0012960759990657785, 0.0028772909445736222, 0.00504698203809888, 0.007780761845897442, 0.011054900287088438, 0.014846309987847422, 0.019132531927487137, 0.023891721371031464  …  1.8576012589883444, 1.8669187520853117, 1.8756974896188225, 1.8847638874308603, 1.8917905879431463, 1.902881675462326, 1.9023588035400723, 1.9299909655861378, 1.8825037944102863, 1.0]
 [7.916735417539222e-9, 0.0023942552773780655, 0.009323370071034573, 0.02042192008254055, 0.035343899135842745, 0.05376187635010657, 0.07536617230272595, 0.09986406790939067, 0.1269790448138673, 0.15645005612239943  …  1.8509136105186716, 1.8585916000276579, 1.8664895900891452, 1.873127279201565, 1.8825835391536376, 1.8841164948618832, 1.90531142005639, 1.8762173321996074, 1.9784909851572074, 1.0]
 [8.71629135976037e-8, 0.01745587228824834, 0.06706791816304745, 0.14494671157302394, 0.24751160642335754, 0.3714705521119453, 0.5138009456452581, 0.6717316202228356, 0.8427259076096071, 1.0244657148949736  …  1.856052705658883, 1.8629729344721433, 1.8696298851238013, 1.8768276953388658, 1.8824053749530683, 1.892111090113291, 1.8912408558651062, 1.9171352641507267, 1.8728687785435112, 1.0]
 [8.530436664276923e-7, 0.12726600200660979, 0.4824543028839816, 1.0287720968254654, 1.733305483676705, 2.566682072979205, 3.502758519480151, 4.5183326883131505, 5.59287864642826, 6.7083028021327795  …  1.8700526286482877, 1.8761847240326033, 1.8825065061952093, 1.8884430770464482, 1.8956204927516433, 1.899792159685713, 1.9118930607221336, 1.9042189167261514, 1.9487418177655664, 1.0]
using CairoMakie
fig = Figure(fontsize=38)
for j in eachindex(sol)
    ax = Axis(fig[1, j], width=600, height=600,
        xlabel="x", ylabel="y",
        title="t = $(sol.t[j])")
    tricontourf!(ax, tri, sol.u[j], levels=0:0.1:1, extendlow=:auto, extendhigh=:auto, colormap=:turbo)
    tightlimits!(ax)
end
resize_to_layout!(fig)
fig
Example block output

Here is how we could convert this into an FVMProblem. Note that the Neumann boundary conditions are expressed as $\grad T\vdot\vu n = 1$ above, but for FVMProblem we need them in the form $\vb q\vdot\vu n = \ldots$. For this problem, $\vb q=-D\grad T$, which gives $\vb q\vdot\vu n = -D$.

_BCs = BoundaryConditions(mesh, (x, y, t, u, p) -> -p.D(x, y, p.Dp), Neumann;
    parameters=(D=diffusion_function, Dp=diffusion_parameters))
fvm_prob = FVMProblem(
    mesh,
    _BCs;
    diffusion_function=let D=diffusion_function
        (x, y, t, u, p) -> D(x, y, p)
    end,
    diffusion_parameters=diffusion_parameters,
    source_function=let S=source_function
        (x, y, t, u, p) -> S(x, y, p) * u
    end,
    final_time=final_time,
    initial_condition=initial_condition
)
fvm_sol = solve(fvm_prob, Tsit5(), saveat=2.0)
retcode: Success
Interpolation: 1st order linear
t: 5-element Vector{Float64}:
 0.0
 2.0
 4.0
 6.0
 8.0
u: 5-element Vector{Vector{Float64}}:
 [0.0, 4.504301608035674e-5, 0.00018017206432142696, 0.00040538714472321063, 0.0007206882572857079, 0.0011260754020089186, 0.0016215485788928425, 0.0022071077879374803, 0.0028827530291428314, 0.0036484843025088956  …  1.882843115174992, 1.8955002026935723, 1.9082473762443133, 1.921084635827215, 1.9340119814422772, 1.9470294130895003, 1.9601369307688843, 1.9733345344804287, 1.986622224224134, 2.0]
 [5.392972715241149e-10, 0.00032839693136766876, 0.001296075999065779, 0.0028772909445736283, 0.005046982038098889, 0.007780761845897449, 0.01105490028708844, 0.014846309987847436, 0.019132531927487165, 0.023891721371031467  …  1.8480635912077554, 1.8576012582667165, 1.8669187542596206, 1.8756974833491835, 1.884763904747085, 1.8917905420078425, 1.9028817932805158, 1.9023585070771212, 1.9299917191546574, 1.882501759209525]
 [7.916735417539246e-9, 0.00239425527737807, 0.009323370071034577, 0.020421920082540585, 0.03534389913584289, 0.053761876350106666, 0.07536617230272606, 0.09986406790939079, 0.12697904481386754, 0.1564500561223995  …  1.842932242715815, 1.850913610004881, 1.8585916015757284, 1.8664895856252661, 1.8731272915304222, 1.882583506448485, 1.884116578746421, 1.905311208979834, 1.876217868727517, 1.9784895361290513]
 [8.716291359760414e-8, 0.0174558722882484, 0.06706791816304758, 0.1449467115730244, 0.24751160642335912, 0.37147055211194646, 0.5138009456452598, 0.6717316202228368, 0.8427259076096096, 1.024465714894976  …  1.8491360538229389, 1.8560527070422026, 1.8629729303041502, 1.8696298971422687, 1.8768276621448807, 1.882405463007841, 1.8921108642640607, 1.8912414241638662, 1.9171338196125511, 1.8728726798811097]
 [8.530436664276935e-7, 0.12726600200660998, 0.48245430288398167, 1.0287720968254657, 1.733305483676712, 2.5666820729792104, 3.5027585194801505, 4.518332688313147, 5.592878646428274, 6.708302802132787  …  1.8638903357449885, 1.8700526280569265, 1.8761847258144198, 1.8825065010573316, 1.8884430912368422, 1.8956204551083347, 1.8997922562359995, 1.9118928177750822, 1.9042195342645372, 1.9487401499483092]

Using the Provided Template

The above code is implemented in LinearReactionDiffusionEquation in FiniteVolumeMethod.jl.

prob = LinearReactionDiffusionEquation(mesh, BCs;
    diffusion_function, diffusion_parameters,
    source_function,  initial_condition, final_time)
sol = solve(prob, Tsit5(); saveat=2)
retcode: Success
Interpolation: 1st order linear
t: 5-element Vector{Float64}:
 0.0
 2.0
 4.0
 6.0
 8.0
u: 5-element Vector{Vector{Float64}}:
 [0.0, 4.504301608035674e-5, 0.00018017206432142696, 0.00040538714472321063, 0.0007206882572857079, 0.0011260754020089186, 0.0016215485788928425, 0.0022071077879374803, 0.0028827530291428314, 0.0036484843025088956  …  1.8955002026935723, 1.9082473762443133, 1.921084635827215, 1.9340119814422772, 1.9470294130895003, 1.9601369307688843, 1.9733345344804287, 1.986622224224134, 2.0, 1.0]
 [5.392972715241139e-10, 0.0003283969313676682, 0.0012960759990657785, 0.0028772909445736222, 0.00504698203809888, 0.007780761845897442, 0.011054900287088438, 0.014846309987847422, 0.019132531927487137, 0.023891721371031464  …  1.8576012589883444, 1.8669187520853117, 1.8756974896188225, 1.8847638874308603, 1.8917905879431463, 1.902881675462326, 1.9023588035400723, 1.9299909655861378, 1.8825037944102863, 1.0]
 [7.916735417539222e-9, 0.0023942552773780655, 0.009323370071034573, 0.02042192008254055, 0.035343899135842745, 0.05376187635010657, 0.07536617230272595, 0.09986406790939067, 0.1269790448138673, 0.15645005612239943  …  1.8509136105186716, 1.8585916000276579, 1.8664895900891452, 1.873127279201565, 1.8825835391536376, 1.8841164948618832, 1.90531142005639, 1.8762173321996074, 1.9784909851572074, 1.0]
 [8.71629135976037e-8, 0.01745587228824834, 0.06706791816304745, 0.14494671157302394, 0.24751160642335754, 0.3714705521119453, 0.5138009456452581, 0.6717316202228356, 0.8427259076096071, 1.0244657148949736  …  1.856052705658883, 1.8629729344721433, 1.8696298851238013, 1.8768276953388658, 1.8824053749530683, 1.892111090113291, 1.8912408558651062, 1.9171352641507267, 1.8728687785435112, 1.0]
 [8.530436664276923e-7, 0.12726600200660979, 0.4824543028839816, 1.0287720968254654, 1.733305483676705, 2.566682072979205, 3.502758519480151, 4.5183326883131505, 5.59287864642826, 6.7083028021327795  …  1.8700526286482877, 1.8761847240326033, 1.8825065061952093, 1.8884430770464482, 1.8956204927516433, 1.899792159685713, 1.9118930607221336, 1.9042189167261514, 1.9487418177655664, 1.0]

Here is a benchmark comparison of LinearReactionDiffusionEquation versus FVMProblem.

using BenchmarkTools
using Sundials
@btime solve($prob, $CVODE_BDF(linear_solver=:GMRES); saveat=$2);
  48.360 ms (1087 allocations: 1.58 MiB)
@btime solve($fvm_prob, $CVODE_BDF(linear_solver=:GMRES); saveat=$2);
  163.686 ms (83267 allocations: 90.84 MiB)

Just the code

An uncommented version of this example is given below. You can view the source code for this file here.

using FiniteVolumeMethod, SparseArrays, OrdinaryDiffEq, LinearAlgebra
const FVM = FiniteVolumeMethod
function linear_source_contributions!(A, mesh, conditions, source_function, source_parameters)
    for i in each_solid_vertex(mesh.triangulation)
        if !FVM.has_condition(conditions, i)
            x, y = get_point(mesh, i)
            A[i, i] += source_function(x, y, source_parameters)
        end
    end
end
function linear_reaction_diffusion_equation(mesh::FVMGeometry,
    BCs::BoundaryConditions,
    ICs::InternalConditions=InternalConditions();
    diffusion_function,
    diffusion_parameters=nothing,
    source_function,
    source_parameters=nothing,
    initial_condition,
    initial_time=0.0,
    final_time)
    conditions = Conditions(mesh, BCs, ICs)
    n = DelaunayTriangulation.num_solid_vertices(mesh.triangulation)
    Afull = zeros(n + 1, n + 1)
    A = @views Afull[begin:end-1, begin:end-1]
    b = @views Afull[begin:end-1, end]
    _ic = vcat(initial_condition, 1)
    FVM.triangle_contributions!(A, mesh, conditions, diffusion_function, diffusion_parameters)
    FVM.boundary_edge_contributions!(A, b, mesh, conditions, diffusion_function, diffusion_parameters)
    linear_source_contributions!(A, mesh, conditions, source_function, source_parameters)
    FVM.apply_dudt_conditions!(b, mesh, conditions)
    FVM.apply_dirichlet_conditions!(_ic, mesh, conditions)
    FVM.fix_missing_vertices!(A, b, mesh)
    Af = sparse(Afull)
    prob = ODEProblem(MatrixOperator(Af), _ic, (initial_time, final_time))
    return prob
end

using DelaunayTriangulation
tri = triangulate_rectangle(0, 1, 0, 1, 150, 150, single_boundary=true)
mesh = FVMGeometry(tri)
BCs = BoundaryConditions(mesh, (x, y, t, u, p) -> one(x), Neumann)
diffusion_function = (x, y, p) -> p.D * x^2 * y
diffusion_parameters = (D=1e-3,)
source_function = (x, y, p) -> (x - 1) * (y - 1)
initial_condition = [x^2 + y^2 for (x, y) in DelaunayTriangulation.each_point(tri)]
final_time = 8.0
prob = linear_reaction_diffusion_equation(mesh, BCs;
    diffusion_function, diffusion_parameters,
    source_function, initial_condition, final_time)

sol = solve(prob, Tsit5(); saveat=2)

using CairoMakie
fig = Figure(fontsize=38)
for j in eachindex(sol)
    ax = Axis(fig[1, j], width=600, height=600,
        xlabel="x", ylabel="y",
        title="t = $(sol.t[j])")
    tricontourf!(ax, tri, sol.u[j], levels=0:0.1:1, extendlow=:auto, extendhigh=:auto, colormap=:turbo)
    tightlimits!(ax)
end
resize_to_layout!(fig)
fig

_BCs = BoundaryConditions(mesh, (x, y, t, u, p) -> -p.D(x, y, p.Dp), Neumann;
    parameters=(D=diffusion_function, Dp=diffusion_parameters))
fvm_prob = FVMProblem(
    mesh,
    _BCs;
    diffusion_function=let D=diffusion_function
        (x, y, t, u, p) -> D(x, y, p)
    end,
    diffusion_parameters=diffusion_parameters,
    source_function=let S=source_function
        (x, y, t, u, p) -> S(x, y, p) * u
    end,
    final_time=final_time,
    initial_condition=initial_condition
)
fvm_sol = solve(fvm_prob, Tsit5(), saveat=2.0)

prob = LinearReactionDiffusionEquation(mesh, BCs;
    diffusion_function, diffusion_parameters,
    source_function,  initial_condition, final_time)
sol = solve(prob, Tsit5(); saveat=2)

This page was generated using Literate.jl.