#!/usr/bin/env python
# coding: utf-8
# # Finite Difference Operators in Many Dimensions using Kronecker Products
# **Prof. Tony Saad (www.tsaad.net)
Department of Chemical Engineering
University of Utah**
#
# It is very easy to build complex 2D and 3D finite difference operators using Kronecker products. We will start by defining a basic forward difference operator for the first derivative. We can choose other operators as well, but this particular case produces tight stencils for Laplacians.
#
# The forward difference is given by
# \begin{equation}
# \frac{\text{d}u}{\text{d}x} = \frac{u_{i+1} - u_{i}}{\Delta x}
# \end{equation}
#
# In operator form, and for a periodic case on a 1D grid, this operator looks like
# \begin{equation}
# \mathbf{D}^+ =
# \left[ {\begin{array}{*{20}{c}}
# { - 1}&1&{}&{}&{}\\
# {}&{ - 1}&1&{}&{}\\
# {}&{}&{ - 1}&1&{}\\
# {}&{}&{}&{ - 1}&1\\
# 1&{}&{}&{}&{ - 1}
# \end{array}} \right]
# \end{equation}
#
# For periodic boundaries, this operator can be defined in Python as
# ```Python
# def d_plus(n, dx=1):
# # periodic forward difference
# d = np.ones(n, dtype=int)
# ud = np.ones(n-1, dtype=int)
# D = diag(-d) + diag(ud,1)
# D[-1,0] = 1
# return D/dx
# ```
# We will also need a backward difference operator, $\mathbf{D}^-$, to enable us to compute second derivatives and Laplacians. It can be shown that $\mathbf{D}^-$ is given by the negative of the transpose of $\mathbf{D}^+$
# \begin{equation}
# \mathbf{D}^- = - \mathbf{D}^{+T}
# \end{equation}
# # Two-Dimensions
# To construct forward and backward difference operators for an entire 2D grid, we can using the Kronecker product. For a uniform structured grid of size $N_x \times N_y$ we can construct four operators: $\mathbf{D}_x^+$, $\mathbf{D}_x^-$, $\mathbf{D}_y^+$, $\mathbf{D}_y^-$ where the subscript refers to the direction in which the derivative is taken, e.g. $\mathbf{D}_y^+ u= \frac{\partial u}{\partial y}$ represents the forward difference approximation of $\frac{\partial u}{\partial y}$.
# The Kronecker product, defined via the symbol $\otimes$, is simply a way to produce matrices based on matrix outer products. Given matrices $\mathbf{A} = a_{ij}$ and $\mathbf{B} = b_{ij}$, then
# \begin{equation}
# \mathbf{A}\otimes\mathbf{B} = \begin{bmatrix} a_{11} \mathbf{B} & \cdots & a_{1n}\mathbf{B} \\ \vdots & \ddots & \vdots \\ a_{m1} \mathbf{B} & \cdots & a_{mn} \mathbf{B} \end{bmatrix}
# \end{equation}
#
# An example from Wikipedia is
#
# \begin{equation}
# \begin{bmatrix}
# 1 & 2 \\
# 3 & 4 \\
# \end{bmatrix}
# \otimes
# \begin{bmatrix}
# 0 & 5 \\
# 6 & 7 \\
# \end{bmatrix}=
# \begin{bmatrix}
# 1 \cdot \begin{bmatrix}
# 0 & 5 \\
# 6 & 7 \\
# \end{bmatrix} &
# 2 \cdot \begin{bmatrix}
# 0 & 5 \\
# 6 & 7 \\
# \end{bmatrix} \\
# 3 \cdot \begin{bmatrix}
# 0 & 5 \\
# 6 & 7 \\
# \end{bmatrix} &
# 4 \cdot \begin{bmatrix}
# 0 & 5 \\
# 6 & 7 \\
# \end{bmatrix} \\
# \end{bmatrix}=
# \begin{bmatrix}
# 1\cdot 0 & 1\cdot 5 & 2\cdot 0 & 2\cdot 5 \\
# 1\cdot 6 & 1\cdot 7 & 2\cdot 6 & 2\cdot 7 \\
# 3\cdot 0 & 3\cdot 5 & 4\cdot 0 & 4\cdot 5 \\
# 3\cdot 6 & 3\cdot 7 & 4\cdot 6 & 4\cdot 7 \\
# \end{bmatrix}.
# \end{equation}
# Back to our finite difference operators, on a 2D grid, we have the following
# \begin{equation}
# \bf{D}_x^ + = \left[ \begin{array}{*{20}{c}}
# \bf{D}^+ &{}&{}&{}&{}\\
# {}&\bf{D}^+&{}&{}&{}\\
# {}&{}& \ddots &{}&{}\\
# {}&{}&{}& \ddots &{}\\
# {}&{}&{}&{}&\bf{D}^+
# \end{array} \right] = \bf{I}_y \otimes \bf{D}^+({N_y})
# \end{equation}
# where $\mathbf{I}_y$ is the identity matrix of size $N_y\times N_y$ and $\mathbf{D}^+$ is of size $N_x\times N_x$. Similarly, we have
# \begin{equation}
# \mathbf{D}_x^- = \mathbf{I}_y\otimes \mathbf{D}^-(N_x)
# \end{equation}
# The effect of the Kronecker product $\mathbf{I}_y\otimes \mathbf{D}$ is to replicate $\mathbf{D}$ by $N_y$ times.
# The situation is a bit more different for the $y$-derivatives. In this case, we want to offset the off-diagonal components of $\mathbf{D}$ by $N_x$, i.e. $(i+1, j) \to (i, j+1)$. To do this, we have to right-multiply $\mathbf{D}$ by $\mathbf{I}_x$.
# \begin{equation}
# \mathbf{D}_y^+ = \mathbf{D}^+(N_y) \otimes \mathbf{I}_x
# \end{equation}
# and
# \begin{equation}
# \mathbf{D}_y^- = \mathbf{D}^-(N_y) \otimes \mathbf{I}_x
# \end{equation}
# where $\mathbf{I}_x$ is the identity matrix of size $N_x\times N_x$ and $\mathbf{D}^{+,-}$ is of size $N_y\times N_y$.
# # Three Dimensions
# In three dimensions, the operators can be constructed as follows
# \begin{equation}
# \mathbf{D_x^{+,-}} = \mathbf{I}_z \otimes \mathbf{I}_y \otimes \mathbf{D}^{+,-} \\
# \mathbf{D_y^{+,-}} = \mathbf{I}_z \otimes \mathbf{D}^{+,-} \otimes \mathbf{I}_x \\
# \mathbf{D_z^{+,-}} = \mathbf{D}^{+,-} \otimes \mathbf{I}_y \otimes \mathbf{I}_x \\
# \end{equation}
# **Bottom Line**
# 1. $\mathbf{I}(N) \otimes \mathbf{B}(M)$ creates a block diagonal matrix of $\mathbf{B}$'s
# 2. $\mathbf{B}(M) \otimes \mathbf{I}(N)$ offsets the off-diagonal terms in $\mathbf{B}$ by $N$
# ## Implementation
# We will implement the Kronecker product formulation in a single class called `GridOps`
# In[48]:
import numpy as np
from numpy import zeros, ones, eye, diag, kron
class GridOps:
'''
Creates x, y, and z-direction 2D, or 3D versions of a 1D operator defined by Op1D.
N : A list or array containing the grid dimensions, [Nx, Ny, Nz].
Op1D: The name of a basic 1D operator. Op1D must be of the form Op1D(n,h) where n is a single valued integer
designating the number of points in that one-dimension and h is the spacing.
Δ : A list or array containing the grid spacing, [dx, dy, dz]. Defaults to [1,1,1]
'''
def __init__(self, Op1D, N, Δ=[1,1,1]):
self.N = N
self.Δ = Δ
self.Op1D = Op1D
def OpX(self):
'''
Creates x-direction 2D, or 3D versions of a 1D operator defined by Op1D.
'''
N = self.N
Δ = self.Δ
Op = self.Op1D
return kron(eye(N[2]), kron(eye(N[1]), Op (N[0],Δ[0]) ) )
def OpY(self):
'''
Creates y-direction 2D, or 3D versions of a 1D operator defined by Op1D.
'''
N = self.N
Δ = self.Δ
Op = self.Op1D
return kron( eye(N[2]), kron(Op(N[1],Δ[1]), eye(N[0]) ) )
def OpZ(self):
'''
Creates z-direction 2D, or 3D versions of a 1D operator defined by Op1D.
'''
N = self.N
Δ = self.Δ
Op = self.Op1D
return kron(Op(N[2],Δ[2]), kron(eye(N[1]), eye(N[0]) ) )
def Sum(self):
'''
Returns the sum of all three operators. Useful for returning a Laplacian for example.
'''
return self.OpX() + self.OpY() + self.OpZ()
# Below are some example 1D finite difference operators
# In[49]:
def d_plus1d(n, h=1):
'''
Defines a basic forward difference operator in 1D (first derivative, first order)
'''
# periodic forward difference
d = np.ones(n, dtype=int)
ud = np.ones(n-1, dtype=int)
D = diag(-d) + diag(ud,1)
D[-1,0] = 1
return D/h
def d_minus1d(n, h=1):
'''
Defines a basic backward difference operator in 1D (first derivative, first order)
'''
return -d_plus1d(n,h).T
def laplacian1d(n, h=1):
'''
Defines a basic 1D Laplacian operator (second derivative), cell centered, second order
'''
result = 0 if n <= 1 else d_plus1d(n,h) @ d_minus1d(n,h)
return result
# In[50]:
import numpy as np
from numpy import zeros, ones, eye, diag, kron
get_ipython().run_line_magic('matplotlib', 'inline')
import matplotlib.pyplot as plt
get_ipython().run_line_magic('config', "InlineBackend.figure_format = 'svg'")
# In[51]:
print(d_plus1d(5))
# In[52]:
print(d_minus1d(5))
# In[53]:
print(laplacian1d(5))
# ## Two Dimensional Example
# In what follows, we build a few 2D versions of the operators we defined above
# In[54]:
N = [3,3,1] # set grid size to 4x4
# To define 2D versions of $\mathbf{D}^{+,-}$ and the 1d Laplacian, we simply call the class `GridOps`
# In[55]:
Dplus2D = GridOps(d_plus1d, N)
Dminus2D = GridOps(d_minus1d, N)
Lap2D = GridOps(laplacian1d, N)
# We can now visualize these operators by accessing the methods defined by `GridOps`. For example, The forward difference version of $\partial/\partial x$ on this 2D Grid is given calling `OpX()` on the corresponding `GridOps`
# In[56]:
Dxp = Dplus2D.OpX()
print(Dxp)
plt.matshow(Dxp)
# Similary, the backward difference representation of $\partial / \partial y$ is
# In[57]:
Dym = Dminus2D.OpY()
print(Dym)
plt.matshow(Dym)
# The Laplacian can be composed by adding `OpX()` and `OpY()'
# In[58]:
DG = Lap2D.OpX() + Lap2D.OpY()
print(DG)
plt.matshow(DG)
# ## Example: Helmholtz Hodge Decomposition
# We use the operators to conduct a projection
# \begin{equation}
# \mathbf{u} = \mathbf{u}_\perp + \nabla \phi
# \end{equation}
# such that $\nabla\cdot\mathbf{u}_\perp = 0$. Then, we take the divergence
# \begin{equation}
# \nabla \cdot \mathbf{u} = \nabla^2 \phi
# \end{equation}
# In terms of operators, we have
# \begin{equation}
# D \mathbf{u} = D G\phi
# \end{equation}
# then
# \begin{equation}
# \phi = (D G)^{-1} D \mathbf{u}
# \end{equation}
# and finally
# \begin{equation}
# \mathbf{u}_\perp = \mathbf{u} - G \phi = \mathbf{u} - G (D G)^{-1} D \mathbf{u}
# \end{equation}
#
#
# In[59]:
nx = 32
ny = 16
N = [nx, ny, 1]
X0 = 0.0
Xl = 1.0
Y0 = 0.0
Yl = 0.5
x_ = np.linspace(0,1,nx)
y_ = np.linspace(0,1,ny)
dx = (Xl-X0)/(nx-1) if nx!=1 else 1
dy = (Yl-Y0)/(ny-1) if ny!=1 else 1
Δ = [dx, dy, 1.0]
X,Y = np.meshgrid(x_,y_)
a = 1.0
b = 0.7
c = 0.2
ux = a*np.exp((-(X-b+0.25)**2)/(2*c**2) -((Y-b+0.25)**2)/(2*c**2))
uy = a*np.exp((-(X-b-0.1)**2)/(2*c**2) -((Y-b+0.1)**2)/(2*c**2))
plt.quiver(ux,uy)
# plt.contourf(X,Y,ux,10,cmap="RdBu_r")
# In[60]:
ux = np.sin(X*Y)
uy = np.cos(4.0*X*Y)
plt.quiver(ux,uy)
# In[61]:
Dplus2D = GridOps(d_plus1d, N, Δ)
Dminus2D = GridOps(d_minus1d, N, Δ)
Lap2D = GridOps(laplacian1d, N, Δ)
div_x = Dplus2D.OpX()
div_y = Dplus2D.OpY()
grad_x = Dminus2D.OpX()
grad_y = Dminus2D.OpY()
DG = div_x@grad_x + div_y@grad_y
DG[0]= 0
DG[0,0] = 1.0
print(np.linalg.cond(DG))
DG_inv = np.linalg.inv(DG)
Du = div_x@ux.ravel() + div_y@uy.ravel()
phi = DG_inv@Du
# In[62]:
uxperp = ux.ravel() - grad_x@phi
uyperp = uy.ravel() - grad_y@phi
plt.quiver(X,Y,uxperp.reshape(nx,ny), uyperp.reshape(nx,ny),pivot='middle')
# In[63]:
div_uperp = div_x@uxperp.ravel() + div_y@uyperp.ravel()
plt.matshow(div_uperp.reshape(ny,nx))
plt.colorbar()
# ## Three Dimensions
# In[64]:
N=[4,4,4]
Dplus3D = GridOps(d_plus1d, N)
Dminus3D = GridOps(d_minus1d, N)
Lap3D = GridOps(laplacian1d, N)
plt.matshow(Dplus3D.OpX())
plt.title('$\partial / \partial x$, forward diff \n')
plt.matshow(Dplus3D.OpY())
plt.title('$\partial / \partial y$, forward diff \n')
plt.matshow(Dplus3D.OpZ())
plt.title('$\partial / \partial z$, forward diff \n')
plt.matshow(Dminus3D.OpX())
plt.title('$\partial / \partial x$, backward diff \n')
plt.matshow(Dminus3D.OpY())
plt.title('$\partial / \partial y$, backward diff \n')
plt.matshow(Dminus3D.OpZ())
plt.title('$\partial / \partial z$, backward diff \n')
DG = Lap3D.Sum()
plt.matshow(DG)
plt.title('$\nabla^2$, central diff \n')
# In[1]:
import urllib
import requests
from IPython.core.display import HTML
def css_styling():
styles = requests.get("https://raw.githubusercontent.com/saadtony/NumericalMethods/master/styles/custom.css")
return HTML(styles.text)
css_styling()
# In[ ]: