begin
import Pkg as _Pkg
haskey(ENV, "PLUTO_PROJECT") && _Pkg.activate(ENV["PLUTO_PROJECT"])
using Revise
using VoronoiFVM
using ExtendableGrids
using GridVisualize
using PlutoUI
using HypertextLiteral
using LinearAlgebra
using LinearSolve
using Test
using CairoMakie
CairoMakie.activate!(; type = "png", visible = false)
GridVisualize.default_plotter!(CairoMakie)
end
CairoMakie
Interface conditions in 1D
This notebooks discusses handling of internal interfaces with VoronoiFVM.jl.
Two subdomains
For a simple stationary diffusion equation with an interior interface, we discuss possible interface conditions between two subdomains.
Let \(\Omega=\Omega_1\cup\Omega_2\) where \(\Omega_1=(-1,0)\) and \(\Omega_2=(0,1)\). Let \(\Gamma_1={-1}\),\(\Gamma_2={1}\) and \(\Gamma_3={0}\).
Regard the following problem:
\(\begin{aligned} -\Delta u_1 &= 0 & \text{in}\quad \Omega_1\\ -\Delta u_2 &= 0 & \text{in}\quad \Omega_2\\ \end{aligned}\)
with exterior boundary conditions
\(u_1|_{\Gamma_1} = g_1\) and \(u_2|_{\Gamma_2} = g_2\)
For the interior boundary (interface) conditions we set
\(\nabla u_1|_{\Gamma_3}+f_1(u_1,u_2)=0\)
\(-\nabla u_2|_{\Gamma_3}+f_2(u_1,u_2)=0\)
where \(f_1\), \(f_2\) are discussed later.
Set up
Create a grid with two subdomins and an interface in the center.
nref = 2
2
begin
hmax = 0.2 / 2.0^nref
hmin = 0.05 / 2.0^nref
X1 = geomspace(-1.0, 0.0, hmax, hmin)
X2 = geomspace(0.0, 1.0, hmin, hmax)
X = glue(X1, X2)
grid = VoronoiFVM.Grid(X)
bfacemask!(grid, [0.0], [0.0], 3)
## Material 1 left of 0
cellmask!(grid, [-1.0], [0.0], 1)
## Material 2 right of 0
cellmask!(grid, [0.0], [1.0], 2)
end;
gridplot(grid; legend = :rt, resolution = (600, 200))
For later use (plotting) extract the two subgrids from the grid
subgrid1 = subgrid(grid, [1]);
subgrid2 = subgrid(grid, [2]);
Define the diffusion flux for the two species in their respective subdomains
function flux!(f, u, edge, data)
if edge.region == 1
f[1] = u[1, 1] - u[1, 2]
end
if edge.region == 2
f[2] = u[2, 1] - u[2, 2]
end
end
flux! (generic function with 1 method)
Specify the outer boundary values.
const g_1 = 1.0
1.0
const g_2 = 0.1
0.1
Create the system. We pass the interface condition function as a parameter.
function make_system(breaction)
physics = VoronoiFVM.Physics(; flux = flux!, breaction = breaction)
## Create system
sys = VoronoiFVM.System(grid, physics; unknown_storage = :sparse)
## Enable species in their respective subregions
enable_species!(sys, 1, [1])
enable_species!(sys, 2, [2])
## Set boundary conditions
for ispec = 1:2
boundary_dirichlet!(sys, ispec, 1, g_1)
boundary_dirichlet!(sys, ispec, 2, g_2)
end
sys
end
make_system (generic function with 1 method)
Stationary solution with zero initial value
function mysolve(sys)
U = solve(sys)
U1 = view(U[1, :], subgrid1)
U2 = view(U[2, :], subgrid2)
U1, U2
end
mysolve (generic function with 1 method)
Plot the results
function plot(U1, U2; title = "")
vis = GridVisualizer(; resolution = (600, 300))
scalarplot!(vis,
subgrid1,
U1;
clear = false,
show = false,
color = :green,
label = "u1")
scalarplot!(vis,
subgrid2,
U2;
clear = false,
show = true,
color = :blue,
label = "u2",
legend = :rt,
title = title,
flimits = (-0.5, 1.5))
end
plot (generic function with 1 method)
No interface reaction
This means we set \(f_1(u_1,u_2)=0\) and \(f_2(u_1,u_2)=0\).
function noreaction(f, u, node, data) end
noreaction (generic function with 1 method)
system1 = make_system(noreaction);
plot(mysolve(system1)...)
The solution consists of two constants defined by the respective Dirichlet boundary conditions at the outer boundary.
Mass action law reaction \(u_1 \leftrightharpoons u_2\)
This is a rather general ansatz where we assume a backward-forward reaction between the two species meeting at the interface with reaction constants \(k_1\) and \(k_2\), respectively.
According to the mass action law, this translates to a reaction rate
\(r(u_1,u_2)=k_1u_1 - k_2u_2\)
and correspondingly
\(f_1(u_1,u_2)=r\)
\(f_2(u_1,u_2)=-r\)
Note, that \(f_i\) is monotonically increasing in \(u_i\) and monotonically decreasing in the respective other argument, leading to an M-Property of the overall discretization matrix.
Note that the "no reaction" case is just a special case where \(k_1,k_2=0\).
function mal_reaction(f, u, node, data)
if node.region == 3
react = k1 * u[1] - k2 * u[2]
f[1] = react
f[2] = -react
end
end
mal_reaction (generic function with 1 method)
system2 = make_system(mal_reaction)
VoronoiFVM.System{Float64, Float64, Int32, Int64, SparseArrays.SparseMatrixCSC{Int32, Int32}}( grid = ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=75, ncells=74, nbfaces=3), physics = Physics(flux=flux!, storage=default_storage, breaction=mal_reaction, ), num_species = 2)
begin
const k1 = 0.1
const k2 = 10
end
10
The back reaction is 100 times stronger than the forward reaction. This means that species 2 is consumed, creating species 1.
Penalty enforcing continuity
Setting \(k_1,k_2\) to a large number leads to another special case of the above reaction - similar to the penalty method to implement the Dirichlet boundary conditions, this lets the reaction equation dominate, which in this case forces \(u_1-u_2=0\) at the interface, and thus continuity.
function penalty_reaction(f, u, node, data)
if node.region == 3
react = 1.0e10 * (u[1] - u[2])
f[1] = react
f[2] = -react
end
end
penalty_reaction (generic function with 1 method)
system3 = make_system(penalty_reaction);
plot(mysolve(system3)...)
Penalty enforcing fixed jump
Instead of enforcing continuity, one can enforce a fixed jump.
const jump = 0.2
0.2
function penalty_jump_reaction(f, u, node, data)
if node.region == 3
react = 1.0e10 * (u[1] - u[2] - jump)
f[1] = react
f[2] = -react
end
end
penalty_jump_reaction (generic function with 1 method)
system3jump = make_system(penalty_jump_reaction);
plot(mysolve(system3jump)...)
Interface recombination
Here, we implement an annihilation reaction \(u_1 + u_2 \to \emptyset\) According to the mass action law, this is implemented via
\(r(u_1,u_2)=k_r u_1 u_2\)
\(f_1(u_1,u_2)=r\)
\(f_2(u_1,u_2)=r\)
function recombination(f, u, node, data)
if node.region == 3
react = k_r * (u[1] * u[2])
f[1] = react
f[2] = react
end
end;
system4 = make_system(recombination);
const k_r = 1000
1000
plot(mysolve(system4)...)
Bot species are consumed at the interface.
Thin conductive interface layer
Let us assume that the interface is of thickness \(d\) which is however small with respect to \(\Omega\) that we want to derive an interface condition from the assumption of an exact continuous solution within the interface.
So let \(\Omega_I=(x_l,x_r)\) be the interface region where we have \(-\Delta u_I=0\) with values \(u_l\), \(u_r\) at the boundaries.
Then we have for the flux in the interface region, \(q_I=\nabla u = \frac1{d}(u_r - u_l)\)
Continuity of fluxes then gives \(f_1=q_I\) and \(f_2=-q_I\).
Continuity of \(u\) gives \(u_{1,I}=u_l, u_{2,I}=u_r\) This gives
\(r=q_I=\frac{1}{d}(u_1-u_{2})\)
\(f_1(u_1,v_1)=r\)
\(f_2(u_1,v_1)=-r\)
and therefore another special case of the mass action law condition.
const d = 1
1
function thinlayer(f, u, node, data)
if node.region == 3
react = (u[1] - u[2]) / d
f[1] = react
f[2] = -react
end
end
thinlayer (generic function with 1 method)
system5 = make_system(thinlayer);
plot(mysolve(system5)...)
The solution looks very similar to the case of the jump condition, however here, the size of the jump is defined by the physics of the interface.
Multiple domains
From the above discussion it seems that discontinuous interface conditions can be formulated in a rather general way via linear or nonlinear robin boundary conditions for each of the adjacent discontinuous species. Technically, it is necessary to be able to access the adjacent bulk data.
In order to streamline the handling of multiple interfaces, we propose an API layer on top of the species handling of VoronoiFVM. We call these "meta species" "quantities".
We define a grid with N=6 subregions
N = 6
6
begin
XX = collect(0:0.1:1)
local xcoord = XX
for i = 1:(N - 1)
xcoord = glue(xcoord, XX .+ i)
end
grid2 = simplexgrid(xcoord)
for i = 1:N
cellmask!(grid2, [i - 1], [i], i)
end
for i = 1:(N - 1)
bfacemask!(grid2, [i], [i], i + 2)
end
end
gridplot(grid2; legend = :lt, resolution = (600, 200))
To work with quantities, we first introduce a new constructor call without the "physics" parameter:
system6 = VoronoiFVM.System(grid2)
VoronoiFVM.System{Float64, Float64, Int32, Int64, Matrix{Int32}}( grid = ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=61, ncells=60, nbfaces=7), physics = Physics(storage=default_storage, ), num_species = 0)
First, we introduce a continuous quantity which we name "cspec". Note that the "species number" can be assigned automatically if not given explicitly.
const cspec = ContinuousQuantity(system6, 1:N; ispec = 1)
ContinuousQuantity{Int32}(1, 1)
A discontinuous quantity can be introduced as well. by default, each reagion gets a new species number. This can be overwritten by the user. It is important that the speces numbers of neighboring regions differ.
const dspec = DiscontinuousQuantity(system6, 1:N; regionspec = [2 + i % 2 for i = 1:N])
DiscontinuousQuantity{Int32}(Int32[3, 2, 3, 2, 3, 2], 2)
For both quantities, we define simple diffusion fluxes:
function flux2(f, u, edge, data)
f[dspec] = u[dspec, 1] - u[dspec, 2]
f[cspec] = u[cspec, 1] - u[cspec, 2]
end
flux2 (generic function with 1 method)
Define a thin layer interface condition for dspec
and an interface source for cspec
.
function breaction2(f, u, node, data)
if node.region > 2
react = (u[dspec, 1] - u[dspec, 2]) / d1
f[dspec, 1] = react
f[dspec, 2] = -react
f[cspec] = -q1
end
end
breaction2 (generic function with 1 method)
Add physics to the system, set dirichlet bc at both ends, and extract subgrids for plotting (until there will be a plotting API for this...)
begin
physics!(system6, VoronoiFVM.Physics(; flux = flux2, breaction = breaction2))
## Set boundary conditions
boundary_dirichlet!(system6, dspec, 1, g_1)
boundary_dirichlet!(system6, dspec, 2, g_2)
boundary_dirichlet!(system6, cspec, 1, 0)
boundary_dirichlet!(system6, cspec, 2, 0)
# ensure that `solve` is called only after this cell
# as mutating circumvents the reactivity of the notebook
physics_ok = true
end;
allsubgrids = subgrids(dspec, system6)
6-element Vector{ExtendableGrid{Float64, Int32}}: ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=11, ncells=10, nbfaces=2) ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=11, ncells=10, nbfaces=2) ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=11, ncells=10, nbfaces=2) ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=11, ncells=10, nbfaces=2) ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=11, ncells=10, nbfaces=2) ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=11, ncells=10, nbfaces=2)
if physics_ok
sol6 = solve(system6; inival = 0.5)
end;
const d1 = 0.1
0.1
const q1 = 0.2
0.2
function plot2(U, subgrids, system6)
dvws = VoronoiFVM.views(U, dspec, allsubgrids, system6)
cvws = VoronoiFVM.views(U, cspec, allsubgrids, system6)
vis = GridVisualizer(; resolution = (600, 300), legend = :rt)
scalarplot!(vis,
allsubgrids,
grid2,
dvws;
flimits = (-0.5, 1.5),
clear = false,
color = :red,
label = "discontinuous species")
scalarplot!(vis,
allsubgrids,
grid2,
cvws;
flimits = (-0.5, 1.5),
clear = false,
color = :green,
label = "continuous species")
reveal(vis)
end
plot2 (generic function with 1 method)
plot2(sol6, subgrids, system6)
Testing
if d1 == 0.1 && N == 6
@test norm(system6, sol6, 2) ≈ 7.0215437706445245
end
Test Passed
Built with Julia 1.11.1 and
CairoMakie 0.12.16ExtendableGrids 1.9.2
GridVisualize 1.7.0
HypertextLiteral 0.9.5
LinearAlgebra 1.11.0
LinearSolve 2.34.0
Pkg 1.11.0
PlutoUI 0.7.60
Revise 3.5.18
Test 1.11.0
VoronoiFVM 1.25.1