#!/usr/bin/env python # coding: utf-8 # ###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2014 [David I. Ketcheson](http://davidketcheson.info) # #An illustrated guide to limiters # ## Or: how to interpolate non-smooth data without creating wiggles # 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. # ##Table of contents # - [Motivation: interpolation and wiggles](#Interpolation-and-wiggles) # - [The simplest limiter: Minmod](#The-simplest-limiter:-Minmod) # - [Other TVD limiters](#TVD-limiters) # - [WENO](#Higher-order-interpolation:-WENO) # In[1]: get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib.pyplot as plt import numpy as np import matplotlib matplotlib.rcParams.update({'font.size': 22}) # In[2]: 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. # ##Interpolation and wiggles # Suppose you're given a set of data samples: # In[3]: 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: # In[4]: 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.hold(True) 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: # In[5]: 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].hold(True) 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. # ## The problem: overshoots! # 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: # In[6]: 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](http://nbviewer.ipython.org/github/ketch/HyperPython/blob/master/Lesson_03_High-resolution_methods.ipynb) for details. # # Is there a better way? # ## The simplest limiter: Minmod # 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: # In[7]: 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]) # In[8]: yy = pw_minmod(x,y,xx) plt.figure(figsize=size) plt.plot(xx,yy,'-k',lw=2) plt.hold(True) 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: # In[9]: 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.hold(True) 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<=i0) 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].hold(True) 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](#The-problem:-overshoots!). # # 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. # #Interactive comparison # 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. # In[12]: from IPython.html.widgets import interact, FloatSlider, RadioButtons from IPython.display import display get_ipython().run_line_magic('matplotlib', 'inline') # In[13]: 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) ax.hold(True) 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) ax.hold(False) 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: # # - Which limiter usually chooses the flattest approximation? Does it always? # - Which limiter usually chooses the steepest approximation? Does it always? # - In which situations do all the limiters give the same slope? Why? # # Higher-order interpolation: WENO # 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: # In[30]: # Note: uses PyWENO v. 0.11.2 import sympy from pyweno import symbolic # In[31]: 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' # In[32]: 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)); # In[33]: 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) plt.hold(True) 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.hold(False) 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. # In[34]: p_opt = stencil_interpolant(x,y,5,0) plot_interpolants(x,y,[p_opt]) plt.title('Quartic interpolant'); # In[35]: fig, ax = plt.subplots(3,1,figsize=(width,10)) names = ['left','right','center'] p = [] for i in range(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: # In[36]: 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. # In[37]: 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. # In[38]: 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}$$ # In[39]: 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 # In[40]: 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): # In[41]: 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. # In[42]: 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)); # In[45]: get_ipython().run_line_magic('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: # In[46]: 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! # ## Comparison of several limiters for advection # 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](http://nbviewer.ipython.org/github/ketch/HyperPython/blob/master/Lesson_01_Advection.ipynb) and then the [lesson on high-resolution methods](http://nbviewer.ipython.org/github/ketch/HyperPython/blob/master/Lesson_03_High-resolution_methods.ipynb). # # The cells below solve the advection equation using several of the limiters we've discussed. To run this part, you need to [install PyClaw](http://www.clawpack.org/installing.html) and Visclaw, which can be most easily accomplished via # # pip install clawpack # In[47]: 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 # In[48]: #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 k0: 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. # In[ ]: