In this demo, we expand on the stationnary nonlinear heat transfer demo and consider a transient heat equation with non-linear heat transfer law including solid/liquid phase change. This demo corresponds to the TTNL02 elementary test case of the code_aster
finite-element software.
Source files:
- Jupyter notebook: mgis_fenics_heat_equation_phase_change.ipynb
- Python file: mgis_fenics_heat_equation_phase_change.py
- MFront behaviour file: HeatTransferPhaseChange.mfront
The transient heat equation writes:
\[ \rho C_p \dfrac{\partial T}{\partial t} = r-\operatorname{div}\mathbf{j} \]
where \(\rho\) is the material density, \(C_p\) the heat capacity (at constant pressure) per unit of mass, \(r\) represents heat sources and \(\mathbf{j}\) is the heat flux.
In the case of phase changes, the heat capacity exhibits large discontinuities near the transition temperature. It is therefore more suitable to work with the enthalpy density defined as:
\[ h(T) = \int_{T_0}^{T} \rho C_p dT \]
yielding the following heat equation: \[ \dfrac{\partial h}{\partial t} = r-\operatorname{div}\mathbf{j} \]
The thermal material is described by the following non linear Fourier Law:
\[ \mathbf{j}=-k\left(T\right)\,\mathbf{\nabla} T \]
where the thermal conductivity \(k\) is initially assumed to be given by:
\[ k\left(T\right)=\begin{cases} k_s & \text{if }T < T_m \\ k_l & \text{if }T > T_m \end{cases} \] where \(k_s\) (resp. \(k_l\)) denotes the solid (resp. liquid) phase conductivity and \(T_m\) is the solid/liquid transition temperature.
The enthalpy is assumed to be given by:
\[ h\left(T\right)=\begin{cases} c_sT & \text{if }T < T_m \\ c_l(T-T_m)+c_sT_m+\Delta h_{s/l} & \text{if }T > T_m \end{cases} \]
where \(c_s=\rho_sC_{p,s}\) (resp. \(c_l=\rho_lC_{p,l}\)) is the volumic heat capacity of the solid (resp. liquid) phase. It can be observed that the enthalpy exhibits a discontinuity at the phase transition equal to \(\Delta h_{s/l}\) which represents the latent heat of fusion per unit volume.
The enthalpy discontinuity \(\Delta h_{s/l}\) poses convergence difficulties for the Newton resolution. A classical remedy consists in considering a smoothed version of the previous law, such as:
\[ k\left(T\right)=\begin{cases} k_s & \text{if }T < T_s \\ k_s + (k_l-k_s)\dfrac{T-T_s}{T_\text{smooth}} & \text{if } T_s \leq T \leq T_l\\ k_l & \text{if }T > T_l \end{cases} \] and \[ h\left(T\right)=\begin{cases} c_sT & \text{if }T < T_s \\ c_sT_s+\left(\dfrac{cs+cl}{2}+\dfrac{\Delta h_{s/l}}{T_\text{smooth}}\right)(T-T_s) & \text{if } T_s \leq T \leq T_l \\ c_l(T-T_l)+c_sT_s+\dfrac{cs+cl}{2}T_\text{smooth}+\Delta h_{s/l} & \text{if }T > T_l \end{cases} \] where \(T_{smooth}=T_l-T_s\) is a small transition temperature interval between \(T_s=T_m-T_\text{smooth}/2\) the solidus temperature and \(T_l=T_m+T_\text{smooth}/2\) the liquidus temperature.
MFront
implementationSimilarly to the stationnary nonlinear heat transfer demo, the MFront
implementation relies on the DefaultGenericBehaviour
DSL and declares the pair of temperature gradient and heat flux. In addition, the volumic enthalpy \(h\) is also declared as an internal state variable. In addition to the two tangent operator blocks ∂j∕∂Δ∇T
and ∂j∕∂ΔT
already discussed in the first demo, we also declare the additional block ∂h∕∂ΔT
, referring to the fact that the enthalpy will vary with the temperature and will enter the transient heat equation.
@DSL DefaultGenericBehaviour;
@Behaviour HeatTransferPhaseChange;
@Author Thomas Helfer / Jérémy Bleyer;
@Date 15 / 02 / 2019;
@Gradient TemperatureGradient ∇T;
"TemperatureGradient");
∇T.setGlossaryName(
@Flux HeatFlux j;
"HeatFlux");
j.setGlossaryName(
@StateVariable real h;
"Enthalpy"); //per unit of volume
h.setEntryName(
@AdditionalTangentOperatorBlock ∂j∕∂ΔT;
@AdditionalTangentOperatorBlock ∂h∕∂ΔT;
We now declare the various material properties corresponding to those of aluminium. The material parameters are assumed to be uniform for both phases. Finally, we also introduce the smoothing temperature width \(T_\text{smooth}\).
@Parameter Tₘ = 933.15; // [K]
"MeltingTemperature");
Tₘ.setEntryName(@Parameter kₛ = 210; // [W/m/K]
"SolidConductivity");
kₛ.setEntryName(@Parameter cₛ = 3.e6; // [J/m^3/K]
"SolidHeatCapacity");
cₛ.setEntryName(@Parameter kₗ = 95; // [W/m/K]
"LiquidConductivity");
kₗ.setEntryName(@Parameter cₗ = 2.58e6; // [J/m^3/K]
"LiquidHeatCapacity");
cₗ.setEntryName(@Parameter Δhₛₗ = 1.08048e9; // [J/m^3]
"FusionEnthalpy");
Δhₛₗ.setEntryName(@Parameter Tₛₘₒₒₜₕ = 0.1; // smoothing temperature width [K]
"Tsmooth"); Tₛₘₒₒₜₕ.setEntryName(
We define some local variables corresponding to the values of the conductivity \(k\), the volumic heat capacity \(c\) and the derivative of the heat conductivity with respect to the temperature.
@LocalVariable thermalconductivity k;
@LocalVariable real c;
@LocalVariable real ∂k∕∂T;
Again, the behaviour integration is straightforward: after computing the temperature at the end of the time step T_
, we compute the thermal conductivity, its derivative with respect to the temperature, the volumic enthalpy and the volumic heat capacity depending on whether T_
belongs to the solid state (\(T\leq T_s\)), the liquid state (\(T\geq T_l\)) or to the transition region (\(T_s \leq T \leq T_l\)). We finish by computing the heat flux.
@Integrator {
const auto T_ = T + ΔT; // current temperature
const auto Tₛ = Tₘ-Tₛₘₒₒₜₕ/2; // solidus temperature
const auto Tₗ = Tₘ+Tₛₘₒₒₜₕ/2; // liquidus temperature
if(T_<Tₛ){ // solid state
k = kₛ;
c = cₛ;
h = cₛ*T_;0.;
∂k∕∂T = else if (T_ > Tₗ) { // liquid state
}
k = kₗ;
c = cₗ;2;
h = cₗ*(T_-Tₗ)+Δhₛₗ+cₛ*Tₛ+(cₛ+cₗ)*Tₛₘₒₒₜₕ/0.;
∂k∕∂T = else { // solid/liquid smooth transition
}
k = kₛ + (kₗ-kₛ)*(T_-Tₛ)/Tₛₘₒₒₜₕ;2+Δhₛₗ/Tₛₘₒₒₜₕ)*(T_-Tₛ);
h = cₛ*Tₛ+((cₛ+cₗ)/
c = Δhₛₗ/(Tₗ-Tₛ);
∂k∕∂T = -(kₗ-kₛ)/Tₛₘₒₒₜₕ;
}// heat flux
j = -k ⋅ (∇T + Δ∇T);// end of @Integrator }
The computation of the tangent operator blocks is then straightforward:
@TangentOperator {
∂j∕∂Δ∇T = -k * tmatrix<N, N, real>::Id();
∂j∕∂ΔT = ∂k∕∂T * (∇T + Δ∇T);
∂h∕∂ΔT = c;// end of @TangentOperator }
We consider a rectanglar domain of length 0.1 with imposed temperatures T0
(resp. Ti
) on the left (resp. right) boundaries. We look here for the temperature field T
using a \(P^2\)-interpolation which is initially at the uniform temperature Ti
.
%matplotlib notebook
import matplotlib.pyplot as plt
from dolfin import *
import mgis.fenics as mf
import numpy as np
= 0.1
length = 0.01
width = 1000
Nx = 5
Ny = RectangleMesh(Point(0., 0.), Point(length, width), Nx, Ny, "crossed")
mesh = np.linspace(0, length, Nx)
x
= FunctionSpace(mesh, "CG", 2)
V = Function(V, name="Temperature")
T
def left(x, on_boundary):
return near(x[0], 0) and on_boundary
def right(x, on_boundary):
return near(x[0], length) and on_boundary
= Constant(853.15)
T0 = Constant(1013.15)
Ti
T.interpolate(Ti)
= [DirichletBC(V, T0, left),
bc DirichletBC(V, Ti, right)]
We now load the material behaviour HeatTransferPhaseChange
and also change the default value of Tsmooth
to a slightly larger one (but still sufficiently small). Note that the mesh must be sufficiently refined to use a smaller value. Indeed, the spatial resolution must be able to capture with a few elements the sharp transition which will occur during the phase change. We also verify that 3 different tangent blocks have indeed been defined, the last one involving the internal state variable Enthalpy
with respect to the temperature.
= mf.MFrontNonlinearMaterial("./src/libBehaviour.so",
material "HeatTransferPhaseChange",
="plane_strain",
hypothesis={"Tsmooth": 1.})
parametersprint(["d{}_d{}".format(*t) for t in material.get_tangent_block_names()])
['dHeatFlux_dTemperatureGradient', 'dHeatFlux_dTemperature', 'dEnthalpy_dTemperature']
The heat equation must also be discretized in time. We use here the \(\theta\)-method and approximate:
\[ \left.\dfrac{\partial h}{\partial t}\right|_{t=t_{n+\theta}} \approx \dfrac{h_{t=t_{n+1}}-h_{t=t_{n}}}{\Delta t} = r_{t=t_{n+\theta}}-\operatorname{div}\mathbf{j}_{t=t_{n+\theta}} \]
where \(\star_{t=t_{n+\theta}}= \theta\star_{t=t_{n+1}}+(1-\theta)\star_{t=t_{n}}\).
The weak formulation therefore reads (in the absence of source terms):
Find \(T\in V\) such that: \[ \int_\Omega \left((h_{t=t_{n+1}}(T)-h_{t=t_{n}})\widehat{T} - \Delta t (\theta\mathbf{j}_{t=t_{n+1}}(T, \nabla T)+(1-\theta)\mathbf{j}_{t=t_{n}})\cdot \nabla \widehat{T} \right)\text{ dx} = 0 \]
in which, at time \(t_{n+1}\), both the enthalpy \(h_{t=t_{n+1}}\) and the heat flux \(\mathbf{j}_{t=t_{n+1}}\) are non-linear functions of the unknown temperature.
We therefore see that the above non-linear problem does not fit into the default form of a MFrontNonlinearProblem
residual. We will therefore have to specify its form manually. To do so, we need to get the functions h
and j
associated to the current values of the enthalpy and the heat flux.
Second, we must call the initialize
method which initializes the functions associated with gradients, fluxes, external and internal state variables objects and the corresponding tangent blocks. All gradients and external state variables must have been registered before calling this method. In this case, we rely on the automatic registration of the temperature and its gradient.
Finally, to implement the \(\theta\) time discretization scheme, we will also need to keep track of the enthalpy and heat flux values at the previous time step. We can simply define these new functions as deep copies of h
and j
. Doing so, h_old
and j_old
will also be Quadrature functions.
= mf.MFrontNonlinearProblem(T, material, quadrature_degree=2, bcs=bc)
problem = problem.get_state_variable("Enthalpy")
h = problem.get_flux("HeatFlux")
j
problem.initialize()
= j.copy(deepcopy=True)
j_old = h.copy(deepcopy=True) h_old
Automatic registration of 'TemperatureGradient' as grad(Temperature).
Automatic registration of 'Temperature' as an external state variable.
We are now ready to define the expression of the above residual. Note that we must use the integration measure dx
associated with the MFrontNonlinearProblem
containing the correct quadrature degree matching that of the various Quadrature functions. Finally, the tangent form can be automatically computed using the compute_tangent_form
method from the residual expression and the structure of the different tangent blocks.
= Constant(0.)
dt = Constant(1.)
theta = TestFunction(V)
T_ = theta*j + (1-theta)*j_old
j_theta = (T_*(h - h_old)-dt*dot(grad(T_), j_theta))*problem.dx
problem.residual problem.compute_tangent_form()
code_aster
resultsWe now implement the time-stepping loop which simply solves the non-linear problem and update the fields corresponding to the values at the previous time step. We also load the values of the one-dimensional temperature field \(T(x, t)\) given in the code_aster
test-case and compare them with what we obtain every second.
= np.loadtxt("results_code_Aster.csv", delimiter=",")
cA_results = np.arange(1, 7)
code_Aster_times = 60
Nsteps = np.linspace(0, 6., Nsteps+1)
times for (t, delta_t) in zip(times[1:], np.diff(times)):
dt.assign(Constant(delta_t))
problem.solve(T.vector())
# update enthalpy
h_old.assign(h) # update heat flux
j_old.assign(j)
= np.isclose(t, code_Aster_times)
sol_time if any(sol_time):
plt.figure()"Time {:0.1f}s".format(t), fontsize=16)
plt.title(= plt.gca()
ax1 '$x$ coordinate')
ax1.set_xlabel('Temperature [°C]')
ax1.set_ylabel(/2)-273.15 for xi in x]), "-b", label="FEniCS/MFront")
ax1.plot(x, np.array([T(xi, width0], cA_results[:, np.where(sol_time)[0]+1], "or", label="Code\_Aster")
ax1.plot(cA_results[:, = material.get_parameter("MeltingTemperature") - 273.15
Tm + 0*x, "--k")
ax1.plot(x, Tm "liquid\nsolid", xy=(0.08, Tm), fontsize=16, va="center")
ax1.annotate(
plt.legend() plt.show()
<IPython.core.display.Javascript object>
<IPython.core.display.Javascript object>
<IPython.core.display.Javascript object>
<IPython.core.display.Javascript object>
<IPython.core.display.Javascript object>
<IPython.core.display.Javascript object>