```python
# Erasmus+ ICCT project (2018-1-SI01-KA203-047081)

# Toggle cell visibility

from IPython.display import HTML
tag = HTML('''
Toggle cell visibility <a href="javascript:code_toggle()">here</a>.''')
display(tag)
```

Toggle cell visibility here.

```python
%matplotlib notebook
import numpy as np
import control as control
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from ipywidgets import widgets
from ipywidgets import interact
import scipy.signal as signal
import sympy as sym

```

## Mechanikai rendszerek

#### Általános tömeg-rugó-csillapítás modell
> A tömeg-rugró-csillapítás modell egy pontszerű tömegekből áll, amiket rugók és csillapítások kapcsolnak össze. A modell alkalmas összetett anyagtulajdonságok, mint például nemlinearitás és viszkoelaszticitás (forrás: [Wikipedia](https://en.wikipedia.org/wiki/Mass-spring-damper_model "Mass-spring-model"))
#### Negyedautó modell
> A negyed autómodellt a lengéscsillapító rendszerek minősítésére alkalmazzák. A tömeg $m_1$ a "rugózó tömeg", ami negyede a jármú teljes tömegének, és a lengéscsillapító rendszer támasztja alá. Ezzel szemben $m_2$ a "nem-rugózó tömeg", ami egy kerék, és a fél tengely tömegének összege, valamint a lengéscsillapító rendszeré és felfüggesztésé. A rendszer merevségét és csillapítását egy ideális rugóállandóval ($k_1$) és egy surlódási együtthatóval ($B$) modellezzük. A gumikerék rugalmasságát egy további, $k_2$ rugóállandóval vesszük figyelembe. (forrás: [Chegg Study](https://www.chegg.com/homework-help/questions-and-answers/figure-p230-shows-1-4-car-model-used-analyze-ride-quality-automotive-suspension-systems-ma-q26244005 "1/4 car model"))

---

### Hogyan kezelhető a példa?
1. A *tömeg-rugó-csillapítás* vallamint a *negyed autómodell* között a megfelelő gombokkal lehet váltani.
2. Az erőhatás függvénye ($F$) lehet *egységugrás*, *impulzus*, *rámpa* és *szinusz* alakú.
3. A megfelelő csúszkákkal módosíthatóak a tömegek ($m$, $m_1$ és $m_2$), rugóállandók ($k$, $k_1$ és $k_2$) és a csillapítás ($B$) értéke, valamint a bemenő jel erősítése és a kezdeti feltételek ($x_0$, $\dot{x}_0$, $y_0$, $\dot{y}_0$).

        Mass-spring-damper
        1/4 car model

```python
# create figure
fig = plt.figure(figsize=(9.8, 4),num='Mechanikai rendszerek')

# add sublot
ax = fig.add_subplot(111)
ax.set_title('Idő válasz')
ax.set_ylabel('bemenet, kimenet')
ax.set_xlabel('$t$ [s]')

ax.grid(which='both', axis='both', color='lightgray')

inputf, = ax.plot([], [])
responsef, = ax.plot([], [])
responsef2, = ax.plot([], [])
arrowf, = ax.plot([],[])

style = {'description_width': 'initial'}

selectSystem=widgets.ToggleButtons(
    options=[('tömeg-rugó-csillapítás',0),('negyedautó modell',1)],
    description='Rendszer: ', style=style) # define toggle buttons
selectForce = widgets.ToggleButtons(
    options=[('egységugrás függvény', 0), ('impulzus függvény', 1), ('rámpa függvény', 2), ('szinusz függvény', 3)],
    description='Válasszon $F$ függvényt: ', style=style)
display(selectSystem)
display(selectForce)

def build_model(M,K,B,M1,M2,B1,K1,K2,amp,x0,xpika0,y0,ypika0,select_System,index):

    num_of_samples = 1000
    total_time = 25
    t = np.linspace(0, total_time, num_of_samples) # time for which response is calculated (start, stop, step)

    global inputf, responsef, responsef2, arrowf

    if select_System==0:

        system0 = control.TransferFunction([1], [M, B, K])

        if index==0:
            inputfunc = np.ones(len(t))*amp
            inputfunc[0]=0
            time, response, xx = control.forced_response(system0, t, inputfunc, X0=[xpika0,x0])

        elif index==1:
            inputfunc=signal.unit_impulse(1000, 0)*amp
            time, response, xx = control.forced_response(system0, t, inputfunc, X0=[xpika0,x0])

        elif index==2:
            inputfunc=t;
            time, response, xx = control.forced_response(system0, t, inputfunc, X0=[xpika0,x0])

        elif index==3:
            inputfunc=np.sin(t)*amp
            time, response, xx = control.forced_response(system0, t, inputfunc, X0=[xpika0,x0])

    elif select_System==1:

        system1=control.TransferFunction([M2, B1, K1+K2], [M1*M2, M1*B1+M2*B1, M2*K1+M1*(K1+K2), K2*B1, K1*K2])
        system2=control.TransferFunction([B1*K1*M2**2, B1**2*K1*M2, B1*K1**2*M2 + 2*B1*K1*K2*M2,
                                          B1**2*K1*K2, B1*K1**2*K2 + B1*K1*K2**2],
                                         [M1**2*M2**2, B1*M1**2*M2 + 2*B1*M1*M2**2,
                                          B1**2*M1*M2 + B1**2*M2**2 + K1*M1**2*M2 + 2*K1*M1*M2**2 + 2*K2*M1**2*M2 + K2*M1*M2**2,
                                          2*B1*K1*M1*M2 + 2*B1*K1*M2**2 + B1*K2*M1**2 + 5*B1*K2*M1*M2 + B1*K2*M2**2,
                                          B1**2*K2*M1 + 2*B1**2*K2*M2 + K1**2*M1*M2 + K1**2*M2**2 + K1*K2*M1**2 + 5*K1*K2*M1*M2 + K1*K2*M2**2 + K2**2*M1**2 + 2*K2**2*M1*M2,
                                          2*B1*K1*K2*M1 + 4*B1*K1*K2*M2 + 3*B1*K2**2*M1 + 2*B1*K2**2*M2,
                                          B1**2*K2**2 + K1**2*K2*M1 + 2*K1**2*K2*M2 + 3*K1*K2**2*M1 + 2*K1*K2**2*M2 + K2**3*M1,
                                          2*B1*K1*K2**2 + B1*K2**3,
                                          K1**2*K2**2 + K1*K2**3])
        if index==0:
            inputfunc = np.ones(len(t))*amp
            inputfunc[0]=0
            time, response, xx = control.forced_response(system1, t, inputfunc, X0=[0,0,xpika0,x0])
            time2, response2, xx2 = control.forced_response(system2, t, inputfunc, X0=[0,0,0,0,0,0,ypika0,y0])

        elif index==1:
            inputfunc=signal.unit_impulse(1000, 0)*amp
            time, response, xx = control.forced_response(system1, t, inputfunc, X0=[0,0,xpika0,x0])
            time2, response2, xx2 = control.forced_response(system2, t, inputfunc, X0=[0,0,0,0,0,0,ypika0,y0])

        elif index==2:
            inputfunc=t;
            time, response, xx = control.forced_response(system1, t, inputfunc, X0=[0,0,xpika0,x0])
            time2, response2, xx2 = control.forced_response(system2, t, inputfunc, X0=[0,0,0,0,0,0,ypika0,y0])

        elif index==3:
            inputfunc=np.sin(t)*amp
            time, response, xx = control.forced_response(system1, t, inputfunc, X0=[0,0,xpika0,x0])
            time2, response2, xx2 = control.forced_response(system2, t, inputfunc, X0=[0,0,0,0,0,0,ypika0,y0])

    ax.lines.remove(responsef)
    ax.lines.remove(inputf)
    ax.lines.remove(responsef2)
    ax.lines.remove(arrowf)

    inputf, = ax.plot(t,inputfunc,label='$F$',color='C0')
    responsef, = ax.plot(time, response,label='$x$',color='C3')

    if select_System==1:
        responsef2, = ax.plot(time, response2,label='$y$',color='C2')
    elif select_System==0:
        responsef2, = ax.plot([],[])

    if index==1:
        if amp>0:
            arrowf, = ax.plot([-0.1,0,0.1],[amp-((amp*0.05)/2),amp,amp-((amp*0.05)/2)],color='C0',linewidth=4)
        elif amp==0:
            arrowf, = ax.plot([],[])
        elif amp<0:
            arrowf, = ax.plot([-0.1,0,0.1],[amp-((amp*0.05)/2),amp,amp-(amp*(0.05)/2)],color='C0',linewidth=4)
    else:
        arrowf, = ax.plot([],[])

    ax.relim()
    ax.autoscale_view()

    ax.legend()

def update_sliders(index):
    global m1_slider, b1_slider, k1_slider, m21_slider, m22_slider, b2_slider, k21_slider, k22_slider
    global x0_slider, xpika0_slider, y0_slider, ypika0_slider

    m1val = [0.1,0.1,0.1,0.1]
    k1val = [1,1,1,1]
    b1val = [0.1,0.1,0.1,0.1]
    m21val = [0.1,0.1,0.1,0.1]
    m22val = [0.1,0.1,0.1,0.1]
    b2val = [0.1,0.1,0.1,0.1]
    k21val = [1,1,1,1]
    k22val = [1,1,1,1]
    x0val = [0,0,0,0]
    xpika0val = [0,0,0,0]
    y0val = [0,0,0,0]
    ypika0val = [0,0,0,0]

    m1_slider.value = m1val[index]
    k1_slider.value = k1val[index]
    b1_slider.value = b1val[index]
    m21_slider.value = m21val[index]
    m22_slider.value = m22val[index]
    b2_slider.value = b2val[index]
    k21_slider.value = k21val[index]
    k22_slider.value = k22val[index]
    x0_slider.value = x0val[index]
    xpika0_slider.value = xpika0val[index]
    y0_slider.value = y0val[index]
    ypika0_slider.value = ypika0val[index]

def draw_controllers(type_select,index):

    global m1_slider, b1_slider, k1_slider, m21_slider, m22_slider, b2_slider, k21_slider, k22_slider
    global x0_slider, xpika0_slider, y0_slider, ypika0_slider

    x0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='$x_0$ [dm]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
    xpika0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='${\dot{x}}_0$ [dm/s]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)

    if type_select==0:

        amp_slider = widgets.FloatSlider(value=1.,min=-2.,max=2.,step=0.1,
            description='Bemenő jel erősítése:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',style=style)

        m1_slider = widgets.FloatSlider(value=.1, min=.01, max=1., step=.01,
                                        description='$m$ [kg]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
        k1_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
                                       description='$k$ [N/m]:',disabled=False,continuous_update=False,
                                       orientation='horizontal',readout=True,readout_format='.1f',)
        b1_slider = widgets.FloatSlider(value=.1,min=0.0,max=0.5,step=.01,
                                        description='$B$ [Ns/m]:',disabled=False,continuous_update=False,
                                        rientation='horizontal',readout=True,readout_format='.2f',)
        m21_slider = widgets.FloatSlider(value=.1,min=.01,max=1.,step=.01,
            description='$m_1$ [kg]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        m22_slider = widgets.FloatSlider(value=.1,min=.0,max=1.,step=.01,
            description='$m_2$ [kg]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        b2_slider = widgets.FloatSlider(value=.1,min=0.0,max=2,step=.01,
            description='$B$ [Ns/m]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        k21_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
            description='$k_1$ [N/m]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',
        )
        k22_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
            description='$k_2$ [N/m]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',
        )

        y0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='$y_0$ [dm]:',disabled=True,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
        ypika0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='${\dot{y}}_0$ [dm/s]:',disabled=True,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
    elif type_select==1:

        amp_slider = widgets.FloatSlider(value=1.,min=-2.,max=2.,step=0.1,
            description='Bemenő jel erősítése:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',style=style)

        m1_slider = widgets.FloatSlider(value=.1, min=.01, max=1., step=.01,
                                        description='$m$ [kg]:',disabled=True,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
        k1_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
                                       description='$k$ [N/m]:',disabled=True,continuous_update=False,
                                       orientation='horizontal',readout=True,readout_format='.1f',)
        b1_slider = widgets.FloatSlider(value=.1,min=0.0,max=0.5,step=.01,
                                        description='$B$ [Ns/m]:',disabled=True,continuous_update=False,
                                        rientation='horizontal',readout=True,readout_format='.2f',)
        m21_slider = widgets.FloatSlider(value=.1,min=.01,max=1.,step=.01,
            description='$m_1$ [kg]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        m22_slider = widgets.FloatSlider(value=.1,min=.0,max=1.,step=.01,
            description='$m_2$ [kg]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        b2_slider = widgets.FloatSlider(value=.1,min=0.0,max=2,step=.01,
            description='$B$ [Ns/m]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        k21_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
            description='$k_1$ [N/m]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',
        )
        k22_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
            description='$k_2$ [N/m]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',
        )

        y0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='$y_0$ [dm]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
        ypika0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='${\dot{y}}_0$ [dm/s]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
    input_data = widgets.interactive_output(build_model, {'M':m1_slider, 'K':k1_slider, 'B':b1_slider, 'M1':m21_slider,
                                                    'M2':m22_slider, 'B1':b2_slider, 'K1':k21_slider, 'K2':k22_slider, 'amp':amp_slider,
                                                    'x0':x0_slider,'xpika0':xpika0_slider,'y0':y0_slider,'ypika0':ypika0_slider,
                                                    'select_System':selectSystem,'index':selectForce})

    input_data2 = widgets.interactive_output(update_sliders, {'index':selectForce})

    box_layout = widgets.Layout(border='1px solid black',
                    width='auto',
                    height='',
                    flex_flow='row',
                    display='flex')

    buttons1=widgets.HBox([widgets.VBox([amp_slider],layout=widgets.Layout(width='auto')),
                         widgets.VBox([x0_slider,xpika0_slider]),
                         widgets.VBox([y0_slider,ypika0_slider])],layout=box_layout)
    display(widgets.VBox([widgets.Label('Válassza meg a bemeneti erősítést és a kezdeti feltételeket:'), buttons1]))
    display(widgets.HBox([widgets.VBox([m1_slider,k1_slider,b1_slider], layout=widgets.Layout(width='45%')),
                          widgets.VBox([m21_slider,m22_slider,k21_slider,k22_slider,b2_slider], layout=widgets.Layout(width='45%'))]), input_data)

widgets.interactive_output(draw_controllers, {'type_select':selectSystem,'index':selectForce})
```

```python

```


# Using Statistical Models in Accelerator Physics

by Kelly Anderson

Statistical methods are used a lot in accelerator physics. Even though my research focuses mostly on single-particle dynamics, accelerators are always used with many particles at once. Particles in an accelerator are sent in bunches or tight groups of particles. This is done for a few reasons, one is that when the most efficient method for accelerating particles is using radio frequency cavities. Even though they provide a large amount of acceleration they can only provide it for a finite window. The particles are sent in bunches that match these windows so they are accelerated.

Another reason is that a larger number of particles means a larger number, and more frequent, collisions, this gives more data to researchers. It is also easier to get two bunches of particles to collide than two individual particles. Bunches will also provide a number of particles from different states to collide which provides more diverse data, but that can be negative in some cases.

Because of the high number of particles, it is best to describe then bunches as a whole. This is where statistical methods come into play. One concept used often is emittance. This emittance refers to the area that a bunch of particles occupies. This could be the physical profile of the bunch or the area that the bunch covers in phase space. The emittance of a bunch has to fit in the aperture of each element which is the area in phase space in which a particle can enter the element and not be lost due to unwanted collision with the surroundings. Even though a bunch’s area in phase space is conserved in many cases, that does not mean its emittance is constant. This is because the area can take up many shapes, like galaxy looking spirals, but the general ellipse that encompasses all particles is generally the emittance that is being referred to and it can grow in size.

When we describe this emittance we refer to it by the statistical average of the momentum and position of all the particles. One can also look at the dynamic aperture of an entire accelerator to know how particles must enter the machine in order to remain stable in cases like storage rings. To do this we look at how each element in the machine maps particles from beginning to the end. Many common elements, like a FODO cell, will rotate the ellipse in phase space which also needs to be taken into account.

\begin{equation}
    \epsilon = \sqrt{\langle{x^2}\rangle \langle{x'^2}\rangle - \langle{x x'}\rangle^2}
\end{equation}

Closer to my research, there are statistical methods used in analyzing particle motion. Starting from a mathematical transformation that maps the initial state of a particle to the final state one can compare that to the data from beam position monitors in the accelerator to test the accuracy of the mapping used.

---
# References

https://drive.google.com/file/d/1k5gs2YPnN-AwNJ3PK7JNBFejN91W15ln/view


# 简单理解 LT-DF SOS RMP2

> 创建时间：2019-10-17；最后修改：2021-06-12

这一节承接上一节对 DF-MP2 的讨论。我们知道 DF-MP2 的计算耗时尽管比传统 MP2 低但仍然是 $O(N^5)$；这一节作的 LT-DF SOS 近似则可以确实地将计算复杂度降低为 $O(N^4)$。

:::{note}

LT-DF SOS MP2 的写法并不是目前通用的写法；可能目前也没有通用的写法。

该名称的含义是 Laplace-Transformation Density-Fitting Scaled Opposite-Spin Second Order Moller-Plesset。

不要被这么长的名称吓到。除了 LT 之外，其它的过程或者非常容易实现 (SOS)，或者是我们已经理解的内容 (DF, MP2)。

:::

```python
%matplotlib notebook

import numpy as np
import scipy
import quadpy
from functools import partial
import matplotlib.pyplot as plt

from pyscf import gto, scf, mp, df

np.einsum = partial(np.einsum, optimize=["greedy", 1024 ** 3 * 2 / 8])
np.set_printoptions(8, linewidth=150, suppress=True)
```

在这篇文档中，我们将会将测评体系更换为乙烯分子。这主要是为了与 Jung et al. [^Jung-Head-Gordon.JCP.2004.121] 文章中 Table I 的数据作比较。

:::{admonition} 冻核近似说明

这篇文章采用了 MP2 的冻核近似 (Frozen Core)；因此我们之后的 MP2 计算也需要相应地采用冻核近似。据我所知 PySCF 没有明确地写出冻核近似代码。因此，我们需要手动指定冻核。对于乙烯分子，两个碳原子有需要被冻结的 1s 轨道，因此被冻结的占据轨道数为 2；而非占轨道的冻结数为零。

程序中使用 `sf` 表示冻结后的占据轨道分割，`Cf` 与 `ef` 表示冻结后的分子轨道系数与轨道能量。为了符号简便起见，其它的程序符号基本沿用上一篇文档。

:::

:::{admonition} 分子结构说明

目前使用的分子结构是在 MP2/6-31G* 下优化的乙烯分子。优化使用 Gaussian 09 rev B01 实现。尽管能重复 Jung et al. [^Jung-Head-Gordon.JCP.2004.121] 文章中乙烯分子的结果，但会在相关能的第 4 位上有些微差距。我认为这并不影响讨论。

MP2 结构优化输入卡 {download}`assets/C2H4-opt.gjf` QCISD(T) 能量输入卡 {download}`assets/C2H4-eng.gjf`

:::

```python
mol = gto.Mole()
mol.atom = """
C   -0.668188207     0.000000075    -0.0000028526
C    0.668188207     0.0000000727    0.0000028526
H   -1.2382848349   -0.0000000262   -0.9232789581
H   -1.2382926466   -0.0000000483    0.9232684342
H    1.2382848349   -0.0000000255    0.9232789581
H    1.2382926466   -0.0000000477   -0.9232684342
"""
mol.basis = "cc-pVTZ"
mol.verbose = 0
mol.build()

natm = mol.natm
nao = nmo = mol.nao
nocc = mol.nelec[0]
nvir = nmo - nocc
so, sv, sa = slice(0, nocc), slice(nocc, nmo), slice(0, nmo)
sf = slice(2, nocc)
```

## 参考值

### 参考值与 SOS-MP2

根据 Jung et al. [^Jung-Head-Gordon.JCP.2004.121] 文章的结果，我们给出下述参考值字典 `ref`；每个键值对应的数值单位是 mH (milli-Hartree)。

```python
ref = {
    "QCISDT": -375.4,
    "SS": -35.5,
    "OS": -264.7,
    "MP2": -375.4 * 0.894,
    "SCS": -375.4 * 0.909,
    "SOS": -375.4 * 0.917,
}
```

其中，键值 `MP2` (Second Order Moller-Plesset), `SCS` (Spin-Component Scaled) 与 `SOS` (Scaled Opposite-Spin) 原则上都可以通过 `SS` (Same-Spin) 与 `OS` (Opposite-Spin) 对相关能的贡献给出：

$$
E_\mathrm{MP2, corr} = 2 E_\mathrm{SS} + E_\mathrm{OS}
$$

```python
print(ref["MP2"])
print(ref["SS"] * 2 + ref["OS"] * 1)
```

    -335.6076
    -335.7

$$
E_\text{SCS-MP2, corr} = 2 \times \frac{1}{3} E_\mathrm{SS} + \frac{6}{5} E_\mathrm{OS}
$$

```python
print(ref["SCS"])
print(ref["SS"] * 2 * 1/3 + ref["OS"] * 6/5)
```

    -341.23859999999996
    -341.3066666666667

$$
E_\text{SOS-MP2, corr} = 1.3 E_\mathrm{OS}
$$

```python
print(ref["SOS"])
print(ref["OS"] * 1.3)
```

    -344.2418
    -344.11

关于这些方法的细节，我们不在这里细究。在这篇文档中，我们只关心到 SOS 方法只使用了 MP2 相关能中 OS 部分的贡献。这对我们后面的讨论至关重要。我们在这篇文档将要重复的就是 SOS-MP2。

### SCF 计算

在继续文档以前，我们需要给出乙烯分子的各种计算结果。大多数代码记号与公式都可以与前两篇文档比对。补充出来的记号除了冻结轨道近似的部分之外，还有

* $D_i^a$ `D_ia`：$\varepsilon_i - \varepsilon_a$

由于我们关心的是 MP2 部分的 DF，因此自洽场即使不使用 DF 也不影响我们的讨论。

```python
scf_normal = scf.RHF(mol).run()
mp2_normal = mp.MP2(scf_normal).run()
```

```python
C, e = scf_normal.mo_coeff, scf_normal.mo_energy
Cf, Cv = C[:, sf], C[:, sv]
ef, ev = e[sf], e[sv]
D_iajb = ef[:, None, None, None] - ev[None, :, None, None] + ef[None, None, :, None] - ev[None, None, None, :]
D_ia = ef[:, None] - ev[None, :]
```

### SS/OS 计算

MP2 能量除了使用上一份文档的 $E_\mathrm{MP2, corr} = T_{ij}^{ab} t_{ij}^{ab} D_{ij}^{ab}$ 之外，还可以使用下述的公式计算：

$$
E_\mathrm{MP2, corr} = \frac{1}{D_{ij}^{ab}} (ia|jb) \big[ 2 (ia|jb) - (ib|ja) \big]
$$

上式可以拆分为

$$
\begin{align}
E_\mathrm{SS} &= \frac{1}{2} \frac{1}{D_{ij}^{ab}} (ia|jb) \big[ (ia|jb) - (ib|ja) \big] \\
E_\mathrm{OS} &= \frac{1}{D_{ij}^{ab}} (ia|jb)^2 \\
E_\mathrm{MP2, corr} &= 2 E_\mathrm{SS} + E_\mathrm{OS}
\end{align}
$$

其拆分依据是通过自旋轨道下的 MP2 表达式产生而来，这里不作展开。为了计算 $E_\mathrm{OS}$，除了 $D_{ij}^{ab}$ `(D_iajb)` 之外，我们还需要 $(ia|jb)$。该变量储存于 `eri_iajb` 中：

```python
eri_iajb = np.einsum("ui, va, uvkl, kj, lb -> iajb", Cf, Cv, mol.intor("int2e"), Cf, Cv)
```

随后我们就可以计算 $E_\mathrm{SS}$ `E_SS_normal` 与 $E_\mathrm{OS}$ `E_OS_normal` 了：

```python
E_SS_normal = 0.5 * (eri_iajb * (eri_iajb - eri_iajb.swapaxes(-1, -3)) / D_iajb).sum()
E_OS_normal = (eri_iajb ** 2 / D_iajb).sum()
print("E_SS       in mH: ", E_SS_normal * 1000)
print("E_OS       in mH: ", E_OS_normal * 1000)
print("E_MP2_corr in mH: ", (E_OS_normal + 2 * E_SS_normal) * 1000)
```

    E_SS       in mH:  -35.46668203417094
    E_OS       in mH:  -264.8091129392841
    E_MP2_corr in mH:  -335.742477007626

这篇文档的主要目标并非重复 $E_\mathrm{MP2, corr}$，而是重复 $E_\text{SOS-MP2, corr}$ `E_SOS_normal`：

```python
E_SOS_normal = 1.3 * E_OS_normal
print("E_SOS_corr in mH: ", E_SOS_normal * 1000)
```

    E_SOS_corr in mH:  -344.25184682106936

## Laplace 变换 (LT) 原理

### $x^{-1}$ LT 数值积分

在这里，我们不讨论更为广泛的 LT 的原理，只讨论与 MP2 计算相关的 $x^{-1} = (D_{ij}^{ab})^{-1}$ 的计算问题。对于 LT 中涉及到 MP2 的问题会放在下一小节中讨论；这里我们只需要知道乙烯分子 $x$ 的取值范围大约会是

```python
print("|D_iajb| max in Hartree: ", np.abs(D_iajb).max())
print("|D_iajb| min in Hartree: ", np.abs(D_iajb).min())
```

    |D_iajb| max in Hartree:  31.40203636576501
    |D_iajb| min in Hartree:  1.0534258481856735

$x^{-1}$ 可以通过下述方式获得：

$$
x^{-1} = \int_0^{+ \infty} e^{- x t} \, \mathrm{d} t
$$

根据格点积分的原理，我们可以将上式在格点 $\{g\}$ 下重新写为

$$
x^{-1} \simeq w_g e^{- x t_g}
$$

现在我们从程序的角度实现上述格点积分。我们知道，$e^{-xt}$ 函数在零点处的值较大，但在远离零点处的值较小。同时，由于 $e^{-xt}$ 本身就是一种指数形式，因此我们容易想到使用指数的方式构造格点。令坐标变量 $t_g$ `grid_points`

$$
t_g = a^g
$$

那么对应的格点权重 $w_g$ `grid_weights` 应当接近

$$
w_g \simeq \frac{\partial}{\partial g} t_g = \log a \cdot a^g
$$

在这里，我们取 $a = 2.5$，而 $g \in \left[ -10, 5 \right)$。这种取法一定程度上是任意的，但考虑到我们希望对 $x \in (0.1, 100)$ 的区间，特别是靠近 $x \in (0.1, 2)$ 的区域有比较良好的近似 (MP2 中主要产生贡献的部分在分母 $x$ 值偏小的区域)，因此这些参数并不是彻底任意地选取的。

```python
grid_points = 2.5 ** np.arange(-12, 6)
grid_weights = np.log(2.5) * grid_points
grid_points
```

    array([ 0.00001678,  0.00004194,  0.00010486,  0.00026214,  0.00065536,  0.0016384 ,  0.004096  ,  0.01024   ,  0.0256    ,  0.064     ,  0.16      ,
            0.4       ,  1.        ,  2.5       ,  6.25      , 15.625     , 39.0625    , 97.65625   ])

举例来说，如果我们要用格点近似 $2^{-1} = 0.5 \simeq w_g e^{- 2 t_g}$，那么程序可以编写如下：

```python
(grid_weights * np.exp(- 1 * grid_points)).sum()
```

    1.0001747291147214

这应该是一个比较有效的近似了。

### LT 数值积分精度

对于更广泛的数值精度分析，我们在上述 LT 格点的条件下，给出如下的图像：

```python
fig, ax = plt.subplots()

x_axis = np.logspace(-2, 3, 100)
y_axis = (grid_weights * np.exp(- x_axis[:, None] * grid_points)).sum(axis=-1) * x_axis
ax.plot(x_axis, y_axis)
ax.plot([1e-2, 1e3], [1.001, 1.001], color="C1", linestyle=":")
ax.plot([1e-2, 1e3], [1, 1], color="black", linestyle=":")
ax.plot([1e-2, 1e3], [0.999, 0.999], color="C1", linestyle=":")
ax.plot([1e-1, 1e-1], [0, 2], color="C2", linestyle=":")
ax.plot([1e2, 1e2], [0, 2], color="C2", linestyle=":")
ax.set_ylim(0.99,1.002)
ax.set_xscale("log")

ax.set_xlabel("$x$")
ax.set_ylabel("Relative error")
ax.set_title("Ratio of $\sum_g w_g e^{-x t_g}$ to $1/x$")
```

    <IPython.core.display.Javascript object>

    Text(0.5, 1.0, 'Ratio of $\\sum_g w_g e^{-x t_g}$ to $1/x$')

上图表示的是在不同 $x$ 取值下，我们所给出的格点积分 $w_g e^{-x t_g}$ 的精度；在 $x \in (0.1, 100)$ 的区间内我们仍然能保证 $0.1\%$ 的精度，但超过这些区域之后，结果就会迅速地变差。

我们知道，若分子呈现近简并的情形，那么 $x = D_{ij}^{ab}$ 会非常小；在这种情况下，LT 所提供的格点积分就很可能无法正确地描述分母 $D_{ij}^{ab}$ 的行为，使得本来在近简并就无法正常计算的 MP2 的情况雪上加霜。但若分子的 HOMO/LUMO 的能级差足够大并且大于 $0.1$ 个 Hartree，那么我们应当预期当前格点下的 LT 近似可以几乎精确地给出相关能。

### LT SOS MP2

现在我们就可以用上刚才的技术，对 LT SOS MP2 进行计算。这并不会提升任何计算效率；这里只是用来验证我们方才作的 LT 近似的有效性。

在 LT 近似下，SOS 能量可以表达为

$$
\begin{align}
E_\text{SOS-MP2, corr} &= 1.3 \frac{1}{D_{ij}^{ab}} (ia|jb)^2 \\
&= - 1.3 (ia|jb)^2 \int_0^{+\infty} e^{D_{ij}^{ab} t} \, \mathrm{d} t \\
&\simeq -1.3 w_g (ia|jb)^2e^{D_{ij}^{ab} t_g}
\end{align}
$$

这里的公式的正负号可能不太直观。这是因为在这篇文档的记号下，$D_{ij}^{ab} < 0$。我们通过如下代码，给出 LT SOS MP2 能量：

```python
E_SOS_LT = - 1.3 * np.einsum("g, iajb, giajb ->", grid_weights, eri_iajb**2, np.exp(D_iajb * grid_points[:, None, None, None, None]))
print("LT-SOS-MP2 corr      in mH: ", E_SOS_LT * 1000)
print("Deviation to SOS-MP2 in mH: ", (E_SOS_LT - E_SOS_normal) * 1000)
```

    LT-SOS-MP2 corr      in mH:  -344.2429384155852
    Deviation to SOS-MP2 in mH:  0.00890840548417593

我们可以发现这是一个有效的近似。

## LT-DF SOS MP2

### DF 环境设置

在继续文档之前，我们需要对 DF 的环境作一些简单的设置。

```python
auxmol = mol.copy()
auxmol.basis = "cc-pvtz-ri"
auxmol.build()

nao_df = auxmol.nao
```

```python
int2c2e = auxmol.intor("int2c2e")
int2c2e.shape
int3c2e = df.incore.aux_e2(mol, auxmol)

int2c2e_half = scipy.linalg.cholesky(int2c2e, lower=True)
V_df_mp2 = scipy.linalg.solve_triangular(int2c2e_half, int3c2e.reshape(-1, nao_df).T, lower=True)\
               .reshape(nao_df, nao, nao).transpose((1, 2, 0))
```

尽管这里的 $V_{ia, P}$ `V_df_ia` 与前一篇文档中的 $V_{ia, P}^\mathrm{MP2}$ 是相同的，但需要注意到这里的轨道 $i$ 是价层的占据轨道，而不能是冻结的占据轨道。

```python
V_df_ia = np.einsum("uvP, ui, va -> iaP", V_df_mp2, Cf, Cv)
```

### LT-DF SOS MP2 表达式

我们对上面提到的 LT SOS MP2 的近似表达式再作 DF 近似：

$$
\begin{align}
E_\text{SOS-MP2, corr} &\simeq -1.3 w_g (ia|jb)^2 e^{D_{ij}^{ab} t_g} \\
&\simeq -1.3 w_g V_{ia, P} V_{jb, P} V_{ia, Q} V_{jb, Q} e^{D_{ij}^{ab} t_g} \\
&= -1.3 w_g V_{ia, P} V_{jb, P} V_{ia, Q} V_{jb, Q} e^{D_i^a t_g} e^{D_j^b t_g}
\end{align}
$$

注意到最后一个等号利用到 $D_{ij}^{ab} = D_i^a + D_j^b$。

```python
E_SOS_LTDF = - 1.3 * np.einsum("g, iaP, jbP, iaQ, jbQ, gia, gjb ->",
                               grid_weights, V_df_ia, V_df_ia, V_df_ia, V_df_ia,
                               np.exp(D_ia * grid_points[:, None, None]),
                               np.exp(D_ia * grid_points[:, None, None]))
print("LT-DF-SOS-MP2 corr   in mH: ", E_SOS_LTDF * 1000)
print("Deviation to SOS-MP2 in mH: ", (E_SOS_LTDF - E_SOS_normal) * 1000)
```

    LT-DF-SOS-MP2 corr   in mH:  -344.1316103156453
    Deviation to SOS-MP2 in mH:  0.12023650542408726

我们认为这基本重复出了原始的 SOS MP2 能量了。

### 简化公式表达

显然上面的公式表达太过于繁杂。在实际实现时，一般先将其中的三个张量缩并为 `X_gPQ`：

$$
X_{gPQ} = V_{ia, P} V_{ia, Q} e^{D_i^a t_g}
$$

```python
X_gPQ = np.einsum("iaP, iaQ, gia -> gPQ", V_df_ia, V_df_ia, np.exp(D_ia * grid_points[:, None, None]))
```

那么 LT-DF 近似后的 SOS MP2 表达式可以简写为

$$
E_\text{SOS-MP2, corr} \simeq -1.3 w_g X_{gPQ}^2
$$

```python
E_SOS_LTDF_simp = - 1.3 * np.einsum("g, gPQ ->", grid_weights, X_gPQ**2)
print("LT-DF-SOS-MP2 corr   in mH: ", E_SOS_LTDF_simp * 1000)
print("Deviation to SOS-MP2 in mH: ", (E_SOS_LTDF_simp - E_SOS_normal) * 1000)
```

    LT-DF-SOS-MP2 corr   in mH:  -344.13161031563544
    Deviation to SOS-MP2 in mH:  0.12023650543391273

### 计算效率比较

这里我们对 SOS MP2，LT SOS MP2 与 LT-DF SOS MP2 的计算效率进行比较，工具是 `np.einsum_path`。我们假定内存的空间是足够多的，并且不考虑为生成各种积分、或各种 DF 三中心积分所需要耗费的时间。

**普通 MP2**

效率耗时最大的部分在双电子积分转换过程。

```python
print(np.einsum_path("ui, va, uvkl, kj, lb -> iajb",
    Cf, Cv, mol.intor("int2e"), Cf, Cv,
    optimize=["greedy", 1024 ** 3 * 2 / 8])[1])  # Given 2GB memory space
```

      Complete contraction:  ui,va,uvkl,kj,lb->iajb
             Naive scaling:  8
         Optimized scaling:  5
          Naive FLOP count:  3.801e+14
      Optimized FLOP count:  2.487e+09
       Theoretical speedup:  152841.286
      Largest intermediate:  9.365e+06 elements
    --------------------------------------------------------------------------
    scaling                  current                                remaining
    --------------------------------------------------------------------------
       5               uvkl,ui->iklv                      va,kj,lb,iklv->iajb
       5               iklv,kj->ijlv                         va,lb,ijlv->iajb
       5               ijlv,va->ijal                            lb,ijal->iajb
       5               ijal,lb->iajb                               iajb->iajb

**DF SOS MP2**

效率耗时最大的部分在从 3c2e 积分生成双电子积分过程。

```python
print(np.einsum_path("iaP, jbP -> iajb",
    V_df_ia, V_df_ia,
    optimize=["greedy", 1024 ** 3 * 2 / 8])[1])  # Given 2GB memory space
```

      Complete contraction:  iaP,jbP->iajb
             Naive scaling:  5
         Optimized scaling:  5
          Naive FLOP count:  2.368e+08
      Optimized FLOP count:  2.368e+08
       Theoretical speedup:  1.000
      Largest intermediate:  4.199e+05 elements
    --------------------------------------------------------------------------
    scaling                  current                                remaining
    --------------------------------------------------------------------------
       5               jbP,iaP->iajb                               iajb->iajb

**LT-DF SOS MP2**

效率耗时最大的部分在生成中间张量 $X_{gPQ}$ 的过程。

```python
print(np.einsum_path("iaP, iaQ, gia -> gPQ",
    V_df_ia, V_df_ia, np.exp(D_ia * grid_points[:, None, None]),
    optimize=["greedy", 1024 ** 3 * 2 / 8])[1])  # Given 2GB memory space
```

      Complete contraction:  iaP,iaQ,gia->gPQ
             Naive scaling:  5
         Optimized scaling:  5
          Naive FLOP count:  2.783e+09
      Optimized FLOP count:  1.858e+09
       Theoretical speedup:  1.497
      Largest intermediate:  3.289e+06 elements
    --------------------------------------------------------------------------
    scaling                  current                                remaining
    --------------------------------------------------------------------------
       4               gia,iaP->igaP                            iaQ,igaP->gPQ
       5               igaP,iaQ->gPQ                                 gPQ->gPQ

对于这个分子而言，放开优化张量缩并过程，结果反而会回到类似于 DF MP2 的缩并过程：

```python
print(np.einsum_path("g, iaP, jbP, iaQ, jbQ, gia, gjb ->",
    grid_weights, V_df_ia, V_df_ia, V_df_ia, V_df_ia,
    np.exp(D_ia * grid_points[:, None, None]),
    np.exp(D_ia * grid_points[:, None, None]),
    optimize=["greedy", 1024 ** 3 * 2 / 8])[1])  # Given 2GB memory space
```

      Complete contraction:  g,iaP,jbP,iaQ,jbQ,gia,gjb->
             Naive scaling:  7
         Optimized scaling:  5
          Naive FLOP count:  4.207e+12
      Optimized FLOP count:  4.892e+08
       Theoretical speedup:  8600.264
      Largest intermediate:  4.199e+05 elements
    --------------------------------------------------------------------------
    scaling                  current                                remaining
    --------------------------------------------------------------------------
       3                  gia,g->iga                iaP,jbP,iaQ,jbQ,gjb,iga->
       5               jbP,iaP->ijab                   iaQ,jbQ,gjb,iga,ijab->
       5               jbQ,iaQ->ijab                      gjb,iga,ijab,ijab->
       4             ijab,ijab->ijab                           gjb,iga,ijab->
       5               ijab,gjb->iga                                iga,iga->
       3                   iga,iga->                                       ->

**简单总结**

事实上，LT-DF SOS MP2 的优势并不体现在小分子的计算上，这从刚才的效率分析上就能看出来。但是，对于大分子 (占据电子数较多的情况，而非基组非常庞大的情况)，LT-DF MP2 就有不小的优势。这是因为耗时最大的部分的时间复杂度分别是

* LT-DF SOS MP2: $O(n_\mathrm{grid} n_\mathrm{nocc} n_\mathrm{nvir} n_\mathrm{aux}^2)$

* DF SOS MP2: $O(n_\mathrm{occ}^2 n_\mathrm{vir}^2 n_\mathrm{aux})$

我们可能会认为 $n_\mathrm{occ} < n_\mathrm{vir}$，而 $n_\mathrm{aux}$ 在基组较大时是 $n_\mathrm{vir}$ 的 3 倍左右。由于 $n_\mathrm{grid}$ 不随体系增大而变化，因此在 $n_\mathrm{grid} \ll n_\mathrm{occ}$ 的情况下，LT 的优势会凸显。

## 备注

这份文档颜文杰提供了 QChem 代码中关于 7 点 LT 格点的支持；尽管最终文档使用了自定义的等比级数格点，但 7 点 LT 格点帮助了文档的完成的过程。

[^Jung-Head-Gordon.JCP.2004.121]: Jung, Y.; Lochan, R. C.; Dutoi, A. D.; Head-Gordon, M. Scaled Opposite-Spin Second Order Møller–Plesset Correlation Energy: An Economical Electronic Structure Method. *J. Chem. Phys.* **2004**, *121* (20), 9793–9802. doi: [10.1063/1.1809602](https://doi.org/10.1063/1.1809602).


# Tutorial-IllinoisGRMHD: mhdflux.C

## Authors: Leo Werneck & Zach Etienne

**This module is currently under development**

## In this tutorial module we explain how the MHD fluxes are evaluated within IllinoisGRMHD.

### Required and recommended citations:

* **(Required)** Etienne, Z. B., Paschalidis, V., Haas R., Mösta P., and Shapiro, S. L. IllinoisGRMHD: an open-source, user-friendly GRMHD code for dynamical spacetimes. Class. Quantum Grav. 32 (2015) 175009. ([arxiv:1501.07276](http://arxiv.org/abs/1501.07276)).
* **(Required)** Noble, S. C., Gammie, C. F., McKinney, J. C., Del Zanna, L. Primitive Variable Solvers for Conservative General Relativistic Magnetohydrodynamics. Astrophysical Journal, 641, 626 (2006) ([astro-ph/0512420](https://arxiv.org/abs/astro-ph/0512420)).
* **(Recommended)** Del Zanna, L., Bucciantini N., Londrillo, P. An efficient shock-capturing central-type scheme for multidimensional relativistic flows - II. Magnetohydrodynamics. A&A 400 (2) 397-413 (2003). DOI: 10.1051/0004-6361:20021641 ([astro-ph/0210618](https://arxiv.org/abs/astro-ph/0210618)).

# Table of Contents
$$\label{toc}$$

This module is organized as follows

0. [Step 0](#src_dir): **Source directory creation**
1. [Step 1](#introduction): **Introduction**
1. [Step 2](#basic_quantities): **Basic quantities**
1. [Step 3](#eos_quantities): **Equation of State (EOS) quantities**
1. [Step 4](#impose_speed_limit_output_u0): **Impose a speed limit and determine $u^{0}$**
1. [Step 5](#magnetic_field_comoving_frame): **The magnetic field measured in the comoving fluid frame, $b^{\mu}$**
1. [Step 6](#min_max_characteristic_speeds): **The minimum and maximum characteristic speeds**
1. [Step 7](#rho_star_flux): **MHD flux: $\rho_{\star}$**
1. [Step 8](#energy_flux): **MHD flux: $\tilde{\tau}$**
1. [Step 9](#momentum_flux): **MHD flux: $\tilde{S}_{i}$**
1. [Step 10](#code_validation): **Code validation**
1. [Step 11](#latex_pdf_output): **Output this module to $\LaTeX$-formatted PDF file**

# Step 0: Source directory creation \[Back to [top](#toc)\]
$$\label{src_dir}$$

We will now use the [cmdline_helper.py NRPy+ module](Tutorial-Tutorial-cmdline_helper.ipynb) to create the source directory within the `IllinoisGRMHD` NRPy+ directory, if it does not exist yet.

```python
# Step 0: Creation of the IllinoisGRMHD source directory
# Step 0a: Add NRPy's directory to the path
# https://stackoverflow.com/questions/16780014/import-file-from-parent-directory
import os,sys
nrpy_dir_path = os.path.join("..","..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)

# Step 0b: Load up cmdline_helper and create the directory
import cmdline_helper as cmd
IGM_src_dir_path = os.path.join("..","src")
cmd.mkdir(IGM_src_dir_path)

# Step 0c: Create the output file path
outfile_path__mhdflux__C = os.path.join(IGM_src_dir_path,"mhdflux.C")
```

# Step 1: Introduction \[Back to [top](#toc)\]
$$\label{introduction}$$

In this tutorial module we will compute the flux terms for the conservative variables $U=\left\{\rho_{\star},\tilde{\tau},\tilde{S}_{i}\right\}$, which are defined in terms of the primitive variables as

$$
\left(
\begin{matrix}
\rho_{\star}\\
\tilde{\tau}\\
\tilde{S}_{i}
\end{matrix}
\right)
=
\left(
\begin{matrix}
\alpha\sqrt{\gamma}\rho_{b}u^{0}\\
\alpha^{2}\sqrt{\gamma}T^{00}-\rho_{\star}\\
\left(\rho_{\star}h + \alpha u^{0}\sqrt{\gamma}b^{2}\right)u_{i} - \alpha\sqrt{\gamma}b^{0}b_{i}
\end{matrix}
\right)\ .
$$

The flux terms for these conservative variables are

$$
\boldsymbol{F}
=
\left(
\begin{matrix}
F^{i}_{\rho_{\star}}\\
F^{i}_{\tilde{\tau}}\\
\left(F_{\tilde{S}}\right)^{j}_{\ i}
\end{matrix}
\right)
=
\left(
\begin{matrix}
\rho_{\star}v^{i}\\
\alpha^{2}\sqrt{\gamma}T^{0j} - \rho_{\star}v^{j}\\
\alpha\sqrt{\gamma}T^{j}_{\ i}
\end{matrix}
\right)\ .
$$

The MHD flux algorithm computes, for each of the fluxes above, the standard Harten-Lax-van Leer (HLL) flux

$$
\boxed{F^{\rm HLL} = \frac{c^{-}F_{r} + c^{+}F_{l} - c^{+}c^{-}\left(U_{r} - U_{l}\right)}{c^{+} + c^{-}}}\ .
$$

As we go through the implementation, we will notice that will need a lot of the functions defined within the `inlined_functions.C` code file of `IllinoisGRMHD`. We will therefore explain the algorithm of each function in quite some detail, so that the reader can go through this tutorial without having to stop and read the [`inlined_functions.C` tutorial module](Tutorial-IllinoisGRMHD_inlined_functions.ipynb). We do encourage the reader to go through the module as well, though, since we will be presenting the math behind each function, but not the code itself.

# Step 2: Basic quantities \[Back to [top](#toc)\]
$$\label{basic_quantities}$$

We start by defining useful quantities, such as $\psi^{\pm4}$, $\psi^{6}$, $\alpha\sqrt{\gamma}$, $\alpha^{-1}$, and $\alpha^{-2}$.

```python
%%writefile $outfile_path__mhdflux__C
//-----------------------------------------------------------------------------
// Compute the flux for advecting rho_star, tau (Font's energy variable),
//  and S_i .
//-----------------------------------------------------------------------------
static inline void mhdflux(int i,int j,int k,const int flux_dirn,CCTK_REAL *Ul,CCTK_REAL *Ur,  CCTK_REAL *FACEVAL,CCTK_REAL *FACEVAL_LAPSE_PSI4,eos_struct &eos, CCTK_REAL Gamma_th,
                           CCTK_REAL &cmax,CCTK_REAL &cmin,
                           CCTK_REAL &rho_star_flux,CCTK_REAL &tau_flux,CCTK_REAL &st_x_flux,CCTK_REAL &st_y_flux,CCTK_REAL &st_z_flux) {

  CCTK_REAL psi4 = FACEVAL_LAPSE_PSI4[PSI4];
  CCTK_REAL psi6 = FACEVAL_LAPSE_PSI4[PSI4]*FACEVAL_LAPSE_PSI4[PSI2];
  CCTK_REAL psim4 = 1.0/(psi4);

  CCTK_REAL alpha_sqrt_gamma = FACEVAL_LAPSE_PSI4[LAPSE]*psi6;
  CCTK_REAL ONE_OVER_LAPSE = 1.0/FACEVAL_LAPSE_PSI4[LAPSE];
  CCTK_REAL ONE_OVER_LAPSE_SQUARED=SQR(ONE_OVER_LAPSE);
```

    Overwriting ../src/mhdflux.C

# Step 3: Equation of State (EOS) quantities \[Back to [top](#toc)\]
$$\label{eos_quantities}$$

Next we compute the quantities $P_{\rm cold}$, $\epsilon_{\rm cold}$, $\frac{dP_{\rm cold}}{d\rho}$, $\epsilon_{\rm th}$, $h$, and $\Gamma_{\rm cold}$. We have written $\rho_{b}\equiv\rho$ so that the discussion constains a notation that is slightly less cluttered with indices.

The function `compute_P_cold__eps_cold__dPcold_drho__eps_th__h__Gamma_cold()` is defined within the `inlined_functions.C` code file of `IllinoisGRMHD`. It currently implementes 3 different cases:

1. **Case 1: $\boldsymbol{\rho_{b} = 0}$**

   In this case, we set $\Gamma_{\rm cold} = \Gamma_{\rm tab}$, i.e. it's tabulated input value, and all other quantities to zero: $P_{\rm cold}=\epsilon_{\rm cold}=\frac{dP_{\rm cold}}{d\rho}=h=\epsilon_{\rm th}=0$

2. **Case 2: Single Polytrope EOS**

   In this case we have have the relations:

   $$
   \boxed{
   \begin{align}
   P_{\rm cold} &= \kappa \rho^{\Gamma_{\rm cold}}\ ,\\
   \epsilon_{\rm cold} &= \left.\left(\frac{P_{\rm cold}}{\rho}\right)\middle/\left(\Gamma_{\rm cold}-1\right)\right.\ ,\\
   \frac{dP_{\rm cold}}{d\rho} &= \frac{\Gamma_{\rm cold}P_{\rm cold}}{\rho}\ ,\\
   \epsilon_{\rm th} &= \left.\left(\frac{P-P_{\rm cold}}{\rho}\right)\middle/\left(\Gamma_{\rm cold}-1\right)\right.\ ,\\
   h &= 1+\epsilon_{\rm cold}+\epsilon_{\rm th} + \frac{P}{\rho}\ ,\\
   \Gamma_{\rm cold} &= \Gamma_{\rm tab}\ .
   \end{align}}
   $$

2. **Case 3: Piecewise Polytrope EOS**

   We now consider a set of dividing densities $\rho_{\min} < \rho_{1} < \cdots < \rho_{n-1} < \rho_{\max}$ such that the pressure and energy density are everywhere continuous. In each interval, the EOS is a single polytrope, with its own $K_{i}$ and $\left(\Gamma_{cold}\right)_{i}\equiv\Gamma_{i}$, so that

   $$
    \boxed{
    P_{\rm cold} =
    \left\{
    \begin{matrix}
    K_{0}\rho^{\Gamma_{0}} & , & \rho \leq \rho_{0}\\
    K_{1}\rho^{\Gamma_{1}} & , & \rho_{0} \leq \rho \leq \rho_{1}\\
    \vdots &  & \vdots\\
    K_{j}\rho^{\Gamma_{j}} & , & \rho_{j-1} \leq \rho \leq \rho_{j}\\
    \vdots &  & \vdots\\
    K_{N-1}\rho^{\Gamma_{N-1}} & , & \rho_{N-2} \leq \rho \leq \rho_{N-1}\\
    K_{N}\rho^{\Gamma_{N}} & , & \rho \geq \rho_{N-1}
    \end{matrix}
    \right.
    }\ .
    $$

   Then, for each set $\left\{P_{i},K_{i},\Gamma_{i}\right\}$ the desired quantities are computed using the same equations used in Case 2.

```python
%%writefile -a $outfile_path__mhdflux__C

  // First compute P_{cold}, \epsilon_{cold}, dP_{cold}/drho, \epsilon_{th}, h, and \Gamma_{cold},
  // for right and left faces:
  CCTK_REAL P_coldr,eps_coldr,dPcold_drhor=0,eps_thr=0,h_r=0,Gamma_coldr;
  compute_P_cold__eps_cold__dPcold_drho__eps_th__h__Gamma_cold(Ur,eos,Gamma_th,P_coldr,eps_coldr,dPcold_drhor,eps_thr,h_r,Gamma_coldr);
  CCTK_REAL P_coldl,eps_coldl,dPcold_drhol=0,eps_thl=0,h_l=0,Gamma_coldl;
  compute_P_cold__eps_cold__dPcold_drho__eps_th__h__Gamma_cold(Ul,eos,Gamma_th,P_coldl,eps_coldl,dPcold_drhol,eps_thl,h_l,Gamma_coldl);
```

    Appending to ../src/mhdflux.C

# Step 4: Impose a speed limit and determine $u^{0}$ \[Back to [top](#toc)\]
$$\label{impose_speed_limit_output_u0}$$

We now call upon the `impose_speed_limit_output_u0()` function inside the `inlined_functions.C` code file of `IllinoisGRMHD`. The basic algorithm performed by this function is summarized here. We start by evaluating the quantity

$$
\begin{align}
{\rm one\_minus\_one\_over\_alpha\_u0\_squared} \equiv A
&= \gamma_{ij}\left(\frac{v^{i}+\beta^{i}}{\alpha}\right)\left(\frac{v^{j}+\beta^{j}}{\alpha}\right)\\
&= \frac{\gamma_{ij}}{\alpha^{2}}\left[\frac{\gamma^{ik}u_{k}}{u^{0}} - \beta^{i} + \beta^{i}\right]\left[\frac{\gamma^{j\ell}u_{\ell}}{u^{0}} - \beta^{j} + \beta^{j}\right]\\
&=\frac{\gamma_{ij}u^{i}u^{j}}{\left(\alpha u^{0}\right)^{2}}\\
&=\frac{\left(\alpha u^{0}\right)^{2}-1}{\left(\alpha u^{0}\right)^{2}}\\
&=1 - \frac{1}{\left(\alpha u^{0}\right)^{2}}\ \\
\implies \boxed{A = 1 - \frac{1}{\left(\alpha u^{0}\right)^{2}}}\ ,
\end{align}
$$

where when going from line 1 to 2 and from line 3 to 4 we have used eqs. (53) and (56) from [Duez *et al.*](https://arxiv.org/pdf/astro-ph/0503420.pdf), respectively. Then we construct the "speed limit quantity"

$$
{\rm ONE\_MINUS\_ONE\_OVER\_GAMMA\_SPEED\_LIMIT\_SQUARED} \equiv B = 1-\frac{1}{\gamma^{2}_{\rm speed\ limit}}\ .
$$

If $A > B$, then we construct the correction factor $C\equiv A / B$, and adjust the velocities using

$$
\boxed{v^{i} \to \left(v^{i}+\beta^{i}\right)C - \beta^{i}}\ .
$$

Finally, since $A$ is evaluated using the first line above, namely

$$
A = \gamma_{ij}\left(\frac{v^{i}+\beta^{i}}{\alpha}\right)\left(\frac{v^{j}+\beta^{j}}{\alpha}\right)\ ,
$$

but we know, from the last line how $A$ and $u^{0}$ are related, we compute

$$
\boxed{u^{0} = \frac{1}{\alpha\sqrt{1-A}}}\ .
$$

```python
%%writefile -a $outfile_path__mhdflux__C

  //Compute face velocities
  // Begin by computing u0
  output_stats stats; stats.failure_checker=0;
  CCTK_REAL u0_r,u0_l;
  impose_speed_limit_output_u0(FACEVAL,Ur,psi4,ONE_OVER_LAPSE,stats,u0_r);
  impose_speed_limit_output_u0(FACEVAL,Ul,psi4,ONE_OVER_LAPSE,stats,u0_l);
```

    Appending to ../src/mhdflux.C

# Step 5: The magnetic field measured in the comoving fluid frame, $b^{\mu}$ \[Back to [top](#toc)\]
$$\label{magnetic_field_comoving_frame}$$

Now we use th function `compute_smallba_b2_and_u_i_over_u0_psi4()` from the `inlined_functions.C` code file of `IllinoisGRMHD`.

We will need the following identities

$$
\begin{align}
v^{i}       &= \frac{u^{i}}{u^{0}}\ ,\\
B^{0}_{(u)} &= \frac{u_{i}B^{i}}{\alpha}\ ,\\
B^{i}_{(u)} &= \frac{1}{u^{0}}\left(\frac{B^{i}}{\alpha} + u^{i}B^{0}_{(u)}\right)\ ,\\
b^{\mu}     &= \frac{B^{\mu}_{(u)}}{\sqrt{4\pi}}\ .
\end{align}
$$

We start by setting the relation

$$
b^{0} = \frac{u_{i}B^{i}}{\alpha\sqrt{4\pi}} \implies \boxed{\alpha\sqrt{4\pi}b^{0} = u_{i}B^{i}}\ .
$$

Then we compute

$$
\begin{align}
b^{i} &= \frac{B^{i}_{(u)}}{\sqrt{4\pi}}\\
      &= \frac{1}{u^{0}\sqrt{4\pi}}\left(\frac{B^{i}}{\alpha} + B^{0}_{(u)}u^{i}\right)\\
      &= \frac{1}{u^{0}\sqrt{4\pi}}\left(\frac{B^{i}}{\alpha} + \sqrt{4\pi}b^{0}u^{i}\right)\\
      &= \frac{1}{\alpha\sqrt{4\pi}}\left(\frac{B^{i}}{u^{0}} + \alpha\sqrt{4\pi}b^{0}\frac{u^{i}}{u^{0}}\right)\\
\implies &\boxed{b^{i} = \frac{1}{\alpha\sqrt{4\pi}}\left(\frac{B^{i}}{u^{0}} + \alpha\sqrt{4\pi}b^{0}v^{i}\right)}\ .
\end{align}
$$

Finally, we compute

$$
\begin{align}
b^{2} &= g_{\mu\nu}b^{\mu}b^{\nu}\\
      &= g_{00}\left(b^{0}\right)^{2} + g_{ij}b^{i}b^{j} + 2g_{0i}b^{0}b^{i}\\
      &= \left(-\alpha^{2} + \gamma_{ij}\beta^{i}\beta^{j}\right)\left(b^{0}\right)^{2} + \gamma_{ij}b^{i}b^{j} + 2b^{0}\gamma_{ij}\beta^{j}b^{i}\\
      &= -\left(\alpha b^{0}\right)^{2} + \gamma_{ij}\left[b^{i}b^{j} + 2b^{0}b^{i}\beta^{j} + \left(b^{0}\right)^{2}\beta^{i}\beta^{j}\right]\\
\implies &\boxed{b^{2} = -\left(\alpha b^{0}\right)^{2} + \gamma_{ij}\left(b^{i} + b^{0}\beta^{i}\right)\left(b^{j} + b^{0}\beta^{j}\right)}
\end{align}
$$

```python
%%writefile -a $outfile_path__mhdflux__C

  //Next compute b^{\mu}, the magnetic field measured in the comoving fluid frame:
  CCTK_REAL ONE_OVER_LAPSE_SQRT_4PI = ONE_OVER_LAPSE*ONE_OVER_SQRT_4PI;
  /***********************************************************/
  /********** RIGHT FACE ************/
  // Note that smallbr[4] = b^a defined in Gammie's paper, on the right face.
  CCTK_REAL u_x_over_u0_psi4r,u_y_over_u0_psi4r,u_z_over_u0_psi4r;
  CCTK_REAL smallbr[NUMVARS_SMALLB];
  // Compute b^{a}, b^2, and u_i over u^0
  compute_smallba_b2_and_u_i_over_u0_psi4(FACEVAL,FACEVAL_LAPSE_PSI4,Ur,u0_r,ONE_OVER_LAPSE_SQRT_4PI,
                                          u_x_over_u0_psi4r,u_y_over_u0_psi4r,u_z_over_u0_psi4r,smallbr);
  // Then compute u_xr,u_yr, and u_zr. We need to set the zeroth component so we can specify U_LOWER{r,l}[{UX,UY,UZ}] (UX=1,UY=2,UZ=3).
  CCTK_REAL U_LOWERr[4] = { 0.0, u_x_over_u0_psi4r*u0_r*FACEVAL_LAPSE_PSI4[PSI4], u_y_over_u0_psi4r*u0_r*FACEVAL_LAPSE_PSI4[PSI4],
                            u_z_over_u0_psi4r*u0_r*FACEVAL_LAPSE_PSI4[PSI4] };
  /********** LEFT FACE ************/
  // Note that smallbl[4] = b^a defined in Gammie's paper, on the left face.
  CCTK_REAL u_x_over_u0_psi4l,u_y_over_u0_psi4l,u_z_over_u0_psi4l;
  CCTK_REAL smallbl[NUMVARS_SMALLB];
  // Compute b^{a}, b^2, and u_i over u^0
  compute_smallba_b2_and_u_i_over_u0_psi4(FACEVAL,FACEVAL_LAPSE_PSI4,Ul,u0_l,ONE_OVER_LAPSE_SQRT_4PI,
                                          u_x_over_u0_psi4l,u_y_over_u0_psi4l,u_z_over_u0_psi4l,smallbl);
  // Then compute u_xr,u_yr, and u_zr. We need to set the zeroth component so we can specify U_LOWER{r,l}[{UX,UY,UZ}]
  CCTK_REAL U_LOWERl[4] = { 0.0, u_x_over_u0_psi4l*u0_l*FACEVAL_LAPSE_PSI4[PSI4], u_y_over_u0_psi4l*u0_l*FACEVAL_LAPSE_PSI4[PSI4],
                            u_z_over_u0_psi4l*u0_l*FACEVAL_LAPSE_PSI4[PSI4] };
  /***********************************************************/
```

    Appending to ../src/mhdflux.C

# Step 6: The minimum and maximum characteristic speeds \[Back to [top](#toc)\]
$$\label{min_max_characteristic_speeds}$$

We will now explain two different functions contained in the `inlined_functions.C` code file of `IllinoisGRMHD`. These functions are `compute_v02()` and `find_cp_cm`. These functions are called with the objective of computing the minimum ($-$) and maximum ($+$) characteristic speeds at each cell interface, $c_{\pm}^{r,l}$.

We approximate the general GRMHD dispersion relation (eq. 27 of [Gammie & McKinney (2003)](https://arxiv.org/pdf/astro-ph/0301509.pdf)) by the simpler expression

$$
\omega_{\rm cm}^{2} = \left[v_{\rm A}^{2} + c_{\rm s}^{2}\left(1-v_{\rm A}^{2}\right)\right]k_{\rm cm}^{2}\ ,
$$

where $\omega_{\rm cm}=-k_{\mu}u^{\mu}$ is the frequency and $k_{\rm cm}^{2} = K_{\mu}K^{\mu}$ the wavenumber of an MHD wave mode in the frame comoving with the fluid, where $K_{\mu}$ is defined as the projection of the wave vector $k^{\nu}$ onto the direction normal to $u^{\nu}$: $K_{\mu} = \left(g_{\mu\nu}+u_{\mu}u_{\nu}\right)k^{\nu}$. $c_{\rm s}$ is the sound speed, and $v_{\rm A}$ is the Alfvén speed, given by

$$
v_{\rm A} = \sqrt{\frac{b^{2}}{\rho_{b}h + b^{2}}}\ .
$$

With these definitions, we may then solve the approximate dispersion relation above along direction $i$, noting that in the comoving frame $k_{\mu} = \left(-\omega,k_{j}\delta^{j}_{\ i}\right)$ and the wave (phase) velocity is $c_{\pm} = \left.\omega\middle/\left(k_{j}\delta^{j}_{\ i}\right)\right.$. The dispersion can then be written as a quadratic equation for $c_{\pm}$:

$$
ac_{\pm}^{2} + bc_{\pm} + c = 0\ ,
$$

with

$$
\boxed{
\begin{align}
a &= \left(1-v_{0}^{2}\right)\left(u^{0}\right)^{2} - v_{0}^{2}g^{00}\ ,\\
b &= 2v_{0}^{2}g^{i0} - 2u^{i}u^{0}\left(1-v^{2}_{0}\right)\ ,\\
c &= \left(1-v_{0}^{2}\right)\left(u^{i}\right)^{2} - v_{0}^{2}g^{ii}\ ,\\
v_{0}^{2} &= v_{\rm A}^{2} + c_{\rm s}^{2}\left(1-v_{\rm A}^{2}\right)\ ,\\
c_{\rm s} &= \left.\left[\frac{dP_{\rm cold}}{d\rho_{b}} + \Gamma_{\rm th}\left(\Gamma_{\rm th}-1\right)\epsilon_{\rm th}\right]\middle/h\right.\ ,\\
c_{+} &= \max\left(\frac{-b \pm \sqrt{b^{2}-4ac}}{2a}\right)\ ,\\
c_{-} &= \min\left(\frac{-b \pm \sqrt{b^{2}-4ac}}{2a}\right)\ .
\end{align}
}
$$

Finally, after the maximum and minimum speeds $c_{\pm}$ have been computed at left and right facs, the standard Harten-Lax-van Leer (HLL), approximate Riemann solve is applied to compute fluxes for the three conservative variables $U = \left\{\rho_{\star},\tilde{\tau},\tilde{S}_{i}\right\}$:

$$
F^{\rm HLL} = \frac{c^{-}F_{r} + c^{+}F_{l} - c^{+}c^{-}\left(U_{r} - U_{l}\right)}{c^{+} + c^{-}}\ ,
$$

where

$$
\boxed{c^{\pm} = \pm\max\left(0,c^{r}_{\pm},c^{l}_{\pm}\right)}\ .
$$

```python
%%writefile -a $outfile_path__mhdflux__C

  // Compute v02 = v_A^2 + c_s^2*(1.0-v_A^2), where c_s = sound speed, and v_A = Alfven velocity
  CCTK_REAL v02r,v02l;
  // First right face
  compute_v02(dPcold_drhor,Gamma_th,eps_thr,h_r,smallbr,Ur,v02r);
  // Then left face.
  compute_v02(dPcold_drhol,Gamma_th,eps_thl,h_l,smallbl,Ul,v02l);

  int offset=flux_dirn-1;

  // Now that we have computed v02 = v_A^2 + c_s^2*(1.0-v_A^2), we can
  //   next compute c_+ and c_- using a simplified dispersion relation.
  //   Note that, in solving the simplified disp. relation, we overestimate
  //   c_+ and c_- by around a factor of 2, making the MHD evolution more
  //   diffusive (and potentially more *stable*) than it could be.
  CCTK_REAL cplusr,cminusr,cplusl,cminusl;
  find_cp_cm(cplusr,cminusr,v02r,u0_r,
             Ur[VX+offset],ONE_OVER_LAPSE_SQUARED,FACEVAL[SHIFTX+offset],psim4,FACEVAL[GUPXX+offset]);
  find_cp_cm(cplusl,cminusl,v02l,u0_l,
             Ul[VX+offset],ONE_OVER_LAPSE_SQUARED,FACEVAL[SHIFTX+offset],psim4,FACEVAL[GUPXX+offset]);

  // Then compute cmax, cmin. This is required for the HLL flux.
  CCTK_REAL cmaxL =  MAX(0.0,MAX(cplusl,cplusr));
  CCTK_REAL cminL = -MIN(0.0,MIN(cminusl,cminusr));
```

    Appending to ../src/mhdflux.C

# Step 7: MHD flux: $\rho_{\star}$ \[Back to [top](#toc)\]
$$\label{rho_star_flux}$$

We now evaluate the flux for $U=\rho_{\star}$. First, remember that

$$
\rho_{\star} = \alpha\sqrt{\gamma}\rho_{b}u^{0}\ .
$$

$$
F^{i}_{\rho_{\star}} = \rho_{\star} v^{i}\ ,
$$

where $i$ is the current flux direction. We can then evaluate the HLL flux in the $i$-direction,

$$
\boxed{F^{\rm HLL} = \frac{c^{-}F_{r} + c^{+}F_{l} - c^{+}c^{-}\left(U_{r} - U_{l}\right)}{c^{+} + c^{-}}}\ .
$$

```python
%%writefile -a $outfile_path__mhdflux__C

  //*********************************************************************
  // density flux = \rho_* v^m, where m is the current flux direction (the m index)
  //*********************************************************************
  CCTK_REAL rho_star_r = alpha_sqrt_gamma*Ur[RHOB]*u0_r;
  CCTK_REAL rho_star_l = alpha_sqrt_gamma*Ul[RHOB]*u0_l;
  CCTK_REAL Fr = rho_star_r*Ur[VX+offset]; // flux_dirn = 2, so offset = 1, implies Ur[VX] -> Ur[VY]
  CCTK_REAL Fl = rho_star_l*Ul[VX+offset]; // flux_dirn = 2, so offset = 1, implies Ul[VX] -> Ul[VY]

  // HLL step for rho_star:
  rho_star_flux = (cminL*Fr + cmaxL*Fl - cminL*cmaxL*(rho_star_r-rho_star_l) )/(cmaxL + cminL);
```

    Appending to ../src/mhdflux.C

# Step 8: MHD flux: $\tilde{\tau}$ \[Back to [top](#toc)\]
$$\label{energy_flux}$$

We then evaluate the flux for $U=\tilde{\tau}$. First let us remember that

$$
\tilde{\tau} = \alpha^{2}\sqrt{\gamma}T^{00} - \rho_{\star}\ .
$$

We then have the flux term

$$
F^{i}_{\tilde{\tau}} = \alpha^{2}\sqrt{\gamma}T^{0j} - \rho_{\star}v^{j}\ ,
$$

where $i$ is the current flux direction. We can then evaluate the HLL flux in the $i$-direction,

$$
\boxed{F^{\rm HLL} = \frac{c^{-}F_{r} + c^{+}F_{l} - c^{+}c^{-}\left(U_{r} - U_{l}\right)}{c^{+} + c^{-}}}\ .
$$

```python
%%writefile -a $outfile_path__mhdflux__C

  //*********************************************************************
  // energy flux = \alpha^2 \sqrt{\gamma} T^{0m} - \rho_* v^m, where m is the current flux direction (the m index)
  //*********************************************************************
  // First compute some useful metric quantities:
  CCTK_REAL alpha_squared_sqrt_gamma = FACEVAL_LAPSE_PSI4[LAPSE]*alpha_sqrt_gamma;
  CCTK_REAL g4uptm =  ONE_OVER_LAPSE_SQUARED*FACEVAL[SHIFTX+offset];
  CCTK_REAL g4uptt = -ONE_OVER_LAPSE_SQUARED;
  /********** RIGHT FACE ************/
  // Compute a couple useful hydro quantities:
  CCTK_REAL rho0_h_plus_b2_r = (Ur[RHOB]*h_r + smallbr[SMALLB2]);
  CCTK_REAL P_plus_half_b2_r = (Ur[PRESSURE]+0.5*smallbr[SMALLB2]);
  // Then compute T^{0m} and the flux:
  CCTK_REAL TUP0m_r = rho0_h_plus_b2_r*SQR(u0_r)*Ur[VX+offset] + P_plus_half_b2_r*g4uptm - smallbr[SMALLBT]*smallbr[SMALLBX+offset];
  Fr = alpha_squared_sqrt_gamma * TUP0m_r - rho_star_r * Ur[VX+offset];
  // Finally compute tau
  CCTK_REAL TUP00_r = rho0_h_plus_b2_r*u0_r*u0_r + P_plus_half_b2_r*g4uptt - smallbr[SMALLBT]*smallbr[SMALLBT];
  CCTK_REAL tau_r = alpha_squared_sqrt_gamma * TUP00_r - rho_star_r;
  /********** LEFT FACE *************/
  // Compute a couple useful hydro quantities:
  CCTK_REAL rho0_h_plus_b2_l = (Ul[RHOB]*h_l + smallbl[SMALLB2]);
  CCTK_REAL P_plus_half_b2_l = (Ul[PRESSURE]+0.5*smallbl[SMALLB2]);
  // Then compute T^{0m} and the flux:
  CCTK_REAL TUP0m_l = rho0_h_plus_b2_l*SQR(u0_l)*Ul[VX+offset] + P_plus_half_b2_l*g4uptm - smallbl[SMALLBT]*smallbl[SMALLBX+offset];
  Fl = alpha_squared_sqrt_gamma * TUP0m_l - rho_star_l * Ul[VX+offset];
  // Finally compute tau
  CCTK_REAL TUP00_l = rho0_h_plus_b2_l*u0_l*u0_l + P_plus_half_b2_l*g4uptt - smallbl[SMALLBT]*smallbl[SMALLBT];
  CCTK_REAL tau_l = alpha_squared_sqrt_gamma * TUP00_l - rho_star_l;

  // HLL step for tau:
  tau_flux = (cminL*Fr + cmaxL*Fl - cminL*cmaxL*(tau_r-tau_l) )/(cmaxL + cminL);
```

    Appending to ../src/mhdflux.C

# Step 9: MHD flux: $\tilde{S}_{i}$ \[Back to [top](#toc)\]
$$\label{momentum_flux}$$

We then evaluate the flux for $U=\tilde{S}_{i}$. First let us remember that

$$
\tilde{S}_{i} = \left(\rho_{\star} h + \alpha u^{0} \sqrt{\gamma}b^{2}\right)u_{i} - \alpha\sqrt{\gamma}b^{0}b_{i}\ .
$$

We then have the flux term

$$
\left(F_{\tilde{S}}\right)^{j}_{\ i} = \alpha\sqrt{\gamma}T^{j}_{\ i}\ ,
$$

where $j$ is the current flux direction and $i$ correspond to the component of $\tilde{S}_{i}$ we are interested in. We can then evaluate the HLL flux in the $i$-direction,

$$
\boxed{F^{\rm HLL} = \frac{c^{-}F_{r} + c^{+}F_{l} - c^{+}c^{-}\left(U_{r} - U_{l}\right)}{c^{+} + c^{-}}}\ .
$$

```python
%%writefile -a $outfile_path__mhdflux__C

  //*********************************************************************
  // momentum flux = \alpha \sqrt{\gamma} T^m_j, where m is the current flux direction (the m index)
  //*********************************************************************
  // b_j = g_{ij} (b^i + b^t shift^i), g_{ij} = physical metric
  //CCTK_REAL sbtr=0,sbtl=0;
  CCTK_REAL smallb_lowerr[NUMVARS_SMALLB],smallb_lowerl[NUMVARS_SMALLB];
  lower_4vector_output_spatial_part(psi4,FACEVAL,smallbr,smallb_lowerr);
  lower_4vector_output_spatial_part(psi4,FACEVAL,smallbl,smallb_lowerl);

  /********** Flux for S_x **********/
  // [S_x flux] = \alpha \sqrt{\gamma} T^m_x, where m is the current flux direction (the m index)
  //    Again, offset = 0 for reconstruction in x direction, 1 for y, and 2 for z
  //    Note that kronecker_delta[flux_dirn][0] = { 1 if flux_dirn==1, 0 otherwise }.
  Fr = alpha_sqrt_gamma*( rho0_h_plus_b2_r*(u0_r*Ur[VX+offset])*U_LOWERr[UX]
                          + P_plus_half_b2_r*kronecker_delta[flux_dirn][0] - smallbr[SMALLBX+offset]*smallb_lowerr[SMALLBX] );
  Fl = alpha_sqrt_gamma*( rho0_h_plus_b2_l*(u0_l*Ul[VX+offset])*U_LOWERl[UX]
                          + P_plus_half_b2_l*kronecker_delta[flux_dirn][0] - smallbl[SMALLBX+offset]*smallb_lowerl[SMALLBX] );

  //        S_x =\alpha\sqrt{\gamma}( T^0_x )
  CCTK_REAL st_x_r = alpha_sqrt_gamma*( rho0_h_plus_b2_r*u0_r*U_LOWERr[UX] - smallbr[SMALLBT]*smallb_lowerr[SMALLBX] );
  CCTK_REAL st_x_l = alpha_sqrt_gamma*( rho0_h_plus_b2_l*u0_l*U_LOWERl[UX] - smallbl[SMALLBT]*smallb_lowerl[SMALLBX] );

  // HLL step for Sx:
  st_x_flux = (cminL*Fr + cmaxL*Fl - cminL*cmaxL*(st_x_r-st_x_l) )/(cmaxL + cminL);

  /********** Flux for S_y **********/
  // [S_y flux] = \alpha \sqrt{\gamma} T^m_y, where m is the current flux direction (the m index)
  //    Again, offset = 1 for reconstruction in x direction, 2 for y, and 3 for z
  //    Note that kronecker_delta[flux_dirn][1] = { 1 if flux_dirn==2, 0 otherwise }.
  Fr = alpha_sqrt_gamma*( rho0_h_plus_b2_r*(u0_r*Ur[VX+offset])*U_LOWERr[UY] + P_plus_half_b2_r*kronecker_delta[flux_dirn][1]
                          - smallbr[SMALLBX+offset]*smallb_lowerr[SMALLBY] );
  Fl = alpha_sqrt_gamma*( rho0_h_plus_b2_l*(u0_l*Ul[VX+offset])*U_LOWERl[UY] + P_plus_half_b2_l*kronecker_delta[flux_dirn][1]
                          - smallbl[SMALLBX+offset]*smallb_lowerl[SMALLBY] );

  //        S_y =\alpha\sqrt{\gamma}( T^0_y )
  CCTK_REAL st_y_r = alpha_sqrt_gamma*( rho0_h_plus_b2_r*u0_r*U_LOWERr[UY] - smallbr[SMALLBT]*smallb_lowerr[SMALLBY] );
  CCTK_REAL st_y_l = alpha_sqrt_gamma*( rho0_h_plus_b2_l*u0_l*U_LOWERl[UY] - smallbl[SMALLBT]*smallb_lowerl[SMALLBY] );

  // HLL step for Sy:
  st_y_flux = (cminL*Fr + cmaxL*Fl - cminL*cmaxL*(st_y_r-st_y_l) )/(cmaxL + cminL);

  /********** Flux for S_z **********/
  // [S_z flux] = \alpha \sqrt{\gamma} T^m_z, where m is the current flux direction (the m index)
  //    Again, offset = 1 for reconstruction in x direction, 2 for y, and 3 for z
  //    Note that kronecker_delta[flux_dirn][2] = { 1 if flux_dirn==3, 0 otherwise }.
  Fr = alpha_sqrt_gamma*( rho0_h_plus_b2_r*(u0_r*Ur[VX+offset])*U_LOWERr[UZ] + P_plus_half_b2_r*kronecker_delta[flux_dirn][2]
                          - smallbr[SMALLBX+offset]*smallb_lowerr[SMALLBZ] );
  Fl = alpha_sqrt_gamma*( rho0_h_plus_b2_l*(u0_l*Ul[VX+offset])*U_LOWERl[UZ] + P_plus_half_b2_l*kronecker_delta[flux_dirn][2]
                          - smallbl[SMALLBX+offset]*smallb_lowerl[SMALLBZ] );

  //        S_z =\alpha\sqrt{\gamma}( T^0_z )
  CCTK_REAL st_z_r = alpha_sqrt_gamma*( rho0_h_plus_b2_r*u0_r*U_LOWERr[UZ] - smallbr[SMALLBT]*smallb_lowerr[SMALLBZ] );
  CCTK_REAL st_z_l = alpha_sqrt_gamma*( rho0_h_plus_b2_l*u0_l*U_LOWERl[UZ] - smallbl[SMALLBT]*smallb_lowerl[SMALLBZ] );

  // HLL step for Sz:
  st_z_flux = (cminL*Fr + cmaxL*Fl - cminL*cmaxL*(st_z_r-st_z_l) )/(cmaxL + cminL);

  cmax = cmaxL;
  cmin = cminL;
}

```

    Appending to ../src/mhdflux.C

# Step 10: Code validation \[Back to [top](#toc)\]
$$\label{code_validation}$$

First we download the original `IllinoisGRMHD` source code and then compare it to the source code generated by this tutorial notebook.

```python
# Verify if the code generated by this tutorial module
# matches the original IllinoisGRMHD source code

# First download the original IllinoisGRMHD source code
import urllib
from os import path

original_IGM_file_url  = "https://bitbucket.org/zach_etienne/wvuthorns/raw/5611b2f0b17135538c9d9d17c7da062abe0401b6/IllinoisGRMHD/src/mhdflux.C"
original_IGM_file_name = "mhdflux-original.C"
original_IGM_file_path = os.path.join(IGM_src_dir_path,original_IGM_file_name)

# Then download the original IllinoisGRMHD source code
# We try it here in a couple of ways in an attempt to keep
# the code more portable
try:
    original_IGM_file_code = urllib.request.urlopen(original_IGM_file_url).read()
    # Write down the file the original IllinoisGRMHD source code
    with open(original_IGM_file_path,"w") as file:
        file.write(original_IGM_file_code)
except:
    try:
        original_IGM_file_code = urllib.urlopen(original_IGM_file_url).read()
        # Write down the file the original IllinoisGRMHD source code
        with open(original_IGM_file_path,"w") as file:
            file.write(original_IGM_file_code)
    except:
        # If all else fails, hope wget does the job
        !wget -O $original_IGM_file_path $original_IGM_file_url

# Perform validation
Validation__mhdflux__C  = !diff $original_IGM_file_path $outfile_path__mhdflux__C

if Validation__mhdflux__C == []:
    # If the validation passes, we do not need to store the original IGM source code file
    !rm $original_IGM_file_path
    print("Validation test for mhdflux.C: PASSED!")
else:
    # If the validation fails, we keep the original IGM source code file
    print("Validation test for mhdflux.C: FAILED!")
    # We also print out the difference between the code generated
    # in this tutorial module and the original IGM source code
    print("Diff:")
    for diff_line in Validation__mhdflux__C:
        print(diff_line)
```

    Validation test for mhdflux.C: FAILED!
    Diff:
    5c5
    < static inline void mhdflux(int i,int j,int k,const int flux_dirn,CCTK_REAL *Ul,CCTK_REAL *Ur,  CCTK_REAL *FACEVAL,CCTK_REAL *FACEVAL_LAPSE_PSI4,eos_struct &eos,
    ---
    > static inline void mhdflux(int i,int j,int k,const int flux_dirn,CCTK_REAL *Ul,CCTK_REAL *Ur,  CCTK_REAL *FACEVAL,CCTK_REAL *FACEVAL_LAPSE_PSI4,eos_struct &eos, CCTK_REAL Gamma_th,
    9d8
    <
    20,23c19,22
    <   CCTK_REAL P_coldr,eps_coldr,dPcold_drhor=0,eps_thr=0,h_r=0,gamma_coldr;
    <   compute_P_cold__eps_cold__dPcold_drho__eps_th__h__gamma_cold(Ur,eos,P_coldr,eps_coldr,dPcold_drhor,eps_thr,h_r,gamma_coldr);
    <   CCTK_REAL P_coldl,eps_coldl,dPcold_drhol=0,eps_thl=0,h_l=0,gamma_coldl;
    <   compute_P_cold__eps_cold__dPcold_drho__eps_th__h__gamma_cold(Ul,eos,P_coldl,eps_coldl,dPcold_drhol,eps_thl,h_l,gamma_coldl);
    ---
    >   CCTK_REAL P_coldr,eps_coldr,dPcold_drhor=0,eps_thr=0,h_r=0,Gamma_coldr;
    >   compute_P_cold__eps_cold__dPcold_drho__eps_th__h__Gamma_cold(Ur,eos,Gamma_th,P_coldr,eps_coldr,dPcold_drhor,eps_thr,h_r,Gamma_coldr);
    >   CCTK_REAL P_coldl,eps_coldl,dPcold_drhol=0,eps_thl=0,h_l=0,Gamma_coldl;
    >   compute_P_cold__eps_cold__dPcold_drho__eps_th__h__Gamma_cold(Ul,eos,Gamma_th,P_coldl,eps_coldl,dPcold_drhol,eps_thl,h_l,Gamma_coldl);
    60c59
    <   compute_v02(dPcold_drhor,eos.gamma_th,eps_thr,h_r,smallbr,Ur,v02r);
    ---
    >   compute_v02(dPcold_drhor,Gamma_th,eps_thr,h_r,smallbr,Ur,v02r);
    62c61
    <   compute_v02(dPcold_drhol,eos.gamma_th,eps_thl,h_l,smallbl,Ul,v02l);
    ---
    >   compute_v02(dPcold_drhol,Gamma_th,eps_thl,h_l,smallbl,Ul,v02l);

# Step 11: Output this module to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-IllinoisGRMHD__mhdflux.pdf](Tutorial-IllinoisGRMHD__mhdflux.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means).

```python
latex_nrpy_style_path = os.path.join(nrpy_dir_path,"latex_nrpy_style.tplx")
#!jupyter nbconvert --to latex --template $latex_nrpy_style_path Tutorial-IllinoisGRMHD__mhdflux.ipynb
#!pdflatex -interaction=batchmode Tutorial-IllinoisGRMHD__mhdflux.tex
#!pdflatex -interaction=batchmode Tutorial-IllinoisGRMHD__mhdflux.tex
#!pdflatex -interaction=batchmode Tutorial-IllinoisGRMHD__mhdflux.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```


# Part II.  Approximate Riemann Solvers

In Part II of this book we present a number of *approximate Riemann solvers*.  We have already seen that for many important hyperbolic systems it is possible to work out the exact Riemann solution for arbitrary left and right states.  However, for complicated nonlinear systems, such as the Euler equations (see [Euler](Euler.ipynb)), this exact solution can only be determined by solving a nonlinear system of algebraic equations for the intermediate states and the waves that connect them.  This can be done to arbitrary precision, but only at some computational expense.  The cost of exactly solving a single Riemann problem may seem insignificant, but it can become prohibitively expensive when the Riemann solver is used as a building block in a finite volume method.  In this case a Riemann problem must be solved at every cell edge at every time step.

For example, if we consider a very coarse grid in one space dimension with only 100 cells and take 100 time steps, then 10,000 Riemann problems must be solved.  In solving practical problems in two or three space dimensions it is not unusual to require the solution of billions or trillions of Riemann problems. In this context it can be very important to develop efficient approximate Riemann solvers that quickly produce a sufficiently good approximation to the true Riemann solution.

The following points have helped to guide the development of approximate Riemann solvers:

 - If the solution is smooth over much of the domain, then the jump in states between neighboring cells will be very small (on the order of $\Delta x$, the cell size) for most of the Riemann problems encountered in the numerical solution. Even if the hyperbolic system being studied is nonlinear, for such data the equations can be approximated by a linearization and we have seen that linear Riemann problems can be solved more easily than nonlinear ones.  Rather than solving a nonlinear system of equations by some iterative method, one need only solve a linear system (provided the eigenvalues and eigenvectors of the Jacobian matrix are known analytically, as they often are for practical problems).  In many cases the solution of this linear system can also be worked out analytically and is easy to implement, so numerical linear algebra is not required.

 - In spite of smoothness over much of the domain, in interesting problems there are often isolated discontinuities such as shock waves that are important to model accurately.  So some Riemann problems arising in a finite volume method may have large jumps between the left and right states.  Hence a robust approximate Riemann solver must also handle these cases without introducing too much error.

 - But even in the case of large jumps in the data, it may not be necessary or worthwhile to solve the Riemann problem exactly.  The information produced by the Riemann solver goes into a numerical method that updates the approximate solution in each grid cell and the exact  structure of the Riemann solution is lost in the process.

Each chapter in this part of the book illustrates some common approximate Riemann solvers in the context of one of the nonlinear systems studied in part 1.  We focus on two popular approaches to devising approximate Riemann solvers, though these are certainly not the only approaches: linearized solvers and two-wave solvers.

## Finite volume methods

We give a short review of Riemann-based finite volume methods to illustrate what is typically needed from a Riemann solver in order to implement such methods. In one space dimension, a finite volume approximation to the solution $q(x,t_n)$ at the $n$th time step consists of discrete values $Q_j^n$, each of which can be viewed as approximating the cell average of the solution over a grid cell $x_{j-1/2} < x < x_{j+1/2}$ for some discrete grid.   The cell length $\Delta x_j = x_{j+1/2} - x_{j-1/2}$ is often uniform, but this is not required.  Many methods for hyperbolic conservation laws are written in *conservation form*, in which the numerical solution is advanced from time $t_n$ to $t_{n+1} = t_n + \Delta t_n$ by the explicit formula

\begin{align}\label{FVupdate}
Q_j^{n+1} = Q_j^n - \frac{\Delta t_n}{\Delta x_j} (F_{j+1/2}^n - F_{j-1/2}^n),
\end{align}

for some definition of the *numerical flux* $F_{j-1/2}^n$, typically based on $Q_{j-1}^n$ and $Q_j^n$ (and possibly other nearby cell values at time $t_n$).

Dividing (\ref{FVupdate}) by $\Delta t_n$ and rearranging, this form can be viewed as a discretization of $q_t + f(q)_x = 0$, provided the numerical flux is *consistent* with the true flux $f(q)$ in a suitable manner. In particular if the $Q_i$ used in defining $F_{j-1/2}^n$ are all equal to the same value $\bar q$, then $F_{j-1/2}^n$ should reduce to $f(\bar q)$.

A big advantage of using conservation form is that the numerical method is conservative. The sum $\sum \Delta x_j Q_j^n$ approximates the integral $\int q(x,t_n)\,dx$.  Multiplying (\ref{FVupdate}) by $\Delta x_j$ and summing shows that at time $t_{n+1}$ this sum only changes due to fluxes at the boundaries of the region in question (due to cancellation of the flux differences when summing), a property shared with the true solution.  For problems with shock waves, using methods in conservation form is particularly important since nonconservative formulations can lead to methods that converge to discontinuous solutions that look fine but are not correct, e.g. the shock wave might propagate at entirely the wrong speed.

### Godunov's method

Trying to compute numerical approximations to hyperbolic problems with strong shock waves is challenging because of the discontinuities in the solution --- classical interpretations of $(F_{j+1/2}^n - F_{j-1/2}^n)/\Delta x_j \approx \partial f/ \partial x$ break down, oscillations near discontinuities often appear, and methods can easily go catastrophically unstable.

The landmark paper of Godunov (Godunov 1959) was the first to suggest using the solution to Riemann problems in defining the numerical flux: $F_{j-1/2}^n$ is obtained by evaluating $f(Q_{j-1/2}^*)$, where $Q_{j-1/2}^*$ is the Riemann solution evaluated along the ray $x/t = 0$ after the standard Riemann problem is solved between states $q_\ell=Q_{j-1}^n$ and $q_r=Q_j^n$ (with the discontinuity placed at $x=t=0$ as usual in the definition of the Riemann problem). If the numerical solution is now defined as a piecewise constant function with value $Q_j^n$ in the $j$th cell at time $t_n$, then the exact solution takes this value along the cell interface $x_{j-1/2}$ for sufficiently small later times $t > t_n$ (until waves from other cell interfaces begin to interact).

The classic Godunov method was developed for gas dynamics and the exact Riemann solution was used, but since only one value is used from this solution and the rest of the structure is thrown away, it is natural to use some *approximate Riemann solver* that more cheaply estimates $Q_{j-1/2}^*$.  The approximations discussed in the next few chapters are often suitable.

Godunov's method turns out to be very robust -- because the shock structure of the solution is used in defining the interface flux, the method generally remains stable provided that the *Courant-Friedrichs-Lewy (CFL) Condition* is satisfied, which restricts the allowable time step relative to the cell sizes and wave speeds by requiring that no wave can pass through more than one grid cell in a single time step.  This is clearly a necessary condition for convergence of the method, based on domain of dependence arguments, and for Godunov's method (with the exact solver) this is generally sufficient as well, as verified in countless simulations (though seemingly impossible to prove in complete generality for nonlinear systems of equations).  When the exact solution is replaced by an approximate solution, the method may not work as well, and so some care has to be used in defining a suitable approximation.

### High-resolution methods

In spite of its robustness, Godunov's method is seldom used as just described because the built-in *numerical viscosity* that gives it robustness also leads to very smeared out solutions, particularly around discontinuities, unless a very fine computational grid is used.  For the advection equation, Godunov's method reduces to the standard "first-order upwind" method and in general it is only first order accurate even on smooth solutions.

A wide variety of higher order Godunov-type (i.e., Riemann solver based) methods have been developed.  One approach first reconstructs better approximations to the solution at each time from the states $Q_j^n$, e.g. a piecewise polynomial that is linear  or quadratic in each grid cell rather than constant, and then uses the states from these polynomials evaluated at the cell interfaces to define the Riemann problems.  Another approach, used in the "wave propagation methods" developed in (LeVeque, 2002), for example, is to take the waves that come out of the Riemann solution based on the original data to also define second order correction terms.  In either case some *limiters* must generally be applied in order to avoid nonphysical oscillations in solutions, particularly when the true solution has discontinuities.  There is a vast literature on such methods; see for example many of the books cited in the [Preface](Preface.ipynb).  For our present purposes the main point is that an approximate Riemann solver is a necessary ingredient in many methods that are commonly used to obtain high-resolution approximations to hyperbolic PDEs.

## Notation and structure of approximate solutions

We consider a single Riemann problem with left state $q_\ell$ and right state $q_r$.  These states might be $Q_{j-1}^n$ and $Q_j^n$ for a typical interace when using Godunov's method, or other states defined from them, e.g. after doing a polynomial reconstruction.  At any rate, from now on we will not discuss the numerical methods or the grid in its totality, but simply focus on how to define an approximate Riemann solution based an an arbitrary pair of states.  The resulting "interface solution" and "interface flux" will be denoted simply by $Q^*$ and $F^*$, respectively, as approximations to the Riemann solution and flux along $x/t =0$ in the similarity solution.

The Riemann solution gives a resolution of the jump $\Delta q = (q_r - q_\ell)$ into a set of propagating waves.  In both of the approaches described below, the approximate Riemann solution consists entirely of traveling discontinuities, i.e., there are no rarefaction waves in the approximate solution, although there may be a discontinuity that approximates such a wave.  One should rightly worry about whether the approximate solution generated with such a method will satisfy the required entropy condition and end up with rarefaction waves where needed rather than entropy-violating shocks, and we address this to some extent in the examples in the following chapters.  It is important to remember that we are discussing the approximate solver that will be used *at every grid interface* in every time step, and the numerical viscosity inherent in the numerical method can lead to rarefactions in the overall numerical approximation even if each approximate Riemann solution lacks rarefactions.  Nonetheless some care is needed, particularly in the case of *transonic* rarefactions, as we will see.

Following (LeVeque, 2002), we refer to these traveling discontinuities as *waves* and denote them by ${\cal W}_p \in {\mathbb R}^m$, where the index $p$ denotes the characteristic family and typically ranges from $1$ to $m$ for a system of $m$ equations, although in an approximate solver the number of waves may be smaller (or possibly larger).  At any rate, they always have the property that
\begin{align}\label{Wsum}
q_r - q_\ell = \sum_{p} {\cal W}_p.
\end{align}
For each wave, the approximate solver must also give a wave speed $s_p \in{\mathbb R}$.  For a linear system such as acoustics, the true solution has this form with the $s_p$ being eigenvalues of the coefficient matrix and each wave is a corresponding eigenvector, as described in [Acoustics](Acoustics.ipynb).  One class of approximate Riemann solvers descussed below is based on approximating a nonlinear problem by a linearization locally at each interface.

Once a set of waves and speeds have been defined, we can define an interface flux as follows: the waves for which $s_p < 0$ are traveling to the left while those with $s_p>0$ are traveling to the right, and so as an interface flux we could use $f(Q^*)$, where $Q^*$ is defined by either
$$
Q^* = q_\ell + \sum_{p: s_p < 0} {\cal W}_p.
$$
or
$$
Q^* = q_r - \sum_{p: s_p > 0} {\cal W}_p.
$$
These two expressions give the same value for $Q^*$ unless there is a wave with $s_p=0$, in which case they could be different if the corresponding ${\cal W}_p$ is nonzero.  However, if we are using the *exact* Riemann solution (e.g. for a linear system or a nonlinear problem in which the solution consists only of shock waves), then a stationary discontinuity with $s_p=0$ must have no jump in flux across it (by the Rankine-Hugoniot condition) and so even if the two values of $Q^*$ differ, the flux $f(Q^*)$ is uniquely defined.  For an approximate Riemann solution this might not be true.

Another way to define the interface flux $F^*$ would be as
\begin{align}\label{Fstar}
F^* = f(q_\ell) + \sum_{p: s_p \leq 0} s_p{\cal W}_p
= f(q_r) - \sum_{p: s_p \geq 0} s_p{\cal W}_p
\end{align}
suggested by the fact that this is the correct flux along $x/t = 0$ for a linear system $f(q)=Aq$ or for a nonlinear system with only shocks in the solution; in these cases the Rankine-Hugoniot condition implies that $s_p{\cal W}_p$ is equal to the jump in flux across each wave.  Note that in this expression the terms in the sum for $s_p=0$ drop out so the two expressions always agree.

The wave propagation algorithms described in (LeVeque, 2002) and implemented in Clawpack use a form of Godunov's method based on the sums appearing in (\ref{Fstar}), called "fluctuations", to update the neighboring cell averages, rather than the flux difference form.  In these methods  the waves and speeds which are further used (after applying a limiter to the waves) to obtain the high-resolution corrections.  An advantage of working with fluctuations, waves, and speeds rather than interface fluxes is that these quantities often make sense also for *non-conservative hyperbolic systems,* such as the variable coefficient linear problem $q_t + A(x)q_x = 0$, for which there is no "flux function".  A Riemann problem is defined by prescribing matrices $A_\ell$ and $A_r$ along with the initial data $q_\ell$ and $q_r$, for example by using the material properties in the grid cells to the left and right of the interface for acoustics through a heterogeneous material.  The waves are then naturally defined using the eigenvectors of $A_\ell$ corresponding to negative eigenvalues for the left-going waves, and using eigenvectors of $A_r$ corresponding to positive eigenvalues for the right-going waves.  See (LeVeque, 2002) for more details.

Updating cell averages by fluctuations rather than flux differencing will give a conservative method (when applied to a hyperbolic problem in conservation form) only if the waves and speeds in the approximate solver satisfy
\begin{align}
\label{adqdf}
\sum_{p=1}^m s_p {\cal W}_p = f(q_r) - f(q_\ell).
\end{align}
This is a natural condition to require of our approximate Riemann solvers in general, even though the flux-differencing form (\ref{FVupdate}) always leads to a conservative method, since this is satisfied by the exact solution in cases where it consists only of discontinuities and each wave satisfies the Rankine-Hugoniot condition.  When (\ref{adqdf}) is satisfied we say the approximate solver is conservative.

## Linearized Riemann solvers
Consider a nonlinear system $q_t + f(q)_x = 0$. If $q_\ell$ and $q_r$ are close to each other, as is often the case over smooth regions of a more general solution, then the nonlinear system can be approximated by a linear problem of the form $q_t + \hat A q_x = 0$.  The coefficient matrix $\hat A$ should be some approximation to  $f'(q_\ell) \approx f'(q_r)$ in the case where $\|q_\ell-q_r\|$ is small.  The idea of a general linearized Riemann solver is to define a matrix $\hat A(q_\ell, q_r)$ that has this property but also makes sense as an approximation in the case when $\|q_\ell-q_r\|$ is not small.  For many nonlinear systems there is a *Roe linearization*, a particular function that works works very well based on ideas introduced originally by Roe (Roe, 1981).  For systems such as the shallow water equations or the Euler equations, there are closed-form expressions for the eigenvalues and eigenvectors of $\hat A$ and the solution of the linearized Riemann problem, leading to efficient solvers.  These will be presented in the next few chapters.

## Two-wave solvers
Since the Riemann solution impacts the overall numerical solution only based on how it modifies the two neighboring solution values, it seems reasonable to consider approximations in which only a single wave propagates in each direction.  The solution will have a single intermediate state $q_m$ such that ${\cal W}_1 = q_m - q_\ell$ and ${\cal W}_2 = q_r-q_m$.  There are apparently $m+2$ values to be determined: the middle state $q_m \in {\mathbb R}^m$ and the speeds $s_1, s_2$.  In order for the approximate solver to be conservative, it must satisfy (\ref{adqdf}), and hence the $m$ conditions
\begin{align}
f(q_r) - f(q_\ell) = s_1 {\cal W}_1 + s_2 {\cal W}_2.
\end{align}
This can be solved for the middle state to find
\begin{align}  \label{AS:middle_state}
q_m = \frac{f(q_r) - f(q_\ell) - s_2 q_r + s_1 q_\ell}{s_1 - s_2}.
\end{align}
It remains only to specify the wave speeds, and it is in this specification that the various two-wave solvers differ.  In the following sections we briefly discuss the choice of wave speed for a scalar problem; the choice for systems will be elaborated in subsequent chapters.

Typically $s_1 < 0 < s_2$ and so the intermediate state we need is $Q^* = q_m$ and $F^* = f(Q^*)$.  However, in some cases both $s_1$ and $s_2$ could have the same sign, in which case $F^*$ is either $f(q_\ell)$ or $f(q_r)$.

In addition to the references provided below, this class of solvers is also an ingredient in the so-called *central schemes*.  Due to the extreme simplicity of two-wave solvers, the resulting central schemes are often even referred to as being "Riemann-solver-free".

### Lax-Friedrichs (LF) and local-Lax-Friedrichs (LLF)

The simplest such solver is the *Lax-Friedrichs method*, in which it is assumed that both waves have the same speed, in opposite directions:
$$-s_1 = s_2 = a,$$
where $a\ge 0$.  Then (\ref{AS:middle_state}) becomes
$$q_m = -\frac{f(q_r) - f(q_\ell)}{2a} + \frac{q_r + q_\ell}{2}.$$
In the original Lax-Friedrichs method, the wave speed $a$ is taken to be the same in every Riemann problem over the entire grid; in the *local Lax Friedrichs (LLF) method*, a different speed $a$ may be chosen for each Riemann problem.

For stability reasons, the wave speed should be chosen at least as large as the fastest wave speed appearing in the true Riemann solution.  However, choosing a wave speed that is too large leads to excess diffusion.  For the LLF method (originally due to Rusanov), the wave speed is chosen as
$$a(q_r, q_\ell) = \max(|f'(q)|)$$
where the maximum is taken over all values of $q$ between $q_r$ and $q_\ell$.  This ensures stability, but may still introduce substantial damping of slower waves.

### Harten-Lax-van Leer (HLL)

A less dissipative solver can be obtained by allowing the left- and right-going waves to have different speeds.
This approach was developed in (Harten, Lax, and van Leer).  The solution is then determined by (\ref{AS:middle_state}).  In the original HLL solver, it was suggested to again to use speeds that bound the possible speeds occurring in the true solution.  For a scalar problem, this translates to
\begin{align*}
s_1 & = \min(f'(q)) \\
s_2 & = \max(f'(q)),
\end{align*}
where again the minimum and maximum are taken over all values between $q_r$ and $q_\ell$.  Many refinements of this choice have been proposed in the context of systems of equations, some of which will be discussed in later chapters.

```python

```