125: Terminal Flux Calculation via Test Functions, 1D
This example demonstrates test function-based flux calculation for computing boundary fluxes in finite volume problems. The method uses auxiliary test functions to convert difficult-to-compute boundary integrals into volume integrals that can be accurately evaluated using the finite volume discretization.
Problem Setup
Consider a 1D reaction-diffusion system on $\Omega = [0,1]$ with two competing species:
\[\begin{aligned} \partial_t u_1 - \nabla \cdot (D_1 \nabla u_1) + R &= 0\\ \partial_t u_2 - \nabla \cdot (D_2 \nabla u_2) - R &= 0 \end{aligned}\]
where the reaction term $R = 10(u_1 - u_2)$ represents conversion between species:
- Species 1 converts to species 2 when $u_1 > u_2$
- Species 2 converts to species 1 when $u_2 > u_1$
Boundary Conditions
- Left boundary ($x=0$): Neumann flux condition for species 1: $\partial_n u_1 = 0.01$
- Right boundary ($x=1$): Dirichlet condition for species 2: $u_2 = 0$
Test Function Method
Computing the exact flux through boundaries in finite volume methods can be challenging. The test function approach provides an elegant solution:
Theory
To compute the flux $\int_{\Gamma} \mathbf{j} \cdot \mathbf{n} \, ds$ through boundary $\Gamma$, we construct a test function $T(x)$ that solves:
\[-\nabla^2 T = 0 \quad \text{in } \Omega\]
with boundary conditions:
\[T = 1\]
on the boundary where we want to measure flux\[T = 0\]
on other boundaries\[\partial_n T = 0\]
on remaining boundaries
Implementation
The example constructs two test functions:
tf1
: Measures flux from boundary 2 to boundary 1tf2
: Measures flux from boundary 1 to boundary 2
Using Green's theorem, the boundary flux integral becomes:
\[\int_{\Gamma} \mathbf{j} \cdot \mathbf{n} \, ds = \int_\Omega \nabla T \cdot \mathbf{j} + T(\nabla \cdot \mathbf{j}) \, d\omega\]
Complete Derivation with Reaction Terms
For our reaction-diffusion system, we have the flux $\mathbf{j} = -D\nabla u$ and the PDE:
\[\partial_t u - \nabla \cdot \mathbf{j} + R = 0\]
Therefore: $\nabla \cdot \mathbf{j} = \partial_t u + R$
For the stationary case ($\partial_t u = 0$), we get: $\nabla \cdot \mathbf{j} = R$
The complete flux calculation becomes:
\[\begin{aligned} \int_{\Gamma} \mathbf{j} \cdot \mathbf{n} \, ds &= \int_{\Gamma} T\mathbf{j} \cdot \mathbf{n} \, ds \quad \text{(since } T=1 \text{ on } \Gamma\text{)}\\ &= \int_{\partial\Omega} T\mathbf{j} \cdot \mathbf{n} \, ds \quad \text{(since } T=0 \text{ on other boundaries)}\\ &= \int_\Omega \nabla \cdot (T \mathbf{j}) \, d\omega \quad \text{(Gauss theorem)}\\ &= \int_\Omega \nabla T \cdot \mathbf{j} \, d\omega + \int_\Omega T \nabla\cdot \mathbf{j} \, d\omega\\ &= \int_\Omega \nabla T \cdot \mathbf{j} \, d\omega + \int_\Omega T \cdot R \, d\omega \end{aligned}\]
Finite Volume Approximation
The VoronoiFVM implementation approximates these integrals using the finite volume discretization:
\[\int_\Omega \nabla T \cdot \mathbf{j} \, d\omega \approx \sum_{k,l} \frac{|\omega_k \cap \omega_l|}{h_{k,l}} g(u_k, u_l) (T_k - T_l)\]
where:
- The sum runs over pairs of neighboring control volumes
\[g(u_k, u_l) = D(u_k - u_l)\]
is the finite volume flux\[|\omega_k \cap \omega_l|/h_{k,l}\]
is the interface area divided by distance
The reaction term integral is approximated as:
\[\int_\Omega T \cdot R \, d\omega \approx \sum_k |\omega_k| T_k R_k\]
This volume integral can be accurately computed using the finite volume discretization.
Parameter Study
The example varies the diffusion coefficients $D_1 = D_2 = \varepsilon$ from 1.0 to 0.01, demonstrating how:
- High diffusion ($\varepsilon = 1.0$): Species profiles are smooth
- Low diffusion ($\varepsilon = 0.01$): Sharp gradients form, requiring careful flux calculation
Physical Interpretation
This models competing transport processes:
- Species injection: Species 1 enters at the left boundary
- Reaction zone: Conversion between species occurs throughout the domain
- Species removal: Species 2 exits at the right boundary
The test function method accurately quantifies the net transport rates between boundaries, which is crucial for:
- Mass balance verification
- Process optimization
- Validation of numerical schemes
For a more comprehensive explanation, see Example225: Terminal flux calculation via test functions, nD.
module Example125_TestFunctions1D
using Printf
using VoronoiFVM
using ExtendableGrids
using GridVisualize
# Mutable struct to hold problem parameters
# This encapsulates diffusion coefficients and other physical parameters
mutable struct ProblemData
eps::Vector{Float64} ## Diffusion coefficients for both species
end
function main(; n = 100, Plotter = nothing, verbose = false, unknown_storage = :sparse, assembly = :edgewise)
# Create a 1D grid with n intervals on [0,1]
h = 1 / n
grid = simplexgrid(collect(0:h:1))
# Initialize problem parameters in data structure
# Diffusion coefficients for both species (will be varied in the loop)
problem_data = ProblemData([1.0, 1.0e-1])
# Define the physics of the problem
physics = VoronoiFVM.Physics(
# Reaction terms: Species 1 converts to species 2 and vice versa
# Rate depends on concentration difference (Le Chatelier principle)
reaction = function (f, u, node, data)
f[1] = 10 * (u[1] - u[2]) ## Species 1 decreases when u1 > u2
f[2] = 10 * (u[2] - u[1]) ## Species 2 increases when u1 > u2
return nothing
end,
# Flux terms: Simple diffusion for both species
# f[i] = D_i * (u[i,1] - u[i,2]) represents diffusive flux
flux = function (f, u, edge, data)
f[1] = data.eps[1] * (u[1, 1] - u[1, 2]) ## Diffusion flux for species 1
f[2] = data.eps[2] * (u[2, 1] - u[2, 2]) ## Diffusion flux for species 2
return nothing
end,
# Storage terms: Simple time derivative terms
storage = function (f, u, node, data)
f[1] = u[1] ## ∂u1/∂t
f[2] = u[2] ## ∂u2/∂t
return nothing
end,
data = problem_data ## Pass problem parameters
)
# Create the finite volume system
sys = VoronoiFVM.System(grid, physics; unknown_storage = unknown_storage, assembly = assembly)
# Enable both species in the single region
enable_species!(sys, 1, [1]) ## Species 1 active in region 1
enable_species!(sys, 2, [1]) ## Species 2 active in region 1
# Set boundary conditions
boundary_neumann!(sys, 1, 1, 0.01) ## Constant flux of species 1 at left boundary (x=0)
boundary_dirichlet!(sys, 2, 2, 0.0) ## Species 2 concentration = 0 at right boundary (x=1)
# Create test function factory for flux calculations
factory = TestFunctionFactory(sys)
# tf1: Test function = 1 at boundary 1, = 0 at boundary 2 (measures flux from right to left)
tf1 = testfunction(factory, [2], [1])
# tf2: Test function = 1 at boundary 2, = 0 at boundary 1 (measures flux from left to right)
tf2 = testfunction(factory, [1], [2])
# Initialize solution arrays
inival = unknowns(sys)
inival[2, :] .= 0.1 ## Initial concentration of species 2
inival[1, :] .= 0.1 ## Initial concentration of species 1
# Configure Newton solver
control = VoronoiFVM.SolverControl()
control.verbose = verbose
control.damp_initial = 0.1 ## Use damping to help convergence
# Initialize flux integral result
I1 = 0
# Set up visualization
p = GridVisualizer(; Plotter = Plotter, layout = (2, 1))
# Parameter study: vary diffusion coefficients to test method robustness
for xeps in [1.0, 0.1, 0.01]
# Update diffusion coefficients in the problem data structure
problem_data.eps = [xeps, xeps] ## Set both species to same diffusion coefficient
# Solve the stationary problem
U = solve(sys; inival, control)
# Calculate flux integral using test function tf1
# This gives the net flux from boundary 2 to boundary 1
I1 = integrate(sys, tf1, U)
# Get grid coordinates for visualization
coord = coordinates(grid)
# Use current solution as initial guess for next iteration
inival .= U
# Visualize the solutions
scalarplot!(p[1, 1], grid, U[1, :]) ## Plot species 1 concentration
scalarplot!(p[2, 1], grid, U[2, :]) ## Plot species 2 concentration
reveal(p)
# Store a test value (concentration at grid point 5)
u5 = U[5]
end
# Return the flux integral for species 1
# This should equal the input flux (0.01) for mass balance
return I1[1]
end
using Test
function runtests()
testval = 0.01
@test main(; unknown_storage = :sparse, assembly = :edgewise) ≈ testval
@test main(; unknown_storage = :sparse, assembly = :cellwise) ≈ testval
@test main(; unknown_storage = :dense, assembly = :cellwise) ≈ testval
@test main(; unknown_storage = :dense, assembly = :edgewise) ≈ testval
return nothing
end
end
This page was generated using Literate.jl.