Lecture 6, Heat transports#
Two-dimensional advection-diffusion of heat
import xarray as xr
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
from IPython.display import HTML
from IPython.display import display
1) Background: two-dimensional advection-diffusion#
1.1) The two-dimensional advection-diffusion equation#
Recall from the last Lecture that the one-dimensional advection-diffusion equation is written as
where
The two-dimensional advection diffusion equation simply adds advection and diffusion operators acting in a second dimensions
where
Throughout the rest of the Climate Modelling module, we will consider
1.2) Reminder: Multivariable shorthand notation#
Conventionally, the two-dimensional advection-diffusion equation is written more succintly as
using the following shorthand notation.
The gradient operator is defined as
such that
The Laplacian operator
such that
The divergence operator is defined as
Note: Since seawater is largely incompressible, we can approximate ocean currents as a non-divergent flow, with
\begin{align} \vec{u} \cdot \nabla T&= u\frac{\partial T(x,y,t)}{\partial x} + v\frac{\partial T(x,y,t)}{\partial y}\newline &= u\frac{\partial T}{\partial x} + v\frac{\partial T}{\partial y} + T\left(\frac{\partial u}{\partial x} + \frac{\partial v}{\partial y}\right)\newline &= \left( u\frac{\partial T}{\partial x} + T\frac{\partial u}{\partial x} \right) + \left( v\frac{\partial T}{\partial y} + \frac{\partial v}{\partial y} \right) \newline &= \frac{\partial (uT)}{\partial x} + \frac{\partial (vT)}{\partial y}\newline &= \nabla \cdot (\vec{u}T) \end{align}
using the product rule (separately in both
This lets us finally re-write the two-dimensional advection-diffusion equation as:
which is the form we will use in our numerical algorithm below.
2) Numerical implementation#
2.1) Discretizing advection in two dimensions#
In the last lecture we saw that in one dimension we can discretize a first-partial derivative in space using the centered finite difference
In two dimensions, we discretize the partial derivative the exact same way, except that we also need to keep track of the cell index
The x-gradient kernel below, is shown below, and is reminiscent of the edge-detection or sharpening kernels used in image processing and machine learning:
plt.imshow([[-1,0,1]], cmap=plt.cm.RdBu_r)
plt.xticks([0,1,2], labels=[-1,0,1]);
plt.yticks([]);

The first-order partial derivative in
Its kernel is shown below.
plt.imshow([[-1,-1],[0,0],[1,1]], cmap=plt.cm.RdBu);
plt.xticks([]);
plt.yticks([0,1,2], labels=[1,0,-1]);

Now that we have discretized the two derivate terms, we can write out the advective tendency for computing
We implement this as a the advect
function. This computes the advective tendency for the
xkernel = np.zeros((1,3))
xkernel[0, :] = np.array([-1,0,1])
ykernel = np.zeros((3,1))
ykernel[:, 0] = np.array([-1,0,1])
class OceanModel:
def __init_(self,T, u, v, deltax, deltay):
self.T = [T]
self.u = u
self.v = v
self.deltax = deltax
self.deltay = deltay
self.j = np.arange(0, self.T[0].shape[0])
self.i = np.arange(0, self.T[0].shape[1])
def advect(self, T):
T_advect = np.zeros((self.j.size, self.i.size))
for j in range(1, T_advect.shape[0] - 1):
for i in range(1, T_advect.shape[1] - 1):
T_advect[j, i] = -(self.u[j, i] * np.sum(xkernel[0, :] * T[j, i-1 : i + 2])/(2*self.deltax) +
self.v[j, i] * np.sum(ykernel[:, 0] * T[j-1:j+2, i])/(2*self.deltay))
return T_advect
2.2) Discretizing diffusion in two dimensions#
Just as with advection, the process for discretizing the diffusion operators effectively consists of repeating the one-dimensional process for
The corresponding
plt.imshow([[1,1],[0,0],[1,1]], cmap=plt.cm.RdBu_r);
plt.xticks([]);
plt.yticks([0,1,2], labels=[1,-1,1]);

plt.imshow([[1,0,1]], cmap=plt.cm.RdBu_r);
plt.xticks([0,1,2], labels=[1,-1,1]);
plt.yticks([]);

xkernel_diff = np.zeros((1, 3))
xkernel_diff[0, :] = np.array([1,-2,1])
ykernel_diff = np.zeros((3,1))
ykernel_diff[:, 0] = np.array([1,-2,1])
Just as we did with advection, we implement a diffuse function using multiple dispatch:
class OceanModel():
def __init__(self, T, deltat, windfield, grid, kappa):
self.T = [T]
self.u = windfield.u
self.v = windfield.v
self.deltay = grid.deltay
self.deltax = grid.deltax
self.deltat = deltat
self.kappa = kappa
self.i = np.arange(0, self.T[0].shape[1])
self.j = np.arange(0, self.T[0].shape[0])
self.t = [0]
self.diffuse_out = []
self.advect_out = []
self.t_ = []
def advect(self, T):
T_advect = np.zeros((self.j.size, self.i.size))
for j in range(1, T_advect.shape[0] - 1):
for i in range(1, T_advect.shape[1] - 1):
T_advect[j, i] = -(self.u[j, i] * np.sum(xkernel[0, :] * T[j, i-1:i+2])/(2*self.deltax) + \
self.v[j, i] * np.sum(ykernel[:, 0] * T[j-1:j+2, i])/(2*self.deltay))
return T_advect
def diffuse(self, T):
T_diffuse= np.zeros((self.j.size, self.i.size))
for j in range(1, T_diffuse.shape[0] - 1):
for i in range(1, T_diffuse.shape[1] - 1):
T_diffuse[j, i] = self.kappa*(np.sum(xkernel_diff[0, :] * T[j, i-1:i+2])/(self.deltax**2) + \
np.sum(ykernel_diff[:, 0] * T[j-1:j+2, i])/(self.deltay**2))
return T_diffuse
2.3) No-flux boundary conditions#
We want to impose the no-flux boundary conditions, which states that
To impose this, we treat
Thus, we can implement the boundary conditions by updating the temperature of the ghost cells to match their interior-point neighbors:
def update_ghostcells(var):
var[0, :] = var[1, :];
var[-1, :] = var[-2, :]
var[:, 0] = var[:, 1]
var[:, -1] = var[:, -2]
return var
B = np.random.rand(6,6)
plt.pcolor(B)
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x13330ee00>

B = update_ghostcells(B)
plt.imshow(B)
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x1333f5660>

2.4) Timestepping#
class OceanModel():
def __init__(self, T, deltat, windfield, grid, kappa):
self.T = [T]
self.u = windfield.u
self.v = windfield.v
self.deltay = grid.deltay
self.deltax = grid.deltax
self.deltat = deltat
self.kappa = kappa
self.i = np.arange(0, self.T[0].shape[1])
self.j = np.arange(0, self.T[0].shape[0])
self.t = [0]
self.diffuse_out = []
self.advect_out = []
self.t_ = []
def advect(self, T):
T_advect = np.zeros((self.j.size, self.i.size))
for j in range(1, T_advect.shape[0] - 1):
for i in range(1, T_advect.shape[1] - 1):
T_advect[j, i] = -(self.u[j, i] * np.sum(xkernel[0, :] * T[j, i-1:i+2])/(2*self.deltax) + \
self.v[j, i] * np.sum(ykernel[:, 0] * T[j-1:j+2, i])/(2*self.deltax))
return T_advect
def diffuse(self, T):
T_diffuse= np.zeros((self.j.size, self.i.size))
for j in range(1, T_diffuse.shape[0] - 1):
for i in range(1, T_diffuse.shape[1] - 1):
T_diffuse[j, i] = self.kappa*(np.sum(xkernel_diff[0, :] * T[j, i-1:i+2])/(self.deltax**2) + \
np.sum(ykernel_diff[:, 0] * T[j-1:j+2, i])/(self.deltay**2))
return T_diffuse
def timestep(self):
T_ = self.T[-1].copy()
T_ = update_ghostcells(T_)
tendencies = self.advect(self.T[-1]) + self.diffuse(self.T[-1])
T_ = self.deltat * tendencies + T_
self.T.append(T_)
self.t.append(self.t[-1] + self.deltat)
class grid:
""" Creates grid for ocean model
Args:
-------
N: Number of grid points (NxN)
L: Length of domain
"""
def __init__(self, N, L):
self.deltax = L / N
self.deltay = L / N
self.xs = np.arange(0-self.deltax/2, L+self.deltax/2, self.deltax)
self.ys = np.arange(-L-self.deltay/2, L+self.deltay/2, self.deltay)
self.N = N
self.L = L
self.Nx = len(self.xs)
self.Ny = len(self.ys)
self.grid, _= np.meshgrid(self.ys, self.xs, sparse=False, indexing='ij')
class windfield:
"""Calculates wind velocity
Args:
------
Grid: class grid()
option: zero, or single gyre
"""
def __init__(self, grid, option = "zero"):
if option == "zero":
self.u = np.zeros((grid.grid.shape))
self.v = np.zeros((grid.grid.shape))
if option == "single_gyre":
u, v = PointVortex(grid)
self.u = u
self.v = v
Initialize
Initialize temperature field with rectangular non-zero box.
my_grid = grid(N = 11, L = 6e6)
my_wind = windfield(my_grid, option = "zero")
my_temp = np.zeros((my_grid.grid.shape))
my_temp[9:13, 4:8] = 1
my_grid.grid.shape
(23, 12)
plt.pcolor(my_grid.xs, my_grid.ys, my_temp)
plt.colorbar(label = "Temperature in °C");
plt.ylabel("Kilometers Latitude");
plt.xlabel("Kilometers Longitude");

deltat = 12*60*60 # 12 Hours
def plot_func(steps, temp, deltat, windfield, grid, kappa, anomaly = False):
my_model = OceanModel(T=temp,
deltat=deltat,
windfield=windfield,
grid=grid,
kappa = kappa)
for step in range(steps):
my_model.timestep()
plt.figure(figsize=(8,8))
lon, lat = np.meshgrid(grid.xs, grid.ys)
if anomaly == True:
plt.pcolor(my_grid.xs, my_grid.ys, my_model.T[-1] - my_model.T[-2], cmap = plt.cm.RdBu_r,
vmin = -0.001, vmax = 0.001)
else:
plt.pcolor(my_grid.xs, my_grid.ys, my_model.T[-1], vmin = 0, vmax = 1)
plt.quiver(lon, lat, windfield.u, windfield.v)
plt.colorbar(label = "Temperature in °C");
plt.ylabel("Kilometers Latitude");
plt.xlabel("Kilometers Longitude");
plt.title("Time: {} days".format(int(step/2)))
plt.show()
interact(plot_func,
temp = fixed(my_temp),
deltat = fixed(deltat),
windfield = fixed(my_wind),
grid = fixed(my_grid),
kappa = fixed(1e4),
anomaly = widgets.Checkbox(description = "Anomaly", value=False),
steps = widgets.IntSlider(description="Timesteps", value = 2, min = 1, max = 10000, step = 1));
Create single gyre windfield#
def diagnose_velocities(stream, G):
dx = np.zeros((G.grid.shape))
dy = np.zeros((G.grid.shape))
dx = (stream[:, 1:] - stream[:, 0:-1])/G.deltax
dy = (stream[1:, :] - stream[0:-1, :])/G.deltay
u = np.zeros((G.grid.shape))
v = np.zeros((G.grid.shape))
u = 0.5*(dy[:, 1:] + dy[:, 0:-1])
v = -0.5*(dx[1:, :] + dx[0:-1, :])
return u, v
def impose_no_flux(u, v):
u[0, :] = 0; v[0, :] = 0;
u[-1,:] = 0; v[-1,:] = 0;
u[:, 0] = 0; v[:, 0] = 0;
u[:,-1] = 0; v[:,-1] = 0;
return u, v
def PointVortex(G, omega = 0.6, a = 0.2, x0=0.5, y0 = 0.):
x = np.arange(-G.deltax/G.L, 1 + G.deltax / G.L, G.deltax / G.L).reshape(1, -1)
y = np.arange(-1-G.deltay/G.L, 1+ G.deltay / G.L, G.deltay / G.L).reshape(-1, 1)
r = np.sqrt((y - y0)**2 + (x - x0)**2)
stream = -omega/4*r**2
stream[r > a] = -omega*a**2/4*(1. + 2*np.log(r[r > a]/a))
u, v = diagnose_velocities(stream, G)
u, v = impose_no_flux(u, v)
u = u * G.L; v = v * G.L
return u, v
my_wind = windfield(my_grid, option="single_gyre")
plt.quiver(my_wind.u, my_wind.v)
<matplotlib.quiver.Quiver at 0x134f329b0>

my_temp = np.zeros((my_grid.grid.shape))
my_temp[8:13, :] = 1
plt.pcolor(my_temp)
<matplotlib.collections.PolyCollection at 0x134f30190>

my_model = OceanModel(T=my_temp,
deltat=deltat,
windfield=my_wind,
grid=my_grid,
kappa = 1e4)
interact(plot_func,
temp = fixed(my_temp),
deltat = fixed(deltat),
windfield = fixed(my_wind),
grid = fixed(my_grid),
kappa = widgets.IntSlider(description="kappa", value = 1e4, min=1e3, max = 1e5, step = 100),
anomaly = widgets.Checkbox(description = "Anomaly", value=False),
steps = widgets.IntSlider(description="Timesteps", value = 1, min = 1, max = 1000, step = 1));
Optional: Create two gyres!