225: Terminal flux calculation via test functions, nD

(source code)

After calculating solutions based on the finite volume method, it may be interesting to obtain information about the solution besides of the graphical representation.

Here, we focus on the following data:

  • integrals of the solution
  • flux through parts of the boundary

Let us define the following reaction - diffusion system in a domain $\Omega$:

\[\begin{aligned} \partial_t u_1 - \nabla \cdot \nabla u_1 + r(u_1, u_2) &= f=1.0\\ \partial_t u_2 - \nabla \cdot \nabla u_1 - r(u_1, u_2) &= 0 \end{aligned}\]

with boundary conditions $u_2=0$ on $\Gamma_2\subset\partial\Omega$ and $r(u_1,u_2)=u_1 + 0.1 u_2$

The source $f$ creates species $u_1$ which reacts to $u_2$, $u_2$ then leaves the domain at boundary $\Gamma_2$.

Stationary problem

For the stationary problem, we have the following flux balances derived from the equations and from Gauss theorem:

\[\begin{aligned} \int_\Omega r(u_1,u_2) d\omega &= \int_\Omega f d\omega \\ \int_\Omega -r(u_1,u_2) d\omega &= \int_{\Gamma_2} \nabla u \cdot \vec n ds \\ \end{aligned}\]

The volume integrals can be approximated based on the finite volume subdivision $\Omega=\cup_{i\in \mathcal N} \omega_i$:

\[\begin{aligned} \int_\Omega r(u_1,u_2) d\omega &\approx \sum_{i\in \mathcal N} |\omega_i| r(u_{1,i},u_{2,i})\\ \int_\Omega f d\omega &\approx \sum_{i\in \mathcal N} |\omega_i| f_i \end{aligned}\]

But what about the boundary integral ? Here, we use a trick to cast the surface integral to the integral to a volume integral with the help of a test function:

Let $T(x)$ be the solution of the Laplace problem $-\nabla^2 T =0$ in $\Omega$ and the boundary conditions

\[\begin{aligned} T &=0 \quad \text{at}\; \Gamma_4\\ T &=1 \quad \text{at}\; \Gamma_2\\ \partial_n T &=0\quad \text{at}\; \Gamma_1,\Gamma_3 \end{aligned}\]

Write $\vec j=-\nabla u$. and assume $\nabla\cdot \vec j + r =f$.

\[\begin{aligned} \int_{\Gamma_2} \vec j \cdot \vec n ds&=\int_{\Gamma_2} T\vec j \cdot \vec n ds \quad \text{due to}\; T=1\; \text{on}\; \Gamma_2\\ &=\int_{\partial\Omega} T\vec j \cdot \vec n ds\quad \text{due to}\; T=0\; \text{on}\; \Gamma_4, \quad\vec j\cdot \vec n=0\; \text{on}\; \Gamma_1, \Gamma_3\\ &= \int_\Omega \nabla \cdot (T \vec j) d\omega \quad \text{(Gauss)}\\ &= \int_\Omega \nabla T \cdot \vec j d\omega + \int_\Omega T \nabla\cdot j d\omega\\ &= \int_\Omega \nabla T \cdot \vec j d\omega + \int_\Omega T(f-r)dω\\ \end{aligned}\]

and we approximate

\[\begin{aligned} \int_\Omega \nabla T \cdot \vec 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) \end{aligned}\]

where the sum runs over pairs of neighboring control volumes.

The integrate method with a test function parameter returns a value for each species, the sign convention assumes that species leaving the domain lead to negative values.

Transient problem

The amount of species created via the source term (measured in F) integrated over time should be equal to the sum of the amount of species left in the domain at the very end of the evolution and the amount of species which left the domain:

\[\int_{t_0}^{t_{end}} \int_\Omega f d\omega dt= \int_\Omega (u_1+u_2)dω + \int_{t_0}^{t_{end}} \int_{\Gamma_2} \nabla u_2 \cdot \vec n ds\]

Literature references:

  • H. Gajewski "Analysis und Numerik von Ladungstransport in Halbleitern", WIAS Berlin, Report No.6
  • Yoder, P. D., K. Gärtner, and W. Fichtner. "A generalized Ramo–Shockley theorem for classical to quantum transport at arbitrary frequencies." Journal of Applied Physics 79.4 (1996): 1951-1954.
  • P. Farrell, N. Rotundo, D. H. Doan, M. Kantner, J. Fuhrmann, and T. Koprucki, "Numerical methods for drift-diffusion models", in Handbook of optoelectronic device modeling and simulation: Lasers, modulators, photodetectors, solar cells, and numerical methods, vol. 2, J. Piprek, Ed. Boca Raton: CRC Press, 2017, pp. 733–771.
module Example225_TestFunctions2D

using VoronoiFVM, GridVisualize, ExtendableGrids

function main(; n = 10, Plotter = nothing, verbose = false, unknown_storage = :sparse, assembly = :edgewise,
              dim = 2, tend = 5, dt = 0.2)
    n = [101, 21, 5]
    X = collect(range(0.0, 1; length = n[dim]))
    if dim == 1
        grid = simplexgrid(X)
        Γ_where_T_equal_1 = [2]
        Γ_where_T_equal_0 = [1]
    elseif dim == 2
        grid = simplexgrid(X, X)
        Γ_where_T_equal_1 = [2]
        Γ_where_T_equal_0 = [4]
    elseif dim == 3
        grid = simplexgrid(X, X, X)
        Γ_where_T_equal_1 = [2]
        Γ_where_T_equal_0 = [4]
    end

    function storage(f, u, node, data)
        f .= u
    end

    function flux(f, u, edge, data)
        f[1] = u[1, 1] - u[1, 2]
        f[2] = u[2, 1] - u[2, 2]
    end

    r(u1, u2) = u1 - 0.1 * u2

    function reaction(f, u, node, data)
        f[1] = r(u[1], u[2])
        f[2] = -r(u[1], u[2])
    end

    function source(f, node, data)
        f[1] = 1.0
    end

    physics = VoronoiFVM.Physics(; flux = flux,
                                 storage = storage,
                                 reaction = reaction,
                                 source = source)

    system = VoronoiFVM.System(grid, physics; assembly = assembly)

    enable_species!(system, 1, [1])
    enable_species!(system, 2, [1])
    boundary_dirichlet!(system, 2, 2, 0.0)

    sol = solve(system; inival = 0.0)

    vis = GridVisualizer(; Plotter = Plotter, layout = (1, 2), resolution = (600, 300),
                         fignumber = 1)
    scalarplot!(vis[1, 1], grid, sol[1, :]; flimits = (0, 1.5), title = "u_1")
    scalarplot!(vis[1, 2], grid, sol[2, :]; flimits = (0, 1.5), title = "u_2", show = true)

    """
        The `integrate` method of `VoronoiFVM`  provides a possibility to calculate
        the volume integral of a function of a solution as described above.
        It returns a `num_specie` x `num_regions` matrix of the integrals
        of the function of the unknowns over the different subdomains (here, we have only one):
    """

    """
        Amount of u_1 and u_2 in the domain aka integral over identity storage function:
    """
    U = integrate(system, storage, sol)

    """
    Amount of species created by source term per unit time:
    """
    F = integrate(system, (f, u, node, data) -> source(f, node, data), sol)

    """
    Amount of  reaction per unit time:
    """
    R = integrate(system, reaction, sol)

    tf = TestFunctionFactory(system)
    T = testfunction(tf, Γ_where_T_equal_0, Γ_where_T_equal_1)

    I = integrate(system, T, sol)

    t0 = 0.0

    control = fixed_timesteps!(VoronoiFVM.NewtonControl(), dt)

    tsol = solve(system; inival = 0.0, times = [t0, tend], control)

    vis1 = GridVisualizer(; Plotter = Plotter, layout = (1, 2), resolution = (600, 300),
                          fignumber = 4)

    for i = 1:length(tsol)
        sol = tsol.u[i]
        scalarplot!(vis1[1, 1], grid, sol[1, :]; flimits = (0, 1.5), clear = true)
        scalarplot!(vis1[1, 2], grid, sol[2, :]; flimits = (0, 1.5), show = true)
    end

    outflow_rate = Float64[]
    for i = 2:length(tsol)
        ofr = integrate(system, T, tsol.u[i], tsol.u[i - 1], tsol.t[i] - tsol.t[i - 1])
        push!(outflow_rate, ofr[2])
    end

    vis2 = GridVisualizer(; Plotter = Plotter, layout = (1, 1), resolution = (600, 300),
                          fignumber = 2)
    scalarplot!(vis2[1, 1], [0, tend], -[I[2], I[2]]; label = "stationary", clear = true)
    scalarplot!(vis2[1, 1], tsol.t[2:end], -outflow_rate; label = "transient", show = true)

    all_outflow = 0.0
    for i = 1:(length(tsol) - 1)
        all_outflow -= outflow_rate[i] * (tsol.t[i + 1] - tsol.t[i])
    end

    Uend = integrate(system, storage, tsol.u[end])
    isapprox(F[1], R[1]; rtol = 1.0e-12) ? true : return false
    isapprox(I[1], 0.0; atol = 1.0e-12) ? true : return false
    isapprox(R[2], I[2]; rtol = 1.0e-12) ? true : return false
    isapprox(F[1] * (tend - t0), (Uend[1] + Uend[2] + all_outflow); rtol = 1.0e-12) ? true :
    return false
end

using Test
function runtests()
    @test main(; dim = 1, unknown_storage = :sparse, assembly = :edgewise)
    @test main(; dim = 1, unknown_storage = :dense, assembly = :edgewise)
    @test main(; dim = 2, unknown_storage = :sparse, assembly = :edgewise)
    @test main(; dim = 2, unknown_storage = :dense, assembly = :edgewise)
    @test main(; dim = 3, unknown_storage = :sparse, assembly = :edgewise)
    @test main(; dim = 3, unknown_storage = :dense, assembly = :edgewise)

    @test main(; dim = 1, unknown_storage = :sparse, assembly = :cellwise)
    @test main(; dim = 1, unknown_storage = :dense, assembly = :cellwise)
    @test main(; dim = 2, unknown_storage = :sparse, assembly = :cellwise)
    @test main(; dim = 2, unknown_storage = :dense, assembly = :cellwise)
    @test main(; dim = 3, unknown_storage = :sparse, assembly = :cellwise)
    @test main(; dim = 3, unknown_storage = :dense, assembly = :cellwise)
end

end

This page was generated using Literate.jl.