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

ut=[D(x)u]+f(x)u.

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

Implementation

Let us now implement the solver. For constructing A, we can use FiniteVolumeMethod.triangle_contributions! as in the previous sections, but we will need an extra function to add f(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

Tt=[103x2yT]+(x1)(y1)T,x[0,1]2,

with Tn^=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.392972715241143e-10, 0.0003283969313676684, 0.0012960759990657792, 0.002877290944573626, 0.005046982038098889, 0.007780761845897448, 0.011054900287088444, 0.014846309987847438, 0.019132531927487165, 0.023891721371031467  …  1.8576012589880762, 1.8669187520861235, 1.8756974896164818, 1.884763887437326, 1.8917905879259957, 1.902881675506313, 1.9023588034293868, 1.9299909658674865, 1.8825037936504285, 1.0]
 [7.916735417539211e-9, 0.002394255277378063, 0.009323370071034556, 0.02042192008254055, 0.03534389913584281, 0.05376187635010653, 0.07536617230272592, 0.09986406790939065, 0.1269790448138673, 0.15645005612239932  …  1.8509136105071604, 1.858591600062336, 1.8664895899891367, 1.8731272794777616, 1.8825835384209586, 1.884116496741108, 1.905311415327742, 1.8762173442191878, 1.978490952695312, 1.0]
 [8.716291359760346e-8, 0.017455872288248277, 0.06706791816304723, 0.14494671157302366, 0.24751160642335782, 0.37147055211194413, 0.5138009456452572, 0.6717316202228347, 0.8427259076096069, 1.0244657148949718  …  1.856052705661518, 1.8629729344642054, 1.869629885146697, 1.8768276952756373, 1.8824053751208059, 1.892111089683071, 1.8912408569476635, 1.9171352613990147, 1.8728687859751985, 1.0]
 [8.530436664276854e-7, 0.1272660020066086, 0.48245430288397767, 1.0287720968254588, 1.7333054836766988, 2.5666820729791873, 3.5027585194801247, 4.5183326883131265, 5.592878646428236, 6.708302802132749  …  1.870052628652889, 1.8761847240187424, 1.8825065062351822, 1.8884430769360512, 1.8956204930445033, 1.8997921589345663, 1.9118930626122232, 1.9042189119217956, 1.9487418307409279, 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 Tn^=1 above, but for FVMProblem we need them in the form qn^=. For this problem, q=DT, which gives qn^=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.392972715241146e-10, 0.00032839693136766865, 0.0012960759990657787, 0.002877290944573627, 0.0050469820380988906, 0.00778076184589744, 0.01105490028708845, 0.014846309987847429, 0.01913253192748716, 0.02389172137103148  …  1.8480635912090653, 1.8576012582625914, 1.8669187542720502, 1.8756974833133426, 1.8847639048460716, 1.8917905417452556, 1.902881793954016, 1.9023585053824088, 1.9299917234623845, 1.8825017475754324]
 [7.916735417539227e-9, 0.0023942552773780694, 0.009323370071034566, 0.020421920082540564, 0.03534389913584279, 0.0537618763501065, 0.07536617230272606, 0.09986406790939072, 0.12697904481386746, 0.15645005612239948  …  1.8429322427231227, 1.8509136099818753, 1.8585916016450483, 1.866489585425376, 1.8731272920825013, 1.8825835049839645, 1.884116582502732, 1.9053111995279264, 1.8762178927529864, 1.9784894712422374]
 [8.716291359760358e-8, 0.017455872288248315, 0.06706791816304726, 0.14494671157302375, 0.24751160642335737, 0.37147055211194385, 0.5138009456452574, 0.671731620222835, 0.8427259076096064, 1.0244657148949723  …  1.8491360538216242, 1.8560527070463413, 1.8629729302916793, 1.8696298971782281, 1.8768276620455648, 1.8824054632712968, 1.8921108635883268, 1.8912414258641945, 1.9171338152905444, 1.8728726915537697]
 [8.530436664276897e-7, 0.12726600200660945, 0.4824543028839793, 1.0287720968254626, 1.7333054836767012, 2.566682072979191, 3.5027585194801385, 4.518332688313142, 5.59287864642825, 6.708302802132766  …  1.8638903357449816, 1.8700526280569478, 1.8761847258143487, 1.8825065010575315, 1.8884430912362884, 1.8956204551098073, 1.8997922562322205, 1.9118928177845889, 1.9042195342403745, 1.9487401500135615]

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.392972715241143e-10, 0.0003283969313676684, 0.0012960759990657792, 0.002877290944573626, 0.005046982038098889, 0.007780761845897448, 0.011054900287088444, 0.014846309987847438, 0.019132531927487165, 0.023891721371031467  …  1.8576012589880762, 1.8669187520861235, 1.8756974896164818, 1.884763887437326, 1.8917905879259957, 1.902881675506313, 1.9023588034293868, 1.9299909658674865, 1.8825037936504285, 1.0]
 [7.916735417539211e-9, 0.002394255277378063, 0.009323370071034556, 0.02042192008254055, 0.03534389913584281, 0.05376187635010653, 0.07536617230272592, 0.09986406790939065, 0.1269790448138673, 0.15645005612239932  …  1.8509136105071604, 1.858591600062336, 1.8664895899891367, 1.8731272794777616, 1.8825835384209586, 1.884116496741108, 1.905311415327742, 1.8762173442191878, 1.978490952695312, 1.0]
 [8.716291359760346e-8, 0.017455872288248277, 0.06706791816304723, 0.14494671157302366, 0.24751160642335782, 0.37147055211194413, 0.5138009456452572, 0.6717316202228347, 0.8427259076096069, 1.0244657148949718  …  1.856052705661518, 1.8629729344642054, 1.869629885146697, 1.8768276952756373, 1.8824053751208059, 1.892111089683071, 1.8912408569476635, 1.9171352613990147, 1.8728687859751985, 1.0]
 [8.530436664276854e-7, 0.1272660020066086, 0.48245430288397767, 1.0287720968254588, 1.7333054836766988, 2.5666820729791873, 3.5027585194801247, 4.5183326883131265, 5.592878646428236, 6.708302802132749  …  1.870052628652889, 1.8761847240187424, 1.8825065062351822, 1.8884430769360512, 1.8956204930445033, 1.8997921589345663, 1.9118930626122232, 1.9042189119217956, 1.9487418307409279, 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.