7.3. Melting of an ice cylinder with natural convection

While is has been a lot of work to develop the rather simple example of an propagating ice front, we can now harvest the fruits of our labor by re-using these equations in higher dimensions and different geometries and add even more equations to the system.

As an example system, we consider the melting of an ice cylinder in a cylindrical bath of water. This has been investigated in Ref. [46], resulting in intriguing scalloped ice shapes due to a Kelvin-Helmholtz instability. We hence transfer the previous system into an axisymmetric variant, with an ice cylinder in the center and a liquid domain outside. In the liquid, also buoyancy driven flow will be relevant, where the density anormaly of water is important.

First of all, a mesh is required. We are lazy guys here: Although the geometry would allow to build the elements by hand, we just use the GmshTemplate to construct a mesh via gmsh (cf. Section 4.3.4). :

from temperature_conduction_propagation import * # Get some equations from the previous example
from pyoomph.equations.navier_stokes import * # We also need a Navier-Stokes equation

# We are lazy and use the Gmsh approach here instead of adding the elements by hand
class TwoDomainMeshAxi(GmshTemplate):
    def __init__(self,R1,R2,H,resolution):
        super(TwoDomainMeshAxi, self).__init__()
        self.R1,self.R2,self.H=R1,R2,H
        self.default_resolution=resolution

    def define_geometry(self):
        p00, p0H=self.point(0,0), self.point(0,self.H)
        pR10, pR1H=self.point(self.R1,0), self.point(self.R1,self.H)
        pR20, pR2H = self.point(self.R2, 0), self.point(self.R2, self.H)
        self.create_lines(pR10,"ice_bottom",p00,"axisymm",p0H,"ice_top",pR1H,"interface",pR10,"liquid_bottom",pR20,"liquid_side",pR2H,"liquid_top",pR1H)
        self.plane_surface("ice_bottom","axisymm","interface","ice_top",name="ice")
        self.plane_surface("liquid_bottom", "liquid_side", "interface", "liquid_top", name="liquid")

If you have read Section 4.3.4, nothing spectacular is happening here.

Next, we do not only solve thermal conduction, but also thermal convection in the liquid domain. We therefore must add the term \(\vec{u}\cdot\nabla T\) to our previously developed ThermalConductionEquation. This is easiest by inheriting from the latter class:

# Augment the conduction equation by advection for the liquid phase
class ThermalAdvectionConductionEquation(ThermalConductionEquation):
    def __init__(self,k,rho,c_p,wind=var("velocity")):
        super(ThermalAdvectionConductionEquation, self).__init__(k=k,rho=rho,c_p=c_p)
        self.wind=wind

    def define_residuals(self):
        super(ThermalAdvectionConductionEquation, self).define_residuals() # define the conduction equation
        T,Ttest=var_and_test("T")
        self.add_residual(weak(self.rho*self.c_p*dot(self.wind,grad(T)),Ttest)) # Just add the advection term

Next, the problem will be defined. Although the problem is entire different from the previous one, let us re-use it to copy the physical parameters as e.g. the conductivities and the latent heat:

# We inherit from the IceFrontProblem to take over the physical parameters. The problem will be quite different:
class IceConvectionProblem(IceFrontProblem):
    def __init__(self):
        super(IceConvectionProblem, self).__init__() # this will set all properties from the parent class
        self.L=4*centi*meter # L is now the cylinder height
        self.R1=1*centi*meter # radius of the ice cylinder
        self.R2=3*centi*meter # outer radius of the liquid cylinder

        self.T_ice=-1*celsius # initial ice cylinder temperature
        self.T_liq=6*celsius

        self.mu_liq=1*milli*pascal*second
        # Water density for buoyancy calculations
        Trel = (var("T") - self.T_eq) / kelvin # bind the relative temperature (measured in Kelvin)
        # Fit for the density anomaly
        self.rho_grav = (0.999849 + 5.77393e-05 * Trel - 7.18258e-06 * Trel ** 2) * gram / (centi * meter )** 3
        self.gravity=9.81*meter/second**2 * vector(0,-1) # gravity direction and strength

        self.resolution=0.05 # mesh resolution

Besides copying the parameters from the previous problem, where L is now used for the height of the cylinder, we need two radii, the viscosity of water and a fit for the density anomaly as function of the temperature. Therefore, we normalize the actual relative temperature \(T-T_\text{eq}\) by the unit \(\:\mathrm{K}\) and plug this into a fit for the liquid water mass density between \(0\:\mathrm{^\circ C}\) and \(20\:\mathrm{^\circ C}\).

The define_problem() method starts again by adding the mesh, but this time we have a two-dimensional mesh and switch to an "axisymmetric" coordinate system. The scales are set as in the previous problem, but we require additional scales for the "velocity" and "pressure" fields:

def define_problem(self):
    # Two-dimensional mesh
    self.add_mesh(TwoDomainMeshAxi(self.R1,self.R2,self.L,self.resolution))
    self.set_coordinate_system("axisymmetric") # axisymmetric coordinate system

    # Similar to the previous problem, scales for nondimensionalization
    self.set_scaling(spatial=self.R1,temporal=1*second)
    self.set_scaling(T=kelvin)
    self.set_scaling(thermal_equation=scale_factor("T") * self.k_ice / scale_factor("spatial") ** 2)
    self.set_scaling(velocity=scale_factor("spatial")/scale_factor("temporal"))
    self.set_scaling(pressure=self.mu_liq*scale_factor("velocity")/scale_factor("spatial"))

Next, the ice equations are assembled. It is essentially the same, except that we must add a few extra DirichletBC terms to fix the mesh at the top and bottom boundary. Furthermore, there is no DirichletBC for the temperature here, except the equilibrium temperature at the "interface". The ice cylinder will just warm up to \(0\:\mathrm{^\circ C}\) over the course of the simulation:

self.add_mesh(TwoDomainMeshAxi(self.R1,self.R2,self.L,self.resolution))
self.set_coordinate_system("axisymmetric") # axisymmetric coordinate system

# Similar to the previous problem, scales for nondimensionalization
self.set_scaling(spatial=self.R1,temporal=1*second)
self.set_scaling(T=kelvin)
self.set_scaling(thermal_equation=scale_factor("T") * self.k_ice / scale_factor("spatial") ** 2)
self.set_scaling(velocity=scale_factor("spatial")/scale_factor("temporal"))
self.set_scaling(pressure=self.mu_liq*scale_factor("velocity")/scale_factor("spatial"))

# Equations for the ice domain
ice_eqs=MeshFileOutput() # Output
ice_eqs+=ThermalConductionEquation(self.k_ice,self.rho_ice,self.cp_ice) # thermal conduction
ice_eqs +=InitialCondition(T=self.T_ice) # initially at ice temperature
ice_eqs+=PseudoElasticMesh() # Mesh motion
ice_eqs+=DirichletBC(mesh_x=0)@"axisymm" # fix mesh at axis of symmetry
ice_eqs += DirichletBC(mesh_y=0) @ "ice_bottom" # and at the bottom
ice_eqs += DirichletBC(mesh_y=self.L) @ "ice_top" # and the top
ice_eqs += DirichletBC(T=self.T_eq)@"interface" # melting temperature at interface

The liquid equations are analogous, except that we use our new class ThermalAdvectionConductionEquation for the convection term and also add a NavierStokesEquations for the flow, together with no-slip boundary conditions at all interfaces. In reality, the density difference between ice and liquid would give rise to a non-zero normal velocity, when ice melts or solidifies, but this is not considered here, since this contribution is tiny. One could enforce this velocity jump via a Lagrange multiplier, but then, we also would allow for outflow somewhere in the domain to compensate for the gained/lost volume, i.e. to be able to satisfy the continuity equation. Since we do not allow any outflow, also one pressure degree of freedom must be fixed to remove the nullspace of the pressure (cf. Section 4.4.4):

# Equations for the liquid domain
liq_eqs=MeshFileOutput() # output
liq_eqs+=ThermalAdvectionConductionEquation(self.k_liq,self.rho_liq,self.cp_liq) # thermal conduction + advection
# Navier-Stokes including Boussinesq-like gravity term
liq_eqs+=NavierStokesEquations(mass_density=self.rho_liq,dynamic_viscosity=self.mu_liq,bulkforce=self.gravity*self.rho_grav)
liq_eqs+=InitialCondition(T=self.T_liq) # Liquid temperature as initial condition
liq_eqs+=PseudoElasticMesh() # Mesh motion
liq_eqs+=DirichletBC(mesh_y=0,velocity_x=0,velocity_y=0)@"liquid_bottom" # no-slip and fixed mesh at all boundaries
liq_eqs += DirichletBC(mesh_y=self.L,velocity_x=0,velocity_y=0) @ "liquid_top"
liq_eqs += DirichletBC(mesh_x=self.R2,T=self.T_liq,velocity_x=0,velocity_y=0)@"liquid_side"
liq_eqs+=DirichletBC(T=self.T_eq,velocity_x=0,velocity_y=0)@"interface" # here the mesh is not fixed, but the temperature is
liq_eqs += DirichletBC(pressure=0) @ "liquid_top/liquid_side" # For pure DirichletBCs, we must fix one pressure degree
Melting of an ice cylinder with natural convection

Fig. 7.3 Dynamics of an ice cylinder melting in a liquid bath along with natural convection due to the density anomaly.

Optionally, we can simplify the problem: Since the front will mainly move in radial direction, we can remove all degrees of freedom associated with the \(y\)-coordinate of the mesh:

# Since we know that the mesh mainly moves in y-direction, we can speed up the calculation by removing the motion in y-direction
ice_eqs += DirichletBC(mesh_y=True)
liq_eqs += DirichletBC(mesh_y=True)

Thereby, we have less degrees of freedom in our system and the computation will speed up.

Finally, the interface equations are added as in the previous example and all equations are added to the problem:

# Interface: Connect the mesh position and impose the front motion
interf_eqs=ConnectMeshAtInterface()
interf_eqs+=IceFrontSpeed(self.latent_heat)

# Add it to the ice side of the interface
ice_eqs+=interf_eqs@"interface"

# and add all equations to the problem
self.add_equations(ice_eqs@"ice"+liq_eqs@"liquid")

The code for execution is trivial:

if __name__=="__main__":
    with IceConvectionProblem() as problem:
        problem.run(400*second,outstep=True,startstep=1*second,maxstep=20*second,temporal_error=1)

The corresponding results are shown in Fig. 7.3. Obviously, the interface indeed does not recede in a straight manner, but is deformed due to the natural convection. Based on the results, one can obviously simplify the problem by neglecting the ice phase, since it becomes isothermal at \(T_\text{eq}=0\) very quickly. This would involve the modification of the IceFrontSpeed, but since this chapter is on multi-domain problems, it will not be addressed here.