Welcome to Riding the wave: Convection problems, the third module of "Practical Numerical Methods with Python".
In the first module, we learned about numerical integration methods for the solution of ordinary differential equations (ODEs). The second module introduced the finite difference method for numerical solution of partial differential equations (PDEs), where we need to discretize both space and time.
This module explores the convection equation in more depth, applied to a traffic-flow problem. We already introduced convection in Lesson 1 of Module 2. This hyperbolic equation is very interesting because the solution can develop shocks, or regions with very high gradient, which are difficult to resolve well with numerical methods.
We will start by introducing the concept of a conservation law, closely related to the convection equation. Then we'll explore different numerical schemes and how they perform when shocks are present.
You know from (non relativistic) physics that mass is conserved. This is one example of a conserved quantity, but there are others (like momentum and energy) and they all obey a conservation law. Let's start with the more intuitive case of conservation of mass.
In any closed system, we know that the mass M in the system does not change, which we can write: DMDt=0. When we change the point of view from a closed system to what engineers call a control volume, mass can move in and out of the volume and conservation of mass is now expressed by:
Let's imagine the control volume as a tiny cylinder of cross-section dA and length dx, like in the sketch below.
If we represent the mass density by ρ, then mass is equal to ρ× volume. For simplicity, let's assume that mass flows in or out of the control volume only in one direction, say, the x-direction. Express the 1D velocity component by u, and conservation of mass for the control volume is translated to a mathematical expression as follows:
∂∂t∫cvρdV+∫csρudA=0where "cv" stands for control volume and "cs" stands for control surface. The first term represents the rate of change of mass in the control volume, and the second term is the rate of flow of mass, with velocity u, across the control surface.
Since the control volume is very small, we can take, to leading order, ρ as a uniform quantity inside it, and the first term in equation (1) can be simplified to the time derivative of density multiplied by the volume of the tiny cylinder, dAdx:
∂∂t∫cvρdV→∂ρ∂tdAdxNow, for the second term in equation (1), we have to do a little more work. The quantity inside the integral is now ρu and, to leading order, we have to take into consideration that this quantity can change in the distance dx. Take ρu to be the value in the center of the cylinder. Then the flow of mass on each side is illustrated in the figure below, where we use a Taylor expansion of the quantity ρu around the center of the control volume (to first order).
Subtracting the negative flux on the left to the positive flux on the right, we arrive at the total flux of mass across the control surfaces, the second term in equation (1):
∫csρudA→∂∂x(ρu)dAdxWe can now put together the equation of conservation of mass for the tiny cylindrical control volume, which after diving by dAdx is:
∂ρ∂t+∂∂x(ρu)=0This is the 1D mass conservation equation in differential form. If we take u to be a constant and take it out of the spatial derivative this equation looks the same as the first PDE we studied: the linear convection equation in Lesson 1 of Module 2. But in the form shown above, it is a typical conservation law. The term under the spatial derivative is called the flux, for reasons that should be clear from our discussion above: it represents amounts of the conserved quantity flowing across the boundary of the control volume.
You can follow the derivation of the full three-dimensional equation of conservation of mass for a flow on this screencast by Prof. Barba (duration 12:47).
from IPython.display import YouTubeVideo
YouTubeVideo('35unQgSaT88')
All conservation laws express the same idea: the variation of a conserved quantity inside a control volume is due to the total flux of that quantity crossing the boundary surface (plus possibly the effect of any sources inside the volume, but let's ignore those for now).
The flux is a fundamental concept in conservation laws: it represents the amount of the quantity that crosses a surface per unit time. Our discussion above was limited to flow in one dimension, but in general the flux has any direction and is a vector quantity. Think about this: if the direction of flow is parallel to the surface, then no quantity comes in or out. We really only care about the component of flux perpendicular to the surface. Mathematically, for a vector flux →F, the amount of the conserved quantity crossing a small surface element is:
→F⋅d→Awhere d→A points in the direction of the outward normal to the surface. A general conservation law for a quantity e is thus (still ignoring possible sources):
∂∂t∫cvedV+∮cs→F⋅d→A=0To obtain a differential form of this conservation equation, we can apply the theorem of Gauss to the second integral, which brings the gradient of →F into play. One way to recognize a conservation law in differential form is that the fluxes appear only under the gradient operator.
Recall the non-linear convection equation from Lesson 1 of Module 2. It was:
∂u∂t+u∂u∂x=0If we look closely at the spatial derivative, we can rewrite this equation as
∂u∂t+∂∂x(u22)=0which is the conservation form of the non-linear convection equation, with flux F=u22.
We've all experienced it: as rush hour approaches certain roads in or out of city centers start getting full of cars, and the speed of travel can reduce to a crawl. Sometimes, the cars stop altogether. If you're a driver, you know that the more cars on the road, the slower your trip will flow.
Traffic flow models seek to describe these everyday experiences with mathematics, to help engineers design better road systems.
Let's review the Lighthill-Whitham-Richards traffic model that was offered as an exercise at the end of Module 2. This model considers cars with a continuous traffic density (average number of cars per unit length of road) rather than keeping track of them individually. If ρ(x)=0, there are no cars at that point x of the road. If ρ(x)=ρmax, traffic is literally bumper to bumper.
If the number of cars on a bounded stretch of road changes, it means that cars are entering or leaving the road somehow. Traffic density obeys a conservation law (!) where the flux is the number of cars leaving the road per unit time. It is given by F=ρu—as with mass conservation, flux equals density times velocity. But don't forget your experience on the road: the speed of travel depends on the car density. Here, u refers not to the speed of each individual car, but to the traffic speed at a given point of the road.
You know from experience that with more cars on the road, the speed of travel decreases. It is also true that if you are traveling at fast speed, you are advised to leave a larger gap with cars ahead. These two considerations lead us to propose a monotonically decreasing u=u(ρ) function. As a first approximation, we may consider the linear function:
u(ρ)=umax(1−ρρmax)The linear model of the behavior of drivers satisfies these experimental observations:
That seems like a reasonable approximation of reality!
Applying a conservation law to the vehicle traffic, the traffic density will obey the following transport equation:
∂ρ∂t+∂F∂x=0where F is the traffic flux, which in the linear traffic-speed model is given by:
F=ρumax(1−ρρmax)We can now use our numerical kung-fu to solve some interesting traffic situations, and check if our simple model gives realistic results!
Let's say that we are examining a road of length 4 where the speed limit is umax=1, fitting 10 cars per unit length (ρmax=10). Now, imagine we have an intersection with a red light at x=2. At the stoplight, traffic is bumper-to-bumper, and the traffic density decreases linearly to zero as we approach the beginning of our road. Ahead of the stoplight, the road is clear.
Mathematically, we can represent this situation with the following initial condition:
ρ(x,0)={ρmaxx20≤x<202≤x≤4Let's see what a plot of that looks like.
import numpy
from matplotlib import pyplot
%matplotlib inline
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
def rho_green_light(x, rho_light):
"""
Computes the "green light" initial condition.
It consists of a shock with a linear distribution behind it.
Parameters
----------
x : numpy.ndaray
Locations on the road as a 1D array of floats.
rho_light : float
Car density at the stoplight.
Returns
-------
rho : numpy.ndarray
The initial car density along the road as a 1D array of floats.
"""
rho = numpy.zeros_like(x)
mask = numpy.where(x < 2.0)
rho[mask] = rho_light * x[mask] / 2.0
return rho
# Set parameters.
nx = 81 # number of locations on the road
L = 4.0 # length of the road
dx = L / (nx - 1) # distance between two consecutive locations
nt = 30 # number of time step to compute
u_max = 1.0 # maximum speed allowed on the road
rho_max = 10.0 # maximum car density allowed on the road
rho_light = 10.0 # car density at the stoplight
# Discretize the road.
x = numpy.linspace(0.0, L, num=nx)
# Compute the initial traffic density.
rho0 = rho_green_light(x, rho_light)
# Plot the initial car density on the road.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel(r'$x$')
pyplot.ylabel(r'$\rho$')
pyplot.grid()
pyplot.plot(x, rho0, color='C0', linestyle='-', linewidth=2)
pyplot.xlim(0.0, L)
pyplot.ylim(-0.5, 11.0);
How does the traffic behave once the light turns green? Cars should slowly start moving forward: the density profile should move to the right. Let's see if the numerical solution agrees with that!
Before we start, let's define a function to calculate the traffic flux. We'll use it in each time step of our numerical solution.
def flux(rho, u_max, rho_max):
"""
Computes the traffic flux F = V * rho.
Parameters
----------
rho : numpy.ndarray
Traffic density along the road as a 1D array of floats.
u_max : float
Maximum speed allowed on the road.
rho_max : float
Maximum car density allowed on the road.
Returns
-------
F : numpy.ndarray
The traffic flux along the road as a 1D array of floats.
"""
F = rho * u_max * (1.0 - rho / rho_max)
return F
Start by using a forward-time, backward-space scheme, like you used in Module 2. The discretized form of our traffic model is:
ρn+1i−ρniΔt+Fni−Fni−1Δx=0Like before, we'll step in time via a for-loop, and we'll operate on all spatial points simultaneously via array operations. In each time step, we also need to call the function that computes the flux. Here is a function that implements in code the forward-time/backward-space difference scheme:
def ftbs(rho0, nt, dt, dx, bc_value, *args):
"""
Computes the history of the traffic density on the road
at a certain time given the initial traffic density.
Parameters
----------
rho0 : numpy.ndarray
The initial car density along the road
as a 1D array of floats.
nt : integer
The number of time steps to compute.
dt : float
The time-step size to integrate.
dx : float
The distance between two consecutive locations.
bc_value : float
The constant density at the first station.
args : list or tuple
Positional arguments to be passed to the flux function.
Returns
-------
rho_hist : list of numpy.ndarray objects
The history of the car density along the road.
"""
rho_hist = [rho0.copy()]
rho = rho0.copy()
for n in range(nt):
# Compute the flux.
F = flux(rho, *args)
# Advance in time.
rho[1:] = rho[1:] - dt / dx * (F[1:] - F[:-1])
# Set the left boundary condition.
rho[0] = bc_value
# Record the time-step solution.
rho_hist.append(rho.copy())
return rho_hist
We're all good to go!
Note: The code above saves the complete traffic density at each time step—we'll use that in a second to create animations with our results.
Running the numerical solution is easy now: we just need to call the function for evolving the initial condition with the forward-time/backward-space scheme.
Let's see how that looks.
# Set the time-step size based on CFL limit.
sigma = 1.0
dt = sigma * dx / u_max # time-step size
# Compute the traffic density at all time steps.
rho_hist = ftbs(rho0, nt, dt, dx, rho0[0], u_max, rho_max)
Let's now create an animation of the traffic flow density using the animation
module of Matplotlib.
from matplotlib import animation
from IPython.display import HTML
# Plot the initial traffic density.
fig = pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel(r'$x$')
pyplot.ylabel(r'$\rho$')
pyplot.grid()
line = pyplot.plot(x, rho0,
color='C0', linestyle='-', linewidth=2)[0]
pyplot.xlim(0.0, L)
pyplot.ylim(-0.5, 11.0)
fig.tight_layout()
def update_plot(n, rho_hist):
"""
Update the line y-data of the Matplotlib figure.
Parameters
----------
n : integer
The time-step index.
rho_hist : list of numpy.ndarray objects
The history of the numerical solution.
"""
fig.suptitle('Time step {:0>2}'.format(n))
line.set_ydata(rho_hist[n])
# Create an animation of the traffic density.
anim = animation.FuncAnimation(fig, update_plot,
frames=nt, fargs=(rho_hist,),
interval=100)
# Display the video.
HTML(anim.to_html5_video())
Yikes! The solution is blowing up. This didn't happen in your traffic-flow exercise (coding assignment) for Module 2! (Thankfully.) What is going on? Is there a bug in the code?
No need to panic. Let's take a closer look at the equation we are solving:
∂ρ∂t+∂F∂x=0Using the chain rule of calculus, rewrite is as follows:
∂ρ∂t+∂F∂ρ∂ρ∂x=0This form of the equation looks like the nonlinear convection equation from Lesson 1 of Module 2, right? This is a wave equation where the wave speed is uwave=∂F∂ρ. That term is:
uwave=∂F∂ρ=umax(1−2ρρmax)See how the wave speed changes sign at ρ=ρmax/2? That means that for the initial conditions given for the green-light problem, the part of the wave under ρ=ρmax/2 will want to move right, whereas the part of the wave over this mark, will move left!
There is no real problem with that in terms of the model, but a scheme that is backward in space is unstable for negative values of the wave speed.
Maybe you noticed that the backward-space discretization is spatially biased: we include the points i and i−1 in the formula. Look again at the stencil and you'll see what we mean.
In fact, the spatial bias was meant to be in the direction of propagation of the wave—this was true when we solved the convection equation (with positive wave speed c), but now we have some problems. Discretization schemes that are biased in the direction that information propagates are called upwind schemes.
Remember when we discussed the characteristic lines for the linear convection equation in lesson 1 of the previous module? Compare the sketch of the characteristic lines with the stencil above. The point is that there is an inherent directionality in the physics, and we want the numerical scheme to have the same directionality. This is one example of choosing an appropriate scheme for the physical problem.
If we wanted to solve the convection equation with negative wave speed, c<0, we would need a spatial bias "slanting left," which we would obtain by using the points i and i+1 in the formula.
But if we have waves traveling in both directions, we are in a bit of a bind. One way to avoid this problem with our traffic flow model is to simply use an initial condition that doesn't produce negative speed. This should work. But later we will learn about other numerical schemes that are able to handle waves in both directions.
Just for a sanity check, let's try the forward-time/backward-space scheme with the initial conditions
ρ(x,0)={2.5x0≤x<202≤x≤4If all values of ρ≤ρmax/2, then ∂F∂ρ is positive everywhere. For these conditions, our forward-time/backward-space scheme shouldn't have any trouble, as all wave speeds are positive.
# Modify some parameters.
nt = 40 # number of time step to compute
rho_light = 5.0 # car density at the stoplight
# Compute the initial traffic density.
rho0 = rho_green_light(x, rho_light)
# Plot the initial traffic density.
fig = pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel(r'$x$')
pyplot.ylabel(r'$\rho$')
pyplot.grid()
line = pyplot.plot(x, rho0,
color='C0', linestyle='-', linewidth=2)[0]
pyplot.hlines(rho_max / 2.0, 0.0, L,
label=r'$\rho_{max} / 2$',
color='black', linestyle='--', linewidth=2)
pyplot.legend()
pyplot.xlim(0.0, L)
pyplot.ylim(-0.5, 11.0)
fig.tight_layout()
# Compute the traffic density at all time steps.
rho_hist = ftbs(rho0, nt, dt, dx, rho0[0], u_max, rho_max)
# Create an animation of the traffic density.
anim = animation.FuncAnimation(fig, update_plot,
frames=nt, fargs=(rho_hist,),
interval=100)
# Display the animation.
HTML(anim.to_html5_video())
Phew! It works! Try this out yourself with different initial conditions.
Also, you can easily create a new function ftfs
to do a forward-time/forward-space scheme, which is stable for negative wave speeds. Unfortunately, forward in space is unstable for positive wave speeds. If you don't want it blowing up, make sure the wave speed is negative everywhere: uwave=∂F∂ρ<0 ∀ x.
Look at that solution again, and you'll get some nice insights of the real physical problem. See how on the trailing edge, a shock is developing? In the context of the traffic flow problem, a shock is a sign of a traffic jam: a region where traffic is heavy and slow next to a region that is free of cars. In the initial condition, the cars in the rear end of the triangle see a mostly empty road (traffic density is low!). They see an empty road and speed up, accordingly. The cars in the peak of the triangle are moving pretty slowly because traffic density is higher there. Eventually the cars that started in the rear will catch up with the rest and form a traffic jam.
Lesson 2 of Module 2 discusses the CFL condition for the linear convection equation. To refresh your memory, for a constant wave speed uwave=c:
σ=cΔtΔx<1What happens for non-linear equations? The wave speed is space- and time-dependent, uwave=uwave(x,t), and the CFL condition needs to apply for every point in space, at every instant of time. We just need σ>1 in one spot, for the whole solution to blow up!
Let's generalize the CFL condition to
σ=max[|uwave|ΔtΔx]<1which in our case is
σ=max[umax|1−2ρρmax|ΔtΔx]<1Here, the closer ρ is to zero, the more likely it is to be unstable.
We know that the green-light problem with density at the stop light ρ=ρlight=4 is stable using a forward-time/backward -space scheme. Earlier, we used umax=1, and Δt/Δx=1, which gives a CFL =1, when ρ=0.
What if we change the conditions slightly, say umax=1.1?
# Set parameters.
rho_light = 4.0
u_max = 1.1
# Compute the initial traffic density.
rho0 = rho_green_light(x, rho_light)
# Compute the traffic density at all time steps.
rho_hist = ftbs(rho0, nt, dt, dx, rho0[0], u_max, rho_max)
# Create an animation of the traffic density.
anim = animation.FuncAnimation(fig, update_plot,
frames=nt, fargs=(rho_hist,),
interval=100)
# Display the video.
HTML(anim.to_html5_video())
That failed miserably! Only by changing umax to 1.1, even an algorithm that we know is stable for this problem, fails. Since we kept Δt/Δx=1, the CFL number for ρ=0 is 1.1. See where the instability begins? Beware the CFL!
Neville D. Fowkes and John J. Mahony, "An Introduction to Mathematical Modelling," Wiley & Sons, 1994. Chapter 14: Traffic Flow.
M. J. Lighthill and G. B. Whitham (1955), On kinematic waves. II. Theory of traffic flow and long crowded roads, Proc. Roy. Soc. A, Vol. 229, pp. 317–345. PDF from amath.colorado.edu, checked Oct. 14, 2014. Original source on the Royal Society site.
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, 'r').read())