Demo - Rayleigh Benard#
Mikael Mortensen (email: mikaem@math.uio.no
), Department of Mathematics, University of Oslo.
Date: November 21, 2019
Summary. Rayleigh-Benard convection arise due to temperature gradients in a fluid. The governing equations are Navier-Stokes coupled (through buoyancy) with an additional temperature equation derived from the first law of thermodynamics, using a linear correlation between density and temperature.
This is a demonstration of how the Python module shenfun can be used to solve for these Rayleigh-Benard cells in a 2D channel with two walls of different temperature in one direction, and periodicity in the other direction. The solver described runs with MPI without any further considerations required from the user. Note that there is also a more physically realistic 3D solver. The solver described in this demo has been implemented in a class in the RayleighBenard2D.py module in the demo folder of shenfun. Below is an example solution, which has been run at a very high Rayleigh number (Ra).
Figure 1: Temperature fluctuations in the Rayleigh Benard flow. The top and bottom walls are kept at different temperatures and this sets up the Rayleigh-Benard convection. The simulation is run at Ra =1,000,000, Pr =0.7 with 256 and 512 quadrature points in x and y-directions, respectively.
The Rayleigh Bénard equations#
The governing equations solved in domain
where
The equations are complemented with boundary conditions
The governing equations contain a non-trivial coupling between velocity, pressure and temperature.
This coupling can be simplified by eliminating the pressure from the equation for the wall-normal velocity
component
This equation is solved with
However, we note that Eqs. (5) and (7) and (8) do not depend on pressure, and,
apparently, on each time step we can solve (5) for
Here
We can now choose basis functions for the spaces, using Shen’s composite bases for either Legendre or
Chebyshev polynomials. For the Fourier space the basis functions are already given. We leave the actual choice
of basis as an implementation option for later. For now we use
To get the required approximation spaces for the entire domain we use tensor products of the one-dimensional spaces in (9)-(13)
Space
where
We can now take a look at why Eq. (6) is needed. If we first solve (5) for
Here we are using the inner product notation
where the exact form of the
weighted scalar product depends on the chosen basis; Legendre has
Inserting now for the known
The
Now perform some exact manipulations in the Fourier direction and introduce the
1D inner product notation for the
By also simplifying the notation using summation of repeated indices, we get the following equation
Now
We see that this equation can be solved for
There is still one more revelation to be made from Eq. (26). When
which is trivially satisfied if
To sum up, with the solution known at
Temporal discretization#
The governing equations are integrated in time using any one of the time steppers available in shenfun. There are several possible IMEX Runge Kutta methods, see integrators.py. The time steppers are used for any generic equation
where
All the timesteppers split one time step into one or several stages. The classes IMEXRK222, IMEXRK3 and IMEXRK443 have 2, 3 and 4 steps, respectively, and the cost is proportional.
Implementation#
To get started we need instances of the approximation spaces discussed in
Eqs. (9) - (17). When the spaces are created we also need
to specify the family and the dimension of each space. Here we simply
choose Chebyshev and Fourier with 100 and 256 quadrature points in
from shenfun import *
N, M = 64, 128
family = 'Chebyshev'
VB = FunctionSpace(N, family, bc=(0, 0, 0, 0))
VD = FunctionSpace(N, family, bc=(0, 0))
VW = FunctionSpace(N, family)
VT = FunctionSpace(N, family, bc=(1, 0))
VF = FunctionSpace(M, 'F', dtype='d')
And then we create tensor product spaces by combining these bases (like in Eqs. (14)-(17)).
W_BF = TensorProductSpace(comm, (VB, VF)) # Wall-normal velocity
W_DF = TensorProductSpace(comm, (VD, VF)) # Streamwise velocity
W_WF = TensorProductSpace(comm, (VW, VF)) # No bc
W_TF = TensorProductSpace(comm, (VT, VF)) # Temperature
BD = VectorSpace([W_BF, W_DF]) # Velocity vector
DD = VectorSpace([W_DF, W_DF]) # Convection vector
W_DFp = W_DF.get_dealiased(padding_factor=1.5)
BDp = BD.get_dealiased(padding_factor=1.5)
Here the VectorSpae
create mixed tensor product spaces by the
Cartesian products BD = W_BF
W_DF
and DD = W_DF
W_DF
.
These mixed space will be used to hold the velocity and convection vectors,
but we will not solve the equations in a coupled manner and continue in the
segregated approach outlined above.
We also need containers for the computed solutions. These are created as
u_ = Function(BD) # Velocity vector, two components
T_ = Function(W_TF) # Temperature
H_ = Function(DD) # Convection vector
uT_ = Function(BD) # u times T
Wall-normal velocity equation#
We implement Eq. (5) using a generic time stepper.
To this end we first need to declare some test- and trial functions, as well as
some model constants and the length of the time step. We store the
PDE time stepper in a dictionary called pdes
just for convenience:
# Specify viscosity and time step size using dimensionless Ra and Pr
Ra = 1000000
Pr = 0.7
nu = np.sqrt(Pr/Ra)
kappa = 1./np.sqrt(Pr*Ra)
dt = 0.025
# Choose timestepper and create instance of class
PDE = IMEXRK3 # IMEX222, IMEXRK443
v = TestFunction(W_BF) # The space we're solving for u in
pdes = {
'u': PDE(v, # test function
div(grad(u_[0])), # u
lambda f: nu*div(grad(f)), # linear operator on u
[Dx(Dx(H_[1], 0, 1), 1, 1)-Dx(H_[0], 1, 2), Dx(T_, 1, 2)],
dt=dt,
solver=chebyshev.la.Biharmonic if family == 'Chebyshev' else la.SolverGeneric1ND,
latex=r"\frac{\partial \nabla^2 u}{\partial t} = \nu \nabla^4 u + \frac{\partial^2 N_y}{\partial x \partial y} - \frac{\partial^2 N_x}{\partial y^2}")
}
pdes['u'].assemble()
Notice the one-to-one resemblance with (5).
The right hand side depends on the convection vector W_TF
, followed by a backward transform to real space.
The velocity is simply transformed backwards with padding.
# Get a mask for setting Nyquist frequency to zero
mask = W_DF.get_mask_nyquist()
Curl = Project(curl(u_), W_WF) # Instance used to compute curl
def compute_convection(u, H):
up = u.backward(padding_factor=1.5).v
curl = Curl().backward(padding_factor=1.5)
H[0] = W_DFp.forward(-curl*up[1])
H[1] = W_DFp.forward(curl*up[0])
H.mask_nyquist(mask)
return H
Note that the convection has a homogeneous Dirichlet boundary condition in the non-periodic direction.
Streamwise velocity#
The streamwise velocity is computed using Eq. (26) and (27).
The first part is done fastest by projecting W_DF
used by
f = dudx = Project(Dx(u_[0], 0, 1), W_DF)
Since
We thus compute
K = W_BF.local_wavenumbers(scaled=True)
K[1][0, 0] = 1 # to avoid division by zero. This component is computed later anyway.
u_[1] = 1j*dudx()/K[1]
which leaves only
v00 = Function(VD)
v0 = TestFunction(VD)
h1 = Function(VD) # convection equal to H_[1, :, 0]
pdes1d = {
'v0': PDE(v0,
v00,
lambda f: nu*div(grad(f)),
-Expr(h1),
dt=dt,
solver=chebyshev.la.Helmholtz if family == 'Chebyshev' else la.Solver,
latex=r"\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - N_y "),
}
pdes1d['v0'].assemble()
A function that computes v
for an integration stage rk
is
def compute_v(rk):
v00[:] = u_[1, :, 0].real
h1[:] = H_[1, :, 0].real
u_[1] = 1j*dudx()/K[1]
pdes1d['v0'].compute_rhs(rk)
u_[1, :, 0] = pdes1d['v0'].solve_step(rk)
Temperature#
The temperature equation (2) is implemented using a Helmholtz solver. The main difficulty with the temperature is the non-homogeneous boundary condition that requires special attention. A non-zero Dirichlet boundary condition is implemented by adding two basis functions to the basis of the function space
with the approximation now becoming
The boundary condition requires
and
We find
Using this approach it is easy to see that any inhomogeneous function
A time stepper for the temperature equation is implemented as
uT_ = Function(BD)
q = TestFunction(W_TF)
pdes['T'] = PDE(q,
T_,
lambda f: kappa*div(grad(f)),
-div(uT_),
dt=dt,
solver=chebyshev.la.Helmholtz if family == 'Chebyshev' else la.SolverGeneric1ND,
latex=r"\frac{\partial T}{\partial t} = \kappa \nabla^2 T - \nabla \cdot \vec{u}T")
pdes['T'].assemble()
The uT_
term is computed with dealiasing as
def compute_uT(u_, T_, uT_):
up = u_.backward(padding_factor=1.5)
Tp = T_.backward(padding_factor=1.5)
uT_ = BDp.forward(up*Tp, uT_)
return uT_
Finally all that is left is to initialize the solution and integrate it forward in time.
%matplotlib inline
import matplotlib.pyplot as plt
# initialization
T_b = Array(W_TF)
X = W_TF.local_mesh(True)
#T_b[:] = 0.5*(1-X[0]) + 0.001*np.random.randn(*T_b.shape)*(1-X[0])*(1+X[0])
T_b[:] = 0.5*(1-X[0]+0.25*np.sin(np.pi*X[0]))+0.001*np.random.randn(*T_b.shape)*(1-X[0])*(1+X[0])
T_ = T_b.forward(T_)
T_.mask_nyquist(mask)
t = 0
tstep = 0
end_time = 15
while t < end_time-1e-8:
for rk in range(PDE.steps()):
compute_convection(u_, H_)
compute_uT(u_, T_, uT_)
pdes['u'].compute_rhs(rk)
pdes['T'].compute_rhs(rk)
pdes['u'].solve_step(rk)
compute_v(rk)
pdes['T'].solve_step(rk)
t += dt
tstep += 1
plt.contourf(X[1], X[0], T_.backward(), 100)
plt.show()
A complete solver implemented in a solver class can be found in
RayleighBenard2D.py.
Note that in the final solver it is also possible to use a HDF5
format that later can be visualized in, e.g., Paraview. The movie in the
beginning of this demo has been created in Paraview.
A. Pandey, J. D. Scheel and J. Schumacher. Turbulent Superstructures in Rayleigh-B’enard Convection, Nature Communications, 9(1), pp. 2118, doi: 10.1038/s41467-018-04478-0, 2018.