{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Approximate solvers for the shallow water equations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this chapter we discuss approximate Riemann solvers for the shallow water equations: \n", "\\begin{align}\n", " h_t + (hu)_x & = 0 \\label{SWA_mass} \\\\\n", " (hu)_t + \\left(hu^2 + \\frac{1}{2}gh^2\\right)_x & = 0. \\label{SWA_mom}\n", "\\end{align} " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To examine the Python code for this chapter, and for the exact Riemann solution, see:\n", "\n", " - [exact_solvers/shallow_water.py](exact_solvers/shallow_water.py) ...\n", " [on github.](https://github.com/clawpack/riemann_book/blob/FA16/exact_solvers/shallow_water.py)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We investigated the exact solution of the Riemann problem for this system in [Shallow_water](Shallow_water.ipynb). Recall that the system has two genuinely nonlinear characteristic fields, so the solution consists of two waves, each of which may be a shock or a rarefaction.\n", "\n", "Recall that the flux Jacobian is \n", "\\begin{align}\n", "f'(q) & = \\begin{pmatrix} 0 & 1 \\\\ -(q_2/q_1)^2 + g q_1 & 2 q_2/q_1 \\end{pmatrix} \n", " = \\begin{pmatrix} 0 & 1 \\\\ -u^2 + g h & 2 u \\end{pmatrix}.\n", "\\end{align} \n", "Its eigenvalues are \n", "\\begin{align} \\label{SWA:char-speeds}\n", " \\lambda_1 & = u - \\sqrt{gh} & \\lambda_2 & = u + \\sqrt{gh},\n", "\\end{align} \n", "with corresponding eigenvectors \n", "\\begin{align} \\label{SWA:fjac-evecs}\n", " r_1 & = \\begin{bmatrix} 1 \\\\ u-\\sqrt{gh} \\end{bmatrix} &\n", " r_2 & = \\begin{bmatrix} 1 \\\\ u+\\sqrt{gh} \\end{bmatrix}.\n", "\\end{align}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Roe solver" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To define a linearized Riemann solver, we consider the quasilinear form $q_t + f'(q)q_x=0$, and replace the Jacobian $f'(q)$ by some matrix $\\hat{A}(q_\\ell,q_r)$ depending on the left and right states. This matrix should satisfy three properties:\n", "\n", "1. Consistency: $\\hat{A}(q_\\ell,q_r) \\to f'(q)$ as $q_\\ell, q_r \\to q$.\n", "2. Hyperbolicity: $\\hat{A}$ must be diagonalizable with real eigenvalues, so that we can define the waves and speeds needed in the approximate solution.\n", "3. Conservation: $\\hat{A}(q_\\ell,q_r)(q_r - q_\\ell) = f(q_r) - f(q_\\ell)$.\n", "\n", "The first two properties are essential for any linearized approximate Riemann solver. As mentioned in [Burgers_approximate](Burgers_approximate.ipynb), the third property is the defining property of a Roe solver. It ensures that if $q_r$ and $q_\\ell$ can be connected by a single shock (i.e., if they lie on a common Hugoniot locus), then the approximate solver will in fact yield the exact solution -- a single discontinuity propagating at the speed given by the Rankine-Hugoniot jump condition. This property is quite useful, since at most points (in space and time) where a shock appears, it is an isolated shock corresponding to one characteristic family. Only at isolated points in space and time do multiple shocks interact.\n", "\n", "Here we derive the Roe solver for the shallow water equations in a simple and straightforward way by applying the conservation requirement. Roe's original paper (Roe, 1981) (which dealt with the Euler equations rather than the shallow water equations) gave a different and possibly more general derivation that is also reviewed in Chapter 15 of (LeVeque, 2002).\n", "\n", "One natural approach is to look for a matrix $\\hat{A}$ that is the flux Jacobian evaluated at some average state: \n", "$$\\hat{A}(q_\\ell,q_r) = f'(\\hat{q}).$$ \n", "This guarantees property 2, and will ensure property 1 as long as $\\hat{q}$ is chosen as some kind of average of $q_\\ell$ and $q_r$.\n", "For the shallow water system, this means that the linearized equations take the form \n", "\\begin{align} \\label{SWA_quasilinear}\n", "h_t + (hu)_x & = 0 \\\\\n", "(hu)_t + (g\\hat{h}-\\hat{u}^2)h_x + 2\\hat{u}(hu)_x & = 0. \\nonumber\n", "\\end{align} \n", "Observe that (\\ref{SWA_quasilinear}) is linear *not in the physical variables $h, u$, but in the conserved variables $h, hu$*. In the context of a Riemann solver, $\\hat{q}$ will be some kind of average of the initial states $q_\\ell, q_r$.\n", "\n", "The characteristic speeds of the linear hyperbolic system (\\ref{SWA_quasilinear}) are just the characteristic speeds of the original shallow water system, evaluated at the average state $\\hat{q}$: \n", "\\begin{align} \\label{SWA:avg-char-speeds}\n", " s_1 & = \\hat{u} - \\sqrt{g\\hat{h}} & s_2 & = \\hat{u} + \\sqrt{g\\hat{h}},\n", "\\end{align} \n", "with corresponding eigenvectors \n", "\\begin{align} \\label{SWA:avg-fjac-evecs}\n", " \\hat{r}_1 = r_1(\\hat{q}) & = \\begin{bmatrix} 1 \\\\ \\hat{u}-\\sqrt{g\\hat{h}} \\end{bmatrix} &\n", " \\hat{r}_2 = r_2(\\hat{q}) & = \\begin{bmatrix} 1 \\\\ \\hat{u}+\\sqrt{g\\hat{h}} \\end{bmatrix}.\n", "\\end{align}\n", "\n", "The linearized solver simply decomposes the jump $q_r-q_\\ell$ in terms\n", "of the eigenvectors, to define the waves: \n", "\\begin{align} \\label{SWA:wave-decomp}\n", "q_r - q_\\ell = \\alpha_1 \\hat{r}_1 + \\alpha_2 \\hat{r}_2 = {\\mathcal W}_1 + {\\mathcal W}_2\n", "\\end{align} \n", "and uses the speeds given by (\\ref{SWA:avg-char-speeds}). We refer to $\\alpha_1, \\alpha_2$ as the\n", "wave strengths; they can be found by solving the system above, which amounts to \n", "\\begin{align}\n", "\\alpha = \\hat{R}^{-1}(q_r-q_\\ell),\n", "\\end{align} \n", "where $\\hat{R} = (\\hat{r}_1 | \\hat{r}_2)$ is the matrix of right eigenvectors, and thus $\\hat{R}^{-1}$ is the matrix whose rows are the left eigenvectors. This leads to the expressions \n", "\\begin{align}\n", "\\alpha_1 & = \\frac{(\\hat{u}+ \\hat{c}) \\delta_1 - \\delta_2}{2\\hat{c}} \\\\\n", "\\alpha_2 & = \\frac{-(\\hat{u} - \\hat{c}) \\delta_1 + \\delta_2}{2\\hat{c}},\n", "\\end{align} \n", "where $\\hat{c} = \\sqrt{g\\hat{h}}$ and $\\delta = q_r -q_\\ell$." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Derivation of Roe averages\n", "It remains only to specify the average state $(\\hat{h},\\hat{u})$. We do so by solving the conservation condition: \n", "\\begin{align} \\label{SWA:roe_cons}\n", "f'(\\hat{q}) (q_r - q_\\ell) & = f(q_r) - f(q_\\ell).\n", "\\end{align} \n", "For convenience we recall here the relevant quantities: \n", "\\begin{align}\n", "f'(\\hat{q}) & = \\begin{pmatrix} 0 & 1 \\\\ -\\hat{u}^2 + g \\hat{h} & 2 \\hat{u} \\end{pmatrix},\n", "\\end{align}\n", "\\begin{align*}\n", "q_r - q_\\ell & = \\begin{pmatrix} h_r - h_\\ell \\\\ h_r u_r - h_\\ell u_\\ell \\end{pmatrix},\n", "& f(q_r) - f(q_\\ell) & = \\begin{pmatrix} h_r u_r - h_\\ell u_\\ell \\\\ h_r u_r^2 - h_\\ell u_\\ell^2 + \\frac{g}{2}(h_r^2-h_\\ell^2)\\end{pmatrix}. \n", "\\end{align*}\n", "The first equation of (\\ref{SWA:roe_cons}) is an identity, satisfied no matter how $\\hat{u}$ and $\\hat{h}$ are chosen. We therefore turn to the second equation: \n", "\\begin{align} \\label{SWA:2nd}\n", " (-\\hat{u}^2 + g\\hat{h})(h_r-h_\\ell) + 2\\hat{u}(h_r u_r - h_\\ell u_\\ell) & = h_r u_r^2 - h_\\ell u_\\ell^2 + \\frac{g}{2}(h_r^2-h_\\ell^2).\n", "\\end{align}\n", "Since (\\ref{SWA:2nd}) must hold for any value of $g$, we can equate separately the terms involving $g$ and those not involving $g$. For the terms with $g$ in them, we get: \n", "\\begin{align} \\label{SWA:h_roe1}\n", "\\hat{h}(h_r-h_\\ell) & = \\frac{1}{2} (h_r^2 - h_\\ell^2),\n", "\\end{align} \n", "which gives the arithmetic average \n", "\\begin{align} \\label{SWA:h_roe}\n", "\\hat{h} = \\frac{h_r + h_\\ell}{2}.\n", "\\end{align}\n", "Equating terms that do not involve $g$, we get a quadratic equation for $\\hat{u}$: \n", "$$(h_r-h_\\ell)\\hat{u}^2 - 2(h_r u_r - h_\\ell u_\\ell) \\hat{u} + h_r u_r^2 - h_\\ell u_\\ell^2 = 0.$$ \n", "The two roots of this equation are \n", "$$\\hat{u}_\\pm = \\frac{h_r u_r - h_\\ell u_\\ell \\mp \\sqrt{h_r h_\\ell} (u_\\ell-u_r)}{h_r-h_\\ell}.$$ \n", "Simplifying these expressions further, we find \n", "\\begin{align} \\label{SWA:u_roe}\n", " \\hat{u}_\\pm = \\frac{\\sqrt{h_r}u_r \\pm \\sqrt{h_\\ell}u_\\ell}{\\sqrt{h_r} \\pm \\sqrt{h_\\ell}},\n", "\\end{align} \n", "where the same sign (plus or minus) must be taken in both numerator and denominator. Either of these expressions can be used for $\\hat{u}$, as long as $h_r \\ne h_\\ell$. The value $\\hat{u}_+$ is preferable for two reasons: first, it is well defined even when $h_r = h_\\ell$; second, it has the appearance of a weighted average of $u$, and its value always lies between $u_\\ell$ and $u_r$." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The values given by (\\ref{SWA:h_roe}) and (\\ref{SWA:u_roe}) (with the plus sign) are known as the *Roe-averaged* depth and velocity. This completes the definition of the approximate Roe solver. An implementation appears below." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide" ] }, "outputs": [], "source": [ "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide" ] }, "outputs": [], "source": [ "%config InlineBackend.figure_format = 'svg'\n", "\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from exact_solvers import shallow_water as sw\n", "from utils import riemann_tools as rt\n", "from ipywidgets import interact\n", "from ipywidgets import widgets" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def shallow_water_roe(q_l, q_r, grav=1.):\n", " \"\"\"\n", " Approximate Roe solver for the shallow water equations.\n", " \"\"\"\n", " \n", " h_l = q_l[0]\n", " hu_l = q_l[1]\n", " u_l = hu_l/h_l\n", " h_r = q_r[0]\n", " hu_r = q_r[1]\n", " u_r = hu_r/h_r\n", " \n", " delta = q_r - q_l\n", " \n", " # Roe averages\n", " h_hat = (h_r + h_l)/2.\n", " u_hat = (np.sqrt(h_r)*u_r + np.sqrt(h_l)*u_l) \\\n", " / (np.sqrt(h_r) + np.sqrt(h_l))\n", " c_hat = np.sqrt(grav*h_hat)\n", " \n", " s1 = u_hat - c_hat\n", " s2 = u_hat + c_hat\n", " \n", " alpha1 = ( (u_hat+c_hat)*delta[0] - delta[1])/(2*c_hat)\n", " alpha2 = (-(u_hat-c_hat)*delta[0] + delta[1])/(2*c_hat)\n", " \n", " h_m = q_l[0] + alpha1\n", " hu_m = q_l[1] + alpha1 * (u_hat - c_hat)\n", " q_m = np.array([h_m, hu_m])\n", " \n", " states = np.column_stack([q_l,q_m,q_r])\n", " speeds = [s1, s2]\n", " wave_types = ['contact','contact']\n", " \n", " def reval(xi):\n", " h_out = (xi0$. In this case, it appears that the 1-wave should be a transonic rarefaction. We take the 1-wave ${\\mathcal W}_1$ of the Roe solver, with speed $s_1$, and split it into two waves, ${\\mathcal W}_{1\\ell} = \\alpha_{1\\ell} r_1$ and ${\\mathcal W}_{1r} = \\alpha_{1r} r_1$, with speeds $\\lambda_1(q_\\ell), \\lambda_1(q_m)$. Two requirements determine the strength of these waves. First, the total jump across them should be equal to the jump across the original wave:\n", "\n", "$${\\mathcal W}_{1\\ell} + {\\mathcal W}_{1r} = {\\mathcal W}_1.$$\n", "\n", "Second, we must maintain conservation:\n", "\n", "$$\\lambda_1(q_\\ell) {\\mathcal W}_{1\\ell} + \\lambda_1(q_m) {\\mathcal W}_{1r} = s_1 {\\mathcal W}_1.$$\n", "\n", "This leads to\n", "\n", "\\begin{align*}\n", "{\\mathcal W}_{1\\ell} & = \\frac{\\lambda_1(q_m)-s_1}{\\lambda_1(q_m)-\\lambda_1(q_\\ell)}{\\mathcal W}_1 \\\\\n", "{\\mathcal W}_{1r} & = \\frac{s_1 - \\lambda_1(q_\\ell)}{\\lambda_1(q_m)-\\lambda_1(q_\\ell)}{\\mathcal W}_1.\n", "\\end{align*}\n", "\n", "We can check in a similar way for a transonic 2-rarefaction, and modify the 2-wave accordingly. Note that only one of the two characteristic fields can be transonic in the Riemann solution for the shallow water equations.\n", "\n", "This entropy fix is implemented in the solver below." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def shallow_water_roe_with_efix(q_l, q_r, grav=1., efix=True):\n", " \"\"\"\n", " Approximate Roe solver for the shallow water equations,\n", " with entropy fix.\n", " \"\"\"\n", " \n", " h_l = q_l[0]\n", " hu_l = q_l[1]\n", " u_l = hu_l/h_l\n", " h_r = q_r[0]\n", " hu_r = q_r[1]\n", " u_r = hu_r/h_r\n", " \n", " delta = q_r - q_l\n", " \n", " # Roe averages\n", " h_hat = (h_r + h_l)/2.\n", " u_hat = (np.sqrt(h_r)*u_r + np.sqrt(h_l)*u_l) / \\\n", " (np.sqrt(h_r) + np.sqrt(h_l))\n", " c_hat = np.sqrt(grav*h_hat)\n", " \n", " s1 = u_hat - c_hat\n", " s2 = u_hat + c_hat\n", " \n", " alpha1 = ( (u_hat+c_hat)*delta[0] - delta[1])/(2*c_hat)\n", " #alpha2 = (-(u_hat-c_hat)*delta[0] + delta[1])/(2*c_hat)\n", " \n", " h_m = q_l[0] + alpha1\n", " hu_m = q_l[1] + alpha1 * (u_hat - c_hat)\n", " u_m = hu_m/h_m\n", " q_m = np.array([h_m, hu_m])\n", " \n", " transonic = None\n", " if efix:\n", " # Check if either wave appears transonic\n", " lambda_1_l = u_l - np.sqrt(grav*h_l)\n", " lambda_1_m = u_m - np.sqrt(grav*h_m)\n", " lambda_2_m = u_m + np.sqrt(grav*h_m)\n", " lambda_2_r = u_r + np.sqrt(grav*h_r)\n", " \n", " if lambda_1_l < 0 < lambda_1_m: # 1-wave appears transonic\n", " transonic = 1\n", " elif lambda_2_m < 0 < lambda_2_r: # 2-wave appears transonic\n", " transonic = 2\n", " \n", " if transonic == 1:\n", " beta = (lambda_1_m - s1)/(lambda_1_m - lambda_1_l)\n", " alpha1_l = beta*alpha1\n", " #alpha_1_r = (1-beta)*alpha1\n", " h_m_new = q_l[0] + alpha1_l\n", " hu_m_new = q_l[1] + alpha1_l * (u_hat-c_hat)\n", " q_m_new = np.array([h_m_new, hu_m_new])\n", " states = np.column_stack([q_l,q_m_new,q_m,q_r])\n", " speeds = [lambda_1_l, lambda_1_m, s2]\n", " wave_types = ['contact']*3\n", " \n", " def reval(xi):\n", " h_out = (xi(Harten, Lax, and van Leer) it was proposed to use lower and upper bounds on the true wave speeds. However, overestimating the wave speeds leads to increased diffusion, and various refinements to the choice of wave speeds were subsequently proposed. Here we choose the wave speeds based on a suggestion of Einfeldt (originally in the context of the Euler equations); the resulting solver is often referred to as an HLLE solver.\n", "\n", "For the shallow water system, the 1-wave speed is always less than or equal to the 2-wave speed. If the 1-wave is a rarefaction, then the minimum wave velocity is given simply by $\\lambda_1(q_\\ell)$, the characteristic speed at the left edge of the 1-rarefaction fan. If the left-going wave speed is a shock, then the minimum wave velocity is the shock speed. We cannot determine the shock speed exactly without solving the Riemann problem -- just what we hope to avoid. Instead, we can use an approximate shock speed based on the Roe average:\n", "\n", "$$\\hat{s}_1 = \\hat{u} - \\hat{c}.$$\n", "\n", "Since we don't know whether the 1-wave should be a shock or a rarefaction, we use a lower bound on these two speeds, by taking their minimum:\n", "\n", "$$s_1 = \\min\\left(\\lambda_1(q_\\ell), \\hat{u}-\\hat{c}\\right).$$\n", "\n", "Similarly, the second wave speed is given by\n", "\n", "$$s_2 = \\max\\left(\\lambda_2(q_r), \\hat{u}+\\hat{c}\\right).$$\n", "\n", "This completes the definition of the solver. Notice that if the states $q_\\ell, q_r$ are connected by a single 2-shock, then the value of $s_2$ will be the exact shock speed (since the Roe speed is exact in this case, and the shock speed will be faster than the characteristic speed ahead of it). Furthermore, by substituting the Rankine-Hugoniot conditions into (\\ref{SWA:hll_middle_state}), one sees that the the middle state in this case will be $q_m = q_\\ell$. Thus the HLLE solver gives the exact solution for this case. Similarly, it yields the exact solution when the initial states are connected by just a 1-shock. It shares this nice property with the Roe solver (as long as the wave speeds are chosen as just described)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def shallow_water_hll(q_l, q_r, grav=1.):\n", " \"\"\"\n", " HLLE approximate solver for the shallow water equations.\n", " \"\"\"\n", " h_l = q_l[0]\n", " hu_l = q_l[1]\n", " u_l = hu_l/h_l\n", " h_r = q_r[0]\n", " hu_r = q_r[1]\n", " u_r = hu_r/h_r\n", " \n", " # Roe averages\n", " h_hat = (h_r + h_l)/2.\n", " u_hat = (np.sqrt(h_r)*u_r + np.sqrt(h_l)*u_l) / \\\n", " (np.sqrt(h_r) + np.sqrt(h_l))\n", " c_hat = np.sqrt(grav*h_hat)\n", "\n", " lambda_1_l = u_l - np.sqrt(grav*h_l)\n", " lambda_2_r = u_r + np.sqrt(grav*h_r)\n", " \n", " s1 = min(lambda_1_l, u_hat - c_hat)\n", " s2 = max(lambda_2_r, u_hat + c_hat)\n", " \n", " h_m = (hu_r - hu_l - s2*h_r + s1*h_l)/(s1-s2)\n", " hu_m = (hu_r*u_r - hu_l*u_l + 0.5*grav*(h_r**2 - h_l**2) \\\n", " - s2*hu_r + s1*hu_l)/(s1-s2)\n", " q_m = np.array([h_m, hu_m])\n", " \n", " states = np.column_stack([q_l,q_m,q_r])\n", " speeds = [s1, s2]\n", " wave_types = ['contact','contact']\n", " \n", " def reval(xi):\n", " h_out = (xi