Heat Equation
Background knowledge
A. The Heat Equation in 1D
1. The differential equation
The heat equation in one dimension is given by:
where:
is the spatial coordinate, is the time, is the spatially-varying thermal diffusivity of the material, is the temperature distribution function, is a time dependent and spatially varying heat source.
2. Initial and boundary conditions
To solve the heat equation, we must specify initial and boundary conditions: 2. Initial Condition: This refers to the temperature distribution at the initial time
- Boundary Conditions: We can specify different types of boundary conditions, i.e., conditions on the boundaries of our domain,
and .
Dirichlet Boundary Conditions: This boundary condition means that the temperature is specified at the boundaries of the domain.
Neumann Boundary Conditions: This boundary condition means that, instead of the temperature, the derivative of the temperature (which is called the heat flux) is specified at the boundaries of the domain.
3. Applications
A simplified example where the heat equation can be used is to find out how the temperature is distributed through the outside, insulating walls of your apartment. Look at the image below (source). 
The insulation wall is made up of several materials, each with their own thermal diffusivities
B. Solving the Heat Equation
1. Disadvantages of the above formulation
Now, if we want to compute the solution of the heat equation as stated above, we run into two difficulties: 2. Lack of exact solutions: For a general function
- A restrictive set of solutions: For the above form of the heat equation to make sense, we must also assume that the second-derivatives of the solution
should exist, and that the first derivatives of the thermal diffusivity should exist. It turns out that this is too strong of a requirement that it not satisfied by many physical systems.
For example, think of the insulation wall - each material in the insulation wall has its own thermal diffusivity which is completely unrelated to the diffusivities of the other materials. As a result,
2. Tackling the above disadvantages using a discrete & weaker formulation
The above disadvantages are the reason why, in practice, the above strong formulation of the heat equation is not useful. Instead, we formulate a discrete, weak version of the equation which is much more useful in practice. The motivation is: 2. Discrete approximation of unknown exact solutions: Since we don't know the exact solution in general, we try to approximate it. This is the process called discretization. In this process, we fix a finite-dimensional vector space of spatially-varying functions
- Weak version of the equation: Since the original equation imposes too strong requirements on the smoothness of
and , we instead work with an integral formulation where only the first derivatives of should make sense, and where is allowed to be discontinuous.
ASSUMPTION: For the sake of simplifying the discussion, from now on we will assume that we are imposing Dirichlet boundary conditions at
and .
This discrete, weak version of the problem at a fixed time
where:
, .
Note the following important things: 2. The above problem tries to find the solution
consists of spatially-varying functions in that satisfy the boundary conditions. We want the integral equation above to be satisfied for all functions
, where the function space consists of spatially-varying functions in that satisfy homogeneous (or, equivalently, zero) boundary conditions.
The finite element method
Now we are at a stage where, if we make a choice for
A. How to choose ?
1. Choosing
In the finite element method, we choose
Meshing the domain
We choose a set of
ASSUMPTION: In the following code, we will always assume that the mesh is uniform. In other words,
is equal to for all .
Defining
The space
on any element (i.e., on the interval
with ) it is a polynomial of degree , at each
, , the function is smooth for some .
Once we do this, the vector-space dimension of
In other words, there are
for some numbers
EXAMPLE: with .
Consider the space of functions that are linear polynomials over each mesh element, and which are
So, we can find 5 basis functions,
using Mantis
using GLMakie
# The size of the domain where to solve our problem
L1 = 1.0
# The degree of the piecewise-polynomial basis functions
p1 = 1
# The number of elements in the mesh
N1 = 4
# The smoothness of the basis functions (must be smaller than the polynomial degree, and
# larger than -1)
k1 = 0
# The number of basis functions in the piecewise-polynomial function space
n1 = N1 * (p1 + 1) - (k1 + 1) * (N1 - 1)
# Create the mesh and the function space
breakpoints1 = LinRange(0.0, L1, N1+1)
line_geo1 = Geometry.CartesianGeometry((breakpoints1,))
B1 = FunctionSpaces.BSplineSpace(line_geo1, p1, k1)
fig1 = Mantis.Plot.plot_basis(
B1;
label_prefix=L"\phi_",
title=L"\text{Basis functions of }V_n",
xlabel=L"x",
ylabel=L"\phi_i(x)",
)
EXAMPLE: with .
Consider now the space of functions that are quadratic polynomials over each mesh element, and which are
So, we can find 6 basis functions,
p2 = 2
k2 = 1
n2 = N1 * (p2 + 1) - (k2 + 1) * (N1 - 1)
B2 = FunctionSpaces.BSplineSpace(line_geo1, p2, k2)
fig2 = Mantis.Plot.plot_basis(
B2;
label_prefix=L"\phi_",
title=L"\text{Basis functions of }V_n",
xlabel=L"x",
ylabel=L"\phi_i(x)",
)
Note: In both of the above examples, the only functions non-zero at
and are and . This means that, in particular, the functions form a basis for . We will use this fact later on.
Since, at each time instant
In other words, the coefficients of the linear combination are time-dependent. But we can say more! Since
That is, the only unknown coefficients in the above expression are
ASSUMPTION: For simplicity, we assume that
and are constants.
B. Assembling the System of ODEs
Now that we have arrived at an explicit form of our approximate solution to the weak problem, let us see how the discrete weak problem leads to a system of ODEs for the coefficients
where we have arranged the unknown coefficients
Some terminology: in the above system of ODEs,
The idea behind assembly is simple. We substitute the assumed form of our discrete solution
Since we need to satisfy the above equation for all
We can rearrange this equation as:
Then, it is easy to see that this equation represents the ODE system at the beginning of this section by defining:
, , , ,
To assemble the matrices
Before choosing our forcing term, it is relevant to briefly analyse the behavior of our solution. We saw that our weak form of the equation is
Since we have Dirichlet boundary conditions (i.e., we enforce the value of the temperature on both sides of our interval), if we prescribe a stationary heat source, the solution will evolve to a stationary state. This stationary state,
or in matrix form
Additionally, if
if the heat source is
We choose a finite element space with
# The size of the domain where to solve our problem
L = 1.0
# The number of elements in the mesh
num_elements = 10
line_geometry = Geometry.create_cartesian_box((0.0,), (L,), (num_elements,))
# The degree of the piecewise-polynomial basis functions
polynomial_degree = 2
# The smoothness of the basis functions (-1 <= smoothness <= p-1)
smoothness = 1
# The piecewise-polynomial function space
V = FunctionSpaces.BSplineSpace(line_geometry, polynomial_degree, smoothness)
# The number of basis functions in the piecewise-polynomial function space
num_basis_functions = FunctionSpaces.get_num_basis(V)
# Use this function space as a differential form
V⁰ = Forms.FormSpace(0, V, L"V^0")
# Thermal diffusivity
const alpha = 1.0
# Analytical solution
u_analytical_expression(x) = [1.0 .+ 0.5*cos.((2.0*π/L)*x[:, 1])]
u_analytical = Mantis.Forms.AnalyticalFormField(
0, u_analytical_expression, line_geometry, L"u_{\text{exact}}"
)
# Right hand side
f_expression(x) = [@. alpha*0.5*(4.0*(π^2)/(L^2))*cos(2.0*π*x[:, 1]/L)]
f = Mantis.Forms.AnalyticalFormField(0, f_expression, line_geometry, "f")
function assemble_system_matrices(
V::Forms.FormSpace,
f::Forms.AnalyticalFormField,
alpha::Float64,
dΩ::Quadrature.AbstractQuadratureRule,
)
# assemble L2 inner-product matrix
weak_form_inputs = Assemblers.WeakFormInputs(V, f)
lhs_expressions, rhs_expressions = Assemblers.L2_projection(weak_form_inputs, dΩ)
weak_form = Assemblers.WeakForm(lhs_expressions, rhs_expressions, weak_form_inputs)
M, _ = Assemblers.assemble(weak_form)
# assemble H1 inner-product matrix
weak_form_inputs = Assemblers.WeakFormInputs(V, f)
lhs_expressions, rhs_expressions = Assemblers.zero_form_hodge_laplacian(
weak_form_inputs, dΩ
)
weak_form = Assemblers.WeakForm(lhs_expressions, rhs_expressions, weak_form_inputs)
# bc = Forms.set_dirichlet_boundary_conditions(V, 0.0)
K, f = Assemblers.assemble(weak_form)
return M, alpha .* K, f
end
# Define the quadrature
quadrature_degree = polynomial_degree + 2
∫ = Quadrature.gauss_legendre(quadrature_degree)
dΩ = Quadrature.StandardQuadrature(∫, num_elements)
# Assemble the matrices
M, K, F = assemble_system_matrices(V⁰, f, alpha, dΩ)
# Remove the unecessary parts of the matrices
F = Vector(F[2:(end - 1)])
K_0 = Vector(K[2:(end - 1), 1])
K_L = Vector(K[2:(end - 1), end])
M = M[2:(end - 1), 2:(end - 1)]
K = K[2:(end - 1), 2:(end - 1)]
# Boundary conditions
u_0 = 1.5
u_L = 1.5
# Solution field
u_h = Mantis.Forms.FormField(V⁰, zeros(Forms.get_num_basis(V⁰)), "u_h")
# With boundary values set
u_h.coefficients[1] = u_0
u_h.coefficients[end] = u_L
# Solve for the unknown coefficients
u_h.coefficients[2:(end - 1)] = K \ (F - u_0*K_0 - u_L*K_L)
# Plot the error with respect to the analytical solution
error = u_analytical - u_h
# Compute the error norm
error_nom = Analysis.L2_norm(error, dΩ)0.0005600033681980221Time Integration
We will now consider a specific time integrator to evolve our solution in time: the midpoint rule.
The midpoint rule is the lowest order Gauss integrator and, for linear systems (as is our case), is an explicit integrator. Additionally, it can be interpreted as a Runge-Kutta method (i.e., it has an associated Butcher tableau). Given a first order ODE in the time interval
with initial condition
where
The spatial discretisation process has left us with the following system of ODEs:
We can integrate this in time using the pre-implemented midpoint rule.
Explicit ODE with a matrix solve
While the integrator is considered explicit, this only holds for simple ODEs. In our case, we have a coupled system of ODEs, so that we do have to use a matrix solver.
# Time step size and final time.
const dt = 0.001
const num_time_steps = 501
# Here, we encode the ODE information for the time integrator. We are using LinearSolve's
# ability to cache the factorised M matrix. Since this matrix does not change per
# timestep, we can reuse the factorisation. This is more efficient than refactorising every
# step.
import LinearSolve as LS
const prob = LS.LinearProblem(M, zeros(Forms.get_num_basis(V⁰)-2))
const linsolve = LS.init(prob)
function heat_equation_solver(output, C::Vector{Float64}, t::Float64)
linsolve.b = (-K * C + F - u_0 * K_0 - u_L * K_L)
output .= LS.solve!(linsolve)
return output
end
const heat_equation = TimeIntegrators.define_explicit_ode(heat_equation_solver)
scheme = TimeIntegrators.EXPLICIT_MIDPOINT
# We also create a helper function to easily evaluate our solution.
function evaluate_solution(u_h)
geometry = Forms.get_geometry(u_h)
num_elements = Geometry.get_num_elements(geometry)
xi = Points.CartesianPoints((LinRange(0.0, 1.0, 25),))
all_x = Vector{Float64}(undef, 25 * num_elements)
all_values = Vector{Float64}(undef, 25 * num_elements)
for element_id in 1:num_elements
form_eval, _ = Forms.evaluate(u_h, element_id, xi)
x = Geometry.evaluate(geometry, element_id, xi)
all_x[((element_id - 1) * 25 + 1):((element_id) * 25)] = x[:]
all_values[((element_id - 1) * 25 + 1):((element_id) * 25)] = form_eval[1]
end
return all_x, all_values
end
# We project the initial condition onto our form space
u_initial_expression(x) = [@. 1.5 + sin((2.0*π/L)*x[:, 1])]
u_initial = Forms.AnalyticalFormField(0, u_initial_expression, line_geometry, "u")
u_hi = Assemblers.solve_L2_projection(V⁰, u_initial, dΩ)
# Enforce the boundary condition on the initial condition, just in case the initial
# condition does not satisfy the boundary conditions already
u_hi.coefficients[1] = u_0
u_hi.coefficients[end] = u_L
# Initialise the time scheme
const u_h_n = TimeIntegrators.initialise_scheme(u_hi.coefficients[2:(end - 1)], scheme)Mantis.TimeIntegrators.TimeIntegrationSolution{Mantis.TimeIntegrators.Explicit{2, 1, Float64, 4, 2, 1}, Nothing, Float64, Nothing}(10, [1.825836023728004; 2.3496538725133327; … ; 0.6503461274866671; 1.1741639762719966;;], Mantis.TimeIntegrators.Explicit{2, 1, Float64, 4, 2, 1}([0.0 0.0; 0.5 0.0], [0.0 1.0], [1.0; 1.0;;], [1.0;;], [0.0, 0.5], Mantis.TimeIntegrators.TimeLevels([0], Int64[], Int64[]), 2), nothing, 0, [6.9290475962142e-310; 6.929085453905e-310; … ; 6.9290459367165e-310; 6.92904593671806e-310;;], [0.0 0.0; 0.0 0.0; … ; 0.0 0.0; 0.0 0.0], [0.0 0.0; 0.0 0.0; … ; 0.0 0.0; 0.0 0.0], nothing, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])Now we are set to march our equation in time. We will also create a video, which is why we set up the time Observable. The all_y variable is a lift, which is Makie's way of expressing a dependency. That is, as soon as we update time, Makie will automatically update all other variables in the plot that depend on time. In our case, this is the all_y variable, which calls TimeIntegrators.time_integrate! to advance our solution.
# We use Printf to print the time in our animation.
using Printf
time = Observable(dt)
all_y = lift(time) do t
TimeIntegrators.time_integrate!(u_h_n, heat_equation, t, dt)
u_hi.coefficients[2:(end - 1)] = TimeIntegrators.get_solution(u_h_n)
all_x, all_values = evaluate_solution(u_hi)
return all_values
end
all_x, all_values = evaluate_solution(u_hi)
fig = lines(
all_x,
all_y;
color=:blue,
axis=(
title=@lift("t = $(@sprintf("%0.2f", round($time, digits = 2)))"),
limits=(0.0, 1.0, 0.0, 3.0),
),
)
xe, ye = evaluate_solution(u_analytical)
lines!(xe, ye; color=:black, label="exact")
record(
fig,
"heat_equation_1d.mp4",
LinRange(dt, dt*num_time_steps, num_time_steps);
framerate=30,
) do t
return time[] = t
end"heat_equation_1d.mp4"This page was generated using Literate.jl.