Tutorial: Solution of the heat equation with Neumann boundary conditions

Similar to the tutorial on linear advection, we will demonstrate how to solve a conservative production-destruction system (PDS) resulting from a PDE discretization and means to improve the performance.

Definition of the conservative production-destruction system

Consider the heat equation

\[\partial_t u(t,x) = \mu \partial_x^2 u(t,x),\quad u(0,x)=u_0(x),\]

with $μ ≥ 0$, $t≥ 0$, $x\in[0,1]$, and homogeneous Neumann boundary conditions. We use a finite volume discretization, i.e., we split the domain $[0, 1]$ into $N$ uniform cells of width $\Delta x = 1 / N$. As degrees of freedom, we use the mean values of $u(t)$ in each cell approximated by the point value $u_i(t)$ in the center of cell $i$. Finally, we use the classical central finite difference discretization of the Laplacian with homogeneous Neumann boundary conditions, resulting in the ODE

\[\partial_t u(t) = L u(t), \quad L = \frac{\mu}{\Delta x^2} \begin{pmatrix} -1 & 1 \\ 1 & -2 & 1 \\ & \ddots & \ddots & \ddots \\ && 1 & -2 & 1 \\ &&& 1 & -1 \end{pmatrix}.\]

The system can be written as a conservative PDS with production terms

\[\begin{aligned} &p_{i,i-1}(t,\mathbf u(t)) = \frac{\mu}{\Delta x^2} u_{i-1}(t),\quad i=2,\dots,N, \\ &p_{i,i+1}(t,\mathbf u(t)) = \frac{\mu}{\Delta x^2} u_{i+1}(t),\quad i=1,\dots,N-1, \end{aligned}\]

and destruction terms $d_{i,j} = p_{j,i}$. In addition, all production and destruction terms not listed are zero.

Solution of the conservative production-destruction system

Now we are ready to define a ConservativePDSProblem and to solve this problem with a method of PositiveIntegrators.jl or OrdinaryDiffEq.jl. In the following we use $N = 100$ nodes and the time domain $t \in [0,1]$. Moreover, we choose the initial condition

\[u_0(x) = \cos(\pi x)^2.\]

x_boundaries = range(0, 1, length = 101)
x = x_boundaries[1:end-1] .+ step(x_boundaries) / 2
u0 = @. cospi(x)^2 # initial solution
tspan = (0.0, 1.0) # time domain

We will choose three different matrix types for the production terms and the resulting linear systems:

  1. standard dense matrices (default)
  2. sparse matrices (from SparseArrays.jl)
  3. tridiagonal matrices (from LinearAlgebra.jl)

Standard dense matrices

using PositiveIntegrators # load ConservativePDSProblem

function heat_eq_P!(P, u, μ, t)
    fill!(P, 0)
    N = length(u)
    Δx = 1 / N
    μ_Δx2 = μ / Δx^2

    let i = 1
        # Neumann boundary condition
        P[i, i + 1] = u[i + 1] * μ_Δx2
    end

    for i in 2:(length(u) - 1)
        # interior stencil
        P[i, i - 1] = u[i - 1] * μ_Δx2
        P[i, i + 1] = u[i + 1] * μ_Δx2
    end

    let i = length(u)
        # Neumann boundary condition
        P[i, i - 1] = u[i - 1] * μ_Δx2
    end

    return nothing
end

μ = 1.0e-2
prob = ConservativePDSProblem(heat_eq_P!, u0, tspan, μ) # create the PDS

sol = solve(prob, MPRK22(1.0); save_everystep = false)
using Plots

plot(x, u0; label = "u0", xguide = "x", yguide = "u")
plot!(x, last(sol.u); label = "u")
Example block output

Sparse matrices

To use different matrix types for the production terms and linear systems, you can use the keyword argument p_prototype of ConservativePDSProblem and PDSProblem.

using SparseArrays
p_prototype = spdiagm(-1 => ones(eltype(u0), length(u0) - 1),
                      +1 => ones(eltype(u0), length(u0) - 1))
prob_sparse = ConservativePDSProblem(heat_eq_P!, u0, tspan, μ;
                                     p_prototype = p_prototype)

sol_sparse = solve(prob_sparse, MPRK22(1.0); save_everystep = false)
plot(x, u0; label = "u0", xguide = "x", yguide = "u")
plot!(x, last(sol_sparse.u); label = "u")
Example block output

Tridiagonal matrices

The sparse matrices used in this case have a very special structure since they are in fact tridiagonal matrices. Thus, we can also use the special matrix type Tridiagonal from the standard library LinearAlgebra.

using LinearAlgebra
p_prototype = Tridiagonal(ones(eltype(u0), length(u0) - 1),
                          ones(eltype(u0), length(u0)),
                          ones(eltype(u0), length(u0) - 1))
prob_tridiagonal = ConservativePDSProblem(heat_eq_P!, u0, tspan, μ;
                                          p_prototype = p_prototype)

sol_tridiagonal = solve(prob_tridiagonal, MPRK22(1.0); save_everystep = false)
plot(x, u0; label = "u0", xguide = "x", yguide = "u")
plot!(x, last(sol_tridiagonal.u); label = "u")
Example block output

Performance comparison

Finally, we use BenchmarkTools.jl to compare the performance of the different implementations.

using BenchmarkTools
@benchmark solve(prob, MPRK22(1.0); save_everystep = false)
BenchmarkTools.Trial: 1015 samples with 1 evaluation.
 Range (minmax):  4.687 ms 11.872 ms   GC (min … max): 0.00% … 0.00%
 Time  (median):     4.788 ms                GC (median):    0.00%
 Time  (mean ± σ):   4.922 ms ± 522.924 μs   GC (mean ± σ):  1.11% ± 3.06%

  ▃█▅▄         ▁▂                                            
  ████▅▁▄▄▄▁▁▁▆████▇▅▅▄▄▄▁▁▁▄▁▁▁▁▄▁▁▄▄▁▁▁▁▁▁▁▄▁▁▁▁▁▁▁▁▅▅▁▄▅ █
  4.69 ms      Histogram: log(frequency) by time      7.31 ms <

 Memory estimate: 5.11 MiB, allocs estimate: 231.
@benchmark solve(prob_sparse, MPRK22(1.0); save_everystep = false)
BenchmarkTools.Trial: 1566 samples with 1 evaluation.
 Range (minmax):  2.963 ms 11.053 ms   GC (min … max): 0.00% … 6.35%
 Time  (median):     3.041 ms                GC (median):    0.00%
 Time  (mean ± σ):   3.189 ms ± 485.687 μs   GC (mean ± σ):  1.43% ± 3.31%

  ▄▇█▄▁                                                       
  █████▇▁▁▆▄▁▁▁▁▁▁▁▁▄▁▁▄▁▁▁▁▁▁▁▁▁▄▄▆▄▅▅▆▆▆▄▆▆▆▆▅█▆▆▇▇▇█▇▇▆▆ █
  2.96 ms      Histogram: log(frequency) by time      4.77 ms <

 Memory estimate: 5.06 MiB, allocs estimate: 2503.

By default, we use an LU factorization for the linear systems. At the time of writing, Julia uses SparseArrays.jl defaulting to UMFPACK from SuiteSparse in this case. However, the linear systems do not necessarily have the structure for which UMFPACK is optimized for. Thus, it is often possible to gain performance by switching to KLU instead.

using LinearSolve
@benchmark solve(prob_sparse, MPRK22(1.0; linsolve = KLUFactorization()); save_everystep = false)
BenchmarkTools.Trial: 7592 samples with 1 evaluation.
 Range (minmax):  598.850 μs  7.470 ms   GC (min … max): 0.00% … 27.22%
 Time  (median):     637.535 μs                GC (median):    0.00%
 Time  (mean ± σ):   656.286 μs ± 178.707 μs   GC (mean ± σ):  1.17% ±  3.65%

      ▁▃▆██▆▅▄▃▂▂▁▂▁▁                                         ▂
  ▄▄▅██████████████████▇▇▆▆▃▆▅▅▄▃▁▁▁▄▅▃▃▃▁▁▁▁▁▃▄▄▅▅▃▅▅▅▆▅▅▄▄▅ █
  599 μs        Histogram: log(frequency) by time        871 μs <

 Memory estimate: 319.02 KiB, allocs estimate: 371.
@benchmark solve(prob_tridiagonal, MPRK22(1.0); save_everystep = false)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (minmax):  231.013 μs  2.337 ms   GC (min … max): 0.00% … 83.96%
 Time  (median):     241.862 μs                GC (median):    0.00%
 Time  (mean ± σ):   278.271 μs ± 143.056 μs   GC (mean ± σ):  3.29% ±  5.94%

    ▃█▆                                                          
  ▂▅███▄▃▃▃▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▁▂▂▂▂▂▂▂▁▂▁▂▄▇▆▄▃▃▃▂▂▂▂▂▂▂ ▃
  231 μs           Histogram: frequency by time          375 μs <

 Memory estimate: 305.02 KiB, allocs estimate: 494.

Package versions

These results were obtained using the following versions.

using InteractiveUtils
versioninfo()
println()

using Pkg
Pkg.status(["PositiveIntegrators", "SparseArrays", "KLU", "LinearSolve", "OrdinaryDiffEq"],
           mode=PKGMODE_MANIFEST)
Julia Version 1.10.5
Commit 6f3fdf7b362 (2024-08-27 14:19 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × AMD EPYC 7763 64-Core Processor
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-15.0.7 (ORCJIT, znver3)
Threads: 1 default, 0 interactive, 1 GC (on 4 virtual cores)
Environment:
  JULIA_PKG_SERVER_REGISTRY_PREFERENCE = eager

Status `~/work/PositiveIntegrators.jl/PositiveIntegrators.jl/docs/Manifest.toml`
  [ef3ab10e] KLU v0.6.0
  [7ed4a6bd] LinearSolve v2.33.0
  [1dea7af3] OrdinaryDiffEq v6.89.0
  [d1b20bf0] PositiveIntegrators v0.2.6 `~/work/PositiveIntegrators.jl/PositiveIntegrators.jl`
  [2f01184e] SparseArrays v1.10.0