Processing math: 100%

This is the first of two notebooks on the Euler equations. In this notebook, we discuss the equations and the structure of the exact solution to the Riemann problem. In the next notebook, we investigate approximate Riemann solvers.

Fluid dynamicsΒΆ

In this chapter we study the system of hyperbolic PDEs that governs the motions of fluids in the absence of viscosity. These consist of conservation laws for mass, momentum, and energy. Together, they are referred to as the compressible Euler equations, or simply the Euler equations. Our discussion here is fairly brief; for much more detail see (Toro, 2013).

We will use ρ(x,t) to denote the fluid density and u(x,t) for its velocity. Then the equation for conservation of mass is just the continuity equation:

ρt+(ρu)x=0.

Momentum conservationΒΆ

The momentum is given by the product of density and velocity, ρu. The momentum flux has two components. First, the momentum is transported in the same way that the density is; this flux is given by the momentum times the density: ρu2.

To understand the second term in the momentum flux, we must realize that a fluid is made up of many tiny molecules. The density and velocity we are modeling are average values over some small region of space. The individual molecules in that region are not all moving with exactly velocity u; that's just their average. Each molecule also has some additional random velocity component. These random velocities are what accounts for the pressure of the fluid, which we'll denote by p. These velocity components also lead to a net flux of momentum. Thus the momentum conservation equation is

(ρu)t+(ρu2+p)x=0.

Energy conservationΒΆ

The energy has two components: internal energy ρe and kinetic energy ρu2/2:

E=ρe+12ρu2.

Like the momentum flux, the energy flux involves both bulk transport (Eu) and transport due to pressure (pu):

Et+(u(E+p))=0.

Equation of stateΒΆ

You may have noticed that we have 4 unknowns (density, momentum, energy, and pressure) but only 3 conservation laws. We need one more relation to close the system. That relation, known as the equation of state, expresses how the pressure is related to the other quantities. We'll focus on the case of a polytropic ideal gas, for which

p=ρe(Ξ³βˆ’1).

Here Ξ³ is the ratio of specific heats, which for air is approximately 1.4.

The Euler equationsΒΆ

We can write the three conservation laws as a single system qt+f(q)x=0 by defining

q=(ρρuE),f(q)=(ρuρu2+pu(E+p)).

In three dimensions, the equations are similar. We have two additional velocity components v,w, and their corresponding fluxes. Additionally, we have to account for fluxes in the y and z directions. We can write the full system as

qt+f(q)x+g(q)y+h(q)z=0

with

q=(ρρuρvρwE),f(q)=(ρuρu2+pρuvρuwu(E+p))g(q)=(ρvρuvρv2+pρvwv(E+p))h(q)=(ρwρuwρvwρw2+pw(E+p)).

Hyperbolic structure of the 1D Euler equationsΒΆ

In our discussion of the structure of these equations, it is convenient to work with the primitive variables (ρ,u,p) rather than the conserved variables. In quasilinear form, we have

[ρup]t+[uρ00u1/ρ0γρu][ρup]x=0.

Characteristic velocitiesΒΆ

In primitive variables, the eigenvalues of the flux Jacobian for the 1D Euler equations are:

Ξ»1=uβˆ’cΞ»2=uΞ»3=u+c

Here c is the sound speed:

c=√γpρ.

The eigenvectors of the flux Jacobian are (again in primitive variables):

r1=[βˆ’Ο/c1βˆ’Οc]r2=[100]r3=[ρ/c1ρc].

Notice that the second characteristic speed, Ξ»2, depends only on u and that u does not change as we move in the direction of r2. In other words, the 2-characteristic velocity is constant on 2-integral curves. We say this characteristic field is linearly degenerate; it admits neither shocks nor rarefactions. In a simple 2-wave, all characteristics are parallel. A jump in this family carries a change only in the density, and is referred to as a contact discontinuity.

The other two fields have characteristic velocities that do vary along the corresponding integral curves; thus the 1-wave and the 3-wave in any Riemann solution will be either a shock or a rarefaction. We say these characteristic fields are genuinely nonlinear.

Mathematically, the pth field is linearly degenerate if

βˆ‡Ξ»p(q)β‹…rp(q)=0

and genuinely nonlinear if

βˆ‡Ξ»p(q)β‹…rp(q)β‰ 0.

Riemann invariantsΒΆ

Since the Euler equations have three components, we expect each integral curve (a 1D set in 3D space) to be defined by two Riemann invariants. These are:

1:s,u+2cΞ³βˆ’12:u,p3:s,uβˆ’2cΞ³βˆ’1.

Here s is the specific entropy:

s=cvlog(p/ργ)+C.

The level sets of these Riemann invariants are two-dimensional surfaces; the intersection of two appropriate level sets defines an integral curve.

Integral curvesΒΆ

The 2-integral curves, of course, are simply lines of constant pressure and velocity (with varying density). Since the field is linearly degenerate, these coincide with the Hugoniot loci. We can determine the form of the 1- and 3-integral curves using the Riemann invariants above. For a curve passing through (ρ0,u0,p0), we find

u(p)=u0Β±2c0Ξ³βˆ’1(1βˆ’(p/p0)(Ξ³βˆ’1)/(2Ξ³)).

Here the plus sign is for 1-waves and the minus sign is for 3-waves.

Below we plot the projection of some integral curves on the pβˆ’u plane.

To do: Discuss how ρ fits into this, or plot 3D integral curves.

In [1]:
%matplotlib inline
from exact_solvers import Euler
import matplotlib.pyplot as plt
import numpy as np
from ipywidgets import widgets
from clawpack import riemann
from utils import riemann_tools
import matplotlib
matplotlib.rcParams.update({'font.size': 12})
from collections import namedtuple
Primitive_State = namedtuple('State', Euler.primitive_variables)
gamma = 1.4
In [2]:
from utils.snapshot_widgets import interact                   # for interactive widgets
#from utils.snapshot_widgets import interact      # for static figure that can be viewed online
Will create static figures with single value of parameters
In [3]:
def plot_integral_curves(plot_1=True,plot_3=False,gamma=1.4,rho_0=1.):
    N = 400
    p = np.linspace(0.,5,N)
    p_0 = 1.
    uu = np.linspace(-3,3,15)
    c_0 = np.sqrt(gamma*p_0/rho_0)
    if plot_1:
        for u_0 in uu:
            u = u_0 + (2*c_0)/(gamma-1.)* \
                (1.-(p/p_0)**((gamma-1)/(2*gamma)))
            plt.plot(p,u,color='coral')
    if plot_3:
        for u_0 in uu:
            u = u_0 - (2*c_0)/(gamma-1.)* \
                (1.-(p/p_0)**((gamma-1)/(2*gamma)))
            plt.plot(p,u,color='maroon')
    plt.xlabel('p'); plt.ylabel('u')
    plt.show()
interact(plot_integral_curves,
         gamma=widgets.FloatSlider(min=1.1,max=3,value=1.4));

The structure of centered rarefaction wavesΒΆ

Rankine-Hugoniot jump conditionsΒΆ

The Hugoniot loci for 1- and 3-shocks are u(p)=u0Β±2c0√2Ξ³(Ξ³βˆ’1)(1βˆ’p/p0√1+Ξ²p/p0),  where Ξ²=(Ξ³+1)/(Ξ³βˆ’1). Here the plus sign is for 1-shocks and the minus sign is for 3-shocks.

To do: Discuss how ρ varies, and maybe plot 3D integral curves.

In [4]:
def plot_hugoniot_loci(plot_1=True,plot_3=False,gamma=1.4,rho_0=1.):
    N = 400
    p = np.linspace(1.e-3,5,N)
    p_0 = 1.
    uu = np.linspace(-3,3,15)
    c_0 = np.sqrt(gamma*p_0/rho_0)
    beta = (gamma+1.)/(gamma-1.)
    if plot_1:
        for u_0 in uu:
            u_1 = u_0 + (2*c_0)/np.sqrt(2*gamma*(gamma-1.))* \
                (1.-p/p_0)/(np.sqrt(1+beta*p/p_0))
            plt.plot(p,u_1,color='coral')
    if plot_3:
        for u_0 in uu:
            u_1 = u_0 - (2*c_0)/np.sqrt(2*gamma*(gamma-1.))* \
                (1.-p/p_0)/(np.sqrt(1+beta*p/p_0))
            plt.plot(p,u_1,color='maroon')
    plt.xlabel('p'); plt.ylabel('u')
    plt.show()
interact(plot_hugoniot_loci,
         gamma=widgets.FloatSlider(min=1.1,max=3,value=1.4));

Entropy conditionΒΆ

Exact solution of the Riemann problemΒΆ

Executing the cell below loads some subroutines that find the exact solution of the Riemann problem. In brief, the Riemann solution is found as follows:

  1. Define a piecewise function giving the middle state velocity um that can be connected to the left state by an entropy-satisfying shock or rarefaction, as a function of the middle-state pressure pm.
  2. Define a piecewise function giving the middle state velocity um that can be connected to the right state by an entropy-satisfying shock or rarefaction, as a function of the middle-state pressure pm.
  3. Use an iterative solver to find the intersection of the two functions defined above.
  4. Use the Riemann invariants to find the intermediate state densities and the solution structure inside any rarefaction waves.

Execute the cell below (after removing #) to bring the Euler solver into the notebook with syntax highlighting, or you can examine it by looking at this file: exact_solvers/Euler.py

In [5]:
#%load exact_solvers/Euler.py

Examples of Riemann solutionsΒΆ

Problem 1: Sod shock tubeΒΆ

First we consider the classic shock tube problem, with high density and pressure on the left, low density and pressure on the right. Both sides are initially at rest. The solution includes a rarefaction, a contact, and a shock.

In [6]:
def riemann_solution(left_state, right_state):
    q_left  = Euler.primitive_to_conservative(*left_state)
    q_right = Euler.primitive_to_conservative(*right_state)

    ex_states, ex_speeds, reval, wave_types = Euler.exact_riemann_solution(q_left ,q_right, gamma)
    
    plot_function = riemann_tools.make_plot_function(ex_states, ex_speeds, reval, wave_types,
                                                     layout='vertical', 
                                                     variable_names=Euler.primitive_variables,
                                                     plot_chars=[Euler.lambda1,Euler.lambda2,Euler.lambda3],
                                                     derived_variables=Euler.cons_to_prim)

    interact(plot_function, t=widgets.FloatSlider(value=0.1,min=0,max=.9),
             which_char=widgets.Dropdown(options=[None,1,2,3],description='Show characteristics'))
In [7]:
left_state  = Primitive_State(Density = 3.,
                              Velocity = 0.,
                              Pressure = 3.)
right_state = Primitive_State(Density = 1.,
                              Velocity = 0.,
                              Pressure = 1.)

riemann_solution(left_state,right_state)

Here is a plot of the solution in the phase plane, showing the integral curve connecting the left and middle states, and the Hugoniot locus connecting the middle and right states.

In [8]:
Euler.phase_plane_plot(left_state, right_state)

Problem 2: Symmetric expansionΒΆ

Next we consider the case of equal densities and pressures, and equal and opposite velocities, with the initial states moving away from each other. The result is two rarefaction waves (the contact has zero strength).

In [9]:
left_state  = Primitive_State(Density = 1.,
                              Velocity = -3.,
                              Pressure = 1.)
right_state = Primitive_State(Density = 1.,
                              Velocity = 3.,
                              Pressure = 1.)

riemann_solution(left_state,right_state);
In [10]:
Euler.phase_plane_plot(left_state, right_state)

Problem 3: Colliding flowsΒΆ

Next, consider the case in which the left and right states are moving toward eachother. This leads to a pair of shocks, with a high-density, high-pressure state in between.

In [11]:
left_state  = Primitive_State(Density = 1.,
                              Velocity = 3.,
                              Pressure = 1.)
right_state = Primitive_State(Density = 1.,
                              Velocity = -3.,
                              Pressure = 1.)

riemann_solution(left_state,right_state)
In [12]:
Euler.phase_plane_plot(left_state, right_state)

Plot particle trajectoriesΒΆ

In the next plot of the Riemann solution in the x-t plane, we also plot the trajectories of a set of particles initially distributed along the x axis at t=0, with the spacing inversely proportional to the density. The evolution of the distance between particles gives an indication of how the density changes.

In [13]:
left_state  = Primitive_State(Density = 3.,
                              Velocity = 0.,
                              Pressure = 3.)
right_state = Primitive_State(Density = 1.,
                              Velocity = 0.,
                              Pressure = 1.)

q_left  = Euler.primitive_to_conservative(*left_state)
q_right = Euler.primitive_to_conservative(*right_state)

ex_states, ex_speeds, reval, wave_types = Euler.exact_riemann_solution(q_left ,q_right, gamma)

def reval_rho_u(x): 
    q = reval(x)
    rho = q[0]
    u = q[1]/q[0]
    rho_u = np.vstack((rho,u))
    return rho_u

# Specify density of trajectories to left and right:
rho_l = q_left[0] / 10.
rho_r = q_right[0] / 10.
x_traj, t_traj, xmax = riemann_tools.compute_riemann_trajectories(ex_states, ex_speeds, reval_rho_u, wave_types,
                              i_vel=1, rho_left=rho_l, rho_right=rho_r)
riemann_tools.plot_riemann_trajectories(x_traj, t_traj, ex_speeds, wave_types)
                                        

Recall that the evolution of the distance between particles gives an indication of how the density changes. Note that it increases across the shock wave and decreases through the rarefaction wave, and that in general there is a jump in density across the contact discontinuity.

Plot Riemann solution with advected colorsΒΆ

The next cell defines a function to plot the Riemann solution with the density plot also showing an advected color to help visualize the flow better. The fluid to the left of x=0 initially is colored red and to the right of x=0 is colored blue, with stripes of different shades of these colors to help visualize the motion of the fluids.

In [14]:
def plot_exact_riemann_solution_stripes(rho_l=3.,u_l=0.,p_l=3.,rho_r=1.,u_r=0.,p_r=1.,t=0.4):    
    q_l = Euler.primitive_to_conservative(rho_l,u_l,p_l)
    q_r = Euler.primitive_to_conservative(rho_r,u_r,p_r)
    
    from matplotlib.mlab import find
    
    x = np.linspace(-1.,1.,1000)
    states, speeds, reval, wave_types = Euler.exact_riemann_solution(q_l, q_r, gamma=gamma)
    q = reval(x/t)
    primitive = Euler.conservative_to_primitive(q[0],q[1],q[2])
    
    # compute particle trajectories:
    def reval_rho_u(x): 
        q = reval(x)
        rho = q[0]
        u = q[1]/q[0]
        rho_u = np.vstack((rho,u))
        return rho_u
    
    # Specify density of trajectories to left and right:
    num_left = 10
    num_right = 10
    rho_left = q_l[0] / 10.
    rho_right = q_r[0] / 10.
    x_traj, t_traj, xmax = riemann_tools.compute_riemann_trajectories(states, speeds, reval_rho_u, wave_types,
                                  i_vel=1, xmax=1, rho_left=rho_left, rho_right=rho_right)
                                                                          
    fig = plt.figure(figsize=(18,6))
    names = ['Density','Velocity','Pressure']
    axes = [0]*3
    for i in range(3):
        axes[i] = fig.add_subplot(1,3,i+1)
        q = primitive[i]
        plt.plot(x,q,linewidth=3)
        plt.title(names[i])
        qmax = max(q)
        qmin = min(q)
        qdiff = qmax - qmin
        axes[i].set_ylim((qmin-0.1*qdiff,qmax+0.1*qdiff))
        axes[i].set_xlim(-xmax,xmax)
                
        if i==0:
            # plot stripes only on density plot
            n = find(t > t_traj)
            if len(n)==0:
                n = 0
            else:
                n = min(n.max(), len(t_traj)-1)

            for i in range(1, x_traj.shape[1]-1):
                j1 = find(x_traj[n,i] > x)
                if len(j1)==0:
                    j1 = 0
                else:
                    j1 = min(j1.max(), len(x)-1)
                j2 = find(x_traj[n,i+1] > x)
                if len(j2)==0:
                    j2 = 0
                else:
                    j2 = min(j2.max(), len(x)-1)

                # set advected color for density plot:
                if x_traj[0,i]<0: 
                    # shades of red for fluid starting from x<0
                    if np.mod(i,2)==0:
                        c = [1,0,0]
                    else:
                        c = [1,0.8,0.8]
                else:
                    # shades of blue for fluid starting from x<0
                    if np.mod(i,2)==0:
                        c = [0,0,1]
                    else:
                        c = [0.8,0.8,1]
                plt.fill_between(x[j1:j2],q[j1:j2],0,color=c)
    plt.show()
 

Make a plot with only a time slider to illustrate this viewpoint with the Sod shock tube data:

In [15]:
def plot_exact_riemann_solution_stripes_t_slider(t):
    plot_exact_riemann_solution_stripes(rho_l=3.,u_l=0.,p_l=3.,rho_r=1.,u_r=0.,p_r=1.,t=t)
    
interact(plot_exact_riemann_solution_stripes_t_slider, 
         t=widgets.FloatSlider(min=0.1,max=1.,step=0.1,value=0.5));

Note the following in the figure above:

  • The edges of each stripe are being advected with the fluid velocity, so you can visualize how the fluid is moving.
  • The width of each stripe initially is inversely proportional to the density of the fluid, so that the total mass of gas within each stripe is the same.
  • The total mass within each stripe remains constant as the flow evolves, and the width of each stripe remains inversely proportional to the local density.
  • The interface between the red and blue gas moves with the contact discontinuity. The velocity and pressure are constant but the density can vary across this wave.

Interactive Riemann solverΒΆ

Here you can set up your own Riemann problem and immediately see the solution. If you don't want to download and run the notebook, an online interactive version is here.

In [16]:
interact(plot_exact_riemann_solution_stripes,
         rho_l=widgets.FloatSlider(min=1.,max=10.,step=0.1,value=3.,description=r'$\rho_l$'),
         u_l=widgets.FloatSlider(min=-10.,max=10.,step=0.1,value=0.,description=r'$u_l$'),
         p_l=widgets.FloatSlider(min=1.,max=10.,step=0.1,value=3.,description=r'$p_l$'),
         rho_r=widgets.FloatSlider(min=1.,max=10.,step=0.1,value=1.,description=r'$\rho_r$'),
         u_r=widgets.FloatSlider(min=-10.,max=10.,step=0.1,value=0.,description=r'$u_r$'),
         p_r=widgets.FloatSlider(min=1.,max=10.,step=0.1,value=1.,description=r'$p_r$'),
         t=widgets.FloatSlider(min=0.1,max=1.,step=0.1,value=0.5));

Riemann problems with vacuumΒΆ

A vacuum state (with zero pressure and density) can arise in the solution of the Riemann problem in two ways:

  1. An initial left or right vacuum state: in this case the Riemann solution consists of a single rarefaction, connecting the non-vacuum state to vacuum.
  2. A problem where the left and right states are not vacuum but middle states are vacuum. Since this means the middle pressure is smaller than that to the left or right, this can occur only if the 1- and 3-waves are both rarefactions. These rarefactions are precisely those required to connect the left and right states to the middle vacuum state.

Initial vacuum stateΒΆ

The velocity plot looks a bit strange, but note that the velocity is undefined in vacuum.

In [17]:
left_state  = Primitive_State(Density =0.,
                              Velocity = 0.,
                              Pressure = 0.)
right_state = Primitive_State(Density = 1.,
                              Velocity = -3.,
                              Pressure = 1.)

riemann_solution(left_state,right_state)
In [18]:
Euler.phase_plane_plot(left_state, right_state)

Middle vacuum stateΒΆ

In [19]:
left_state  = Primitive_State(Density =1.,
                              Velocity = -10.,
                              Pressure = 1.)
right_state = Primitive_State(Density = 1.,
                              Velocity = 10.,
                              Pressure = 1.)

riemann_solution(left_state,right_state)
In [20]:
Euler.phase_plane_plot(left_state, right_state)
In [21]: