Transient heat transfer

No further words, let's start by importing the required toolset:

using CairoMakie
using LinearAlgebra
using Printf
using Unitful

# Stefan-Boltzmann constant.
const σ = 5.67e-08u"W/(m^2*K^4)"

First problem

Implementation of section 2.3 of Nithiarasu et al. (2016) of a transient heat transfer problem. This is the logical extension of what has been explored in a previous study. The overall problem is stated in terms of the inertia matrix $\mathbf{C}$, the stiffness $\mathbf{K}$ and the forcing function $\mathbf{f}$. The following first order differential equation of temperature $\mathcal{T}$ is to be solved:

\[\mathbf{C}\dot{\mathcal{T}}+\mathbf{K}\mathcal{T}=\mathbf{f}\]

Notice here that the temperature $\mathcal{T}$ here is a vector corresponding to the three problem components, the steel part, the furnace gas, and the outer refractory walls.

Because the model is stated in lumped form, i.e. simplified to a zero-dimensional space, we add a phase of calculation of the heat capacities of materials in compatible units to remain within the formulation proposed in the reference. In this lumped format, the elements of matrix $\mathbf{C}$ are given in units of energy per unit temperature because they already incorporate the effect bodies masses through $C_{i,i}=m_i{}c_{P,i}$ where $i$ indicates de component index.

To get a dynamics that is interpretable in the real world we will start by computing reasonable orders of magnitude of the $C_{i,i}$ by providing the masses of the bodies and typical values of specific heats for the involved materials. Considering a steel sphere of 10 cm placed floating in a 40 cm air environment limited by a spherical refractory with outer shell of 60 cm we can estimate these masses, in the same order, as:

m = 1u"kg" * [4.2, 0.033, 240.0]

Next we provide the specific heats of the materials already multiplied by the above masses:

cp(T) = m[1] * 6.00e+03u"J/(kg*K)"
cg(T) = m[2] * 1.00e+03u"J/(kg*K)"
cw(T) = m[3] * 9.00e+02u"J/(kg*K)"

Since our goal is to make the problem fully treated in matrix form, we stack the specific heats together. Since matrix $\mathrm{C}$ is diagonal and composed by these specific heats, its inverse is simply the reciprocal of its diagonal elements, what is trivial to prove. This allows the problem to be reworked as

\[\dot{\mathcal{T}}+\mathbf{C}^{-1}\mathbf{K}\mathcal{T}=\mathbf{C}^{-1}\mathbf{f}\]

Setting the derivative term to be alone could be interesting for testing different time-stepping strategies, for instance. Matrix $\mathrm{C}^{-1}$ is provided by inertiainv below:

specificheat(T) = [cp(T[1]); cg(T[2]); cw(T[3])]

inertiainv(T) = diagm(1 ./ specificheat(T))

We inspect the inverse inertia matrix:

inertiainv([300.0, 300.0, 300.0])
3×3 Matrix{Unitful.Quantity{Float64, 𝚯 𝐓^2 𝐋^-2 𝐌^-1, Unitful.FreeUnits{(J^-1, K), 𝚯 𝐓^2 𝐋^-2 𝐌^-1, nothing}}}:
 3.96825e-5 K J^-1       0.0 K J^-1         0.0 K J^-1
        0.0 K J^-1  0.030303 K J^-1         0.0 K J^-1
        0.0 K J^-1       0.0 K J^-1  4.62963e-6 K J^-1

Stiffness matrix $\mathrm{K}$ in this problem provides the convective heat transfer terms. It is given by

\[\mathbf{K} = \begin{bmatrix} h_pA_p & -h_pA_p & 0 \\ -h_pA_p & h_pA_p + h_gA_g & -h_gA_g \\ 0 & -h_gA_g & h_gA_g + h_wA_w \end{bmatrix}\]

Since this matrix does not contain any element that depends on temperature it needs to be computed only once and we will not seek an optimized way to evaluate it. The code below is generic for any layered structure as the one being modelled here. Notice that the pairwise interactions lead to a tridiagonal structure.

function stiffness(h, A)
    p = @. h * A
    q = -1 * p[1:end-1]
    p[2:end] -= q
    return diagm(0=>p, -1=>q, 1=>q)
end

For testing its implementation and later simulating the system we provide already the set of convective heat transfer coefficients on h and heat transfer areas in A. The order of magnitude of areas was kept compatible with the geometrical description provided before.

h = [100.0, 100.0, 10.0] * 1u"W/(m^2*K)"
A = [0.032, 0.500, 1.15] * 1u"m^2"

stiffness(h, A)
3×3 Matrix{Unitful.Quantity{Float64, 𝐋^2 𝐌 𝚯^-1 𝐓^-3, Unitful.FreeUnits{(K^-1, W), 𝐋^2 𝐌 𝚯^-1 𝐓^-3, nothing}}}:
  3.2 W K^-1   -3.2 W K^-1    0.0 W K^-1
 -3.2 W K^-1   53.2 W K^-1  -50.0 W K^-1
  0.0 W K^-1  -50.0 W K^-1   61.5 W K^-1

To conclude problem specification, a function forcing for evaluation of $\mathbf{f}$ is provided below. Vector $\mathbf{f}$ is mainly where the description of radiative heat transfer is represented and is given as:

\[\mathbf{f} = \begin{bmatrix} -\epsilon_{p}\sigma{}A_p(\mathcal{T}_p^4-\mathcal{T}_w^4) \\ 0 \\ h_wA_wT_a + \epsilon_{p}\sigma{}A_p(\mathcal{T}_p^4-\mathcal{T}_w^4) - \epsilon_{w}\sigma{}A_w(\mathcal{T}_w^4-T_a^4) \end{bmatrix}\]

function forcing(T, h, A, Ta, ϵp, ϵw)
    f1 = -ϵp*σ*A[1]*(T[1]^4-T[3]^4)
    f3 = h[3]*A[3]*Ta - f1 - ϵw*σ*A[3]*(T[3]^4 - Ta^4)
    return [ f1; 0u"W"; f3 ]
end

The remaining parameters to close the problem are the emissivity of the materials and external temperature, all provided below.

ϵp = 0.8
ϵw = 0.3
Ta = 313.15u"K"

forcing([Ta, Ta, Ta], h, A, Ta, ϵp, ϵw)
3-element Vector{Unitful.Quantity{Float64, 𝐋^2 𝐌 𝐓^-3, Unitful.FreeUnits{(W,), 𝐋^2 𝐌 𝐓^-3, nothing}}}:
     -0.0 W
      0.0 W
 3601.225 W

With that last element of the base equation in hands we can derive the time-stepping approach to be used. The derivative term is expanded as a forward finite difference as

\[\frac{\mathcal{T}_{n+1}-\mathcal{T}_n}{\tau} + \mathbf{L}^\prime\mathcal{T} =\mathbf{R}^\prime\mathbf{f}\]

If you go back to the inspection of the inverse inertia matrix above, you will observe that the term corresponding to the gas is about 3 orders of magnitude above the others. That means that the dynamics of that component of the system is much slower than the others and the problem is somewhat stiff. To introduce a first layer of robustness in the integration we can try an implicit scheme by evaluating all terms in the above equation at instant $n+1$:

\[\mathcal{T}_{n+1}-\mathcal{T}_n + \tau\mathbf{L}^\prime_{n+1}\mathcal{T}_{n+1} =\tau\mathbf{R}^\prime_{n+1}\mathbf{f}_{n+1}\]

This equation can be rearranged by splitting linear terms in $\mathcal{T}_{n+1}$ on the left-hand side as:

\[\left(\mathbf{I}+\tau\mathbf{L}^\prime_{n+1}\right)\mathcal{T}_{n+1}= \tau\mathbf{R}^\prime_{n+1}\mathbf{f}_{n+1}+\mathcal{T}_n\]

this can be further simplified with the notation:

\[\mathbf{L}_{n+1}\mathcal{T}_{n+1}=\mathbf{R}_{n+1}+\mathcal{T}_n\]

It is useful to simplify thing until last step because it tells us what functionalities we need in a computer implementation. The problem of finding $\mathcal{T}_{n+1}$ can be seem as a nonlinear iteration $\mathcal{T}_{n+1}=L^{-1}_{n+1}(R_{n+1}+\mathcal{T}_{n})$ followed by update of both $\mathbf{L}_{n+1}$ and $\mathbf{R}_{n+1}$ with the new estimate of temperatures. Thus, we need functions to update both these matrices during the solution. Since these rely on several parameters we can encapsulate then in a higher order function with all fixed parameters and return a function that performs the update for the new temperature estimate, as implemented below.

function buildlhsmatrix(τ, h, A)
    K = stiffness(h, A)
    I = diagm([1, 1, 1] * 1u"1")
    return (Tₖ) -> I + τ * inertiainv(Tₖ) * K
end
function buildrhsvector(τ, h, A, Ta, ϵp, ϵw)
    F(T) = τ * forcing(T, h, A, Ta, ϵp, ϵw)
    return (Tₖ) -> inertiainv(Tₖ) * F(Tₖ)
end

As per the previous discussion, the following snipped emulates one time-step iteration:

T = [1273.0u"K", Ta, Ta]
τ = 1.0u"s"

K = buildlhsmatrix(τ, h, A)
B = buildrhsvector(τ, h, A, Ta, ϵp, ϵw)

# Because of factorization this does not works with Unitful.
# Units are stripped and system becomes inconsistent.
# Tnew = K(T) \ (B(T) .+ T)

Tnew = inv(K(T)) * (B(T) .+ T)
3-element Vector{Unitful.Quantity{Float64, 𝚯, Unitful.FreeUnits{(K,), 𝚯, nothing}}}:
 1272.731963441228 K
 348.7875119473756 K
  313.175824937037 K

Function steptime below performs up to a maximum number of iterations as illustrated above and checks for convergence of the time-step.

function steptime(T₀, K, B; maxiter = 50, rtol = 1.0e-12, β = 1.0)
    Tᵢ = copy(T₀)
    Tⱼ = copy(T₀)
    Δ = 9.0e+100

    for j in range(1, maxiter)
        Tⱼ[:] = inv(K(Tⱼ)) * (B(Tⱼ) .+ T₀)
        Tⱼ[:] = relaxation(Tᵢ, Tⱼ, β)
        Δ = residual(Tᵢ, Tⱼ)
        Tᵢ[:] = Tⱼ[:]

        if Δ < rtol
            return Tᵢ, j, Δ
        end
    end

    @warn("No convergence during step @ $(@sprintf("%.6e", Δ))")
    return Tᵢ, maxiter, Δ
end

A relaxation step for eventually helping convergence was introduced above as:

relaxation(Tᵢ, Tⱼ, β) = @. β * Tⱼ + (1-β) * Tᵢ

A common choice of residual in this sort of problem is the maximum relative absolute change:

residual(Tᵢ, Tⱼ) = maximum(abs.(Tⱼ .- Tᵢ) ./ Tᵢ)

Time integration is now just a matter of repeating time-stepping and storing solution:

function integrate(t, T, K, B; maxiter, rtol, β)
    U = copy(T)
    nvars = length(T)
    solution = zeros(length(t), nvars + 2)
    solution[1, 1:nvars] = ustrip(U)

    for (i, tᵢ) in enumerate(t[1:end])
        (U[:], niters, Δ) = steptime(U, K, B; maxiter, rtol, β)
        solution[i, 1:nvars] = ustrip(U)
        solution[i, nvars+1] = niters
        solution[i, nvars+2] = Δ
    end

    return solution
end

This completes the tooling for simulation of the process. Below we simulate 24 hours of the dynamics starting with a hot metallic sphere and the furnace equilibrated with the external environment.

tend = 24*3600

T = [1273.0u"K", Ta, Ta]
t = range(0, tend, convert(Int64, tend / 8))
τ = step(t) * 1u"s"

K = buildlhsmatrix(τ, h, A)
B = buildrhsvector(τ, h, A, Ta, ϵp, ϵw)

maxiter = 80
rtol = 1.0e-12
β = 0.99

@time solution = integrate(t, T, K, B; maxiter, rtol, β);
10800×5 Matrix{Float64}:
 1270.88   366.821  313.389  7.0  1.4458e-13
 1268.78   370.779  313.633  7.0  1.0425e-14
 1266.69   371.159  313.877  6.0  7.68818e-14
 1264.61   371.282  314.119  6.0  1.63616e-14
 1262.54   371.385  314.36   6.0  1.65685e-14
 1260.47   371.487  314.6    6.0  1.65956e-14
 1258.42   371.587  314.838  6.0  1.68034e-14
 1256.38   371.688  315.076  6.0  1.70117e-14
 1254.35   371.787  315.312  6.0  1.72205e-14
 1252.32   371.886  315.546  6.0  1.72483e-14
    ⋮                             
  315.163  314.361  314.31   5.0  2.6874e-14
  315.162  314.361  314.309  5.0  2.6874e-14
  315.161  314.36   314.309  5.0  2.68741e-14
  315.16   314.36   314.308  5.0  2.68742e-14
  315.159  314.359  314.308  5.0  2.66939e-14
  315.158  314.359  314.307  5.0  2.6694e-14
  315.157  314.358  314.307  5.0  2.6694e-14
  315.157  314.358  314.306  5.0  2.68745e-14
  315.156  314.357  314.306  5.0  2.66942e-14

Below we can inspect the solution and the residuals at the end of each time-step. For the solution to make sense it is important that every single step must converge during time advancement. Note the leap in gas temperature during the first time steps resulting from the problem stiffness. As a matter of fact, a smaller time-step should have been used for properly resolving its dynamics in the early stages or even better, an adaptative time-stepping approach should have been adopted.

with_theme() do
    tk = t / 3600

    fig = Figure(size = (700, 700))

    ax1 = Axis(fig[1, 1])
    lines!(ax1, tk, solution[:, 1]; color = :black, label = "Metal")
    hlines!(ax1, ustrip(Ta); color = :blue, label = "Environment")
    axislegend(ax1; position = :rt)

    ax2 = Axis(fig[2, 1])
    lines!(ax2, tk, solution[:, 2]; color = :green, label = "Gas")
    lines!(ax2, tk, solution[:, 3]; color = :red,   label = "Wall")
    hlines!(ax2, ustrip(Ta); color = :blue, label = "Environment")
    axislegend(ax2; position = :rt)

    ax3 = Axis(fig[3, 1])
    lines!(ax3, tk, solution[:, 5]; color = :black)

    ax3.xlabel = "Time [h]"
    ax1.ylabel = "Temperature [K]"
    ax2.ylabel = "Temperature [K]"
    ax3.ylabel = "Residual"

    ax1.xticks = 0:2:tk[end]
    ax2.xticks = 0:2:tk[end]
    ax3.xticks = 0:2:tk[end]

    ax1.yticks = 300:200:1300
    ax2.yticks = 300:20:400

    xlims!(ax1, extrema(ax1.xticks.val))
    xlims!(ax2, extrema(ax2.xticks.val))
    xlims!(ax3, extrema(ax3.xticks.val))

    ylims!(ax1, extrema(ax1.yticks.val))
    ylims!(ax2, extrema(ax2.yticks.val))

    fig
end

Using ModelingToolkit

In the future...