Figure 1: Visualization of the temperature time evolution on a unit square where the prescribed temperature on the upper and lower parts of the boundary increase with time.
In this example we extend the heat equation by a time dependent term, i.e. ∂u∂t−∇⋅(k∇u)=fx∈Ω,
where u is the unknown temperature field, k the heat conductivity, f the heat source and Ω the domain. For simplicity, we hard code f=0.1 and k=10−3. We define homogeneous Dirichlet boundary conditions along the left and right edge of the domain. u(x,t)=0x∈∂Ω1,
Further, we define heterogeneous Dirichlet boundary conditions at the top and bottom edge ∂Ω2. We choose a linearly increasing function a(t) that describes the temperature at this boundary u(x,t)=a(t)x∈∂Ω2.
Now we solve the problem in Ferrite. What follows is a program spliced with comments.
First we load Ferrite, and some other packages we need.
using Ferrite, SparseArrays, WriteVTK
We create the same grid as in the heat equation example.
grid = generate_grid(Quadrilateral, (100, 100));
Again, we define the structs that are responsible for the shape_value
and shape_gradient
evaluation.
ip = Lagrange{RefQuadrilateral, 1}()
qr = QuadratureRule{RefQuadrilateral}(2)
cellvalues = CellValues(qr, ip);
After this, we can define the DofHandler
and distribute the DOFs of the problem.
dh = DofHandler(grid)
add!(dh, :u, ip)
close!(dh);
By means of the DofHandler
we can allocate the needed SparseMatrixCSC
.
M
refers here to the so called mass matrix, which always occurs in time related terms, i.e.
Mij=∫Ωviuj dΩ,
K = allocate_matrix(dh);
M = allocate_matrix(dh);
We also preallocate the right hand side
f = zeros(ndofs(dh));
In order to define the time dependent problem, we need some end time T
and something that describes
the linearly increasing Dirichlet boundary condition on ∂Ω2.
max_temp = 100
Δt = 1
T = 200
t_rise = 100
ch = ConstraintHandler(dh);
Here, we define the boundary condition related to ∂Ω1.
∂Ω₁ = union(getfacetset.((grid,), ["left", "right"])...)
dbc = Dirichlet(:u, ∂Ω₁, (x, t) -> 0)
add!(ch, dbc);
While the next code block corresponds to the linearly increasing temperature description on ∂Ω2
until t=t_rise
, and then keep constant
∂Ω₂ = union(getfacetset.((grid,), ["top", "bottom"])...)
dbc = Dirichlet(:u, ∂Ω₂, (x, t) -> max_temp * clamp(t / t_rise, 0, 1))
add!(ch, dbc)
close!(ch)
update!(ch, 0.0);
As in the heat equation example we define a doassemble!
function that assembles the diffusion parts of the equation:
function doassemble_K!(K::SparseMatrixCSC, f::Vector, cellvalues::CellValues, dh::DofHandler)
n_basefuncs = getnbasefunctions(cellvalues)
Ke = zeros(n_basefuncs, n_basefuncs)
fe = zeros(n_basefuncs)
assembler = start_assemble(K, f)
for cell in CellIterator(dh)
fill!(Ke, 0)
fill!(fe, 0)
reinit!(cellvalues, cell)
for q_point in 1:getnquadpoints(cellvalues)
dΩ = getdetJdV(cellvalues, q_point)
for i in 1:n_basefuncs
v = shape_value(cellvalues, q_point, i)
∇v = shape_gradient(cellvalues, q_point, i)
fe[i] += 0.1 * v * dΩ
for j in 1:n_basefuncs
∇u = shape_gradient(cellvalues, q_point, j)
Ke[i, j] += 1.0e-3 * (∇v ⋅ ∇u) * dΩ
end
end
end
assemble!(assembler, celldofs(cell), Ke, fe)
end
return K, f
end
doassemble_K! (generic function with 1 method)
In addition to the diffusive part, we also need a function that assembles the mass matrix M
.
function doassemble_M!(M::SparseMatrixCSC, cellvalues::CellValues, dh::DofHandler)
n_basefuncs = getnbasefunctions(cellvalues)
Me = zeros(n_basefuncs, n_basefuncs)
assembler = start_assemble(M)
for cell in CellIterator(dh)
fill!(Me, 0)
reinit!(cellvalues, cell)
for q_point in 1:getnquadpoints(cellvalues)
dΩ = getdetJdV(cellvalues, q_point)
for i in 1:n_basefuncs
v = shape_value(cellvalues, q_point, i)
for j in 1:n_basefuncs
u = shape_value(cellvalues, q_point, j)
Me[i, j] += (v * u) * dΩ
end
end
end
assemble!(assembler, celldofs(cell), Me)
end
return M
end
doassemble_M! (generic function with 1 method)
We first assemble all parts in the prior allocated SparseMatrixCSC
.
K, f = doassemble_K!(K, f, cellvalues, dh)
M = doassemble_M!(M, cellvalues, dh)
A = (Δt .* K) + M;
Now, we need to save all boundary condition related values of the unaltered system matrix A
, which is done
by get_rhs_data
. The function returns a RHSData
struct, which contains all needed information to apply
the boundary conditions solely on the right-hand-side vector of the problem.
rhsdata = get_rhs_data(ch, A);
We set the values at initial time step, denoted by uₙ, to a bubble-shape described by (x21−1)(x22−1), such that it is zero at the boundaries and the maximum temperature in the center.
uₙ = zeros(length(f));
apply_analytical!(uₙ, dh, :u, x -> (x[1]^2 - 1) * (x[2]^2 - 1) * max_temp);
Here, we apply once the boundary conditions to the system matrix A
.
apply!(A, ch);
To store the solution, we initialize a paraview collection (.pvd) file,
pvd = paraview_collection("transient-heat")
VTKGridFile("transient-heat-0", dh) do vtk
write_solution(vtk, dh, uₙ)
pvd[0.0] = vtk
end
VTKGridFile for the closed file "transient-heat-0.vtu".
At this point everything is set up and we can finally approach the time loop.
for (step, t) in enumerate(Δt:Δt:T)
#First of all, we need to update the Dirichlet boundary condition values.
update!(ch, t)
#Secondly, we compute the right-hand-side of the problem.
b = Δt .* f .+ M * uₙ
#Then, we can apply the boundary conditions of the current time step.
apply_rhs!(rhsdata, b, ch)
#Finally, we can solve the time step and save the solution afterwards.
u = A \ b
VTKGridFile("transient-heat-$step", dh) do vtk
write_solution(vtk, dh, u)
pvd[t] = vtk
end
#At the end of the time loop, we set the previous solution to the current one and go to the next time step.
uₙ .= u
end
In order to use the .pvd file we need to store it to the disk, which is done by:
vtk_save(pvd);
This notebook was generated using Literate.jl.