|
| 1 | +--- |
| 2 | +title: Solving the heat equation with diffusion-implicit time-stepping |
| 3 | +author: Charles Kawczynski |
| 4 | +--- |
| 5 | + |
| 6 | +In this tutorial, we'll be solving the heat equation: |
| 7 | + |
| 8 | +```math |
| 9 | +∂_t T = α ∇²(T) + β \sin(γ z) |
| 10 | +``` |
| 11 | + |
| 12 | +with boundary conditions: ``∇T(z=a) = ∇T_{bottom}, T(z=b) = T_{top}``. We'll solve these equations numerically using Finite Difference Method on cell faces. The same exercise could easily be done on cell centers. |
| 13 | + |
| 14 | +## Code loading and parameters |
| 15 | + |
| 16 | +First, we'll use / import some packages: |
| 17 | + |
| 18 | +```julia |
| 19 | +import Plots |
| 20 | +using LinearAlgebra |
| 21 | +using DiffEqBase |
| 22 | +using OrdinaryDiffEq: SplitODEProblem, solve, IMEXEuler |
| 23 | +import SciMLBase |
| 24 | +``` |
| 25 | + |
| 26 | +Next, we'll define some global problem parameters: |
| 27 | +```julia |
| 28 | +a,b, n = 0, 1, 10 # zmin, zmax, number of cells |
| 29 | +n̂_min, n̂_max = -1, 1 # Outward facing unit vectors |
| 30 | +α = 100; # thermal diffusivity, larger means more stiff |
| 31 | +β, γ = 10000, π; # source term coefficients |
| 32 | +Δt = 1000; # timestep size |
| 33 | +N_t = 10; # number of timesteps to take |
| 34 | +FT = Float64; # float type |
| 35 | +Δz = FT(b-a)/FT(n) |
| 36 | +Δz² = Δz^2; |
| 37 | +∇²_op = [1/Δz², -2/Δz², 1/Δz²]; # interior Laplacian operator |
| 38 | +∇T_bottom = 10; # Temperature gradient at the top |
| 39 | +T_top = 1; # Temperature at the bottom |
| 40 | +S(z) = β*sin(γ*z) # source term, (sin for easy integration) |
| 41 | +zf = range(a, b, length=n+1); # coordinates on cell faces |
| 42 | +``` |
| 43 | + |
| 44 | +## Derivation of analytic solution |
| 45 | +Here, we'll derive the analytic solution: |
| 46 | + |
| 47 | +```math |
| 48 | +\frac{∂²T}{∂²z} = -S(z)/α = -β \sin(γ z)/α \\ |
| 49 | +\frac{∂T}{∂z} = β \cos(γ z)/(γ α)+c_1 \\ |
| 50 | +T(z) = β \sin(γ z)/(γ^2 α)+c_1 z+c_2, \qquad \text{(generic solution)} |
| 51 | +``` |
| 52 | +Apply bottom boundary condition: |
| 53 | +```math |
| 54 | +\frac{∂T}{∂z}(a) = β \cos(γ a)/(γ α)+c_1 = ∇T_{bottom} \\ |
| 55 | +c_1 = ∇T_{bottom}-β \cos(γ a)/(γ α) |
| 56 | +``` |
| 57 | + |
| 58 | +Apply top boundary condition: |
| 59 | +```math |
| 60 | +T(b) = β \sin(γ b)/(γ^2 α)+c_1 b+c_2 = T_{top} \\ |
| 61 | +c_2 = T_{top}-(β \sin(γ b)/(γ^2 α)+c_1 b) |
| 62 | +``` |
| 63 | + |
| 64 | +And now let's define this in a julia function: |
| 65 | +```julia |
| 66 | +function T_analytic(z) # Analytic steady state solution |
| 67 | + c1 = ∇T_bottom-β*cos(γ*a)/(γ*α) |
| 68 | + c2 = T_top-(β*sin(γ*b)/(γ^2*α)+c1*b) |
| 69 | + return β*sin(γ*z)/(γ^2*α)+c1*z+c2 |
| 70 | +end |
| 71 | +``` |
| 72 | + |
| 73 | +## Derive the temporal discretization |
| 74 | + |
| 75 | +Here, we'll derivation the matrix form of the temporal discretization we wish to use (diffusion-implicit and explicit Euler): |
| 76 | +```math |
| 77 | +∂_t T = α ∇²T + S \\ |
| 78 | +(T^{n+1}-T^n) = Δt (α ∇²T^{n+1} + S) \\ |
| 79 | +(T^{n+1} - Δt α ∇²T^{n+1}) = T^n + Δt S \\ |
| 80 | +(I - Δt α ∇²) T^{n+1} = T^n + Δt S |
| 81 | +``` |
| 82 | + |
| 83 | +Note that, since the ``∇²`` reaches to boundary points, we'll need to modify the stencils to account for boundary conditions. |
| 84 | + |
| 85 | +## Derive the finite difference stencil |
| 86 | + |
| 87 | +For the interior domain, a central and second-order finite difference stencil is simply: |
| 88 | + |
| 89 | +```math |
| 90 | +∇²f = f_{i-1}/Δz² -2f_i/Δz² + f_{i+1}/Δz², \qquad \text{or} \\ |
| 91 | +∇² = [1/Δz², -2/Δz², 1/Δz²] \\ |
| 92 | +``` |
| 93 | + |
| 94 | +At the boundaries, we need to modify the stencil to account for Dirichlet and Neumann BCs. Using the following index denotion: |
| 95 | + |
| 96 | + - `i` first interior index |
| 97 | + - `b` boundary index |
| 98 | + - `g` ghost index |
| 99 | + |
| 100 | +the Dirichlet boundary stencil & source: |
| 101 | +```math |
| 102 | +∂_t T = α (T[i-1]+T[b]-2 T[i])/Δz² + S \\ |
| 103 | +∂_t T = α (T[i-1]-2 T[i])/Δz² + S + α T[b] / Δz² |
| 104 | +``` |
| 105 | + |
| 106 | +and Neumann boundary stencil & source: |
| 107 | +```math |
| 108 | +∇T_{bottom} n̂ = (T[g] - T[i])/(2Δz), \qquad n̂ = [-1,1] ∈ [zmin,zmax] \\ |
| 109 | +T[i] + 2 Δz ∇T_{bottom} n̂ = T[g] \\ |
| 110 | +∂_t T = α (((T[i] + 2 Δz ∇T_{bottom} n̂) - T[b])/Δz - (T[b] - T[i])/Δz)/Δz + S \\ |
| 111 | +∂_t T = α (((T[i]) - T[b])/Δz - (T[b] - T[i])/Δz)/Δz + S + α 2 Δz ∇T_{bottom} n̂/Δz² \\ |
| 112 | +∂_t T = α (2 T[i] - 2 T[b])/Δz² + S + 2α ∇T_{bottom} n̂/Δz |
| 113 | +``` |
| 114 | + |
| 115 | +## Define the discrete diffusion operator |
| 116 | +```julia |
| 117 | +# Initialize interior and boundary stencils: |
| 118 | +∇² = Tridiagonal( |
| 119 | + ones(FT, n) .* ∇²_op[1], |
| 120 | + ones(FT, n+1) .* ∇²_op[2], |
| 121 | + ones(FT, n) .* ∇²_op[3] |
| 122 | +); |
| 123 | + |
| 124 | +# Modify boundary stencil to account for BCs |
| 125 | + |
| 126 | +∇².d[1] = -2/Δz² |
| 127 | +∇².du[1] = +2/Δz² |
| 128 | + |
| 129 | +# Modify boundary stencil to account for BCs |
| 130 | +∇².du[n] = 0 # modified stencil |
| 131 | +∇².d[n+1] = 0 # to ensure `∂_t T = 0` at `z=zmax` |
| 132 | +∇².dl[n] = 0 # to ensure `∂_t T = 0` at `z=zmax` |
| 133 | +D = α .* ∇² |
| 134 | +``` |
| 135 | + |
| 136 | +## Define boundary source |
| 137 | +Here, we'll compute the boundary source (``α T[b] / Δz²``) |
| 138 | +```julia |
| 139 | +AT_b = zeros(FT, n+1); |
| 140 | +AT_b[1] = α*2/Δz*∇T_bottom*n̂_min; |
| 141 | +AT_b[end-1] = α*T_top/Δz²; |
| 142 | +``` |
| 143 | + |
| 144 | +## Set initial condition |
| 145 | +Let's just initialize the solution to `1`, and also set the top boundary condition: |
| 146 | +```julia |
| 147 | +T = zeros(FT, n+1); |
| 148 | +T .= 1; |
| 149 | +T[n+1] = T_top; # set top BC |
| 150 | +``` |
| 151 | + |
| 152 | +## Define right-hand side sources |
| 153 | +Here, we define the right-hand side (RHS) sources: |
| 154 | +```julia |
| 155 | +function rhs!(dT, T, params, t) |
| 156 | + n = params.n |
| 157 | + i = 1:n # interior domain |
| 158 | + dT[i] .= S.(zf[i]) .+ AT_b[i] |
| 159 | + return dT |
| 160 | +end; |
| 161 | +``` |
| 162 | + |
| 163 | +Next, we'll pacakge up parameters needed in the RHS function, define the ODE problem, and solve. |
| 164 | +```julia |
| 165 | +params = (;n) |
| 166 | + |
| 167 | +tspan = (FT(0), N_t*FT(Δt)) |
| 168 | + |
| 169 | +prob = SplitODEProblem( |
| 170 | + SciMLBase.DiffEqArrayOperator( |
| 171 | + D, |
| 172 | + ), |
| 173 | + rhs!, |
| 174 | + T, |
| 175 | + tspan, |
| 176 | + params |
| 177 | +) |
| 178 | +alg = IMEXEuler(linsolve=LinSolveFactorize(lu!)) |
| 179 | +println("Solving...") |
| 180 | +sol = solve( |
| 181 | + prob, |
| 182 | + alg, |
| 183 | + dt = Δt, |
| 184 | + saveat = range(FT(0), N_t*FT(Δt), length=5), |
| 185 | + progress = true, |
| 186 | + progress_message = (dt, u, p, t) -> t, |
| 187 | +); |
| 188 | +``` |
| 189 | + |
| 190 | +# Visualizing results |
| 191 | + |
| 192 | +Now, let's visualize the results of the solution and error: |
| 193 | +```julia |
| 194 | +T_end = sol.u[end] |
| 195 | + |
| 196 | +p1 = Plots.plot(zf, T_analytic.(zf), label="analytic", markershape=:circle, markersize=6) |
| 197 | +p1 = Plots.plot!(p1, zf, T_end, label="numerical", markershape=:diamond) |
| 198 | +p1 = Plots.plot!(p1, title="T ∈ cell faces") |
| 199 | + |
| 200 | +p2 = Plots.plot(zf, abs.(T_end .- T_analytic.(zf)), label="error", markershape=:circle, markersize=6) |
| 201 | +p2 = Plots.plot!(p2, title="T ∈ cell faces") |
| 202 | + |
| 203 | +Plots.plot(p1, p2) |
| 204 | +``` |
0 commit comments