#!/usr/bin/env python
# coding: utf-8
# In[2]:
from IPython.display import HTML
s="""
2D Laplace Equation
""";
h=HTML(s); h
# # Understand the Problem
#
# * What is the 2D pressure field for the **Laplace equation** when the initial conditions are **zero everywhere** and the x-boundary conditions are 0 and 1 and the y-boundary conditions are von Neumann (zero gradient)?
#
# * The Laplace Equation for pressure is described as follows:
#
# $$ {\partial^2 p \over \partial x^2} + {\partial^2 p \over \partial y^2} = 0 $$
#
# * This equation has an analytical solution:
#
# $$ p(x,y) = {x \over 4} - 4 \sum_{m=1 (odd)}^\infty {{sinh(m \pi x) cos(m \pi y)} \over {(m \pi)^2sinh(2 \pi m)}} $$
#
# ***
#
# # Formulate the Problem
#
# ## Input Data:
#
# This problem has **no temporal component**, so we use a number of iterations.
#
# * `niter` = 51 (maximum number of iterations)
# * `nx` = 21 (number of x spatial points)
# * `ny` = 11 (number of y spatial points)
# * `xmax` = 2
# * `ymax` = 1
# * `infinity` = 100 (a large number)
#
# ## Initial Conditions:
#
# * $\forall (x, y) \text{ if } n = 0 \rightarrow p = 0$
#
# ## Boundary Conditions:
#
# * $\forall (n, y) \text{ if } x = 0 \rightarrow p = 0$
# * $\forall (n, y) \text{ if } x = 2 \rightarrow p = y$
# * $\forall (n, x) \text{ if } y = 0 \rightarrow {\partial p} / {\partial y} = 0$
# * $\forall (n, x) \text{ if } y = 1 \rightarrow {\partial p} / {\partial y} = 0$
#
# ## Output Data:
#
# * $\forall (x,y,n) \ p = ?$
#
# ***
#
# # Design Algorithm to Solve Problem
#
# ## Space-time discretisation:
#
# * i $\rightarrow$ index of grid in x
# * j $\rightarrow$ index of grid in y
# * n $\rightarrow$ index of iterations
# * m $\rightarrow$ index of odd numbers in analytical solution
#
# ## Numerical scheme
#
# * 2nd order CD in space
#
# ## Discrete equation
#
# $$ {{p_{i+1,j}^n -2p_{i,j}^n + p_{i-1,j}^n} \over \Delta x^2} +
# {{p_{i,j+1}^n -2p_{i,j}^n + p_{i,j-1}^n} \over \Delta y^2} = 0 $$
#
# ## Transpose
#
# $$ p_{i,j}^{n} = { {\Delta y^2 (p_{i+1,j}^{n}+p_{i-1,j}^{n}) + \Delta x^2 (p_{i,j+1}^{n}+p_{i,j-1}^{n})} \over {2(\Delta x^2 + \Delta y^2)} } $$
#
# To use this as an iterative formula, $p_{i,j}^{n} = p_{i,j}^{n+1}$ **This looks a bit dodgy** but is the only thing that makes sense, unless we equate the RHS to forward differencing
#
# ## Pseudo-code
#
# niter = 51
# nx = 21
# xmax = 2
# ny = 11
# ymax = 1
# dx = xmax/(nx-1)
# dy = ymax/(ny-1)
# infinity = 100
#
# # Dirichlet Boundary Conditions
#
# # p boundary, left:
# p(0,:,:) = 0
# # p boundary, right:
# p(nx-1,:,:) = ymax
#
# # von Neumann Boundary Conditions
#
# # Values for correction at boundaries:
# for j between 0 and ny-1
# jpos(j) = j + 1
# jneg(j) = j - 1
#
# # Set Reflection:
# # j: -1 0, 1,... ny-2, ny-1, ny
# # jpos: start => => end
# # jneg: start => => end
#
# jpos(ny-1) = ny-2 # i.e. ny = ny-2
# jneg(0) = 1 # i.e. -1 = 1
#
# # Initial Conditions
#
# p(:,:,0) = 0
#
# # Numerical Computation
#
# for n between 0 and niter-1
# for i between 1 and nx-1
# for j between 0 and ny
# p(i,j,n+1) = { dy^2 * [ p(i+1,j,n)+p(i-1,j,n) ]
# + dx^2 * [ p(i,jpos(j),n)+p(i,jneg(j),n) ] }
# / {2(dx^2 + dy^2)}
#
#
# # Analytical Solution
#
# p_coefficient(i,j,1) = { 4 * [sinh(1*pi*x(i)]*[cos(1*pi*y(j)] }
# / { [(1*pi)**2]*sinh(2*pi*1) }
#
# for m between 3 and infinity in odd steps
# for i between 0 and nx-1
# for j between 0 and ny-1
# p_coefficient(i,j) += {[sinh(m*pi*x(i)]*[cos(m*pi*y(j)] }
# / { [(m*pi)**2]*sinh(2*pi*m) }
#
# for i between 0 and nx-1
# p_analytical(i,j)= (x(i) / 4) - 4*p_coefficient(i,j)
#
# ***
# # Implement Algorithm in Python
# In[2]:
def laplace_equation_numerical(niter, nx, ny, xmax, ymax):
"""
Returns the velocity field and distance for 2D linear convection
"""
# Increments:
dx = xmax/(nx-1)
dy = ymax/(ny-1)
# Initialise data structures:
import numpy as np
p = np.zeros(((nx,ny,niter)))
x = np.zeros(nx)
y = np.zeros(ny)
jpos = np.zeros(ny)
jneg = np.zeros(ny)
# X Loop
for i in range(0,nx):
x[i] = i*dx
# Y Loop
for j in range(0,ny):
y[j] = j*dy
# Initial conditions
p[:,:,0] = 0
# Dirichlet Boundary Conditions:
# p boundary, left:
p[0,:,:] = 0
# p boundary, right:
for j in range(0,ny):
p[nx-1,j,:] = y[j]
# von Neumann Boundary Conditions:
# Values for correction at boundaries:
for j in range(0,ny-1):
jpos[j] = j + 1
jneg[j] = j - 1
# Set Reflection:
jpos[ny-1] = ny-2
jneg[0] = 1
# Loop
for n in range(0,niter-1):
for i in range(1,nx-1):
for j in range(0,ny):
p[i,j,n+1] = (( dy**2 * ( p[i+1,j,n]+p[i-1,j,n] )
+ dx**2 * ( p[i,jpos[j],n]+p[i,jneg[j],n] ) )
/ (2*(dx**2 + dy**2)))
return p, x, y
# In[3]:
p1, x1, y1 = laplace_equation_numerical(100, 51, 51, 2.0, 1.0)
# In[4]:
def plot_2D(p,x,nt,title):
"""
Plots the 1D velocity field
"""
import matplotlib.pyplot as plt
import matplotlib.cm as cm
plt.figure()
ax=plt.subplot(111)
colour=iter(cm.rainbow(np.linspace(0,20,nt)))
for n in range(0,nt,20):
c=next(colour)
ax.plot(x,p[:,-1,n],linestyle='-',c=c,label='n='+str(n)+' numerical')
box=ax.get_position()
ax.set_position([box.x0, box.y0, box.width*1.5,box.height*1.5])
ax.legend( bbox_to_anchor=(1.02,1), loc=2)
plt.xlabel('x (m)')
plt.ylabel('p (Pa)')
plt.ylim([0,1.0])
plt.xlim([0,2.0])
plt.title(title)
plt.show()
# In[5]:
def plot_3D(p,x,y,time,title,label):
"""
Plots the 2D velocity field
"""
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig=plt.figure(figsize=(11,7),dpi=100)
ax=fig.gca(projection='3d')
ax.set_xlabel('x (m)')
ax.set_ylabel('y (m)')
ax.set_zlabel(label)
ax.set_xlim(0,2)
ax.set_ylim(0,1)
ax.view_init(30,225)
Y,X=np.meshgrid(y,x) #note meshgrid uses y,x not x,y!!!
surf=ax.plot_surface(X,Y,p[:,:,time], rstride=1, cstride=1)
plt.title(title)
plt.show()
# In[6]:
plot_3D(p1,x1,y1,0,'Figure 1: Initial Conditions: n=0, niter=100, nx=51, ny=51','p (Pa)')
plot_3D(p1,x1,y1,99,'Figure 2: Final Conditions: n=99, niter=100, nx=51, ny=51','p (Pa)')
plot_2D(p1,x1,99,'Figure 3: At Far Neumann Boundary: n=99, niter=100, nx=51, ny=51')
# ***
#
# # Introduce Relative Error
#
# * The above shows that the numerical algorithm works, but depends on the number of iterations for convergence.
# * I want to remove this dependence and use an error. I define the error like this (the denominator is n+1 to avoid divide by zero):
#
# $$ \text{Relative Error} = {
# {\sum_{n=0}^{n=n_{max}}(\left\vert p_{n+1} \right\vert - \left\vert p_{n} \right\vert)} \over {\sum_{n=0}^{n=n_{max}}(\left\vert p_{n+1} \right\vert}
# }$$
#
# * So now we need a target Relative Error, so choose $1 \times 10^{-2}$
#
# * This changes the pseudo-code a bit.
#
# * For safety we can still include the maximum number of iterations, but maybe increase it so that the while loop determines what happens?
#
# * We also need a new plotting function, as we need to step through less iterations in 2D.
#
# ## Pseudo-code 2
#
#
# error_target=1*10^-2
# niter = 500
#
# # Numerical Computation
#
# while error is greater than error_target
# for n between 0 and niter-1
# for i between 1 and nx-1
# for j between 1 and ny-1
# p(i,j,n+1) = { dy^2 * [ p(i+1,j,n)+p(i-1,j,n) ] +
# dx^2 * [ p(i,jpos(j),n)+p(i,jneg(j),n) ] }
# / {2(dx^2 + dy^2)}
# error = [ sum(abs(p(i,j,n+1)-abs(p(i,j,n)) ] / sum(abs(p(i,j,n+1)))
#
# ## Implement Algorithm in Python 2
#
# In[2]:
def laplace_equation_numerical_2(error_target, niter, nx, ny, xmax, ymax):
"""
Returns the velocity field and distance for 2D linear convection
"""
# Increments:
dx = xmax/(nx-1)
dy = ymax/(ny-1)
# Initialise data structures:
import numpy as np
p = np.zeros(((nx,ny,niter)))
x = np.zeros(nx)
y = np.zeros(ny)
jpos = np.zeros(ny)
jneg = np.zeros(ny)
# X Loop
for i in range(0,nx):
x[i] = i*dx
# Y Loop
for j in range(0,ny):
y[j] = j*dy
# Initial conditions
p[:,:,0] = 0
# Dirichlet Boundary Conditions:
# p boundary, left:
p[0,:,:] = 0
# p boundary, right:
for j in range(0,ny):
p[nx-1,j,:] = y[j]
# von Neumann Boundary Conditions:
# Values for correction at boundaries:
for j in range(0,ny):
jpos[j] = j + 1
jneg[j] = j - 1
# Set Reflection:
jpos[ny-1] = ny-2
jneg[0] = 1
while True:
for n in range(0,niter-1):
for i in range(1,nx-1):
for j in range(0,ny):
p[i,j,n+1] = (( dy**2 * ( p[i+1,j,n]+p[i-1,j,n] )
+ dx**2 * ( p[i,jpos[j],n]+p[i,jneg[j],n] ) )
/ (2*(dx**2 + dy**2)))
error = (np.sum(np.abs(p[i,j,n+1])-np.abs(p[i,j,n]) ) /
np.sum(np.abs(p[i,j,n+1]) ))
print "n = " + str(n) + " completed"
if(error < error_target):
break
break
return p, x, y
# In[8]:
p2, x2, y2 = laplace_equation_numerical_2(1.0e-2, 500, 51, 26, 2.0, 1.0)
# In[9]:
def plot_2D_2(p,x,nt,title):
"""
Plots the 1D velocity field
"""
import matplotlib.pyplot as plt
import matplotlib.cm as cm
plt.figure()
ax=plt.subplot(111)
colour=iter(cm.rainbow(np.linspace(0,1,nt+1)))
for n in range(0,nt+1,1):
c=next(colour)
ax.plot(x,p[:,-1,n],linestyle='-',c=c,label='n='+str(n)+' numerical')
box=ax.get_position()
ax.set_position([box.x0, box.y0, box.width*1.5,box.height*1.5])
ax.legend( bbox_to_anchor=(1.02,1), loc=2)
plt.xlabel('x (m)')
plt.ylabel('p (Pa)')
plt.ylim([0,1.0])
plt.xlim([0,2.0])
plt.title(title)
plt.show()
# In[21]:
plot_3D(p2,x2,y2,0,'Figure 1: Initial Conditions: n=0, niter=25, n=51, nx=51, ny=51','p (Pa)')
plot_3D(p2,x2,y2,16,'Figure 2: Final Conditions: n=16, niter=25, nx=51, ny=51','p (Pa)')
plot_2D_2(p1,x1,16,'Figure 3: At Far Neumann Boundary: n=16, niter=25, nx=51, ny=51')
# ***
#
# # Introduce Analytical Solution
#
# * Ok so far so good, we now have a solution that converges in 25 iterations with a defined relative error.
# * Now we need to introduce the analytical solution and compare the numerical value at n=24 with the analytical solution - we already did the pseudo code for this
#
# ## Implement Analytical Solution in Python
# In[11]:
def laplace_equation_analytical(INFINITY, NX, NY, XMAX, YMAX):
from math import pi as PI
import math as ma
# Initialise data structures
p_coefficient = np.zeros((NX, NY))
p_analytical = np.zeros((NX, NY))
x = np.zeros(NX)
y = np.zeros(NY)
# Constants
DX = XMAX/(NX-1)
DY = YMAX/(NY-1)
# X Loop
for i in range(0,NX):
x[i] = i*DX
# Y Loop
for j in range(0,NY):
y[j] = j*DY
# Analytical solution
for i in range(0, NX):
for j in range(0, NY):
p_coefficient[i,j] = ( (ma.sinh(1.0*PI*x[i]) * ma.cos(1.0*PI*y[j]) )
/ ( ((1.0*PI)**2.0) * ma.sinh(2.0*PI*1.0) ) )
for m in range(3, INFINITY, 2):
for i in range(0, NX):
for j in range(0, NY):
p_coefficient[i,j] += ( (ma.sinh(m*PI*x[i]) * ma.cos(m*PI*y[j]) )
/ ( ((m*PI)**2.0) * ma.sinh(2.0*PI*m) ) )
for i in range (0, NX):
for j in range(0, NY):
p_analytical[i,j]= (x[i] / 4.0) - (4.0 * p_coefficient[i,j])
return p_analytical, x, y
# In[22]:
p3, x3, y3 = laplace_equation_analytical(100, 51, 26, 2.0, 1.0)
# In[23]:
def plot_3D_2(p,x,y,title):
"""
Plots the 2D velocity field
"""
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig=plt.figure(figsize=(11,7),dpi=100)
ax=fig.gca(projection='3d')
ax.set_xlabel('x (m)')
ax.set_ylabel('y (m)')
ax.set_zlabel('p (Pa)')
ax.set_xlim(0,2)
ax.set_ylim(0,1)
ax.view_init(30,225)
Y,X=np.meshgrid(y,x) #note meshgrid uses y,x not x,y!!!
surf=ax.plot_surface(X,Y,p[:,:], rstride=1, cstride=1)
plt.title(title)
plt.show()
# In[24]:
plot_3D_2(p3,x3,y3,'Figure 1: Analytical Solution: nx=51, ny=26')
# ***
#
# # Why doesn't the analytical solution agree with the numerical solution?
#
# * Possible cause: the residual error is large in the numerical solution
# * We also think we can solve this equation more efficiently
#
# ## Numerical scheme
#
# * 2nd order CD in space
#
# ## Discrete equation
#
# $$ {{p_{i+1,j}^n -2p_{i,j}^n + p_{i-1,j}^n} \over \Delta x^2} +
# {{p_{i,j+1}^n -2p_{i,j}^n + p_{i,j-1}^n} \over \Delta y^2} = 0 $$
#
# * Assume $\Delta x = \Delta y = h$
#
# $$ {{p_{i+1,j}^n -2p_{i,j}^n + p_{i-1,j}^n} \over h^2} +
# {{p_{i,j+1}^n -2p_{i,j}^n + p_{i,j-1}^n} \over h^2} = 0 $$
#
# * Introduce false timestepping - 1st order FD in time:
#
# $$ {{p_{i+1,j}^n -2p_{i,j}^n + p_{i-1,j}^n} \over h^2} +
# {{p_{i,j+1}^n -2p_{i,j}^n + p_{i,j-1}^n} \over h^2} =
# {{p_{i,j}^{n+1} - p_{i,j}^n} \over {\Delta t}} $$
#
# ## Transpose
#
# $$ p_{i,j}^{n+1} = p_{i,j}^n + r(p_{i+1,j}^{n}+p_{i-1,j}^{n}+p_{i,j+1}^{n}+p_{i,j-1}^{n} -4 p_{i,j}^n)$$
#
# where: $r = {{\Delta t} \over {h^2}}$
#
# * The maximum $r$ for stability is 0.25 for this scheme (.....).
# * This will also give the fastest convergence
#
# So the pressure is just the average of it's neighbours:
#
# $$ p_{i,j}^{n+1} = 0.25(p_{i+1,j}^{n}+p_{i-1,j}^{n}+p_{i,j+1}^{n}+p_{i,j-1}^{n})$$
#
# ***
#
# # Implement efficient algorithm in Python
# In[15]:
def laplace_equation_numerical_3(error_target, niter, nx, ny, xmax, ymax):
"""
Returns the velocity field and distance for 2D linear convection
"""
# Increments:
dx = xmax/(nx-1)
dy = ymax/(ny-1)
# Initialise data structures:
import numpy as np
p = np.zeros(((nx,ny,niter)))
x = np.zeros(nx)
y = np.zeros(ny)
jpos = np.zeros(ny)
jneg = np.zeros(ny)
# X Loop
for i in range(0,nx):
x[i] = i*dx
# Y Loop
for j in range(0,ny):
y[j] = j*dy
# Initial conditions
p[:,:,0] = 0
# Dirichlet Boundary Conditions:
# p boundary, left:
p[0,:,:] = 0
# p boundary, right:
for j in range(0,ny):
p[nx-1,j,:] = y[j]
# von Neumann Boundary Conditions:
# Values for correction at boundaries:
for j in range(0,ny):
jpos[j] = j + 1
jneg[j] = j - 1
# Set Reflection:
jpos[ny-1] = ny-2
jneg[0] = 1
while True:
for n in range(0,niter-1):
for i in range(1,nx-1):
for j in range(0,ny):
p[i,j,n+1] = 0.25*( p[i+1,j,n]+p[i-1,j,n]+p[i,jpos[j],n]+p[i,jneg[j],n] )
error = (np.sum(np.abs(p[i,j,n+1])-np.abs(p[i,j,n]) ) /
np.sum(np.abs(p[i,j,n+1]) ))
if(error < error_target):
print "n = " + str(n) + " completed"
break
break
return p, x, y
# ## Improvements in the Parameters
#
# * I increased the maximum number of iterations to 3000
# * I reduced the relative error to $1 \times 10^{-6}$
# In[42]:
p4, x4, y4 = laplace_equation_numerical_3(1.0e-6, 5000, 51, 26, 2.0, 1.0)
# In[43]:
plot_3D(p4,x4,y4,3129,'Figure 2: Final Conditions: n=3129, niter=3130, nx=51, ny=26','p (Pa)')
# ***
#
# # Compare Improved Results with the Analytical Solution
#
# * This is clearly much better than before, so it's worth comparing the numerical and analytical solutions
# In[33]:
def plot_diffusion(p4,p3,x4,NY,TIME,TITLE):
"""
Plots the 1D velocity field
"""
import matplotlib.pyplot as plt
import matplotlib.cm as cm
plt.figure()
ax=plt.subplot(111)
colour=iter(cm.rainbow(np.linspace(0,5,NY)))
for j in range(0,NY,5):
c=next(colour)
ax.plot(x4,p4[:,j,TIME],'ko', markerfacecolor='none', alpha=0.5, label='j='+str(j)+' numerical')
ax.plot(x4,p3[:,j],linestyle='-',c=c,label='j='+str(j)+' analytical')
box=ax.get_position()
ax.set_position([box.x0, box.y0, box.width*1.5,box.height*1.5])
ax.legend( bbox_to_anchor=(1.02,1), loc=2)
plt.xlabel('x (m)')
plt.ylabel('p (Pa)')
#plt.ylim([0,8.0])
#plt.xlim([0,2.0*PI])
plt.title(TITLE)
plt.show()
# In[44]:
plot_diffusion(p4,p3,x4,26,3129,'Analytical vs Numerical')
# # Analytical solution vs the numerical solution
#
# * It was important to arrive at an efficient algorithm to allow a large number of iterations or small accuracy $1 \times 10^{-6}$ or large number of iterations 5000
# * Ensuring the spatial steps matched was important for this algorithm to apply
# * The accuracy of the numerical solution is heavily dependent on the number of iterations or the relative error (whichever is limiting) **especially important where the values in the solution are small and not changing very much**
#
# # Notes
#
# 1) Relation between physics and numerics
#
# * Laplace operator is typical for diffusion, diffusion is an isotropic phenomenon.
#
# * It has to be discretised with a central difference - numerics must be consistent with the physics that it represents.
#
# * Convection is anisotropic so numerical scheme must be directionally biased.
#
# 2) Numerical scheme
#
# * Second order CD in both x and y is the most widely used numerical scheme for $ \nabla^2 $
#
# * Also known as "five point difference operator"
#
# 3) Matrix
#
# * This scheme is an iterative method for a steady state (artificial time variable)
#
# * Let $ \Delta x = \Delta y = h$ then
#
# $$ {{p_{i+1,j}^n -2p_{i,j}^n + p_{i-1,j}^n} \over h^2} + {{p_{i,j+1}^n -2p_{i,j}^n + p_{i,j-1}^n} \over h^2} = 0 $$
#
# * Linear system of equations in **pentadiagonal** coefficient matrix
#
# * Equivalent to Point Jacobi method
#
#
# In[19]: