Many interesting wave phenomena -- like fluid dynamics, lasers, and water waves -- are described by nonlinear hyperbolic partial differential equations. The solutions of these problems are discontinuous. So-called limiters (sometimes referred to as slope limiters or flux limiters are one of the key ingredients in approximating these discontinuous solutions.
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import matplotlib
matplotlib.rcParams.update({'font.size': 22})
import mpld3 # Skip this cell if you don't have mpld3 installed
mpld3.enable_notebook() # or just go and do it now: pip install mpld3
# This allows you to zoom and pan the plots.
Suppose you're given a set of data samples:
k = 5
x=np.arange(-k+1,k)
y=np.sin(x/2.)+1.
width = 12
size = (width,4)
plt.figure(figsize=size)
plt.plot(x,y,'or',markersize=10,alpha=0.5)
plt.axis( (-k, k, -0.1, 2.1) );
Now what you really want to know is, what is the state of the system at the points halfway between your samples? And to figure that out, you need to guess at what's going on in the times in-between those samples. The simplest approximation would be to assume that the system just jumps from one value to the next somewhere in-between:
def piecewise_constant_interp(x,y,xx):
"From samples (x,y) generate piecewise constant function sampled at points xx."
diff = np.abs(x.reshape(1,-1) - xx.reshape(-1,1)) # Here we use numpy broadcasting.
closest = np.argmin(diff,axis=1)
return y[closest]
xx = np.linspace(-k+1,k-1,1000)
yy = piecewise_constant_interp(x,y,xx)
plt.figure(figsize=size)
plt.plot(xx,yy,'-k',lw=2)
plt.plot(x,y,'or',markersize=10,alpha=0.5)
plt.axis( (-k, k, -0.1, 2.1) );
plt.title('Piecewise-constant approximation',fontsize=20);
For this set of data, you don't really believe that's what's happening, do you? But our goal is to deal with systems that exhibit non-smooth (possibly discontinuous) behavior, so we need to at least admit the possibility of sudden jumps. That's why we won't simply "connect the dots" to get a continuous approximation.
Instead, we can try to approximate the slope around each of our sample points. The simplest way to do so is using finite differences. If we let $\sigma_i$ denote our approximation of the slope at $x_i$, then three common approximations are:
Forward difference: $\sigma_i = \frac{y_{i+1}-y_i}{x_{i+1}-x_i}$
Backward difference: $\sigma_i = \frac{y_i - y_{i-1}}{x_i - x_{i-1}}$
Centered difference: $\sigma_i = \frac{y_{i+1}-y_{i-1}}{x_{i+1}-x_{i-1}}$
Here's what each of these approximations looks like for our data:
def piecewise_linear_interp(x,y,xx, fd='centered'):
"From samples (x,y) generate piecewise-linear function sampled at points xx using finite difference slopes."
diff = np.abs(x.reshape(1,-1) - xx.reshape(-1,1))
closest = np.argmin(diff,axis=1)
sigma = np.zeros_like(y)
if fd == 'centered':
sigma[1:-1] = (y[2:]-y[:-2])/(x[2:]-x[:-2])
elif fd == 'forward':
sigma[:-1] = (y[1:]-y[:-1])/(x[1:]-x[:-1])
elif fd == 'backward':
sigma[1:] = (y[1:]-y[:-1])/(x[1:]-x[:-1])
return y[closest] + sigma[closest]*(xx-x[closest])
def compare_fd(x,y,xx, axis=(-4, 4, -0.1, 2.1)):
fig, ax = plt.subplots(3,1,figsize=(width,8))
for i, fd in enumerate( ('centered','forward','backward') ):
yy = piecewise_linear_interp(x,y,xx,fd=fd)
ax[i].plot(xx,yy,'-k',lw=2)
ax[i].plot(x,y,'or',markersize=10,alpha=0.5)
ax[i].axis( axis );
ax[i].text(.5,.9,fd,
horizontalalignment='center',
transform=ax[i].transAxes,fontsize=20)
compare_fd(x,y,xx)
I've used $\sigma=0$ for the points at the edges where we don't have enough data to compute the appropriate slope.
Looking closely, you can see that each of these approximations adds little jumps (called "overshoots") in some region where the data itself was monotone. Worse still, each of them generates negative values, whereas the original values were non-negative! If our data represent concentrations or probabilities, then we have no way to make sense of negative values.
Things look even worse if we take data samples from a function that is in fact discontinuous:
y = np.sin(x/2.)+1. + 2.*(x>0)
compare_fd(x,y,xx,axis=(-4,4,-0.5,4.8))
Now all three approaches have large, obvious overshoots. This becomes even more problematic when actually solving a hyperbolic PDE; see Lesson 3 of my HyperPython course for details.
Is there a better way?
We'd like to avoid those overshoots and ensure that monotone regions of the data give monotone interpolations. We can do that by choosing the slope $\sigma_i$ small enough that the interpolant near $x_i$ stays bounded between the neighboring averages $(y_{i-1}+y_i)/2$ and $(y_i+y_{i+1})/2$. There's an easy way to do that: just compute the forward and backward differences (like we did above), and then use whichever is smaller in absolute value. If $y_i$ is an extremum, then to avoid increasing the overall range of the data we always choose $\sigma_i=0$.
Here's what that looks like:
def pw_minmod(x,y,xx):
"From samples (x,y) generate piecewise-linear function sampled at points xx using Minmod slopes."
diff = np.abs(x.reshape(1,-1) - xx.reshape(-1,1))
closest = np.argmin(diff,axis=1)
forward = np.zeros_like(y)
backward = np.zeros_like(y)
sigma = np.zeros_like(y)
forward[:-1] = (y[1:]-y[:-1])/(x[1:]-x[:-1])
backward[1:] = (y[1:]-y[:-1])/(x[1:]-x[:-1])
sigma = (np.sign(forward)+np.sign(backward))/2. * np.minimum(np.abs(forward),np.abs(backward))
return y[closest] + sigma[closest]*(xx-x[closest])
yy = pw_minmod(x,y,xx)
plt.figure(figsize=size)
plt.plot(xx,yy,'-k',lw=2)
plt.plot(x,y,'or',markersize=10,alpha=0.5)
plt.axis( (-4,4,-0.5,4.8) );
plt.title('Minmod approximation',fontsize=20);
Let's apply minmod to a monotone sequence of values, to illustrate the average-boundedness property:
from matplotlib.patches import Rectangle
y = np.exp(x/3.)
yy = pw_minmod(x,y,xx)
plt.figure(figsize=(width,6))
plt.plot(xx,yy,'-k',lw=2)
plt.plot(x,y,'or',markersize=10,alpha=0.5)
plt.axis( (-4,4,-0.1,4.1) );
plt.title('minmod approximation',fontsize=20);
for i in range(len(y)-1):
if 1<=i<len(y):
x_avgs = [(x[i]+x[i-1])/2.,(x[i]+x[i+1])/2.]
y_avgs = [(y[i]+y[i-1])/2.,(y[i]+y[i+1])/2.]
currentAxis = plt.gca()
currentAxis.add_patch(Rectangle((x_avgs[0], y_avgs[0]),
x_avgs[1]-x_avgs[0], y_avgs[1]-y_avgs[0],
facecolor="grey",alpha=0.5))
The grey regions show the average of each value and its neighbors; the Minmod interpolant is guaranteed to stay within these bounds, so that the right edge of one interpolated region doesn't "overshoot" the leftmost value of the next.
In fact, the minmod limiter guarantees an important mathematical property: it does not increase the total variation (TV) of the data. For discrete data, TV is defined as
$$ TV(y) = \sum_i |y_i - y_{i-1}|.$$Essentially, TV is a measure of how much a function wiggles! This total variation diminishing (TVD) property of minmod is nice, because the true solutions of scalar hyperbolic PDEs have the same property. Because of this, in the 1970s-80s, researchers developed many other limiters that enforce the same property; they are referred to as
Just for fun, let's look at 3 more such limiters. If we define $F_i, B_i$ as forward- and backward-difference slope approximations at point $i$, and $\theta_i$ as the ratio of the two, then each of these limiters can be described in terms of a function $\phi(\theta)$. The slope to be used is given by
$$\sigma_i = \phi(\theta_i) B_i.$$The limiter names and functions are:
Monotonized centered difference (MC): $$\phi(\theta) = \max(0,\min((1+\theta)/2,2,\theta))$$
van Leer: $$\phi(\theta) = \frac{\theta + |\theta|}{1+ |\theta|}$$
Superbee: $$\phi(\theta) = \max(0,\min(1,2\theta),\min(2,\theta))$$
Here's what each of them looks like for our data:
def phi(theta,limiter):
if limiter == 'minmod':
phi = (1+np.sign(theta))/2. * np.minimum(1,theta)
elif limiter == 'vanleer':
phi = (theta + np.abs(theta))/(1+np.abs(theta))
elif limiter == 'MC':
phi = np.maximum(0,np.minimum( (1.+theta)/2., np.minimum(2.,theta)))
elif limiter == 'superbee':
phi = np.maximum(0,np.maximum(np.minimum(1.,2*theta),np.minimum(2.,theta)))
return phi
def pw_limited(x,y,xx,limiter='minmod'):
"From samples (x,y) generate piecewise-linear function sampled at points xx using Minmod slopes."
diff = np.abs(x.reshape(1,-1) - xx.reshape(-1,1))
closest = np.argmin(diff,axis=1)
forward = np.zeros_like(y)
backward = np.zeros_like(y)
theta = np.zeros_like(y)
forward[:-1] = (y[1:]-y[:-1])/(x[1:]-x[:-1])
backward[1:] = (y[1:]-y[:-1])/(x[1:]-x[:-1])
theta[1:-1] = forward[1:-1]/backward[1:-1]
sigma = phi(theta,limiter) * backward
return y[closest] + sigma[closest]*(xx-x[closest])
y = np.sin(x/2.)+1. + 2.*(x>0)
fig, ax = plt.subplots(4,1,figsize=(width,10))
for i, limiter in enumerate( ('minmod', 'vanleer','superbee','MC') ):
yy = pw_limited(x,y,xx,limiter=limiter)
ax[i].plot(xx,yy,'-k',lw=2)
ax[i].plot(x,y,'or',markersize=10,alpha=0.5)
ax[i].axis( (-4,4,-0.1,4.4) );
ax[i].text(.8,.2,limiter,
horizontalalignment='center',
transform=ax[i].transAxes,fontsize=20)
Compare these with the finite difference approximations above.
If you look closely (or zoom in) you'll notice that -- except for minmod -- all the limiters do produce some overshoot near the discontinuity. What gives? Well, these limiters are used within a larger algorithm for solving hyperbolic PDEs, and it turns out that if the overshoots are small enough, they'll go away in a full step of the algorithm. These limiters produce "small enough" overshoots so that no oscillations appear in the PDE solution.
In each region, these limiter take three data points and give back a linear interpolant. It's illuminating to compare their behavior on a single set of 3 points. Note that the interactive plot below doesn't work on nbviewer; you'll need to download and run the notebook yourself.
from ipywidgets import interact, FloatSlider, RadioButtons
from IPython.display import display
%matplotlib inline
xx = np.linspace(-0.5,0.5)
def compare_limiters(y1,y3):
fig, ax = plt.subplots(figsize=(width,4))
x = np.array((-1.,0.,1.))
y = np.array((y1,0.,y3))
ax.set_xlim(-1.1,1.1)
ax.set_ylim(-1.1,1.1)
if y1 == 0:
theta = y3
else:
theta = y3/(-y1)
forward_slope = y3
backward_slope = -y1
plt.fill_between(xx,xx*forward_slope,xx*backward_slope,color='k',alpha=0.2,zorder=0)
for limiter in ('minmod', 'vanleer','superbee','MC'):
sigma = phi(np.array(theta),limiter)*(-y1)
ax.plot(xx,sigma*xx,alpha=0.5,lw=2)
ax.legend( ('','minmod', 'vanleer','superbee','MC'), loc='best' )
ax.plot(x,y,'ok',markersize=15,alpha=0.5)
#return fig
interact(compare_limiters,y1=FloatSlider(min=-1., max=1., step=0.1, value=-0.3,description='$y_{i-1}$',labelcolor='k'),#,orientation='vertical'),
y3=FloatSlider(min=-1., max=1., step=0.1, value=0.8,description='$y_{i+1}$'));#,orientation='vertical'));
The shaded region in the plot above shows the range of slopes that would give at least 2nd-order accuracy. Play with the sliders and answer the following questions:
If we want to get higher-order accuracy (in smooth regions), then we have to give up the TVD property -- at least, in the sense defined above. The most common approach for higher order non-oscillatory piecewise interpolation is known as weighted essentially non-oscillatory (WENO) interpolation.
WENO is a very effective technique for interpolating or reconstructing functions that contain discontinuities without introducing oscillations. We'll focus on 5th-order WENO interpolation, which is the most commonly used.
Let's generate some function values to interpolate:
# Note: uses PyWENO v. 0.11.2
import sympy
from pyweno import symbolic
import mpld3 # Skip this cell if you don't have mpld3 installed
mpld3.enable_notebook() # or just go and do it now: pip install mpld3
# This allows you to zoom and pan the plots.
matplotlib.rcParams.update({'font.size': 18})
colors = 'brg'
weno_order = 5 # must be odd
k = (weno_order+1)/2
size = (width,4)
plt.figure(figsize=size)
x=np.arange(-k+1,k)
y=np.random.rand(len(x))
#y = np.array((1.,1.,1.,0.,0.))
plt.plot(x,y,'ok')
plt.axis((-(k-.5),k-.5,-0.5,2.1));
def stencil_interpolant(x,y,n,offset):
"""Return the polynomial interpolant (of degree n-1)
through the points (x_j,y_j) for offset <= j <= offset+n-1.
"""
return np.poly1d(np.polyfit(x[offset:offset+n],y[offset:offset+n],n-1))
def plot_interpolants(x,y,interpolants,axis=None,color='kbrg'):
if axis is None:
fig, axis = plt.subplots(figsize=size)
xc = np.linspace(-0.5,0.5)
xx = np.linspace(-(k-1),k-1)
for i, interpolant in enumerate(interpolants):
axis.plot(xx,interpolant(xx),'-'+color[i])
axis.plot(xc,interpolant(xc),'-'+color[i],linewidth=5,alpha=0.5)
axis.plot(x,y,'ok')
axis.axis((-(k-.5),k-.5,-0.5,2.1));
Ordinary polynomial interpolation yields an oscillatory polynomial that also exceeds the bounds of the data:
For application to hyperbolic conservation laws, our main interest is in getting values of the function at the half-integer points (interfaces). Let's suppose we're trying to interpolate around $x=0$, at $x=\pm 1/2$. Instead of using all 5 points, we could just use three points, which might give us a less oscillatory interpolant, at least in that interval. Using the 5 points we're given, there are three natural choices of interpolation stencil: the leftmost three, the middle three, or the rightmost three. Let's see what each of these quadratic interpolants looks like.
p_opt = stencil_interpolant(x,y,5,0)
plot_interpolants(x,y,[p_opt])
plt.title('Quartic interpolant');
fig, ax = plt.subplots(3,1,figsize=(width,10))
names = ['left','right','center']
p = []
for i in range(int(k)):
p.append(stencil_interpolant(x,y,k,i))
plot_interpolants(x,y,[p[i]],axis=ax[i],color=[colors[i]])
ax[i].set_title(names[i]+' interpolant')
Here are all three quadratic interpolants together with the quartic interpolant for comparison:
plot_interpolants(x,y,p+[p_opt],color='brgk')
The quadratic interpolants look less oscillatory, but they're also less accurate. The WENO idea is to use the high-order interpolant (with all 5 points) if the data is smooth, but to use one of the lower-order interpolants (or a combination of them) if the data is not smooth. This is achieved by computing point values of the interpolant as weighted averages of the point values of the candidate polynomials, e.g.
$$y_{x_{i-1/2}} = w_{1,-1/2} p_\text{left}(x_{i-1/2}) + w_{2,-1/2} p_\text{center}(x_{i-1/2}) + w_{3,-1/2} p_\text{right}(x_{i-1/2}).$$Of course, there is some particular set of weights that gives the quartic interpolant:
$$y_{x_{i-1/2}} = \gamma_{1,-1/2} p_\text{left}(x_{i-1/2}) + \gamma_{2,-1/2} p_\text{center}(x_{i-1/2}) + \gamma_{3,-1/2} p_\text{right}(x_{i-1/2}).$$We will want to have $w_{j,-1/2} \approx \gamma_{j,-1/2}$ for smooth data.
def compute_opt_weights(k,xi):
"""
Get the optimal weights (gamma) at points xi.
"""
if not hasattr(xi,'__iter__'): xi = [xi]
opt_weights = symbolic.optimal_weights(k,xi)
gamma = {}
for i, xi_val in enumerate(xi):
gamma[xi_val] = np.empty(k)
for j in range(k):
gamma[xi_val][j] = opt_weights[0][(i,j)]
return gamma
gamma = compute_opt_weights(k,(-1,0.5,1))
print "$\gamma_{j,-1/2}$:", gamma[-1]
print "$\gamma_{j,+1/2}$:", gamma[1]
How does one determine if a polynomial is non-oscillatory? There are several ways proposed in the literature, but the original and most widely used is the weighted Sobolev norm:
$$\beta = \sum_{l=1}^k \Delta x^{2l-1} \int_{x_{i-1/2}}^{x_{i+1/2}} \left(\frac{d^l}{dx^l}p(x)\right)^2 dx.$$Put simply, $\beta$ is a scaled sum of the square $L^2$ norms of all the derivatives of the polynomial over the interval where it will be used. The scaling is chosen to make the "smoothness indicator" $\beta$ independent of the choice of $\Delta x$ (note that $\Delta x = 1$ in our example data).
As each of the interpolants above is a linear function of the values $y_i$, the smoothness indicators are quadratic functions of the $y_i$ and can be expressed in the generic form
$$\beta = \sum_{m=-2}^{2} \sum_{n=-2}^{m} C_{m,n} y_{i-k+m} y_{i-k+n}$$Of course, the coefficients $C_{m,n}$ will be different for each of the candidate polynomials $p_\text{left},p_\text{center},p_\text{right}$. We can use the Python package PyWeno to automatically compute these coefficients and then apply them to our data.
def compute_smoothness_indicators(y,k):
C = symbolic.jiang_shu_smoothness_coefficients(k)
beta = np.zeros((k,1))
for m in range(k):
for n in range(m+1):
for r in range(len(beta)):
beta[r] = beta[r] + C[(r,n,m)] * y[r+m] * y[r+n]
return beta
beta = compute_smoothness_indicators(y,k)
print beta
Next we use these smoothness indicators to determine a weighting for the candidate polynomials. Observe that a large smoothness indicator means a polynomial has large derivatives, so we will want to give it less weight (perhaps they should be called non-smoothness indicators).
$$\tilde{w}_j = \frac{\gamma_j}{(\epsilon + \beta_j)^2}$$Here $\epsilon$ is a small number used to avoid division by zero. We also normalize the weights so that they sum to unity:
$$w_j = \frac{\tilde{w}_j}{\sum_j\tilde{w}_j}$$def compute_weights(gamma, beta, epsilon=1.e-6):
k = len(beta)
w = np.empty(k)
for j in range(k):
w[j] = gamma[j]/(epsilon+beta[j])**2
wsum = np.sum(w)
return w/wsum
q = {}
for xi in (-1,1):
w = compute_weights(gamma[xi],beta)
q[xi] = w[0]*p[0](xi/2.) + w[1]*p[1](xi/2.) + w[2]*p[2](xi/2.)
Here are the final reconstructed values given by WENO (indicated by the large grey circles):
plot_interpolants(x,y,p+[p_opt],color=['b','r','g','k'])
plt.hold(True)
plt.plot(-0.5,q[-1],'ok',alpha=0.3,markersize=15)
plt.plot(0.5,q[1],'ok',alpha=0.3,markersize=15)
plt.axis((-(k-.5),k-.5,-0.5,2.1));
Here's some code to plot everything for some given $(x,y)$ values.
styles = { 'left' : 'b', 'center' : 'r', 'right' : 'g'}
size = (16,4); fs = 20
def WENO_visualization(x,y,xi=(-1,1)):
"""
(x,y): data to interpolate
xi: points at which to evaluate interpolant (w.r.t. (-1,1) reference interval)
"""
xx = np.linspace(np.min(x),np.max(x))
color=['b','r','g']
plt.figure(figsize=size)
plt.hold(True)
ax1 = plt.subplot2grid((1,8), (0,0), colspan=6)
ax2 = plt.subplot2grid((1,8), (0,6))
ax3 = plt.subplot2grid((1,8), (0,7))
K = len(y)
k = (K+1)/2
assert len(x)==K
p_opt=np.poly1d(np.polyfit(x,y,K-1))
p = {}
for name, offset in zip(('left','right','center'),range(k)):
p[name] = stencil_interpolant(x,y,k,offset)
gamma = compute_opt_weights(k,xi)
beta = compute_smoothness_indicators(y,k)
w = {}; q = {}
for loc in xi:
w[loc] = compute_weights(gamma[loc],beta)
q[loc] = w[loc][0]*p['left'](loc/2.) \
+ w[loc][1]*p['center'](loc/2.) \
+ w[loc][2]*p['right'](loc/2.)
ax2.bar(range(3),w[-1],color=color,align='center');
ax2.set_title(r'$w_{i-1/2}$',fontsize=fs)
ax3.bar(range(3),w[1],color=color,align='center')
ax3.set_title(r'$w_{i+1/2}$',fontsize=fs)
for ax in (ax2,ax3):
ax.set_xticks(range(3));
ax.set_xticklabels(('left','center','right'))
ax.set_ylim(0,1); ax.set_yticks((0,1))
for name, interpolant in p.iteritems():
ax1.plot(xx,interpolant(xx),styles[name])
xc = np.linspace(-0.5,0.5)
ax1.plot(xc,interpolant(xc),styles[name],linewidth=5)
ax1.plot(x,y,'ok')
ax1.hold(True)
ax1.plot(xx,p_opt(xx),'-k',x,y,'ok',linewidth=2)
for loc in xi:
ax1.plot(loc/2.,q[loc],'ok', alpha=0.3,markersize=15)
ax1.plot(loc/2.,q[loc],'ok',alpha=0.3,markersize=15)
ax1.axis((-(k-0.8),k-0.8,-0.5,2.1));
%matplotlib inline
y=np.random.rand(len(x))
WENO_visualization(x,y)
The bar charts on the right show the relative weight given to each of the quadratic interpolants when computing the left and right interpolated values. Try running the box above a few times, or insert your own $y$ values. What happens if you use a step function for $y$? Let's see:
y=np.array( (1,1,1,0,0) )
WENO_visualization(x,y)
For a perfect step function, WENO picks the flat interpolant, just like any TVD limiter would!
In practice, all of these limiters are used as components of numerical solvers for hyperbolic PDEs. The simplest hyperbolic PDE is the advection equation:
$$ q_t + a q_x = 0.$$The solution $q$ simply translates at velocity $a$; if you're not familiar with this, take a look at my HyperPython lesson on advection and then the lesson on high-resolution methods.
The cells below solve the advection equation using several of the limiters we've discussed. To run this part, you need to install PyClaw and Visclaw, which can be most easily accomplished via
pip install clawpack
from clawpack import pyclaw
from clawpack import riemann
import matplotlib
from matplotlib import animation
from clawpack.visclaw.JSAnimation import IPython_display
def setup(scheme='minmod',cfl_max=0.9,IC='gauss_square',mx=100):
if 'weno' in scheme:
solver = pyclaw.SharpClawSolver1D(riemann.advection_1D)
else:
solver = pyclaw.ClawSolver1D(riemann.advection_1D)
solver.bc_lower[0] = pyclaw.BC.periodic
solver.bc_upper[0] = pyclaw.BC.periodic
if scheme in ('minmod','superbee','MC','vanleer'):
solver.limiters = getattr(pyclaw.limiters.tvd,scheme)
#elif scheme == 'CT':
#solver.limiters = pyclaw.limiters.tvd.cada_torrilhon_limiter
elif scheme == 'Lax-Wendroff':
solver.limiters = 0
elif scheme == 'first-order':
solver.order = 1
elif 'weno' in scheme:
solver.weno_order = int(scheme[4:]) #weno5, weno7, ...
else:
raise Exception('Unrecognized limiter')
solver.cfl_max = cfl_max
solver.cfl_desired = cfl_max*0.9
x = pyclaw.Dimension(0.0,1.0,mx)
domain = pyclaw.Domain(x)
num_eqn = 1
state = pyclaw.State(domain,num_eqn)
state.problem_data['u']=1.
grid = state.grid
xc = grid.x.centers
if IC=='gauss_square':
beta=200.; x0=0.3
state.q[0,:] = np.exp(-beta * (xc-x0)**2) + (xc>0.6)*(xc<0.8)
elif IC=='wavepacket':
beta=100.; x0=0.5
state.q[0,:] = np.exp(-beta * (xc-x0)**2) * np.sin(80.*xc)
else:
raise Exception('Unrecognized initial condition.')
claw = pyclaw.Controller()
claw.solution = pyclaw.Solution(state,domain)
claw.solver = solver
claw.keep_copy = True
claw.output_format = None
claw.tfinal =10.0
return claw
#This cell may take a few seconds to run
results = []
schemes = ('first-order','Lax-Wendroff','minmod','superbee','MC','vanleer','weno5','weno7','weno9')
for scheme in schemes:
claw = setup(scheme=scheme)
claw.verbosity = 0
claw.run()
results.append(claw.frames)
def animate(results,ymin=-0.1):
fig = plt.figure(figsize=(width,8))
N = len(results)
n = int(np.ceil(np.sqrt(N)))
axes = []
gs1 = matplotlib.gridspec.GridSpec(n, n)
gs1.update(wspace=0.,hspace=0.)
for i in range(n):
for j in range(n):
k = n*i + j
if k<N:
axes.append(plt.subplot(gs1[i,j]));
if i<n-1:
axes[-1].xaxis.set_ticklabels(())
if j>0:
axes[-1].yaxis.set_ticklabels(())
lines = [0]*len(schemes)
for i in range(len(lines)):
lines[i], = axes[i].plot([], [], lw=2)
xc = results[0][0].p_centers[0]
for i,ax in enumerate(axes):
ax.set_xlim(0,1); ax.set_ylim(ymin,1.3)
#ax.grid()
ax.set_title(schemes[i], x = 0.5, y=0.85 )
ax.plot(xc,results[i][0].q[0,:],color='k',alpha=0.3)
def fplot(frame_number):
fig.suptitle('Solution after %s cycles' % frame_number, fontsize=20)
for i, line in enumerate(lines):
line.set_data(xc,results[i][frame_number].q[0,:])
return lines,
return matplotlib.animation.FuncAnimation(fig, fplot, frames=len(claw.frames), interval=30)
animate(results)
In the plot above, the solution advects across the full domain once between each frame of the animation (the boundary is periodic). By stepping through the animation, you can see how each limiter modifies the shape of the solution over time. The Lax-Wendroff method is based on a centered-difference approximation with no limiting; notice that it creates oscillations and is also less accurate than the limiter-based methods. For the advection equation, oscillations and overshoots are not a serious problem, but in the context of fluid dynamics or water wave simulations, they can be catastrophic.