- So 21 Oktober 2018
- ComputationalFluidDynamics
- Peter Schuhmacher
- #flow model, #FVM, #curvilinear grid
Compass notation
We introduce a local compass notation for the variable \(\mathbf{T}\) and it's coefficients \(\mathbf{D}\). In compass notation we indicate the grid points with capitals and the faces to the neighbouring cells with small letters.
N
|
|
nw-----n-----ne
| | |
| | |
W-----w-----P-----e-----E
| | |
| | |
sw-----s-----se
|
|
S
Finite Volume Method (FVM) 1-dimensional
The basic equation
The stationary diffusion equation may be written as
and the time dependent (= instationary) as
\(T\) descibes the status of a of quantity, the temperature e.g., \(\Gamma\) is the diffusion coefficient, and \(f\) stands for an additional, external source (as solar heating e.g.). The diffusion equation describes the change of the heat flux. If \(\mathbf{H}\) is the heat flux, then diffusion is defined by the following generic set of equations:
which drives the change of \(T\).
Divergence theorem
The divergence theorem is the base for the finite volume method (FVM). So let's recapitulate some identities:
Let's have a look at the right hand side: The surface of a control volume (CV) is defined by the straight-line segements of each side. In the 1 dimensional case the CV has 2 sides, \(S_e\)and \(S_w\). The positive flux of the divergence theorem is defined as the normal outward flux, \(\mathbf{H}_e + \mathbf{H}_w\). But since we define the direction of the \(\mathbf{\hat{x}}\) -axis as positive direction the normal outward flux will have the form, \(\mathbf{H}_e - \mathbf{H}_w\).
FVM Ansatz
The principle of the finite volume method FVM is:
- integrate the equation over the control volume CV
- Use the divergence theorem to replace the divergence terms by the flux through the surface integrated over the surface
- replace the flux by the expression with the temperature gradient
With \(\mathbf{F} = \Gamma \nabla \phi\) this gives for the right hand side of the divergence theorem:
and using the compass notation we get the following stencil for the system matrix
Fixed boundary condition
For a fixed boundary value on face \(w\) at the left/west side of the domain we can write:
We approximate the \(\tilde{w}\)-labeld terms with
So we get the following stencil for the system matrix at the west boundary
Flux/gradient boundary condition
For a Neumann boundary condition on face e the flux has to be prescribed. Since the term \(\Gamma_e \frac{T_E - T_P}{d_E} \, S_e $ is the flux at $e\) it can be replaced by a perscribed value. So we get the following stencil for the system matrix at the east boundary:
Implicit (Euler backward) time schema
In the implicit time schema the tendency term \(\mathbf{T}_t\) is discretizised as
The unknown quantity \(\mathbf{T}^{n}\) is on the left side of the equation including the equation system which has to be solved at each time step
The stencil for the implicit schema is now
Introducing \(\tilde{\mathbf{A}}\) we find the following form of discretisation:
Implementation
import numpy as np
import pandas as pd
import scipy.sparse as sps
from scipy.sparse.linalg import bicgstab
import matplotlib.pylab as plt
np.set_printoptions(linewidth=200, precision=3)
Signals
This are auxiliary functions to initialize the parameter values. We have prepared a collection of signals here: https://alpynepyano.github.io/healthyNumerics/posts/ecg-waves-python.html
class signal:
def scale(self,t): return (t-t.min())/(t.max()-t.min())
def add_noise(self,t): return np.random.randn(t.shape[0])
def line(self,t): return self.scale(t)
def uSignal(self,t): return np.array(0.5*np.sign(t) + 0.5,dtype=int)
def wSignal(self,t,dt): return self.uSignal(t)-self.uSignal(t-dt)
def expSignal(self,t): return np.exp(t)
def minus_exp_square(self,t): return np.exp(-t**2)
The FVM grid
We generate a 1-dimensional grid with irregular spacing between the grid points. We start with the location of the faces and compute then the grid point in the center.
class grid1D:
def __init__(self, nxc, Lx, sCase):
self.nxc = nxc # number of faces
self.Lx = Lx # length of x-domain
sT = {'stretch_regular':self.stretch_regular, # mapping of stretching types
'stretch_exp' :self.stretch_exp}
self.stretchT = sT[sCase] # selects the type of stretching
def scale(self, a): return (a-a.min())/(a.max()-a.min())# scaling to [0..1]
def xi(self): return np.linspace(0,1.0,self.nxc) # logical/computational grid
def stretch_regular(self): return np.linspace(0,1.0,self.nxc) # regular grid, no stretching
def stretch_exp(self): # exponantial stretching
ix = self.xi(); return self.scale(np.exp(2*ix))
def xc(self): return self.Lx*self.stretchT() # face x-coordinate
def xp(self): return self.centerPoint(self.xc()) # center point coordinate
def dx(self): # spacing between P's
xc = self.Lx*self.stretchT()
x = np.r_[xc[0],self.centerPoint(xc),xc[self.nxc-1]]
return np.diff(x)
def centerPoint(self,x): mx=x.shape[0]; return 0.5*(x[0:mx-1] + x[1:mx]) # center point P method
def Swe(self): return np.ones((self.nxc)) # areas of the faces west-east
def cvP(self):
Swe = self.Swe(); mx=Swe.shape[0];
return 0.5*(Swe[0:mx-1] + Swe[1:mx])*np.diff(self.xc()) # volumes of the CV
#--- MAIN -------------------------------------------------------
#--- Input ------------------------------------------------------
nxc = 28 # number of faces/corners
Lx = 0.02 # length of x-domain
#Lx = 0.3 # length of x-domain
nxp = nxc-1 # number of central points P
#g = grid1D(nxc,Lx,'stretch_regular') # 2 types
g = grid1D(nxc,Lx,'stretch_exp') # type of grid strechting
Graphics of the grid
#--- Graphics -----------------
#--- plot the grid ------------
xc = g.xc() # get the corners/faces points w, e
xp = g.xp() # get the central points P
def plot_grid(ax0,xc,xp, y_base=0):
ax0.plot(xc, y_base+0*xc, c='k', ls='--', marker="|",ms=20, lw=2, alpha=0.5 )
ax0.scatter(xp, y_base+0*xp,c='r', marker='o', s=90, alpha=0.75)
with plt.style.context('fast'):
fig, ax0 = plt.subplots(figsize=(20,4))
plot_grid(ax0,xc,xp)
grid_legend = np.array(['xc: face points w', 'xp: center grid points P' ])
plt.margins(0.1); plt.grid(axis='y'); plt.legend(grid_legend,prop={'size': 15})
plt.title("Finite Volume Grid",fontsize=25, fontweight='bold')
plt.show()
Parameters and boundary conditions
We will run the following case; which is given here: http://ftp.demec.ufpr.br/disciplinas/TM702/Versteeg_Malalasekera_2ed.pdf :
Case B
- a plate with thikness: \(L = 2 cm\)
- thermal conductivitiy, constant over the domain: \(\Gamma = 0.5 \; W/(m \cdot K)\)
- uniform heat generation \(q = 1000\; kW/m^3\)
- temperatur at the west boundary, constant: \(T_w = 100^o C\)
- temperatur at the east boundary, constant: \(T_e = 200^o C\)
class system1D:
def __init__(self,g,sig,case):
self.g = g
self.nxc = g.nxc
self.nxp = self.nxc-1
self.sig = sig
self.case = case
def b(self): return np.zeros((self.nxp)) # initialize local rhs
def DWE(self,g): return self.gam()*g.Swe()/g.dx() # build the non-zero elements of the system matrix
#----- input section ------------------------------------------------------------------------------
def T(self):
if self.case=='B': return 0.1*np.random.randn((self.nxp)) # initial value, random gauss [-0.1 .. +0.1]
if self.case=='C': return 273.0*np.ones((self.nxp))
def q(self,g):
if self.case=='B': return 1000*(10**3) *g.cvP() # set source term
if self.case=='C': return 0.00 # set source term
def gam(self):
if self.case =='B': return 0.5*np.ones((self.nxc)) # set diffusion coefficient
if self.case =='C': return 0.1*np.ones((self.nxc)) # set diffusion coefficient
def T_west_fix(self):
if self.case =='B': return 100.0 # set T_fix at w-boundary
if self.case =='C': return 273.0 # set T_fix at w-boundary
def T_east_fix(self):
if self.case =='B': return 200.0 # set T_fix at e-boundary
if self.case =='C': return 273.0 # set T_fix at e-boundary
def insert_boundary_conditions(sT,DWE,b):
#----- input section ------------------------------------------------------------------------------
west_fix = True #---- west boundary # select type of boundary condition at w
if west_fix:
b[0] = sT.T_west_fix() * DWE[0];
else: T_west_flux = 0; DWE[0] = T_west_flux
east_fix = True #---- east boundary # select type of boundary condition at e
if east_fix:
b[-1] = sT.T_east_fix() * DWE[-1];
else: DWE[-1] = 0.0
return DWE,b
The analytical solution
def Tcase_B(x):
L, gamma, Q = 0.02, 0.5, 1000*10**3;
Tw, Te = 100.0, 200.0;
return Tw + x*( (Te-Tw)/L + 0.5*Q*(L-x)/gamma)
x = xp.copy()
TB = Tcase_B(x)
with plt.style.context('fast'):
fig, ax0 = plt.subplots(figsize=(20,4))
plt.plot(x,TB,'o-');
plt.title('Case B: analytical solution', fontweight='bold');
plt.grid(axis='y');plt.show()
The numerical solution
The numerical solution hardly differs from the analytical solution. Both are computed on the stretched grid.
def solve_stationary_system(DWE,RHS,T):
def print_convergence(cc):
if cc==0 : print("bicg-stab: successful exit --> c =",cc)
if cc >0 : print("bicg-stab: convergence to tolerance not achieved, number of iterations is = ", cc)
if cc <0 : print("bicg-stab: illegal input or breakdown --> c =",cc)
#---- center P ----
DP = -DWE[0:nxp] - DWE[1:nxc];
#---- system matrix D ----
DW = DWE[1:nxp]; DE = DWE[1:nxp]
D = sps.diags( diagonals=[DW, DP, DE],
offsets=[-1, 0, 1],
shape=(nxp, nxp), format='csr')
#---- numerical solver
X = bicgstab(D,-RHS,x0=T,tol=1e-06, maxiter=None);
print_convergence(X[1])
return X[0] # X[0] := solved T
#----- main --------------------
sig = signal() # get access to the signals
sT = system1D(g,sig,case='B') # instantiate a system for this case
qT = sT.q(g) # set the source term
#---- main ---------------------
DWE = sT.DWE(g) # build the non-zero elements of the system matrix
bT = sT.b() # initialize the local rhs of this system
DWE,bT = insert_boundary_conditions(sT,DWE,bT) # insert the boundary conditions: changes DWE and b
RHS = bT+qT # update the right hand side of the complete system
T = sT.T() # initialize T
T = solve_stationary_system(DWE,RHS,T) # run the numerical sover
bicg-stab: successful exit --> c = 0
Graphics
G = pd.DataFrame({ 'T numerical':T,
'TB analytical':TB
})
#print('T-TB', T-TB)
ybase = np.min(T)-0.1*(np.max(T)-np.min(T))
with plt.style.context('fast'):
fig, ax0 = plt.subplots(figsize=(20,4))
plt.plot(xp,G, marker='s',lw=4, alpha=0.4);
plot_grid(ax0,xc,xp, y_base=ybase)
grid_legend = np.array(['xc: face points w', 'xp: center grid points P' ])
plt.legend(G.columns, prop={'size': 15})
plt.margins(0.1); plt.grid(axis='y'); #plt.legend(prop={'size': 15})
plt.title("Case B, Temperature: numerical and analytical solution",fontsize=20, fontweight='bold')
plt.show()
A time dependent solution is given here