004: 3D Nonlinear Reaction-Diffusion System with Coupled Species

(source code)

This example implements a 3D nonlinear reaction-diffusion system with three coupled species on a unit cube domain $Ω = [0,1]^3$. The system demonstrates various linear solver techniques for handling the resulting large, coupled nonlinear equations.

Mathematical Formulation

The system solves the following coupled partial differential equations for three species $u_1$, $u_2$, and $u_3$:

\[\frac{\partial u_i}{\partial t} + \nabla \cdot \mathbf{J}_i + R_i = S_i \quad \text{in } Ω, \quad i = 1, 2, 3\]

Reaction Terms

The nonlinear reaction terms create a cyclic coupling between the three species:

\[\begin{aligned} R_1 &= 100 u_1 - u_2^2\\ R_2 &= 100 u_2 - u_3^2\\ R_3 &= 100 u_3 - u_1^2 \end{aligned}\]

Flux Terms

The flux $J_i$ for each species involves nonlinear, cross-diffusion effects. For each edge connecting two nodes, the flux is computed as:

\[\begin{aligned} J_1 &= -\varepsilon (u_1+u_2)\nabla u_1^2 \\ J_2 &= -\varepsilon (u_2+u_3)\nabla u_2^2 \\ J_3 &= -\varepsilon (u_3+u_1)\nabla u_3^2 \end{aligned}\]

where $\varepsilon = 1.0$ is the diffusion parameter, and $u_i^{(1)}$, $u_i^{(2)}$ denote the values of species $i$ at the two nodes of an edge. The flux combines cross-diffusion (species coupling through averaged concentrations) with nonlinear concentration-dependent transport.

Source Terms

All three species have identical source terms with a Gaussian profile centered at $(0.5, 0.5)$:

\[S_i = \exp\left(-20\left((x_1 - 0.5)^2 + (x_2 - 0.5)^2\right)\right), \quad i = 1, 2, 3\]

Boundary Conditions

Dirichlet boundary conditions are applied:

  • On regions 2 and 4: $u_i = 1$ for all species $i = 1, 2, 3$
  • Homogeneous Neumann BC otherwise

This example serves as benchmark code for comparing the efficiency of different linear solver approaches for large-scale, coupled nonlinear PDE systems.

module DevEx004_EquationBlock3D

# under development

using Printf
using VoronoiFVM
using ExtendableGrids
using GridVisualize
using LinearSolve
using ExtendableSparse
using ExtendableSparse: ILUZeroPreconBuilder, JacobiPreconBuilder
using SparseArrays
import AMGCLWrap, Metis
using AlgebraicMultigrid
using LinearAlgebra
import Pardiso
using Test

function main(; nref = 0, Plotter = nothing, npart = 20, assembly = :edgewise, tol = 1.0e-7, kwargs...)

Create a uniform 3D grid on the unit cube [0,1]³ Grid resolution increases exponentially with nref parameter

    X = range(0, 1.0, length = 5 * 2^nref + 1)
    grid = simplexgrid(X, X, X)

Enable parallel processing if multiple threads are available Partition the grid using METIS for load balancing across threads

    if Threads.nthreads() > 1
        grid = partition(grid, PlainMetisPartitioning(; npart); nodes = true, edges = true)
    end
    nn = num_nodes(grid)

Diffusion parameter ε appearing in the flux terms

    eps = 1.0

Define the nonlinear reaction terms creating cyclic coupling between species R₁ = 100u₁ - u₂², R₂ = 100u₂ - u₃², R₃ = 100u₃ - u₁²

    function reaction(f, u, node, data)
        f[1] = 100 * u[1] - u[2]^2
        f[2] = 100 * u[2] - u[3]^2
        f[3] = 100 * u[3] - u[1]^2
        return nothing
    end

Define nonlinear cross-diffusion flux terms J₁ = -ε(u₁+u₂)∇u₁², J₂ = -ε(u₂+u₃)∇u₂², J₃ = -ε(u₃+u₁)∇u₃² Cross-diffusion coefficients are averaged between edge nodes

    function flux(f, u, edge, data)
        d1 = (u[2, 1] + u[2, 2]) / 2  # Average u₂ concentration for u₁ flux
        d2 = (u[3, 1] + u[3, 2]) / 2  # Average u₃ concentration for u₂ flux
        d3 = (u[1, 1] + u[1, 2]) / 2  # Average u₁ concentration for u₃ flux
        f[1] = eps * (d1 + d2) * (u[1, 1]^2 - u[1, 2]^2)
        f[2] = eps * (d2 + d3) * (u[2, 1]^2 - u[2, 2]^2)
        f[3] = eps * (d3 + d1) * (u[3, 1]^2 - u[3, 2]^2)
        return nothing
    end

Define Gaussian source terms centered at (0.5, 0.5) for all species S = exp(-20((x₁-0.5)² + (x₂-0.5)²))

    function source(f, node, data)
        x1 = node[1] - 0.5
        x2 = node[2] - 0.5
        f[1] = exp(-20.0 * (x1^2 + x2^2))
        f[2] = f[1]
        f[3] = f[1]
        return nothing
    end

Define storage term for time-dependent problems (identity operator)

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

Set Dirichlet boundary conditions: u = 1 on regions 2 and 4 for all species Homogeneous Neumann conditions elsewhere

    function bcondition(f, u, node, data)
        for species in (1, 2, 3)
            boundary_dirichlet!(
                f,
                u,
                node;
                species,
                region = 2,
                value = 1,
            )
            boundary_dirichlet!(
                f,
                u,
                node;
                species,
                region = 4,
                value = 1,
            )
        end
        return nothing
    end

Assemble the VoronoiFVM system with all physics components Three coupled species with nonlinear cross-diffusion and reaction terms

    sys = VoronoiFVM.System(
        grid; reaction, flux, source, storage, bcondition, assembly,
        species = [1, 2, 3],
    )

Choose platform-appropriate direct solver for reference solution UMFPACK on macOS, MKL Pardiso on other platforms

    if Sys.isapple()
        direct_solver = LinearSolve.UMFPACKFactorization()
        println("Direct solver: UMFPACK")
    else
        direct_solver = LinearSolve.MKLPardisoFactorize()
        println("Direct solver: MKL Pardiso")
    end

Solve with direct method to obtain reference solution

    @time "Direct solution" direct_sol = solve(sys; inival = 0.5, method_linear = direct_solver, kwargs...)
    println()

    ok = true

Benchmark 1: AMGCL algebraic multigrid solver with block structure Exploits the 3-species block structure for efficient multigrid

    @time "AMGCL" amgcl_sol = solve(sys; inival = 0.5, method_linear = AMGCLWrap.AMGSolverAlgorithm(blocksize = 3), kwargs...)
    @show n = norm(amgcl_sol - direct_sol, Inf)
    ok = ok && n < tol
    println()

Benchmark 2: ILU(0) preconditioned GMRES Block ILU(0) factorization respects 3-species coupling structure

    @time "ilu0-gmres" ilu0_sol = solve(
        sys; inival = 0.5,
        method_linear = KrylovJL_GMRES(precs = ILUZeroPreconBuilder(blocksize = 3)),
        keepcurrent_linear = false,
        kwargs...
    )
    @show n = norm(ilu0_sol - direct_sol, Inf)
    ok = ok && n < tol
    println()

Benchmark 3: Direct solver as preconditioner for GMRES Uses exact factorization from first Newton step as preconditioner in the subsequent steps

    @time "Direct-gmres" pgmres_sol = solve(
        sys; inival = 0.5,
        method_linear = KrylovJL_GMRES(precs = LinearSolvePreconBuilder(direct_solver)),
        keepcurrent_linear = false,
        kwargs...
    )
    @show n = norm(pgmres_sol - direct_sol, Inf)
    ok = ok && n < tol
    println()

Benchmark 4: Block GMRES with direct solver preconditioner Applies block structure to GMRES iteration with direct preconditioning Partitioning separates the three species into distinct blocks

    @time "Direct-blockgmres" pbgmres_sol = solve(
        sys; inival = 0.5,
        method_linear = KrylovJL_GMRES(
            precs = BlockPreconBuilder(
                precs = LinearSolvePreconBuilder(direct_solver),
                partitioning = A -> [1:3:size(A, 1), 2:3:size(A, 1), 3:3:size(A, 1)]
            )
        ),
        keepcurrent_linear = false,
        kwargs...
    )
    @show n = norm(pbgmres_sol - direct_sol, Inf)
    ok = ok && n < tol
    println()

Benchmark 5: Block GMRES with AMGCL preconditioner Combines block structure with algebraic multigrid preconditioning More scalable than direct methods for large 3D systems

    if !Sys.isapple() # getting some unclear OMP problems here...
        @time "AMGCL-blockgmres" amgclbgmres_sol = solve(
            sys; inival = 0.5,
            method_linear = KrylovJL_GMRES(
                precs = BlockPreconBuilder(
                    precs = AMGCLWrap.AMGPreconBuilder(),
                    partitioning = A -> [1:3:size(A, 1), 2:3:size(A, 1), 3:3:size(A, 1)]
                )
            ),
            keepcurrent_linear = false,
            kwargs...
        )
        @show n = norm(amgclbgmres_sol - direct_sol, Inf)
        ok = ok && n < tol
        println()
    end

Benchmark 6: Block GMRES with native Julia AMG preconditioner Uses AlgebraicMultigrid.jl instead of AMGCL for comparison Smoothed aggregation AMG with block structure

    @time "AMG-blockgmres" amgbgmres_sol = solve(
        sys; inival = 0.5,
        method_linear = KrylovJL_GMRES(
            precs = BlockPreconBuilder(
                precs = AlgebraicMultigrid.SmoothedAggregationPreconBuilder(),
                partitioning = A -> [1:3:size(A, 1), 2:3:size(A, 1), 3:3:size(A, 1)]
            )
        ),
        keepcurrent_linear = false,
        kwargs...
    )
    @show n = norm(amgbgmres_sol - direct_sol, Inf)
    ok = ok && n < tol
    println()

    return ok
end


using Test
function runtests()
    @test main() == true
    return nothing
end

end

This page was generated using Literate.jl.