import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as optimize

Sampling the Riemann solution#

Once we have the star state, we need to know which state is on the interface of our cells&mdash%that is the state that we will use to evaluate the fluxes through the interface.

To determine that state we need to know how fast each of the three waves are moving (and in which direction) and then we simply look for which state is left behind on the interface by the waves moving away from it.

Riemann solution structure

We also know the following about the wave structure:

  • The middle wave is always a contact discontinuity. Pressure and velocity are constant across it

  • The left and right waves are either a shock or rarefaction

    • Rarefaction:

      • Entropy is constant across the wave

      • Riemann invariants tell us how to connect the solution across the wave to the star region

    • Shock:

      • Must be dissipative—entropy is not conserved

      • Jump conditions tell us how to connect to the star state across the shock

Consider the following 4 cases:

Riemann state

Cases (a) and (d) represent supersonic flow to the left or right—all 3 waves are on one side of the interface.

  • For case (a), all 3 waves are to the left of the interface, so state \(R\) is on the interface.

  • For case (d) is similar, all 3 waves are to the right of the interface, so state \(L\) is on the interface.

In cases (b) and (c), one the “\(+\)” and “\(-\)” waves are on either side of the interface and the only difference is the center (”\(0\)”) wave, or contact discontinuity. So we would determine which of the star states is on the interface based on the sign of the contact discontinuity’s speed.

  • For case (b), the contact is moving to the left so \(R_\star\) state is on the interface

  • For case (c), the contact is moving to the right, so the \(L_\star\) state is on the interface.

Wave speeds#

Shock case#

For a shock, the speed of the shock comes from the Rankine-Hugoniot conditions.

For the left wave, the shock speed is:

\[ S = u_L - c_L \sqrt{\frac{\gamma + 1}{2\gamma} \left (\frac{p_\star}{p_L} \right ) + \frac{\gamma -1}{2\gamma}} \]

For the right wave, it is:

\[ S = u_R + c_R \sqrt{\frac{\gamma + 1}{2\gamma} \left (\frac{p_\star}{p_R} \right ) + \frac{\gamma -1}{2\gamma}} \]

Contact discontinuity#

A contact discontinuity propagates in the star region, and the velocity there is \(u_\star\) and is constant in that region, so the contact just propagates at this speed:

\[S_0 = u_\star\]

Rarefaction#

A rarefaction is subsonic, but it is spread out (it is not a discontinuity). We call the leading part the “head” and the trailing part the “tail”. The head and tail move at different speeds, just \(u \pm c\) corresponding to the region they abut.

For a left rarefaction:

\[S_\mathrm{head} = u_L - c_L\]
\[S_\mathrm{tail} = u_\star - c_\star\]

For a right rarefaction:

\[S_\mathrm{head} = u_R + c_R\]
\[S_\mathrm{tail} = u_\star + c_\star\]

Note that \(|S_\mathrm{head}| > |S_\mathrm{tail}|\), so the rarefaction spreads out over time.

Also, it is possible for the rarefaction to span the interface—with the head on one side and the tail on the other. We’ll deal with this shortly.

Density#

To compute the rarefaction speed, we need to know the density in the star state (so we can evaluate \(c_\star\)). We will need this anyway for either a shock or a rarefaction to compute the final fluxes if a star state is on the interface.

For a rarefaction, since entropy is constant, we have:

\[\frac{p_s}{\rho_s^\gamma} = \frac{p_\star}{\rho_{\star,s}^\gamma}\]

where \(s \in {L, R}\) is the state outside of the star region.

For a shock, the jump conditions tell us the density:

\[\rho_{\star,s} = {\rho_s} \frac{\frac{p_\star}{p_s} + \frac{\gamma-1}{\gamma+1}}{\frac{p_\star}{p_s}\frac{\gamma-1}{\gamma+1} + 1}\]

Sampling#

The sampling procedure is the following:

  • Look at the sign of the contact speed, \(S_0\):

    • If \(S_0 > 0\) then we are either in the \(L\) or \(L_\star\) state

      • Look at the speed of the left shock or rarefaction to determine if we are in the \(L\) or \(L_\star\) state

    • If \(S_0 < 0\) then we are either in the \(R\) or \(R_\star\) state

      • Look at the speed of the right shock or rarefaction to determine if we are in the \(R\) or \(R_\star\) state

The one catch is when the rarefaction spans the interface, e.g., \(S_\mathrm{head} < 0 < S_\mathrm{tail}\) for a left rarefaction.

rarefaction spanning the interface

In this case, we would use the Riemann invariant to find the state at the location of the interface inside the structure of the rarefaction.

Implementation#

Here we extend our RiemannProblem class to add methods to find the solution if we are a shock or rarefaction and to do the sampling:

class State:
    """ a simple object to hold a primitive variable state """

    def __init__(self, p=1.0, u=0.0, rho=1.0):
        self.p = p
        self.u = u
        self.rho = rho

    def __str__(self):
        return f"rho: {self.rho}; u: {self.u}; p: {self.p}"

class RiemannProblem:
    """ a class to define a Riemann problem.  It takes a left
        and right state.  Note: we assume a constant gamma """

    def __init__(self, left_state, right_state, gamma=1.4):
        self.left = left_state
        self.right = right_state
        self.gamma = gamma

        self.ustar = None
        self.pstar = None

    def __str__(self):
        return f"pstar = {self.pstar}, ustar = {self.ustar}"

    def u_hugoniot(self, p, side):
        """define the Hugoniot curve, u(p)."""

        if side == "left":
            state = self.left
            s = 1.0
        elif side == "right":
            state = self.right
            s = -1.0

        c = np.sqrt(self.gamma*state.p/state.rho)

        if p < state.p:
            # rarefaction
            u = state.u + s*(2.0*c/(self.gamma-1.0))* \
                (1.0 - (p/state.p)**((self.gamma-1.0)/(2.0*self.gamma)))
        else:
            # shock
            beta = (self.gamma+1.0)/(self.gamma-1.0)
            u = state.u + s*(2.0*c/np.sqrt(2.0*self.gamma*(self.gamma-1.0)))* \
                (1.0 - p/state.p)/np.sqrt(1.0 + beta*p/state.p)

        return u

    def find_star_state(self, p_min=0.001, p_max=1000.0):
        """ root find the Hugoniot curve to find ustar, pstar """

        # we need to root-find on
        self.pstar = optimize.brentq(
            lambda p: self.u_hugoniot(p, "left") - self.u_hugoniot(p, "right"),
            p_min, p_max)
        self.ustar = self.u_hugoniot(self.pstar, "left")


    def shock_solution(self, sgn, state):
        """return the interface solution considering a shock"""

        p_ratio = self.pstar/state.p
        c = np.sqrt(self.gamma*state.p/state.rho)

        # Toro, eq. 4.52 / 4.59
        S = state.u + sgn*c*np.sqrt(0.5*(self.gamma + 1.0)/self.gamma*p_ratio +
                                    0.5*(self.gamma - 1.0)/self.gamma)

        # are we to the left or right of the shock?
        if (self.ustar < 0 and S < 0) or (self.ustar > 0 and S > 0):
            # R/L region
            solution = state
        else:
            # * region -- get rhostar from Toro, eq. 4.50 / 4.57
            gam_fac = (self.gamma - 1.0)/(self.gamma + 1.0)
            rhostar = state.rho * (p_ratio + gam_fac)/(gam_fac * p_ratio + 1.0)
            solution = State(rho=rhostar, u=self.ustar, p=self.pstar)

        return solution

    def rarefaction_solution(self, sgn, state):
        """return the interface solution considering a rarefaction wave"""

        # find the speed of the head and tail of the rarefaction fan

        # isentropic (Toro eq. 4.54 / 4.61)
        p_ratio = self.pstar/state.p
        c = np.sqrt(self.gamma*state.p/state.rho)
        cstar = c*p_ratio**((self.gamma-1.0)/(2*self.gamma))

        lambda_head = state.u + sgn*c
        lambda_tail = self.ustar + sgn*cstar

        gam_fac = (self.gamma - 1.0)/(self.gamma + 1.0)

        if (sgn > 0 and lambda_head < 0) or (sgn < 0 and lambda_head > 0):
            # R/L region
            solution = state

        elif (sgn > 0 and lambda_tail > 0) or (sgn < 0 and lambda_tail < 0):
            # * region, we use the isentropic density (Toro 4.53 / 4.60)
            solution = State(rho = state.rho*p_ratio**(1.0/self.gamma),
                             u = self.ustar, p = self.pstar)

        else:
            # we are in the fan -- Toro 4.56 / 4.63
            rho = state.rho * (2/(self.gamma + 1.0) -
                               sgn*gam_fac*state.u/c)**(2.0/(self.gamma-1.0))
            u = 2.0/(self.gamma + 1.0) * ( -sgn*c + 0.5*(self.gamma - 1.0)*state.u)
            p = state.p * (2/(self.gamma + 1.0) -
                           sgn*gam_fac*state.u/c)**(2.0*self.gamma/(self.gamma-1.0))
            solution = State(rho=rho, u=u, p=p)

        return solution

    def sample_solution(self):
        """given the star state (ustar, pstar), find the state on the interface"""

        if self.ustar < 0:
            # we are in the R* or R region
            state = self.right
            sgn = 1.0
        else:
            # we are in the L* or L region
            state = self.left
            sgn = -1.0

        # is the non-contact wave a shock or rarefaction?
        if self.pstar > state.p:
            # compression! we are a shock
            solution = self.shock_solution(sgn, state)

        else:
            # rarefaction
            solution = self.rarefaction_solution(sgn, state)

        return solution
left = State(p=1.0, u=0.0, rho=1.0)
right = State(p=0.1, u=0.0, rho=0.125)

rp = RiemannProblem(left, right)
rp.find_star_state()

interface_state = rp.sample_solution()
print(interface_state)
rho: 0.4263194281784952; u: 0.9274526200489498; p: 0.30313017805064685

Once we have the interface state, we can compute the conservative fluxes:

\[\begin{split}F_{i+1/2} = \left ( \begin{array}{c} \rho_{i+1/2} u_{i+1/2} \\ \rho_{i+1/2} (u_{i+1/2})^2 + p_{i+1/2} \\ u_{i+1/2} p_{i+1/2} / (\gamma - 1) + \frac{1}{2} \rho_{i+1/2} (u_{i+1/2})^3 + u_{i+1/2} p_{i+1/2} \end{array} \right )\end{split}\]