# Details of defining a model
$$
\def\n{\mathbf{n}}
\def\x{\mathbf{x}}
\def\N{\mathbb{\mathbb{N}}}
\def\X{\mathbb{X}}
\def\NX{\mathbb{\N_0^\X}}
\def\C{\mathcal{C}}
\def\Jc{\mathcal{J}_c}
\def\DM{\Delta M_{c,j}}
\newcommand\diff{\mathop{}\!\mathrm{d}}
\def\Xc{\mathbf{X}_c}
\def\Yc{\mathbf{Y}_c}
\newcommand{\muset}[1]{\dot{\{}#1\dot{\}}}
$$
This notebook provides a more detailed view of how `TransitionClass`es of a population model are defined.
It describes the objects hidden behind syntactic sugar when writing something like
`TransitionClass([x] + [y] -to> [x+y], 'k_F')`

This will hopefully help deciphering error messages when making mistakes in the "short-hand" notation.

```python
# initialize sympy printing (for latex output)
from sympy import init_printing
init_printing()

# import functions and classes for compartment models
from compartor import *
```

## Basics

The left-hand-side and right-hand-site of transitions are expressions involving zero or more `Compartments`.

To define a compartment $[\x]$, first create a content variable $\x \in \N_0^D$, then define a compartment $[\x]$ with content $\x$.

```python
x = Content('x')
Compartment(x)
```

Content variables are $D$-dimensional. In equations, `x[d]` can be used to denote the copy number of chemical species `d`.

Next, we can build a stoichiometric equation using the `Transition` class, which takes two parameter: the reactants expression and the products expression. For empty reactants or products, use `EmptySet`.
The following defines the "Exit" transition from the nested birth-death process example.

```python
transition_E = Transition(Compartment(x), EmptySet, name='E')
display(transition_E)
```

`name="E"` is an optional parameter, defining the subscript used for the transition class. (It is only used for displaying equations, e.g., the $h_E$ over the arrow in the above example.)

To define a propensity for the transition class, we have to define a content-independent rate constant $k_c$ and a reactant tuning function $g_c$.

```python
k_E = Constant('k_E')
g_E = 1
```

To specify the transition class with its propensity, use the constructor `TransitionClass(transition_c, k_c, g_c)`.

```python
Exit = TransitionClass(transition_E, k_E, g_E)
display(Exit)
```

The optional 4th argument of `TransitionClass` specifies the outcome distribution $\pi_c$.
This must be defined, if content variables occur in products that do not occur in reactants.

The type of $\pi_c$ is `OutcomeDistribution`, which is a class comprising
* an expression or symbol to use for displaying $\pi_c$ in compound expressions
* a function `conditional_expectation` that takes an expression over reactants $\Xc$, and returns it's expectation over all product compartment variables.

There are generators for several predefined outcome distributions. If nothing is specified, as in the above "Exit" transition example, `OutcomeDistribution.Identity()` is used. This has symbol $1$ and `conditional_expectation` returns the expression over reactants $\Xc$ as is. This is appropriate when $\Yc = \emptyset$ or, more precisely, if all content variables occurring in products already occur in reactants.

Other generators include
* `Poisson()` which specifies a poisson distribution over a scalar, and
* `Uniform()` which specifies a uniform distribution over a scalar.

For example, `Poisson()` can be used to define an "Intake" transition class as follows:

```python
from sympy import Symbol
y = Content('y')
transition_I = Transition(EmptySet, Compartment(y), name='I')
k_I = Constant('k_I')
g_I = 1
pi_I = OutcomeDistribution.Poisson(
    Symbol("\pi_{Poiss}(y; \lambda)"),
    y[0],
    Symbol("\lambda", positive=True))
Intake = TransitionClass(transition_I, k_I, g_I, pi_I)
display(Intake)
```

When displaying a `TransitionClass`, the assembled propensity function is shown, where $w(\n;\Xc)$ is derived from the reactants in the stoichiometric equation.
The function `display_propensity_details()` displays the constituent expressions $k_c$, $g_c$, and $\pi_c$.

```python
display_propensity_details(Exit)
```

To specify a reaction network, collect the transition classes in a list.
The function `display_transition_classes()` displays the equations and propensities of the network.

```python
transitions = [Exit, Intake]
display_transition_classes(transitions)
```

$\displaystyle \begin{align} \left[x\right]&\overset{h_{E}}{\longrightarrow}\emptyset && h_{E} = k_{E} n{\left(x \right)}\\\emptyset&\overset{h_{I}}{\longrightarrow}\left[y\right] && h_{I} = \pi_{Poiss}(y; \lambda) k_{I} \end{align}$

## Chemical Events

To specify chemical reactions, we need the `ContentChange()` class. It is constructed with $D$ (number of species) integer arguments, specifying the change (to a compartment variable) in each species.

For example, the "birth" transition from the nested birth-death process example can be defined as

```python
x = Content('x')
Transition(Compartment(x), Compartment(x + ContentChange(1)), name='b')
```

Note, that `Compartment(...)` in the product encloses `x + ContentChange(1)`, so the `+` acts on the compartment content, rather than specifying a set of compartments.

Besides addition, also multiplication can be used inside compartment content equations, e.g.

```python
Transition(Compartment(x), Compartment(2*x + ContentChange(1)))
```

## Compartment Events

To specify compartment events, simply construct (multi-)sets of compartments using `+`.

For example, a "Coagulation" transition can be specified as

```python
x = Content('x')
y = Content('y')
Transition(Compartment(x) + Compartment(y), Compartment(x + y), name='C')
```

In principle, also multiplication can be used, e.g., `2*Compartment(x)` means "two compartments of content 'x'".
But the moment derivation mechanics is not ready for that yet...

## Example: nested birth-death process

Finally, as a complete example, here is the definition of nested birth-death process.

```python
y = Content('y')
x = Content('x')

# Intake
transition_I = Transition(EmptySet, Compartment(y), name='I')
k_I = Constant('k_I')
g_I = 1
pi_I = OutcomeDistribution.Poisson(Symbol("\pi_{Poiss}(y; \lambda)"), y[0], Symbol("\lambda"))
Intake = TransitionClass(transition_I, k_I, g_I, pi_I)

# Exit
transition_E = Transition(Compartment(x), EmptySet, name='E')
k_E = Constant('k_E')
g_E = 1
Exit = TransitionClass(transition_E, k_E, g_E)

# birth
transition_b = Transition(Compartment(x), Compartment(x + ContentChange(1)), name='b')
k_b = Constant('k_b')
g_b = 1
Birth = TransitionClass(transition_b, k_b, g_b)

# death
transition_d = Transition(Compartment(x), Compartment(x + ContentChange(-1)), name='d')
k_d = Constant('k_d')
g_d = x[0] # TODO x should be enough here, in case D=1.
Death = TransitionClass(transition_d, k_d, g_d)

transitions = [Intake, Exit, Birth, Death]
display_transition_classes(transitions)
```

$\displaystyle \begin{align} \emptyset&\overset{h_{I}}{\longrightarrow}\left[y\right] && h_{I} = \pi_{Poiss}(y; \lambda) k_{I}\\\left[x\right]&\overset{h_{E}}{\longrightarrow}\emptyset && h_{E} = k_{E} n{\left(x \right)}\\\left[x\right]&\overset{h_{b}}{\longrightarrow}\left[\left( 1\right) + x\right] && h_{b} = k_{b} n{\left(x \right)}\\\left[x\right]&\overset{h_{d}}{\longrightarrow}\left[\left( -1\right) + x\right] && h_{d} = k_{d} n{\left(x \right)} {x}_{0} \end{align}$

The short-hand definition of the same model is given below. The two are 100% equivalent, they generate the same objects in the `transitions` list.

```python
x = Content('x')
y = Content('y')

pi_I = OutcomeDistribution.Poisson(Symbol("\pi_{Poiss}(y; \lambda)"), y[0], Symbol("\lambda"))

Intake = TransitionClass( {}  -to> [y],   'k_I', pi=pi_I, name='I')
Exit   = TransitionClass( [x] -to> {},    'k_E',          name='E')
Birth  = TransitionClass( [x] -to> [x+1], 'k_b',          name='b')
Death  = TransitionClass( [x] -to> [x-1], 'k_d', x[0],    name='d')

transitions = [Intake, Exit, Birth, Death]
display_transition_classes(transitions)
```

$\displaystyle \begin{align} \emptyset&\overset{h_{I}}{\longrightarrow}\left[y\right] && h_{I} = \pi_{Poiss}(y; \lambda) k_{I}\\\left[x\right]&\overset{h_{E}}{\longrightarrow}\emptyset && h_{E} = k_{E} n{\left(x \right)}\\\left[x\right]&\overset{h_{b}}{\longrightarrow}\left[\left( 1\right) + x\right] && h_{b} = k_{b} n{\left(x \right)}\\\left[x\right]&\overset{h_{d}}{\longrightarrow}\left[\left( -1\right) + x\right] && h_{d} = k_{d} n{\left(x \right)} {x}_{0} \end{align}$


# System Properties

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Communications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Classes of Systems

The spectral and temporal characteristics of a linear-time invariant (LTI) system are given by its transfer function $H(s)$ and impulse response $h(t) = \mathcal{L}^{-1} \{ H(s) \}$, respectively. However it is useful to introduce some higher-level properties and classes of LTI systems. These are for instance useful for the classification of existing systems or the design of systems with specific properties.

### Real-Valued System

A real-valued system is a system whose output signal $y(t) = \mathcal{H} \{ x(t) \}$ is real-valued $y(t) \in \mathbb{R}$ for a real-valued input signal $x(t) \in \mathbb{R}$. Since the output signal $y(t)$ of an LTI system is given by convolving the input signal $x(t)$ with its impulse response $h(t)$, the impulse response $h(t)$ of a real-valued system has to be real-valued

\begin{equation}
h(t) \in \mathbb{R}
\end{equation}

If existing, the transfer function $H(s) = \mathcal{L} \{ h(t) \}$ shows conjugate complex symmetry

\begin{equation}
H(s) = H^*(s^*)
\end{equation}

for a real-valued system due to the [symmetry of the Laplace transform for real-valued signals](../laplace_transform/properties.ipynb#Symmetry-for-Real-Valued-Signals). It follows, that the poles and zeros of a rational transfer function $H(s)$ are either real-valued or complex conjugate pairs.

If existing, the transfer function $H(j \omega) = \mathcal{F} \{ h(t) \}$ in the Fourier domain shows also conjugate complex symmetry

\begin{equation}
H(j \omega) = H^*(-j \omega)
\end{equation}

for a real-valued system due to the [symmetry of the Fourier transform for real-valued signals](../fourier_transform/properties.ipynb#Real-valued-signals). The magnitude spectrum $|H(j \omega)|$ shows even symmetry and the phase $\varphi(j \omega)$ odd symmetry

\begin{align}
|H(j \omega)| &= |H(-j \omega)| \\
\varphi(j \omega) &= - \varphi(-j \omega)
\end{align}

Due to these symmetry relations often only the positive part $\omega \geq 0$ of the magnitude spectrum and phase are plotted. Note that such a representation is ambiguous without the additional knowledge that the system is real-valued.

Real-valued systems are of paramount importance in practical applications, since physically existing signals are real-valued. The symmetry relations derived above have to be considered when designing real-valued systems in the spectral domain.

### Distortionless System

A distortionless system is a system whose output signal $x(t)$ is the attenuated and delayed version of the input signal $y(t)$

\begin{equation}
y(t) = H_0 \cdot x(t - \tau)
\end{equation}

where $H_0 \in \mathbb{R}$ denotes the attenuation and $\tau \in \mathbb{R}$ the delay. The impulse response of a distortionless system is consequently given as

\begin{equation}
h(t) = H_0 \cdot \delta(t - \tau)
\end{equation}

This can be concluded from the [sifting property of the Dirac pulse](../continuous_signals/standard_signals.ipynb#Dirac-Impulse). Its transfer function $H(j \omega) = \mathcal{F} \{ h(t) \}$ reads

\begin{equation}
H(j \omega) = H_0 \cdot e^{- j \omega \tau}
\end{equation}

It follows that the magnitude response of a distortonless system  is constant $| H(j \omega) | = H_0$, the phase response is linearly dependent on the frequency $\varphi_\text{H}(j \omega) = - \omega \tau$, and the [phase and group delay](../systems_spectral_domain/phase_group_delay.ipynb) are constant $t_p = t_g = \tau$.

The characteristics of a distortionless system are often the desired properties for sensors, actuators, amplifiers and transmission paths. In this context the distortionless system serves as an idealized model for such elements or is used as goal for the equalization of non-ideal elements.

### Linear-Phase System

Systems whose unwrapped phase $\varphi_\text{H}(j \omega) = \arg \{ H(j \omega) \}$ scales linearly with frequency

\begin{equation}
\varphi_\text{H}(j \omega) = - c \cdot \omega + d
\end{equation}

with $c, d \in \mathbb{R}$ are termed *linear-phase systems*. There are no constraints with respect to their magnitude response $|H(j \omega)|$. The transfer function $H(j \omega)$ is given as

\begin{equation}
H(j \omega) = |H(j \omega)| \; e^{j (- c \omega + d)}
\end{equation}

The group delay of a linear-phase system is constant

\begin{equation}
t_g(\omega) = - \frac{\partial (- c \cdot \omega + d)}{\partial \omega} = c
\end{equation}

Systems with a linear phase are often desired in practical applications, since all spectral components of a signal are delayed by the same amount when passing the system. The distortionless system introduced above can be seen as a special linear-phase system with $|H(j \omega)| = H_0$, $c = \tau$ and $d=0$.

The impulse response of a linear-phase system shows a specific symmetry. Let's first assume that $c=0$ and $d=0$. Splitting $|H(j \omega)| \in \mathbb{R}$ into its odd and even part, and considering the [symmetries of the Fourier transform](../fourier_transform/properties.ipynb#Symmetries) yields

\begin{equation}
h(t) = h^*(-t)
\end{equation}

This result can be generalized to the case $c \neq 0$ by applying the [temporal shift theorem](../fourier_transform/theorems.ipynb#Temporal-Shift-Theorem)

\begin{equation}
h(t - c) = h^*(-t + c)
\end{equation}

The impulse response of a linear-phase system exhibits complex conjugate symmetry with respect to the time instant $t-c$. In the general case $d \neq 0$, a constant phase shift is added.

**Example**

A real-valued linear-phase system with the following transfer function

\begin{equation}
H(j \omega) = \Lambda(\omega) \cdot e^{-j c \omega}
\end{equation}

is considered. Its impulse response $h(t) = \mathcal{F}^{-1} \{ H(j \omega) \}$ is derived by applying the [duality principle](../fourier_transform/properties.ipynb#Duality) to the [Fourier transformation of the triangular signal](../fourier_transform/theorems.ipynb#Transformation-of-the-triangular-signal) and considering the [shift theorem](../fourier_transform/theorems.ipynb#Temporal-Shift-Theorem)

\begin{equation}
h(t) = \frac{1}{2 \pi} \text{sinc}^2 \left( \frac{t - c}{2} \right)
\end{equation}

It is straightforward to show that the impulse response fulfills the properties stated above. For illustration, the impulse response is plotted for $c=5$

```python
%matplotlib inline
import sympy as sym
sym.init_printing()

t = sym.symbols('t', real=True)

h = 1/(2*sym.pi) * sym.sinc((t-5)/2)**2
sym.plot(h, (t, -5, 15), xlabel='$t$', ylabel='$h(t)$');
```

**Exercise**

The minimum-phase system with transfer function

\begin{equation}
H(j \omega) = \text{rect}(\omega - \frac{1}{2}) \cdot e^{-j c \omega}
\end{equation}

is investigated in this exercise.

* Is the system real-valued?
* Derive the impulse response $h(t)$ of the minimum-phase system.
* Do the above derived symmetries hold for the impulse response?

### Minimum-Phase System

For a given magnitude response $|H(j \omega)|$ often the realization which has the minimum possible group delay $t_g(\omega)$ is desired. The group delay quantifies the frequency dependent delay a system introduces to a signal. For many applications this delay should be as small as possible. A [causal and stable system](causality_stability.ipynb) with rational transfer function $H(s)$ is minimum-phase iff it has no zeros and poles in the right $s$-half-plane. Such a system is termed [*minimum-phase system*](https://en.wikipedia.org/wiki/Minimum_phase). It has the minimum possible group delay for a given magnitude response.

In order to proof this, the dependence of the magnitude response $|H(j \omega)|$ and phase $\varphi_\text{H}(j \omega)$ on the locations of the poles and zeros of $H(s)$ is analyzed. It is assumed in the following that the poles of the system are located in the left $s$-half-plane. The magnitude response of the system can then be written as

\begin{equation}
|H(j \omega)| = |K| \cdot \frac{\prod_{\mu=0}^{Q} | j \omega - s_{0 \mu} |}{\prod_{\nu=0}^{P} | j \omega - s_{\infty \nu}|}
= |K| \cdot \frac{\prod_{\mu=0}^{Q} \sqrt{\Re \{ s_{0 \mu} \}^2 + (\omega - \Im \{ s_{0 \mu} \} )^2 }}{\prod_{\nu=0}^{P} \sqrt{\Re \{ s_{\infty \nu} \}^2 + (\omega - \Im \{ s_{\infty \nu} \} )^2 }}
\end{equation}

where $s_{0 \mu}$ and $s_{\infty \nu}$ denote the $\mu$-th zero and $\nu$-th pole of $H(s)$, and $Q$ and $P$ the total number of zeros and poles, respectively. The magnitude response for a given frequency $\omega$ is determined by the relative distances of the poles and zeros to the point $s = j \omega$ on the imaginary axis. Mirroring the zeros at the imaginary axis does not change the magnitude response. This is due to the fact that $| j \omega - s_{0 \mu} | = | j \omega + s^*_{0 \mu} |$ holds since only the sign of the real part inside the magnitude changes.

However, the group delay is different for zeros in the left and right $s$-half-plane. In the [foundations of the Bode plots](../systems_spectral_domain/bode_plot.ipynb#Bode-Plots) it has been shown that the phase $\varphi_\text{H}(j \omega)$ of a rational transfer function $H(s)$ can be written as

\begin{equation}
\varphi_H(j \omega) = \sum_{\mu=0}^{Q} \arg (j \omega - s_{0 \mu}) - \sum_{\nu=0}^{P} \arg (j \omega - s_{\infty \nu})
\end{equation}

Since the contributions of the individual zeros are superimposed, let's first look at the contribution of a single zero. The group delay for a single zero $s_0 = \Re \{ s_0 \} + j \Im \{ s_0 \}$ can be derived as

\begin{equation}
t_g(\omega) = - \frac{d}{d \omega} \arg (j \omega - s_0) =
\frac{\Re \{ s_0 \}}{(\omega - \Im \{ s_0 \})^2 + \Re \{s_0\}} =
\frac{\Re \{ s_0 \}}{\omega^2 - 2 \omega \Im \{ s_0 \} + |s_0|^2}
\end{equation}

For a given magnitude response, the denominator is invariant to a mirroring of the zero at the imaginary axis. However, the numerator $\Re \{ s_0 \} < 0$ for a zero in the left $s$-half-plane and $\Re \{ s_0 \} > 0$ for a zero in the right $s$-half-plane. Hence, the group delay for the zero in the left $s$-half-plane is always smaller than for its mirror-imaged zero in the right $s$-half-plane. Minimizing the group delay for each zero minimizes the overall group delay.

A system where zeros are located in the left and right $s$-half-plane is called *non-minimum-phase system* or *mixed-phase system*. A system where all zeros are located in the right $s$-half-plane is called *maximum-phase system*.

**Example**

A real-valued minimum-phase system (MPS) with the following transfer function is investigated

\begin{equation}
H_\text{MPS}(j \omega) = \frac{(j \omega - s_0)(j \omega - s_0^*)}{j \omega - s_\infty}
\end{equation}

The system has one real-valued pole $s_\infty$, and a pair of complex conjugate zeros $s_0$ and $s_0^*$. The corresponding non-minimum-phase system (NMPS) with equal magnitude response $|H_\text{MPS}(j \omega)| = |H_\text{NMPS}(j \omega)|$ is yielded by mirroring the zeros at the imaginary axis

\begin{equation}
H_\text{NMPS}(j \omega) = \frac{(j \omega + s_0^*)(j \omega + s_0)}{j \omega - s_\infty}
\end{equation}

The magnitude response and group delay for both systems is computed and plotted for $s_\infty = -2$ and $s_0 = - 1 + j$. First the transfer functions are defined and the group delays are computed

```python
w = sym.symbols('omega', real=True)
jw = sym.I * w
s_0 = - 1 + sym.I
s_inf = -2

H_MPS = (jw - s_0)*(jw - sym.conjugate(s_0))/(jw - s_inf)
H_NMPS = (jw + sym.conjugate(s_0))*(jw + s_0)/(jw - s_inf)

tg_MPS = - sym.diff(sym.arg(H_MPS), w)
tg_NMPS = - sym.diff(sym.arg(H_NMPS), w)
```

The magnitude response $|H_\text{MPS}(j \omega)|$ of the MPS is illustrated by the green line and the magnitude response $|H_\text{NMPS}(j \omega)|$ of the NMPS by the red line, respectively. Note that both overlap.

```python
p1 = sym.plot(sym.Abs(H_MPS), (w, -5, 5), line_color='g', xlabel='$\omega$', ylabel='$|H(j \omega)|$', show=False)
p2 = sym.plot(sym.Abs(H_NMPS), (w, -5, 5), line_color='r', show=False)
p1.extend(p2)
p1.show()
```

The same color scheme is used for the group delay of both systems

```python
p1 = sym.plot(tg_MPS, (w, -5, 5), line_color='g', xlabel='$\omega$', ylabel='$t_g(\omega)$', show=False)
p2 = sym.plot(tg_NMPS, (w, -5, 5), line_color='r', show=False)
p1.extend(p2)
p1.show()
```

**Exercise**

* Add a second pair of complex conjugate zeros to above MPS.
* For your MPS system derive the transfer function $H(j \omega)$ of the corresponding maximum-phase and one mixed-phase system.
* Compute the group delays for all three systems.
* In what range is the group delay of the mixed-phase system located with respect to the minimum- and maximum-phase system?

### All-Pass

An [all-pass](https://en.wikipedia.org/wiki/All-pass_filter) is a system with constant magnitude response $|H(j \omega)| = H_0$ but frequency dependent phase $\varphi_\text{H}(j \omega)$. It allows to modify the phase of a signal without modifying its magnitude. The [distortionless system](#Distortionless-System) is a special case of an all-pass with $\varphi_\text{H}(j \omega) = - \omega \tau$. A stable system with rational transfer function $H(s)$ is an causal all-pass iff for each pole in the left $s$-half-plane there exists a zero in the right $s$-half-plane which is mirrored at the imaginary axis $\Re \{ s \} = 0$.

The magnitude response $|H(j \omega)|$ of a system $H(s)$ in dependence of its poles and zeros has already been derived above as

\begin{equation}
|H(j \omega)| = |K| \cdot \frac{\prod_{\mu=0}^{Q} | j \omega - s_{0 \mu} |}{\prod_{\nu=0}^{P} | j \omega - s_{\infty \nu}|}
\end{equation}

For a pole $s_{\infty \mu}$ and its mirror-imaged zero $s_{0 \nu} = - s_{\infty \mu}^*$ the following holds

\begin{equation}
|j \omega - s_{\infty \mu}| = |j \omega - s_{0 \nu}|
\end{equation}

since only the sign of the real part of the mirror-imaged zero changes. Introducing this result into $|H(j \omega)|$ above for all pole/zero pairs yields that the magnitude response of an all-pass is constant.

**Example**

The properties of a real-valued 2nd-order all-pass with transfer function

\begin{equation}
H(j \omega) = \frac{(j \omega - s_0)(j \omega - s_0^*)}{(j \omega - s_\infty)(j \omega - s_\infty^*)}
\end{equation}

are investigated. The pole is chosen as $s_\infty = -1 + j$, and the zero due to the required symmetry as $s_0 = - s_\infty^*$. First the transfer function is defined

```python
s_inf = -1 + sym.I
s_0 = - sym.conjugate(s_inf)

H = (jw - s_0)*(jw - sym.conjugate(s_0))/((jw - s_inf)*(jw - sym.conjugate(s_inf)))
H
```

The magnitude response $|H(j \omega)|$ of the all-pass is plotted

```python
sym.plot(sym.Abs(H), (w, -5, 5), xlabel='$\omega$', ylabel='$|H(j \omega)|$', ylim=(0, 1.2));
```

as well as its phase $\varphi_\text{H}(j \omega)$

```python
sym.plot(sym.arg(H), (w, -5, 5), xlabel='$\omega$', ylabel=r'$\varphi(j \omega)$');
```

**Copyright**

The notebooks are provided as [Open Educational Resource](https://de.wikipedia.org/wiki/Open_Educational_Resources). Feel free to use the notebooks for your own educational purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Lecture Notes on Signals and Systems* by Sascha Spors.


# Start-to-Finish Example: Setting up Polytropic [TOV](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation) Initial Data, in Curvilinear Coordinates

## Authors: Zach Etienne & Phil Chang
### Formatting improvements courtesy Brandon Clark

## This module sets up initial data for a TOV star in *spherical, isotropic coordinates*, using the *Numerical* ADM Spherical to BSSN Curvilinear initial data module (numerical = BSSN $\lambda^i$'s are computed using finite-difference derivatives instead of exact expressions).

**This module has been validated to exhibit convergence to zero of the Hamiltonian constraint violation at the expected order to the exact solution (see [plots](#convergence) at bottom). Note that convergence at the surface of the star will be lower order due to the sharp drop to zero in $T^{\mu\nu}$.**

### NRPy+ Source Code for this module:
* [TOV/TOV_Solver.py](../edit/TOV/TOV_Solver.py) [\[**tutorial**\]](Tutorial-ADM_Initial_Data-TOV.ipynb): Tolman-Oppenheimer-Volkoff (TOV) initial data; defines all ADM variables and nonzero $T^{\mu\nu}$ components in Spherical basis.
* [BSSN/ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py](../edit/BSSN/ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py); [\[**tutorial**\]](Tutorial-ADM_Initial_Data-Converting_Numerical_ADM_Spherical_or_Cartesian_to_BSSNCurvilinear.ipynb): *Numerical* Spherical ADM$\to$Curvilinear BSSN converter function
* [BSSN/BSSN_constraints.py](../edit/BSSN/BSSN_constraints.py); [\[**tutorial**\]](Tutorial-BSSN_constraints.ipynb): Hamiltonian constraint in BSSN curvilinear basis/coordinates

## Introduction:
Here we use NRPy+ to generate initial data for a [simple polytrope TOV star](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation).

The entire algorithm is outlined below, with NRPy+-based components highlighted in green.

1. Allocate memory for gridfunctions, including temporary storage for the RK4 time integration.
1. Set gridfunction values to initial data (**This module**).
1. Evolve the system forward in time using RK4 time integration. At each RK4 substep, do the following:
    1. Evaluate BSSN RHS expressions.
    1. Apply singular, curvilinear coordinate boundary conditions [*a la* the SENR/NRPy+ paper](https://arxiv.org/abs/1712.07658)
    1. Apply constraints on conformal 3-metric: $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$
1. At the end of each iteration in time, output the Hamiltonian constraint violation.
1. Repeat above steps at two numerical resolutions to confirm convergence to zero.

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

This module is organized as follows

1. [Step 1](#initializenrpy): Set core NRPy+ parameters for numerical grids and reference metric
1. [Step 2](#adm_id_tov): Set up ADM initial data for polytropic TOV Star
    1. [Step 2.a](#tov_interp): Interpolating the TOV data file as needed
    1. [Step 2.b](#source): Compute source terms $S_{ij}$, $S_{i}$, $S$, and $\rho$
    1. [Step 2.c](#jacobian): Jacobian transformation on the ADM/BSSN source terms
    1. [Step 2.d](#tensor): Rescale tensorial quantities
1. [Step 3](#adm_id_spacetime): Convert ADM spacetime quantity initial data from Spherical to BSSN Curvilinear coordinates
1. [Step 4](#validate): Validating that the TOV initial data satisfy the Hamiltonian constraint
    1. [Step 4.a](#ham_const_output): Output the Hamiltonian Constraint
    1. [Step 4.b](#apply_bcs): Apply singular, curvilinear coordinate boundary conditions
    1. [Step 4.c](#enforce3metric): Enforce conformal 3-metric $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$
1. [Step 5](#mainc): $\rm{TOV\_Playground.c}$: The Main C Code
1. [Step 6](#plot): Plotting the single-neutron-star initial data
1. [Step 7](#convergence): Validation: Convergence of numerical errors (Hamiltonian constraint violation) to zero
1. [Step 8](#latex_pdf_output): Output this module to $\LaTeX$-formatted PDF file

# Step 1: Set core NRPy+ parameters for numerical grids and reference metric \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

```python
# First we import needed core NRPy+ modules
from outputC import *
import NRPy_param_funcs as par
import grid as gri
import loop as lp
import indexedexp as ixp
import finite_difference as fin
import reference_metric as rfm

thismodule = "TOV_ID_setup"

# Set spatial dimension (must be 3 for BSSN)
DIM = 3
par.set_parval_from_str("grid::DIM",DIM)

# Then we set the coordinate system for the numerical grid
par.set_parval_from_str("reference_metric::CoordSystem","Spherical")
rfm.reference_metric() # Create ReU, ReDD needed for rescaling B-L initial data, generating BSSN RHSs, etc.

import BSSN.BSSN_quantities as Bq
par.set_parval_from_str("BSSN.BSSN_quantities::EvolvedConformalFactor_cf","phi")

#################
# Next output C headers related to the numerical grids we just set up:
#################

# First output the coordinate bounds xxmin[] and xxmax[]:
with open("BSSN/xxminmax.h", "w") as file:
    file.write("const REAL xxmin[3] = {"+str(rfm.xxmin[0])+","+str(rfm.xxmin[1])+","+str(rfm.xxmin[2])+"};\n")
    file.write("const REAL xxmax[3] = {"+str(rfm.xxmax[0])+","+str(rfm.xxmax[1])+","+str(rfm.xxmax[2])+"};\n")

# Generic coordinate NRPy+ file output, Part 2: output the conversion from (x0,x1,x2) to Cartesian (x,y,z)
outputC([rfm.xxCart[0],rfm.xxCart[1],rfm.xxCart[2]],["xCart[0]","xCart[1]","xCart[2]"],
        "BSSN/xxCart.h")

# Register the Hamiltonian as a gridfunction, to be used later.
H = gri.register_gridfunctions("AUX","H")
```

    Wrote to file "BSSN/xxCart.h"

# Step 2: Set up ADM initial data for polytropic TOV Star \[Back to [top](#toc)\]
$$\label{adm_id_tov}$$

As documented [in the TOV Initial Data NRPy+ Tutorial Module](Tutorial-TOV_Initial_Data.ipynb) ([older version here](Tutorial-GRMHD_UnitConversion.ipynb)), we will now set up TOV initial data, storing the densely-sampled result to file (***Courtesy Phil Chang***).

The TOV solver uses an ODE integration routine provided by scipy, so we first make sure that scipy is installed:

```python
!pip install scipy > /dev/null
```

Next we call the [TOV.TOV_Solver() function](../edit/TOV/TOV_Solver.py) ([NRPy+ Tutorial module](Tutorial-ADM_Initial_Data-TOV.ipynb)) to set up the initial data, using the default parameters for initial data. This function outputs the solution to a file named "outputTOVpolytrope.txt".

```python
import TOV.TOV_Solver as TOV
TOV.TOV_Solver()
```

    Just generated a TOV star with R_Schw = 0.9565681425227097 , M = 0.14050303285288188 , M/R_Schw = 0.1468824086931645 .

## Step 2.a: Interpolating the TOV data file as needed \[Back to [top](#toc)\]
$$\label{tov_interp}$$

The TOV data file just written stored $\left(r,\rho(r),P(r),M(r),e^{\nu(r)}\right)$, where $\rho(r)$ is the total mass-energy density (cf. $\rho_{\text{baryonic}}$).

**METRIC DATA IN TERMS OF ADM QUANTITIES**

The [TOV line element](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation) in *Schwarzschild coordinates* is written (in the $-+++$ form):
$$
ds^2 = - c^2 e^\nu dt^2 + \left(1 - \frac{2GM}{rc^2}\right)^{-1} dr^2 + r^2 d\Omega^2.
$$

In *isotropic coordinates* with $G=c=1$ (i.e., the coordinate system and units we'd prefer to use), the ($-+++$ form) line element is written:
$$
ds^2 = - e^{\nu} dt^2 + e^{4\phi} \left(d\bar{r}^2 + \bar{r}^2 d\Omega^2\right),
$$
where $\phi$ here is the *conformal factor*.

The ADM 3+1 line element for this diagonal metric in isotropic spherical coordinates is given by:
$$
ds^2 = (-\alpha^2 + \beta_k \beta^k) dt^2 + \gamma_{\bar{r}\bar{r}} d\bar{r}^2 + \gamma_{\theta\theta} d\theta^2+ \gamma_{\phi\phi} d\phi^2,
$$

from which we can immediately read off the ADM quantities:
\begin{align}
\alpha &= e^{\nu(\bar{r})/2} \\
\beta^k &= 0 \\
\gamma_{\bar{r}\bar{r}} &= e^{4\phi}\\
\gamma_{\theta\theta} &= e^{4\phi} \bar{r}^2 \\
\gamma_{\phi\phi} &= e^{4\phi} \bar{r}^2 \sin^2 \theta \\
\end{align}

**STRESS-ENERGY TENSOR $T^{\mu\nu}$**

We will also need the stress-energy tensor $T^{\mu\nu}$. [As discussed here](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation), the stress-energy tensor is diagonal:

\begin{align}
T^t_t &= -\rho \\
T^i_j &= P \delta^i_j \\
\text{All other components of }T^\mu_\nu &= 0.
\end{align}

Since $\beta^i=0$ the inverse metric expression simplifies to (Eq. 4.49 in [Gourgoulhon](https://arxiv.org/pdf/gr-qc/0703035.pdf)):
$$
g^{\mu\nu} = \begin{pmatrix}
-\frac{1}{\alpha^2} & \frac{\beta^i}{\alpha^2} \\
\frac{\beta^i}{\alpha^2} & \gamma^{ij} - \frac{\beta^i\beta^j}{\alpha^2}
\end{pmatrix} =
\begin{pmatrix}
-\frac{1}{\alpha^2} & 0 \\
0 & \gamma^{ij}
\end{pmatrix},
$$

and since the 3-metric is diagonal we get

\begin{align}
\gamma^{\bar{r}\bar{r}} &= e^{-4\phi}\\
\gamma^{\theta\theta} &= e^{-4\phi}\frac{1}{\bar{r}^2} \\
\gamma^{\phi\phi} &= e^{-4\phi}\frac{1}{\bar{r}^2 \sin^2 \theta}.
\end{align}

Thus raising $T^\mu_\nu$ yields a diagonal $T^{\mu\nu}$

\begin{align}
T^{tt} &= -g^{tt} \rho = \frac{1}{\alpha^2} \rho = e^{-\nu(\bar{r})} \rho \\
T^{\bar{r}\bar{r}} &= g^{\bar{r}\bar{r}} P = \frac{1}{e^{4 \phi}} P \\
T^{\theta\theta} &= g^{\theta\theta} P = \frac{1}{e^{4 \phi}\bar{r}^2} P\\
T^{\phi\phi} &= g^{\phi\phi} P = \frac{1}{e^{4\phi}\bar{r}^2 \sin^2 \theta} P
\end{align}

As all input quantities are functions of $r$, we will simply read the solution from file and interpolate it to the values of $r$ needed by the initial data.

1. First we define functions `ID_TOV_ADM_quantities()` and `ID_TOV_TUPMUNU()` that call the [1D TOV interpolator function](../edit/TOV/tov_interp.h) to evaluate the ADM spacetime quantities and $T^{\mu\nu}$, respectively, at any given point $(r,\theta,\phi)$ in the Spherical basis. All quantities are defined as above.
1. Next we will construct the BSSN/ADM source terms $\{S_{ij},S_{i},S,\rho\}$ in the Spherical basis
1. Then we will perform the Jacobian transformation on $\{S_{ij},S_{i},S,\rho\}$ to the desired (xx0,xx1,xx2) basis
1. Next we call the *Numerical* Spherical ADM$\to$Curvilinear BSSN converter function to conver the above ADM quantities to the rescaled BSSN quantities in the desired curvilinear coordinate system: [BSSN/ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py](../edit/BSSN/ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py); [\[**tutorial**\]](Tutorial-ADM_Initial_Data-Converting_Numerical_ADM_Spherical_or_Cartesian_to_BSSNCurvilinear.ipynb).

```python
with open("BSSN/ID_TOV_ADM_quantities.h", "w") as file:
    file.write("""
// This function takes as input either (x,y,z) or (r,th,ph) and outputs
//   all ADM quantities in the Cartesian or Spherical basis, respectively.
void ID_TOV_ADM_quantities(
                     const REAL xyz_or_rthph[3],

                     const ID_inputs other_inputs,

                     REAL *gammaDD00,REAL *gammaDD01,REAL *gammaDD02,REAL *gammaDD11,REAL *gammaDD12,REAL *gammaDD22,
                     REAL *KDD00,REAL *KDD01,REAL *KDD02,REAL *KDD11,REAL *KDD12,REAL *KDD22,
                     REAL *alpha,
                     REAL *betaU0,REAL *betaU1,REAL *betaU2,
                     REAL *BU0,REAL *BU1,REAL *BU2) {
      const REAL r  = xyz_or_rthph[0];
      const REAL th = xyz_or_rthph[1];
      const REAL ph = xyz_or_rthph[2];

      REAL rho,P,M,expnu,exp4phi;
      TOV_interpolate_1D(r,other_inputs.Rbar,other_inputs.Rbar_idx,other_inputs.interp_stencil_size,
                         other_inputs.numlines_in_file,
                         other_inputs.r_Schw_arr,other_inputs.rho_arr,other_inputs.P_arr,other_inputs.M_arr,
                         other_inputs.expnu_arr,other_inputs.exp4phi_arr,other_inputs.rbar_arr,
                         &rho,&P,&M,&expnu,&exp4phi);

      *alpha = sqrt(expnu);
      // \gamma_{rbar rbar} = exp(4 phi)
      *gammaDD00 = exp4phi;
      // \gamma_{thth} = r^2 * exp(4 phi)
      *gammaDD11 = r*r * exp4phi;
      // \gamma_{phph} = r^2 sin^2(th) * exp(4 phi)
      *gammaDD22 = r*r*sin(th)*sin(th) * exp4phi;

      // All other quantities ARE ZERO:
      *gammaDD01 = 0.0;
      *gammaDD02 = 0.0;
      *gammaDD12 = 0.0;

      *KDD00 = 0.0;
      *KDD01 = 0.0;
      *KDD02 = 0.0;
      *KDD11 = 0.0;
      *KDD12 = 0.0;
      *KDD22 = 0.0;

      *betaU0 = 0.0;
      *betaU1 = 0.0;
      *betaU2 = 0.0;

      *BU0 = 0.0;
      *BU1 = 0.0;
      *BU2 = 0.0;
}\n""")

with open("BSSN/ID_TOV_TUPMUNU.h", "w") as file:
    file.write("""
// This function takes as input either (x,y,z) or (r,th,ph) and outputs
//   all ADM quantities in the Cartesian or Spherical basis, respectively.
void ID_TOV_TUPMUNU(const REAL xyz_or_rthph[3], const ID_inputs other_inputs,

                    REAL *T4UU00,REAL *T4UU01,REAL *T4UU02,REAL *T4UU03,
                    /**/         REAL *T4UU11,REAL *T4UU12,REAL *T4UU13,
                    /**/                      REAL *T4UU22,REAL *T4UU23,
                    /**/                                   REAL *T4UU33) {
      const REAL r  = xyz_or_rthph[0];
      const REAL th = xyz_or_rthph[1];
      const REAL ph = xyz_or_rthph[2];

      REAL rho,P,M,expnu,exp4phi;
      TOV_interpolate_1D(r,other_inputs.Rbar,other_inputs.Rbar_idx,other_inputs.interp_stencil_size,
                         other_inputs.numlines_in_file,
                         other_inputs.r_Schw_arr,other_inputs.rho_arr,other_inputs.P_arr,other_inputs.M_arr,
                         other_inputs.expnu_arr,other_inputs.exp4phi_arr,other_inputs.rbar_arr,
                         &rho,&P,&M,&expnu,&exp4phi);

      //T^tt = e^(-nu) * rho
      *T4UU00 = rho / expnu;
      //T^rr = P / exp4phi
      *T4UU11 = P / exp4phi;
      //T^thth = P / (r^2 * exp4phi)
      *T4UU22 = P / (r*r * exp4phi);
      //T^phph = P / (r^2 * sin^2(th) * exp4phi)
      *T4UU33 = P / (r*r * sin(th)*sin(th) * exp4phi);

      // All other components ARE ZERO:
      *T4UU01 = 0; *T4UU02 = 0; *T4UU03 = 0;
      /**/         *T4UU12 = 0; *T4UU13 = 0;
      /**/                      *T4UU23 = 0;
}\n""")
```

## Step 2.b: Compute source terms $S_{ij}$, $S_{i}$, $S$, and $\rho$ \[Back to [top](#toc)\]
$$\label{source}$$

Compute source terms $S_{ij}$, $S_{i}$, $S$, and $\rho$, via Eqs. 10 of [Baumgarte, Montero, Cordero-Carrión, and Müller](https://arxiv.org/pdf/1211.6632.pdf):

\begin{array}
\ S_{ij} &= \gamma_{i \mu} \gamma_{j \nu} T^{\mu \nu} \\
S_{i} &= -\gamma_{i\mu} n_\nu T^{\mu\nu} \\
S &= \gamma^{ij} S_{ij} \\
\rho &= n_\mu n_\nu T^{\mu\nu},
\end{array}

ID_TOV_TUPMUNU() provides numerical values for $T^{\mu\nu}$, but we do not have $\gamma_{\mu \nu}$ or $n_\mu$ directly. So here we will construct the latter quantities.

First, B&S Eq. 2.27 defines $\gamma_{\mu \nu}$ as:

$$\gamma_{\mu\nu} = g_{\mu\nu} + n_\mu n_\nu$$

where

$$
n_\mu = \{-\alpha,0,0,0\}.
$$

So we will first need to construct the 4-metric based on ADM quantities. This is provided by Eq 4.47 in [Gourgoulhon](https://arxiv.org/pdf/gr-qc/0703035.pdf):
$$
g_{\mu\nu} = \begin{pmatrix}
-\alpha^2 + \beta^k \beta_k & \beta_i \\
\beta_j & \gamma_{ij}
\end{pmatrix}.
$$

```python
gammaDD = ixp.declarerank2("gammaDD", "sym01",DIM=3)
betaU   = ixp.declarerank1("betaU",DIM=3)
alpha   = sp.symbols("alpha")

# To get \gamma_{\mu \nu} = gammabar4DD[mu][nu], we'll need to construct the 4-metric, using Eq. 2.122 in B&S:

# Eq. 2.121 in B&S
betaD = ixp.zerorank1()
for i in range(DIM):
    for j in range(DIM):
        betaD[i] += gammaDD[i][j]*betaU[j]

# Now compute the beta contraction.
beta2 = sp.sympify(0)
for i in range(DIM):
    beta2 += betaU[i]*betaD[i]

# Eq. 2.122 in B&S
g4DD = ixp.zerorank2(DIM=4)
g4DD[0][0] = -alpha**2 + beta2
for i in range(DIM):
    g4DD[i+1][0] = g4DD[0][i+1] = betaD[i]
for i in range(DIM):
    for j in range(DIM):
        g4DD[i+1][j+1] = gammaDD[i][j]
```

Now let's construct $\gamma_{\mu\nu}$=`gamma4DD[mu][nu]` via $\gamma_{\mu\nu} = g_{\mu\nu} + n_\mu n_\nu$:

```python
n4D = ixp.zerorank1(DIM=4)
n4D[0] = -alpha

gamma4DD = ixp.zerorank2(DIM=4)
for mu in range(4):
    for nu in range(4):
        gamma4DD[mu][nu] = g4DD[mu][nu] + n4D[mu]*n4D[nu]
```

We now have all we need to construct the BSSN source terms in the current basis (for TOV, the Spherical basis):

\begin{array}
\ S_{ij} &= \gamma_{i \mu} \gamma_{j \nu} T^{\mu \nu} \\
S_{i} &= -\gamma_{i\mu} n_\nu T^{\mu\nu} \\
S &= \gamma^{ij} S_{ij} \\
\rho &= n_\mu n_\nu T^{\mu\nu},
\end{array}

```python
T4UU = ixp.declarerank2("T4UU", "sym01",DIM=4)

SDD = ixp.zerorank2()
SD  = ixp.zerorank1()
S   = sp.sympify(0)
rho = sp.sympify(0)

for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            for nu in range(4):
                SDD[i][j] += gamma4DD[i+1][mu]*gamma4DD[j+1][nu] * T4UU[mu][nu]

for i in range(DIM):
    for mu in range(4):
        for nu in range(4):
            SD[i] += -gamma4DD[i+1][mu]*n4D[nu] * T4UU[mu][nu]

gammaUU, gammaDET = ixp.symm_matrix_inverter3x3(gammaDD)
for i in range(DIM):
    for j in range(DIM):
        S += gammaUU[i][j]*SDD[i][j]

for mu in range(4):
    for nu in range(4):
        rho += n4D[mu]*n4D[nu] * T4UU[mu][nu]
```

## Step 2.c: Jacobian transformation on the ADM/BSSN source terms \[Back to [top](#toc)\]
$$\label{jacobian}$$

The following discussion holds for either Spherical or Cartesian input data, so for simplicity let's just assume the data are given in Spherical coordinates.

All ADM tensors and vectors are in the Spherical coordinate basis $x^i_{\rm Sph} = (r,\theta,\phi)$, but we need them in the curvilinear coordinate basis $x^i_{\rm rfm}= ({\rm xx0},{\rm xx1},{\rm xx2})$ set by the "reference_metric::CoordSystem" variable. Empirically speaking, it is far easier to write $(x({\rm xx0},{\rm xx1},{\rm xx2}),y({\rm xx0},{\rm xx1},{\rm xx2}),z({\rm xx0},{\rm xx1},{\rm xx2}))$ than the inverse, so we will compute the Jacobian matrix

$$
{\rm Jac\_dUSph\_dDrfmUD[i][j]} = \frac{\partial x^i_{\rm Sph}}{\partial x^j_{\rm rfm}},
$$

via exact differentiation (courtesy SymPy), and the inverse Jacobian
$$
{\rm Jac\_dUrfm\_dDSphUD[i][j]} = \frac{\partial x^i_{\rm rfm}}{\partial x^j_{\rm Sph}},
$$

using NRPy+'s ${\rm generic\_matrix\_inverter3x3()}$ function. In terms of these, the transformation of ADM/BSSN source terms from Spherical to "reference_metric::CoordSystem" coordinates may be written:

\begin{align}
S^{\rm rfm}_{ij} &=
\frac{\partial x^\ell_{\rm Sph}}{\partial x^i_{\rm rfm}}
\frac{\partial x^m_{\rm Sph}}{\partial x^j_{\rm rfm}} S^{\rm Sph}_{\ell m}\\
S^{\rm rfm}_{i} &=
\frac{\partial x^\ell_{\rm Sph}}{\partial x^i_{\rm rfm}}
S^{\rm Sph}_{\ell}
\end{align}

```python
# UIUC Black Hole initial data are given in Spherical coordinates.
CoordType_in = "Spherical"

SSphorCartDD = ixp.zerorank2()
SSphorCartD  = ixp.zerorank1()

# Copy what we had written above, which was in Spherical coordinates, to the new tensors SSphorCartDD / SSphorCartD:
for i in range(3):
    SSphorCartD[i] = SD[i]
    for j in range(3):
        SSphorCartDD[i][j] = SDD[i][j]

# Zero out the original tensors; we're going to store the result to SD and SDD:
SDD = ixp.zerorank2()
SD  = ixp.zerorank1()

# Make sure that rfm.reference_metric() has been called.
#    We'll need the variables it defines throughout this module.
if rfm.have_already_called_reference_metric_function == False:
    print("Error. Called Convert_Spherical_ADM_to_BSSN_curvilinear() without")
    print("       first setting up reference metric, by calling rfm.reference_metric().")
    exit(1)

r_th_ph_or_Cart_xyz_oID_xx = []
if CoordType_in == "Spherical":
    r_th_ph_or_Cart_xyz_oID_xx = rfm.xxSph
elif CoordType_in == "Cartesian":
    r_th_ph_or_Cart_xyz_oID_xx = rfm.xxCart
else:
    print("Error: Can only convert ADM Cartesian or Spherical initial data to BSSN Curvilinear coords.")
    exit(1)

# Next apply Jacobian transformations to convert into the (xx0,xx1,xx2) basis

# rho and S are scalar, so no Jacobian transformations are necessary.

Jac_dUSphorCart_dDrfmUD = ixp.zerorank2()
for i in range(DIM):
    for j in range(DIM):
        Jac_dUSphorCart_dDrfmUD[i][j] = sp.diff(r_th_ph_or_Cart_xyz_oID_xx[i],rfm.xx[j])

Jac_dUrfm_dDSphorCartUD, dummyDET = ixp.generic_matrix_inverter3x3(Jac_dUSphorCart_dDrfmUD)

for i in range(DIM):
    for j in range(DIM):
        SD[i] += Jac_dUSphorCart_dDrfmUD[j][i] * SSphorCartD[j]
        for k in range(DIM):
            for l in range(DIM):
                SDD[i][j] += Jac_dUSphorCart_dDrfmUD[k][i]*Jac_dUSphorCart_dDrfmUD[l][j] * SSphorCartDD[k][l]
```

## Step 2.d: Rescale tensorial quantities \[Back to [top](#toc)\]
$$\label{tensor}$$

We rescale tensorial quantities according to the prescription described in the [BSSN in curvilinear coordinates tutorial module](Tutorial-BSSNCurvilinear.ipynb) (also [Ruchlin *et al.*](https://arxiv.org/pdf/1712.07658.pdf)):

Since $\text{ReD[i]}=1/\text{ReU[i]}$, we have

\begin{align}
s_{ij} &= S_{ij} /\text{ReDD[i][j]}\\
s_{i} &= S_i \text{ReU[i]}
\end{align}

```python
# Finally rescale the tensorial quantities:
sD = ixp.zerorank1()
sDD = ixp.zerorank2()
for i in range(DIM):
    sD[i] = SD[i] * rfm.ReU[i] # ReD[i] = 1/ReU[i]
    for j in range(DIM):
        sDD[i][j] = SDD[i][j] / rfm.ReDD[i][j]
```

Next we use NRPy+ to write a C function that reads in $T^{\mu\nu}$ in the given (Spherical) basis and outputs the source terms $\{S_{ij},S_{i},S,\rho\}$ in the (xx0,xx1,xx2) basis:

```python
with open("BSSN/ID_TOV_BSSN_Source_Terms.h", "w") as file:
    file.write("""void ID_TOV_BSSN_Source_Terms(
                    REAL xx0xx1xx2[3],

                    const REAL gammaDD00,const REAL gammaDD01,const REAL gammaDD02,
                    /**/                 const REAL gammaDD11,const REAL gammaDD12,
                    /**/                                      const REAL gammaDD22,

                    const REAL betaU0,const REAL betaU1,const REAL betaU2,
                    const REAL alpha,

                    const REAL T4UU00,const REAL T4UU01,const REAL T4UU02,const REAL T4UU03,
                    /**/              const REAL T4UU11,const REAL T4UU12,const REAL T4UU13,
                    /**/                                const REAL T4UU22,const REAL T4UU23,
                    /**/                                                  const REAL T4UU33,

                    REAL *sDD00,REAL *sDD01,REAL *sDD02,
                    /**/        REAL *sDD11,REAL *sDD12,
                    /**/                    REAL *sDD22,

                    REAL *sD0, REAL *sD1, REAL *sD2,

                    REAL *S, REAL *rho) {

    const REAL xx0 = xx0xx1xx2[0];
    const REAL xx1 = xx0xx1xx2[1];
    const REAL xx2 = xx0xx1xx2[2];\n""")
outCparams = "preindent=1,outCfileaccess=a,outCverbose=False,includebraces=False"

outputC([sDD[0][0],sDD[0][1],sDD[0][2],sDD[1][1],sDD[1][2],sDD[2][2],
         sD[0],sD[1],sD[2], S, rho],
        ["*sDD00","*sDD01","*sDD02","*sDD11","*sDD12","*sDD22",
         "*sD0","*sD1","*sD2","*S","*rho"], "BSSN/ID_TOV_BSSN_Source_Terms.h",outCparams)

with open("BSSN/ID_TOV_BSSN_Source_Terms.h", "a") as file:
    file.write("}\n")
```

    Appended to file "BSSN/ID_TOV_BSSN_Source_Terms.h"

# Step 3: Convert ADM spacetime quantity initial data from Spherical to BSSN Curvilinear coordinates \[Back to [top](#toc)\]
$$\label{adm_id_spacetime}$$

We convert ADM spacetime quantity initial data from Spherical to BSSN Curvilinear coordinates [as documented in the corresponding tutorial module](Tutorial-ADM_Initial_Data-Converting_Numerical_ADM_Spherical_or_Cartesian_to_BSSNCurvilinear.ipynb)

```python
import BSSN.ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear as AtoBnum
AtoBnum.Convert_Spherical_or_Cartesian_ADM_to_BSSN_curvilinear("Spherical","ID_TOV_ADM_quantities")
```

    Appended to file "BSSN/ID_ADM_xx0xx1xx2_to_BSSN_xx0xx1xx2__ALL_BUT_LAMBDAs.h"
    Appended to file "BSSN/ID_ADM_xx0xx1xx2_to_BSSN_xx0xx1xx2__ALL_BUT_LAMBDAs.h"

# Step 4: Validating that the TOV initial data satisfy the Hamiltonian constraint \[Back to [top](#toc)\]
$$\label{validate}$$

We will validate that the TOV initial data satisfy the Hamiltonian constraint, modulo numerical finite differencing error

## Step 4.a: Output the Hamiltonian constraint \[Back to [top](#toc)\]
$$\label{ham_const_output}$$

First output the Hamiltonian constraint [as documented in the corresponding NRPy+ tutorial module](Tutorial-BSSN_constraints.ipynb)

```python
import BSSN.BSSN_constraints as bssncon
bssncon.output_C__Hamiltonian_h(add_T4UUmunu_source_terms=True)
```

    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::M_PI
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::RMAX
    Generating optimized C code for Hamiltonian constraint. May take a while, depending on CoordSystem.
    Finished in 14.390434265136719 seconds.
    Output C implementation of Hamiltonian constraint to BSSN/Hamiltonian.h

## Step 4.b: Apply singular, curvilinear coordinate boundary conditions \[Back to [top](#toc)\]
$$\label{apply_bcs}$$

Next apply singular, curvilinear coordinate boundary conditions [as documented in the corresponding NRPy+ tutorial module](Tutorial-Start_to_Finish-Curvilinear_BCs.ipynb).

```python
import CurviBoundaryConditions.CurviBoundaryConditions as cbcs
cbcs.Set_up_CurviBoundaryConditions()
```

    Wrote to file "CurviBoundaryConditions/gridfunction_defines.h"
    Wrote to file "CurviBoundaryConditions/set_parity_conditions.h"
    Wrote to file "CurviBoundaryConditions/xxCart.h"
    Wrote to file "CurviBoundaryConditions/xxminmax.h"
    Wrote to file "CurviBoundaryConditions/Cart_to_xx.h"

## Step 4.c: Enforce conformal 3-metric $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint \[Back to [top](#toc)\]
$$\label{enforce3metric}$$

Then enforce conformal 3-metric $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint (Eq. 53 of [Ruchlin, Etienne, and Baumgarte (2018)](https://arxiv.org/abs/1712.07658)), as [documented in the corresponding NRPy+ tutorial module](Tutorial-BSSN-Enforcing_Determinant_gammabar_equals_gammahat_Constraint.ipynb).

Applying curvilinear boundary conditions should affect the initial data at the outer boundary, and will in general cause the $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint to be violated there. Thus after we apply these boundary conditions, we must always call the routine for enforcing the $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint:

```python
import BSSN.Enforce_Detgammabar_Constraint as EGC
EGC.output_Enforce_Detgammabar_Constraint_Ccode()
```

    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::M_PI
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::RMAX
    Output C implementation of det(gammabar) constraint to file BSSN/enforce_detgammabar_constraint.h

# Step 5: $\rm{TOV\_Playground.c}$: The Main C Code \[Back to [top](#toc)\]
$$\label{mainc}$$

```python
# Part P0: Set the number of ghost cells, from NRPy+'s FD_CENTDERIVS_ORDER
with open("BSSN/NGHOSTS.h", "w") as file:
    file.write("// Part P0: Set the number of ghost zones, from NRPy+'s FD_CENTDERIVS_ORDER\n")
    # Upwinding in BSSN requires that NGHOSTS = FD_CENTDERIVS_ORDER/2 + 1 <- Notice the +1.
    file.write("#define NGHOSTS "+str(int(par.parval_from_str("finite_difference::FD_CENTDERIVS_ORDER")/2)+1)+"\n")
```

```python
%%writefile BSSN/TOV_Playground.c

// Part P1: Import needed header files
#include "NGHOSTS.h" // A NRPy+-generated file, which is set based on FD_CENTDERIVS_ORDER.
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "stdint.h" // Needed for Windows GCC 6.x compatibility

// Part P2: Add needed #define's to set data type, the IDX4() macro, and the gridfunctions
// Part P2a: set REAL=double, so that all floating point numbers are stored to at least ~16 significant digits.
#define REAL double

// Step P3: Set free parameters for the numerical grid
const REAL RMAX    = 3.0;

typedef struct __ID_inputs {
    REAL Rbar;
    int Rbar_idx;
    int interp_stencil_size;
    int numlines_in_file;
    REAL *r_Schw_arr,*rho_arr,*P_arr,*M_arr,*expnu_arr,*exp4phi_arr,*rbar_arr;
} ID_inputs;

// Part P4b: Declare the IDX4(gf,i,j,k) macro, which enables us to store 4-dimensions of
//           data in a 1D array. In this case, consecutive values of "i"
//           (all other indices held to a fixed value) are consecutive in memory, where
//           consecutive values of "j" (fixing all other indices) are separated by
//           Nxx_plus_2NGHOSTS[0] elements in memory. Similarly, consecutive values of
//           "k" are separated by Nxx_plus_2NGHOSTS[0]*Nxx_plus_2NGHOSTS[1] in memory, etc.
#define IDX4(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS[0] * ( (j) + Nxx_plus_2NGHOSTS[1] * ( (k) + Nxx_plus_2NGHOSTS[2] * (g) ) ) )
#define IDX3(i,j,k) ( (i) + Nxx_plus_2NGHOSTS[0] * ( (j) + Nxx_plus_2NGHOSTS[1] * (k) ) )
// Assuming idx = IDX3(i,j,k). Much faster if idx can be reused over and over:
#define IDX4pt(g,idx)   ( (idx) + (Nxx_plus_2NGHOSTS[0]*Nxx_plus_2NGHOSTS[1]*Nxx_plus_2NGHOSTS[2]) * (g) )

// Part P4c: Set #define's for BSSN gridfunctions. C code generated above
#include "../CurviBoundaryConditions/gridfunction_defines.h"

#define LOOP_REGION(i0min,i0max, i1min,i1max, i2min,i2max) \
  for(int i2=i2min;i2<i2max;i2++) for(int i1=i1min;i1<i1max;i1++) for(int i0=i0min;i0<i0max;i0++)

// Step P5: Function for converting uniform grid coord
//         (xx[0][i0],xx[1][i1],xx[2][i2]) to
//          corresponding Cartesian coordinate.
void xxCart(REAL *xx[3],const int i0,const int i1,const int i2, REAL xCart[3]) {
    REAL xx0 = xx[0][i0];
    REAL xx1 = xx[1][i1];
    REAL xx2 = xx[2][i2];
#include "xxCart.h"
}

// Step P6: Include basic functions needed to impose curvilinear
//          parity and boundary conditions.
#include "../CurviBoundaryConditions/curvilinear_parity_and_outer_boundary_conditions.h"

// Step P7: Function for enforcing the gammabar=gammahat constraint:
#include "enforce_detgammabar_constraint.h"

// Part P8: Declare all functions for setting up UIUC initial data.
/* Routines to interpolate the TOV solution and convert to ADM & T^{munu}: */
#include "../TOV/tov_interp.h"
#include "ID_TOV_ADM_quantities.h"
#include "ID_TOV_TUPMUNU.h"

/* Next perform the basis conversion and compute all needed BSSN quantities */
#include "ID_TOV_BSSN_Source_Terms.h"
#include "ID_ADM_xx0xx1xx2_to_BSSN_xx0xx1xx2__ALL_BUT_LAMBDAs.h"
#include "ID_BSSN__ALL_BUT_LAMBDAs.h"
#include "ID_BSSN_lambdas.h"

// Part P9: Declare function for computing the Hamiltonian
//           constraint violation, which should converge to
//           zero with increasing numerical resolution.
void Hamiltonian_constraint(const int Nxx[3],const int Nxx_plus_2NGHOSTS[3],const REAL dxx[3], REAL *xx[3],
                            REAL *in_gfs, REAL *aux_gfs) {
#include "Hamiltonian.h"
}

// main() function:
// Step 0: Read command-line input, set up grid structure, allocate memory for gridfunctions, set up coordinates
// Step 1: Set up scalar wave initial data
// Step 2: Evolve scalar wave initial data forward in time using Method of Lines with RK4 algorithm,
//         applying quadratic extrapolation outer boundary conditions.
// Step 3: Output relative error between numerical and exact solution.
// Step 4: Free all allocated memory
int main(int argc, const char *argv[]) {
    // Step 0a: Read command-line input, error out if nonconformant
    if(argc != 4 || atoi(argv[1]) < NGHOSTS) {
        fprintf(stderr,"Error: Expected three command-line arguments: ./TOV_Playground Nx0 Nx1 Nx2,\n");
        fprintf(stderr,"where Nx[0,1,2] is the number of grid points in the 0, 1, and 2 directions.\n");
        fprintf(stderr,"Nx[] MUST BE larger than NGHOSTS (= %d)\n",NGHOSTS);
        exit(1);
    }
    // Step 0b: Set up numerical grid structure, first in space...
    if(atoi(argv[1])%2 != 0 || atoi(argv[2])%2 != 0 || atoi(argv[2])%2 != 0) {
        fprintf(stderr,"Error: Cannot guarantee a proper cell-centered grid if number of grid cells not set to even number.\n");
        fprintf(stderr,"       For example, in case of angular directions, proper symmetry zones will not exist.\n");
        exit(1);
    }
    const int Nxx[3] = { atoi(argv[1]), atoi(argv[2]), atoi(argv[3]) };
    const int Nxx_plus_2NGHOSTS[3] = { Nxx[0]+2*NGHOSTS, Nxx[1]+2*NGHOSTS, Nxx[2]+2*NGHOSTS };
    const int Nxx_plus_2NGHOSTS_tot = Nxx_plus_2NGHOSTS[0]*Nxx_plus_2NGHOSTS[1]*Nxx_plus_2NGHOSTS[2];
#include "xxminmax.h"

    /* TOV INPUT ROUTINE */
    // Open the data file:
    char filename[100];
    sprintf(filename,"./outputTOVpolytrope.txt");
    FILE *in1Dpolytrope = fopen(filename, "r");
    if (in1Dpolytrope == NULL) {
        fprintf(stderr,"ERROR: could not open file %s\n",filename);
        exit(1);
    }
    // Count the number of lines in the data file:
    int numlines_in_file = count_num_lines_in_file(in1Dpolytrope);
    // Allocate space for all data arrays:
    REAL *r_Schw_arr  = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *rho_arr     = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *P_arr       = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *M_arr       = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *expnu_arr   = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *exp4phi_arr = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *rbar_arr    = (REAL *)malloc(sizeof(REAL)*numlines_in_file);

    // Read from the data file, filling in arrays
    if(read_datafile__set_arrays(in1Dpolytrope, r_Schw_arr,rho_arr,P_arr,M_arr,expnu_arr,exp4phi_arr,rbar_arr) == 1) {
        fprintf(stderr,"ERROR WHEN READING FILE %s!\n",filename);
        exit(1);
    }
    fclose(in1Dpolytrope);
    REAL Rbar = -100;
    int Rbar_idx = -100;
    for(int i=1;i<numlines_in_file;i++) {
        if(rho_arr[i-1]>0 && rho_arr[i]==0) { Rbar = rbar_arr[i-1]; Rbar_idx = i-1; }
    }
    if(Rbar<0) {
        fprintf(stderr,"Error: could not find rbar=Rbar from data file.\n");
        exit(1);
    }

    ID_inputs TOV_in;
    TOV_in.Rbar = Rbar;
    TOV_in.Rbar_idx = Rbar_idx;

    const int interp_stencil_size = 12;
    TOV_in.interp_stencil_size = interp_stencil_size;
    TOV_in.numlines_in_file = numlines_in_file;

    TOV_in.r_Schw_arr = r_Schw_arr;
    TOV_in.rho_arr = rho_arr;
    TOV_in.P_arr = P_arr;
    TOV_in.M_arr = M_arr;
    TOV_in.expnu_arr = expnu_arr;
    TOV_in.exp4phi_arr = exp4phi_arr;
    TOV_in.rbar_arr = rbar_arr;
    /* END TOV INPUT ROUTINE */

    // Step 0c: Allocate memory for gridfunctions
    REAL *evol_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *aux_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_AUX_GFS * Nxx_plus_2NGHOSTS_tot);

    // Step 0d: Set up space and time coordinates
    // Step 0d.i: Set \Delta x^i on uniform grids.
    REAL dxx[3];
    for(int i=0;i<3;i++) dxx[i] = (xxmax[i] - xxmin[i]) / ((REAL)Nxx[i]);

    // Step 0d.ii: Set up uniform coordinate grids
    REAL *xx[3];
    for(int i=0;i<3;i++) {
        xx[i] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS[i]);
        for(int j=0;j<Nxx_plus_2NGHOSTS[i];j++) {
            xx[i][j] = xxmin[i] + ((REAL)(j-NGHOSTS) + (1.0/2.0))*dxx[i]; // Cell-centered grid.
        }
    }

    // Step 0e: Find ghostzone mappings and parities:
    gz_map *bc_gz_map = (gz_map *)malloc(sizeof(gz_map)*Nxx_plus_2NGHOSTS_tot);
    parity_condition *bc_parity_conditions = (parity_condition *)malloc(sizeof(parity_condition)*Nxx_plus_2NGHOSTS_tot);
    set_up_bc_gz_map_and_parity_conditions(Nxx_plus_2NGHOSTS,xx,dxx,xxmin,xxmax,  bc_gz_map, bc_parity_conditions);

    // Step 1: Set up initial data to an exact solution at time=0:
    ID_BSSN__ALL_BUT_LAMBDAs(Nxx_plus_2NGHOSTS,xx,TOV_in, evol_gfs);
    apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, evol_gfs);
    enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, evol_gfs);
    ID_BSSN_lambdas(Nxx, Nxx_plus_2NGHOSTS, xx,dxx, evol_gfs);
    apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, evol_gfs);
    enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, evol_gfs);
{
    #pragma omp parallel for
    for(int i2=0; i2<Nxx_plus_2NGHOSTS[2]; i2++) {
        const REAL xx2 = xx[2][i2];
        for(int i1=0; i1<Nxx_plus_2NGHOSTS[1]; i1++) {
            const REAL xx1 = xx[1][i1];
            for(int i0=0; i0<Nxx_plus_2NGHOSTS[0]; i0++) {
                const REAL xx0 = xx[0][i0];
                REAL xx0xx1xx2[3] = {xx0,xx1,xx2};
                REAL gammaDD00,gammaDD01,gammaDD02,gammaDD11,gammaDD12,gammaDD22;
                REAL KDD00,KDD01,KDD02,KDD11,KDD12,KDD22;
                REAL alpha,betaU0,betaU1,betaU2;
                REAL BU0,BU1,BU2;
                REAL xyz_or_rthph[3] =  {xx0,xx1,xx2}; //FIXME

                // FIRST INTERPOLATE TO SET THE ADM AND TMUNU QUANTITIES
                ID_TOV_ADM_quantities(xyz_or_rthph, TOV_in,

                    &gammaDD00,&gammaDD01,&gammaDD02,&gammaDD11,&gammaDD12,&gammaDD22,
                    &KDD00,&KDD01,&KDD02,&KDD11,&KDD12,&KDD22,
                    &alpha,  &betaU0,&betaU1,&betaU2,  &BU0,&BU1,&BU2);

                REAL T4UU00,T4UU01,T4UU02,T4UU03,
                /**/        T4UU11,T4UU12,T4UU13,
                /**/               T4UU22,T4UU23,
                /**/                      T4UU33;

                ID_TOV_TUPMUNU(xyz_or_rthph, TOV_in,

                    &T4UU00,&T4UU01,&T4UU02,&T4UU03,
                    /**/    &T4UU11,&T4UU12,&T4UU13,
                    /**/            &T4UU22,&T4UU23,
                    /**/                    &T4UU33);

                const int idx = IDX3(i0,i1,i2);
                // THEN EVALUATE THE BSSN SOURCE TERMS
                ID_TOV_BSSN_Source_Terms(xx0xx1xx2,
                                         gammaDD00, gammaDD01, gammaDD02,
                    /**/                            gammaDD11, gammaDD12,
                    /**/                                       gammaDD22,

                    betaU0,betaU1,betaU2, alpha,

                    T4UU00, T4UU01, T4UU02, T4UU03,
                    /**/    T4UU11, T4UU12, T4UU13,
                    /**/            T4UU22, T4UU23,
                    /**/                    T4UU33,

                    &aux_gfs[IDX4pt(SDD00GF,idx)], &aux_gfs[IDX4pt(SDD01GF,idx)], &aux_gfs[IDX4pt(SDD02GF,idx)],
                    /**/                           &aux_gfs[IDX4pt(SDD11GF,idx)], &aux_gfs[IDX4pt(SDD12GF,idx)],
                    /**/                                                          &aux_gfs[IDX4pt(SDD22GF,idx)],

                    &aux_gfs[IDX4pt(SD0GF,idx)], &aux_gfs[IDX4pt(SD1GF,idx)], &aux_gfs[IDX4pt(SD2GF,idx)],

                    &aux_gfs[IDX4pt(SGF,idx)], &aux_gfs[IDX4pt(RHOGF,idx)]);
                //aux_gfs[IDX4pt(SGF,idx)] = T4UU11*gammaDD00;
            }
        }
    }
}

    // Step 1b: Apply boundary conditions, as initial data
    //          are sometimes ill-defined in ghost zones.
    //          E.g., spherical initial data might not be
    //          properly defined at points where r=-1.
//    apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, evol_gfs);
//    enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, evol_gfs);

    // Step 2: Evaluate Hamiltonian constraint violation
    Hamiltonian_constraint(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, evol_gfs, aux_gfs);

    /* Step 3: Output relative error between numerical and exact solution, */
    const int i0MIN=NGHOSTS; // In spherical, r=Delta r/2.
    const int i1mid=Nxx_plus_2NGHOSTS[1]/2;
    const int i2mid=Nxx_plus_2NGHOSTS[2]/2;
    LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS[0]-NGHOSTS, i1mid,i1mid+1, NGHOSTS,Nxx_plus_2NGHOSTS[2]-NGHOSTS) {
        REAL xx0 = xx[0][i0];
        REAL xx1 = xx[1][i1];
        REAL xx2 = xx[2][i2];
        REAL xCart[3];
#include "xxCart.h"
        int idx = IDX3(i0,i1,i2);
        printf("%e %e %e %e\n",xCart[0],xCart[1], aux_gfs[IDX4pt(SGF,idx)],log10(fabs(aux_gfs[IDX4pt(HGF,idx)])));
    }

    /* Step 4: Free all allocated memory */
    free(bc_gz_map);
    free(bc_parity_conditions);
    free(rbar_arr);
    free(rho_arr);
    free(P_arr);
    free(M_arr);
    free(expnu_arr);

    free(aux_gfs);
    free(evol_gfs);
    for(int i=0;i<3;i++) free(xx[i]);
    return 0;
}
```

    Overwriting BSSN/TOV_Playground.c

```python
!rm -f TOV_Playground out96.txt
!gcc -Ofast -march=native -ftree-parallelize-loops=2 -fopenmp BSSN/TOV_Playground.c -o TOV_Playground -lm
!taskset -c 0 ./TOV_Playground 96 96 96 > out96.txt

# Alternate Code
# !taskset -c 0,1 ./TOV_Playground 96 96 96 > out96.txt
# !gcc -fopenmp -O BSSN/TOV_Playground.c -o TOV_Playground -lm
# !gcc -Ofast -march=native -ftree-parallelize-loops=2 -fopenmp BSSN/TOV_Playground.c -o TOV_Playground -lm

# Windows Code
# import os
# !gcc -Ofast -march=native -fopenmp BSSN/TOV_Playground.c -o TOV_Playground -lm
# N_physical_cores = 4
# for resolution in [96, 48]:
#     script = ""
#     check_for_taskset = !which taskset >/dev/null && echo $?
#     if check_for_taskset == ['0']:
#         script += "taskset -c 0"
#         for i in range(N_physical_cores-1):
#             script += ","+str(i+1)
#         script += " "
#     exec_string = os.path.join(".", "TOV")
#     script += exec_string + "_Playground "+str(str(resolution)+" ")*3+" > out"+str(resolution)+".txt"
#     print("Executing `"+script+"`...")
#     os.system(script)
# print("Finished this code cell.")
```

# Step 6: Plotting the single-neutron-star initial data \[Back to [top](#toc)\]
$$\label{plot}$$

Here we plot the conformal factor of these initial data on a 2D grid, such that darker colors imply stronger gravitational fields. Hence, we see the single neutron star centered at the origin: $x/M=y/M=z/M=0$, where $M$ is an arbitrary mass scale (conventionally the [ADM mass](https://en.wikipedia.org/w/index.php?title=ADM_formalism&oldid=846335453) is chosen), and our formulation of Einstein's equations adopt $G=c=1$ [geometrized units](https://en.wikipedia.org/w/index.php?title=Geometrized_unit_system&oldid=861682626).

```python
import numpy as np
from scipy.interpolate import griddata
from pylab import savefig
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from IPython.display import Image

x96,y96,valuesCF96,valuesHam96 = np.loadtxt('out96.txt').T #Transposed for easier unpacking

bounds = 2.0
pl_xmin = -bounds
pl_xmax = +bounds
pl_ymin = -bounds
pl_ymax = +bounds

grid_x, grid_y = np.mgrid[pl_xmin:pl_xmax:100j, pl_ymin:pl_ymax:100j]
points96 = np.zeros((len(x96), 2))
for i in range(len(x96)):
    points96[i][0] = x96[i]
    points96[i][1] = y96[i]

grid96 = griddata(points96, valuesCF96, (grid_x, grid_y), method='nearest')
grid96cub = griddata(points96, valuesCF96, (grid_x, grid_y), method='cubic')

plt.clf()
plt.title("TOV Polytrope Initial Data: log10(Density)")
plt.xlabel("x/M")
plt.ylabel("y/M")

# fig, ax = plt.subplots()
# ax.plot(grid96cub.T, extent=(pl_xmin,pl_xmax, pl_ymin,pl_ymax))
# plt.close(fig)
fig96cf = plt.imshow(grid96.T, extent=(pl_xmin,pl_xmax, pl_ymin,pl_ymax))
cb = plt.colorbar(fig96cf)
savefig("BHB.png")
from IPython.display import Image
Image("BHB.png")
# #           interpolation='nearest', cmap=cm.gist_rainbow)
```

# Step 7: Validation: Convergence of numerical errors (Hamiltonian constraint violation) to zero \[Back to [top](#toc)\]
$$\label{convergence}$$

The equations behind these initial data solve Einstein's equations exactly, at a single instant in time. One reflection of this solution is that the Hamiltonian constraint violation should be exactly zero in the initial data.

However, when evaluated on numerical grids, the Hamiltonian constraint violation will *not* generally evaluate to zero due to the associated numerical derivatives not being exact. However, these numerical derivatives (finite difference derivatives in this case) should *converge* to the exact derivatives as the density of numerical sampling points approaches infinity.

In this case, all of our finite difference derivatives agree with the exact solution, with an error term that drops with the uniform gridspacing to the fourth power: $\left(\Delta x^i\right)^4$.

Here, as in the [Start-to-Finish Scalar Wave (Cartesian grids) NRPy+ tutorial](Tutorial-Start_to_Finish-ScalarWave.ipynb) and the [Start-to-Finish Scalar Wave (curvilinear grids) NRPy+ tutorial](Tutorial-Start_to_Finish-ScalarWaveCurvilinear.ipynb) we confirm this convergence.

First, let's take a look at what the numerical error looks like on the x-y plane at a given numerical resolution, plotting $\log_{10}|H|$, where $H$ is the Hamiltonian constraint violation:

```python
grid96 = griddata(points96, valuesHam96, (grid_x, grid_y), method='nearest')
grid96cub = griddata(points96, valuesHam96, (grid_x, grid_y), method='cubic')

# fig, ax = plt.subplots()

plt.clf()
plt.title("96^3 Numerical Err.: log_{10}|Ham|")
plt.xlabel("x/M")
plt.ylabel("y/M")

fig96cub = plt.imshow(grid96cub.T, extent=(pl_xmin,pl_xmax, pl_ymin,pl_ymax))
cb = plt.colorbar(fig96cub)
```

Next, we set up the same initial data but on a lower-resolution, $48\times 8\times 2$ grid (axisymmetric in the $\phi$ direction). Since the constraint violation (numerical error associated with the fourth-order-accurate, finite-difference derivatives) should converge to zero with the uniform gridspacing to the fourth power: $\left(\Delta x^i\right)^4$, we expect the constraint violation will increase (relative to the $96\times 16\times 2$ grid) by a factor of $\left(96/48\right)^4$. Here we demonstrate that indeed this order of convergence is observed as expected, *except* at the star's surface where the stress-energy tensor $T^{\mu\nu}$ sharply drops to zero.

```python
!rm -f out48.txt
!taskset -c 0 ./TOV_Playground 48 48 48 > out48.txt

x48,y48,valuesCF48,valuesHam48 = np.loadtxt('out48.txt').T #Transposed for easier unpacking
points48 = np.zeros((len(x48), 2))
for i in range(len(x48)):
    points48[i][0] = x48[i]
    points48[i][1] = y48[i]

grid48 = griddata(points48, valuesHam48, (grid_x, grid_y), method='cubic')

griddiff_48_minus_96 = np.zeros((100,100))
griddiff_48_minus_96_1darray = np.zeros(100*100)
gridx_1darray_yeq0 = np.zeros(100)
grid48_1darray_yeq0 = np.zeros(100)
grid96_1darray_yeq0 = np.zeros(100)
count = 0
outarray = []
for i in range(100):
    for j in range(100):
        griddiff_48_minus_96[i][j] = grid48[i][j] - grid96[i][j]
        griddiff_48_minus_96_1darray[count] = griddiff_48_minus_96[i][j]
        if j==49:
            gridx_1darray_yeq0[i] = grid_x[i][j]
            grid48_1darray_yeq0[i] = grid48[i][j] + np.log10((48./96.)**4)
            grid96_1darray_yeq0[i] = grid96[i][j]
        count = count + 1

plt.clf()
fig, ax = plt.subplots()
plt.title("Plot Demonstrating 4th-order Convergence")
plt.xlabel("x/M")
plt.ylabel("log10(Relative error)")

ax.plot(gridx_1darray_yeq0, grid96_1darray_yeq0, 'k-', label='Nr=96')
ax.plot(gridx_1darray_yeq0, grid48_1darray_yeq0, 'k--', label='Nr=48, mult by (48/96)^4')
ax.set_ylim([-12.5,1.5])

legend = ax.legend(loc='lower right', shadow=True, fontsize='x-large')
legend.get_frame().set_facecolor('C1')
plt.show()
```

# Step 8: 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-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.pdf](Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
!jupyter nbconvert --to latex --template latex_nrpy_style.tplx Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.ipynb
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.tex
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.tex
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.ipynb to latex
    [NbConvertApp] Support files will be in Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data_files/
    [NbConvertApp] Making directory Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data_files
    [NbConvertApp] Making directory Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data_files
    [NbConvertApp] Making directory Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data_files
    [NbConvertApp] Writing 137736 bytes to Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.tex
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode


# Generalisation error

In this experiment we vary the architecture of a feedforward ReLU network
and examine the (average) Bayesian generalization error as a function over the architecture.

We estimate the (average) Bayesian generalisation error as
\begin{equation}
E_n B_g(n) \approx E_n \frac{1}{n'} \sum_{i=1}^{n'} \log \frac{q(y_i|x_i)}{p(y_i |x_i,, \mathcal D_n)}
\end{equation}
where $\mathcal D_n$ is the training set and $n'$ is the size of the test set.
Note that the predictive distribution $p(y_i |x_i,, \mathcal D_n)$ is with respect to inverse temperature $\beta =1$

To begin, we set some global parameters for this experiment.

```python
from __future__ import print_function
import sys
sys.path.append('../')
from main import *

class Args:

    dataset = 'ffrelu_synthetic'
    sanity_check = True
    syntheticsamplesize = 500

    network = 'ffrelu'
    posterior_method = 'implicit'
    batchsize = 100
    epochs = 200
    epsilon_mc = 100
    pretrainDepochs = 10
    trainDepochs = 2
    n_hidden_D = 50
    num_hidden_layers_D = 2
    n_hidden_G = 50
    num_hidden_layers_G = 2

    lr_primal = 1e-3
    lr_dual = 5e-2

    beta_auto_liberal = False
    beta_auto_conservative = False
    beta_auto_oracle = False
    betasbegin = 0.1
    betasend = 1.5
    betalogscale = True
    numbetas = 10

    elasticnet_alpha = 1.0
    R = 200

    MCs = 10

    log_interval = 50

    cuda = False

args = Args()
kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {}

```

We design the true distribution to be realizable.
In particular we generate univariate Gaussian (mean $0$ and variance $1$) input data $X$
and set the univariate target $Y$ to be Gaussian with variance 1 and
mean given by ```ffrelu```$(X,H_1=2,H_2=1)$ according to the following network

```python
class ffrelu(nn.Module):
    def __init__(self,input_dim, output_dim,H1,H2):
        super(ffrelu, self).__init__()
        self.fc1 = nn.Linear(input_dim, H1)
        self.fc2 = nn.Linear(H1, H2)
        self.fc3 = nn.Linear(H2, output_dim)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
```

Below are some helper functions:
- ```train``` outputs an estimate of the RLCT based on a grid of inverse temperatures $\beta$
- ```compute_predictive_dist``` takes in a generator trained on $D_n$ at $\beta = 1$ and computes $p(y_i|x_i,D_n)$ by sampling
- ```compute_Bg``` uses the result of ```compute_predictive_dist``` and the same testing dataset to compute $B_g = \sum_{i=1}^{n'} \log \frac{q(y_i|x_i)}{p(y_i |x_i,, \mathcal D_n)}$

```python
def train(args, train_loader, valid_loader):

    # get a grid of inverse temperatures [beta_1/log n, \ldots, beta_k/log n]
    set_betas(args)

    mc = 1
    saveimgpath = None
    nll_betas_implicit = np.empty(0)

    for beta_index in range(args.betas.shape[0]):

        # train implicit variational inference
        print('Begin training IVI')
        G = train_implicitVI(train_loader, valid_loader, args, mc, beta_index, saveimgpath)
        print('Finished training IVI')
        nllw_array_implicit = approxinf_nll_implicit(train_loader, G, args)
        nllw_mean_implicit = sum(nllw_array_implicit)/len(nllw_array_implicit)
        nll_betas_implicit = np.append(nll_betas_implicit, nllw_mean_implicit)

    ivi_robust, ivi_ols = lsfit_lambda(nll_betas_implicit, args, saveimgname=None)

    return ivi_robust

def compute_predictive_dist(args, G, loader):

    R = 1000
    eps = torch.randn(R, args.epsilon_dim)
    sampled_weights = G(eps)
    wholex = loader.dataset[:][0]
    wholey = loader.dataset[:][1]
    list_of_param_dicts = weights_to_dict(args, sampled_weights)

    pred_logprob = 0
    for param_dict in list_of_param_dicts:

        h1 = F.relu((wholex @ param_dict['W1']) + param_dict['B1'])
        h2 = F.relu((h1 @ param_dict['W2']) + param_dict['B2'])
        mean = (h2 @ param_dict['W3']) + param_dict['B3']
        pred_logprob += -(np.log(2 * np.pi) + (wholey - mean) ** 2) / 2

    return pred_logprob/R

def compute_Bg(pred_logprob,loader,args):

    wholex = loader.dataset[:][0]
    wholey = loader.dataset[:][1]
    true_logprob = -(np.log(2 * np.pi) + (wholey - args.true_mean(wholex)) ** 2) / 2

    return (true_logprob-pred_logprob).mean()
```

We range over the model's architecture as determined by $H_1$ and $H_2$.
For each architecture we estimate the RLCT and calculate the average Bayesian generalization error.
Hopefully we can see the relationship $E_n B_g(n) \approx \lambda/n$.

```python
args.input_dim = 1
args.output_dim = 1
args.true_mean = models.ffrelu(args.input_dim, args.output_dim, 2, 1)

H1range = range(1,4)
H2range = range(1,3)
results = []

for H1 in H1range:
    for H2 in H2range:

        args.H1 = H1
        args.H2 = H2
        args.model, args.w_dim = retrieve_model(args)
        args.epsilon_dim = args.w_dim

        Bg = np.empty(args.MCs)
        rlct = np.empty(args.MCs)

        for mc in range(0,args.MCs):

            train_loader, valid_loader, test_loader = get_dataset_by_id(args, kwargs)

            args.betas = [1.0]
            beta_index = 0
            G = train_implicitVI(train_loader, valid_loader, args, mc, beta_index, saveimgpath=None)
            pred = compute_predictive_dist(args, G, test_loader)
            Bg[mc] = compute_Bg(pred,test_loader,args)

            rlct[mc] = train(args, train_loader, valid_loader)

        print('H1: {}, H2: {}'.format(H1,H2))
        print('E_n Bg(n): {}'.format(Bg.mean()))
        print('hat RLCT/n: {}'.format(rlct.mean()/args.syntheticsamplesize))
        results += {'H1':H1, 'H2':H2, 'E_n Bg(n)': Bg.mean(), 'hat RLCT/n': rlct.mean()/ args.syntheticsamplesize}

with open('generalization_ffrelu.pkl', 'wb') as f:
    pickle.dump(results, f)

```


| |Pierre Proulx, ing, professeur|
|:---|:---|
|Département de génie chimique et de génie biotechnologique |** GCH200-Phénomènes d'échanges I **|

### Section18.4, Réacteur chimique homogène

Voir les hypothèses dans le livre de Transport Phenomena.

```python
#
# Pierre Proulx
#
#
# Préparation de l'affichage et des outils de calcul symbolique
#
import sympy as sp
from IPython.display import *
sp.init_printing(use_latex=True)
%matplotlib inline
```

```python
# Paramètres, variables et fonctions

z,L,k_1,C_A0,D_AB,S,delta_z=sp.symbols('z,L,k_1,C_A0,D_AB,S,delta_z')
N_A=sp.symbols('N_A')
C_A=sp.Function('C_A')(z)
```

```python
#
# Bilan sur le réacteur, divisé tout de suite par le volume différentiel
#
eq=(S*N_A(z)-S*N_A(z+delta_z)-k_1*C_A*S*delta_z)/(S*delta_z)
display(eq)
```

```python
eq=sp.limit(eq,delta_z,0).doit()
display(eq)
```

```python
fick=-D_AB*sp.diff(C_A,z)
eq=eq.subs(N_A(z),fick)
display(eq)
```

```python
C_A=sp.dsolve(eq,C_A)
display(C_A)
C=C_A                    # je garde une copie de l'expression pour plus tard
```

```python
C_A=C_A.rhs
# Pose et solution des 2 équations de conditions aux limites pour C1 et C2
condition_1=sp.Eq(C_A.diff(z).subs(z,L ),0)
condition_2=sp.Eq(C_A.subs(z,0)-C_A0,0)
constantes=sp.solve([condition_1,condition_2],sp.symbols('C1,C2') )
display(constantes)
```

```python
C_A1=C_A.subs(constantes)
display(C_A1.simplify())
```

On peut maintenant utiliser la solution pour calculer , par exemple, la quantité de la substance A qui réagit dans le réacteur. Elle est obtenue en multipliant le flux molaire en z=0 par la surface, donc:

$ W_A = S * N_A $ en z=0 donc $ W_A = S * (-D_{AB}) \frac {dC_A}{dz}_{z=0} $

```python
W_A=S*C_A1.diff(z).subs(z,0)
display(W_A)
```

____

# Reprenons l'exemple précédent , en modifiant la condition à la limite z=L

____

## Le problème développé à la section 18.4 est identique au problème d'ailette traité à la section 10.7.

### 1. Au lieu de considérer la fond du réacteur comme imperméable, on pourrait considérer qu'il contient un catalyseur rapide. La condition en z=L deviendrait que $C_{AL}=0$

```python
#
# Reprenons C, la copie de C_A qu'on a fait avant d'y substituer les conditions aux limites
#
C_A=C.rhs
display(C_A)
```

```python
#
# Pose et solution des 2 équations de conditions aux limites pour C1 et C2
condition_1=sp.Eq(C_A.subs(z,L ),0)
condition_2=sp.Eq(C_A.subs(z,0)-C_A0,0)
constantes=sp.solve([condition_1,condition_2],sp.symbols('C1,C2') )
display(constantes)
```

```python
C_A2=C_A.subs(constantes)
display(C_A2.simplify())
```

____

### 2. Au lieu de considérer la fond du réacteur comme imperméable, on pourrait considérer qu'il contient un catalyseur réel. La condition en z=L deviendrait que la concentration est déterminée par la vitesse de réaction de surface donc:
### $ -D_{AB} \frac {dC_A}{dz} = k_s C_A $ en z=L

```python
#
# Reprenons encore C, la copie de C_A avant d'y substituer les conditions aux limites
#
C_A=C.rhs
display(C_A)
```

```python
#
# Pose et solution des 2 équations de conditions aux limites pour C1 et C2
k_s=sp.symbols('k_s')
condition_1=sp.Eq(-D_AB*C_A.diff(z).subs(z,L)-k_s*C_A.subs(z,L))
condition_2=sp.Eq(C_A.subs(z,0)-C_A0,0)
constantes=sp.solve([condition_1,condition_2],sp.symbols('C1,C2') )
display(constantes)
```

```python
C_A3=C_A.subs(constantes)
display(C_A3.simplify())
```

```python
#
# On peut maintenant tracer, en donnant des valeurs pour les paramètres.
#
dico={'D_AB':1.e-10,'C_A0':1,'L':0.1,'k_1':1.e-7,'k_s':1e-8}
C_A1p=C_A1.subs(dico).evalf()
C_A2p=C_A2.subs(dico).evalf()
C_A3p=C_A3.subs(dico).evalf()
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize']=8,6
p=sp.plot((C_A1p,(z,0,L.subs(dico))),(C_A2p,(z,0,L.subs(dico))),(C_A3p,(z,0,L.subs(dico))),
        title='Concentration vs z',ylabel='C_A',show=False,legend=True)
p[0].line_color = 'blue'
p[0].label='Fond du réacteur imperméable'
p[1].line_color = 'black'
p[1].label='Fond du réacteur catalyseur rapide'
p[2].line_color = 'red'
p[2].label='Fond du réacteur catalyseur réel'
p.show()
```


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license © 2014 L.A. Barba, C.D. Cooper, G.F. Forsyth.  Based on [CFD Python](https://github.com/barbagroup/CFDPython), © 2013 L.A. Barba, also under CC-BY license.

# Relax and hold steady

Welcome to the second notebook of *"Relax and hold steady: elliptic problems"*, **Module 5** of the course [**"Practical Numerical Methods with Python"**](http://openedx.seas.gwu.edu/courses/GW/MAE6286/2014_fall/about). Are you relaxed yet?

In the [previous notebook](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/05_relax/05_01_2D.Laplace.Equation.ipynb), you learned to use Jacobi iterations to solve Laplace's equation. The iterations *relax* the solution from an initial guess to the final, steady-state solution. You also saw again that the way we treat boundary conditions can influence our solution. Using a first-order approximation of the Neumann boundary messed up our spatial convergence in the whole domain! (We expected second-order spatial convergence from the central difference scheme, but we got closer to first order.) This was easily fixed by using a second-order scheme for the Neumann boundary. *It's always good to check that you get the expected order of convergence.*

A word of warning: in this course module, we will introduce a different use of the word *"convergence"*. Before, we used it to refer to the decay of the truncation errors (in space and time) with a decrease in the grid spacing ($\Delta x$ and $\Delta t$). Now, we also have a relaxation scheme, and we use the word convergence to mean that the iterative solution approaches the exact solution of the linear system. Sometimes, this is called *algebraic convergence*. We'll concern ourselves with this in the next lesson. But first, let's play with Poisson.

## Poisson equation

The **Poisson equation** has a forcing function that drives the solution to its steady state. Unlike the Laplace equation, Poisson's equation involves imposed values inside the field (a.k.a., sources):

$$\frac{\partial ^2 p}{\partial x^2} + \frac{\partial ^2 p}{\partial y^2} = b$$

In discretized form, this looks almost the same as [the Laplace Equation](./05_01_2D.Laplace.Equation.ipynb), except for the source term on the right-hand side:

$$\frac{p_{i+1,j}^{k}-2p_{i,j}^{k}+p_{i-1,j}^{k}}{\Delta x^2}+\frac{p_{i,j+1}^{k}-2 p_{i,j}^{k}+p_{i,j-1}^{k}}{\Delta y^2}=b_{i,j}^{k}$$

As before, we rearrange this to obtain an equation for $p$ at point $i,j$, based on its neighbors:

$$p_{i,j}^{k+1}=\frac{(p_{i+1,j}^{k}+p_{i-1,j}^{k})\Delta y^2+(p_{i,j+1}^{k}+p_{i,j-1}^{k})\Delta x^2-b_{i,j}^{k}\Delta x^2\Delta y^2}{2(\Delta x^2+\Delta y^2)}$$

It's slightly more complicated than the Laplace equation, but nothing we can't handle.

### An example problem

Let's consider the following Poisson equation:

\begin{equation}
\nabla^2 p = -2\left(\frac{\pi}{2}\right)^2\sin\left( \frac{\pi x}{L} \right) \cos\left(\frac{\pi y}{L}\right)
\end{equation}

in the domain

$$\left\lbrace \begin{align*}
0 &\leq x\leq 1  \\
-0.5 &\leq y \leq 0.5
\end{align*} \right.$$

with Dirichlet boundary conditions

$$p=0 \text{ at } \left\lbrace
\begin{align*}
x&=0\\
y&=0\\
y&=-0.5\\
y&=0.5
\end{align*} \right.$$

To solve this equation, we assume an initial state of $p=0$ everywhere, apply the boundary conditions and then iteratively relax the system until we converge on a solution.

To start, let's import libraries and set up our spatial mesh.

```python
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import pyplot, cm
from math import pi
import numpy
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

```python
# Parameters
nx = 41
ny = 41
xmin = 0
xmax = 1
ymin = -0.5
ymax = 0.5

l2_target = 2e-7
```

```python
def poisson_IG(nx, ny, xmax, xmin, ymax, ymin):
    '''Initialize the Poisson problem initial guess and other variables
    Parameters:
    ----------
    nx : int
        number of mesh points in x
    ny : int
        number of mesh points in y
    xmax: float
        maximum value of x in mesh
    xmin: float
        minimum value of x in mesh
    ymax: float
        maximum value of y in mesh
    ymin: float
        minimum value of y in mesh

    Returns:
    -------
    X  : 2D array of floats
        X-position of mesh
    Y  : 2D array of floats
        Y-position of mesh
    p_i: 2D array of floats
        initial guess of p
    b  : 2D array of floats
        forcing function
    dx : float
        mesh size in x direction
    dy : float
        mesh size in y direction
    '''

    dx = (xmax-xmin)/(nx-1)
    dy = (ymax-ymin)/(ny-1)

    # Mesh
    x  = numpy.linspace(xmin,xmax,nx)
    y  = numpy.linspace(ymin,ymax,ny)
    X,Y = numpy.meshgrid(x,y)

    # Source
    L = xmax-xmin
    b = -2*(pi/L)**2*numpy.sin(pi*X/L)*numpy.cos(pi*Y/L)

    # Initialize
    p_i  = numpy.zeros((ny,nx))

    return X, Y, x, y, p_i, b, dx, dy, L
```

The Jacobi iterations need an exit condition, based on some norm of the difference between two consecutive iterations. We can use the same relative L2-norm that we wrote for the Laplace exit condition, so we saved the function into a helper Python file for easy importing.

```python
from laplace_helper import L2_rel_error
```

Now, what value to choose for the exit condition? We saw in the previous notebook that with an exit tolerance of $10^{-8}$, we could converge well for the different grids we tried, and observe second-order spatial convergence (with the second-order Neumann BC). We speculated in the end that we might be able to use a less stringent exit tolerance, since the spatial error was a lot larger (around $0.0002$ for the finer grid). Here, we'll try with $2\times 10^{-7}$. Go ahead and try with different values and see what you get!

It's time to write the function to solve the Poisson equation.  Notice that all of the boundaries in this problem are Dirichlet boundaries, so no BC updates required!

There's also one extra piece we're adding in here.  To later examine the convergence of the iterative process, we will save the L2-norm of the difference between successive solutions. A plot of this quantity with respect to the iteration number will be an indication of how fast the relaxation scheme is converging.

```python
def poisson_2d(p, b, dx, dy, l2_target):
    '''Performs Jacobi relaxation

    Parameters:
    ----------
    p : 2D array of floats
        Initial guess
    b : 2D array of floats
        Source term
    dx: float
        Mesh spacing in x direction
    dy: float
        Mesh spacing in y direction
    l2_target: float
        Target difference between two consecutive iterates

    Returns:
    -------
    p: 2D array of float
        Distribution after relaxation
    '''

    l2_norm = 1
    iterations = 0
    l2_conv = []

    while l2_norm > l2_target:

        pd = p.copy()

        p[1:-1,1:-1] = 1/(2*(dx**2 + dy**2)) * \
                        ((pd[1:-1,2:]+pd[1:-1,:-2])*dy**2 +\
                        (pd[2:,1:-1] + pd[:-2,1:-1])*dx**2 -\
                         b[1:-1,1:-1]*dx**2*dy**2)

        # BCs are automatically enforced

        l2_norm = L2_rel_error(pd,p)
        iterations += 1
        l2_conv.append(l2_norm)

    print('Number of Jacobi iterations: {0:d}'.format(iterations))
    return p, l2_conv
```

The `initialize` function above sets the array `p_i` to all zeros for an initial guess at the solution.  We can use the `plot_3D` function we wrote in the previous notebook to explore the field $p$, before and after the relaxation. We saved this plotting function into the helper Python file, so we can re-use it here.

```python
from laplace_helper import plot_3D
```

Now we initialize all of the problem variables and plot!

```python
X, Y, x, y, p_i, b, dx, dy, L = poisson_IG(nx, ny, xmax, xmin, ymax, ymin)
plot_3D(x, y, p_i)
```

That looks suitably boring.  Zeros everywhere and boundaries held at zero.  If this were a Laplace problem we would already be done!

But the Poisson problem has a source term that will evolve this zero initial guess to something different. Let's run our relaxation scheme and see what effect the forcing function has on `p`.

```python
p, l2_conv = poisson_2d(p_i.copy(), b, dx, dy, l2_target)
```

    Number of Jacobi iterations: 3125

It took 3,125 iterations to converge to the exit criterion (that's quite a lot, don't you think? Let's now take a look at a plot of the final field:

```python
plot_3D(x,y,p)
```

Something has definitely happened.  That looks good, but what about the error? This problem has the following analytical solution:

\begin{equation}
p(x,y) = \sin{\left(\frac{x\pi}{L} \right)}\cos{\left(\frac{y\pi}{L} \right)}
\end{equation}

Time to compare the calculated solution to the analytical one. Let's do that.

```python
def p_analytical(X,Y,L):
    ''' Computes analytical solution to Poisson problem
    Parameters:
    ----------
    X: 2D array of float
        Mesh with x components
    Y: 2D array of float
        Mesh with y components
    L: float
        Size of domain
    Returns:
    -------
    Analytical solution
    '''
    return numpy.sin(X*pi/L)*numpy.cos(Y*pi/L)
```

```python
p_an = p_analytical(X,Y,L)
```

```python
error = L2_rel_error(p,p_an)
error
```

    0.00044962635353597519

That seems small enough. Of course, each application problem can have different accuracy requirements.

### Algebraic convergence

Remember that we saved the L2-norm of the difference between two consecutive iterations. The purpose of that was to look at how the relaxation scheme *converges*, in algebraic sense: with consecutive solutions getting closer and closer to each other. Let's use a line plot for this.

```python
pyplot.figure(figsize=(8,6))
pyplot.grid(True)
pyplot.xlabel(r'iterations', fontsize=18)
pyplot.ylabel(r'$L_2$-norm', fontsize=18)
pyplot.semilogy(numpy.arange(len(l2_conv)), l2_conv, lw=2, color='k');
```

It looks like in the beginning, iterations started converging pretty fast, but they quickly adopted a slower rate. As we saw before, it took more than 3,000 iterations to get to our target difference between two consecutive solutions (in L2-norm). That is a *lot* of iterations, and we would really like to relax faster! No worries, we'll learn to do that in the next notebook.

### Spatial convergence

For a sanity check, let's make sure the solution is achieving the expected second-order convergence in space.

```python
nx_values = [11,21,41,81]

error = numpy.zeros_like(nx_values, dtype=numpy.float)

for i, nx in enumerate(nx_values):

    ny = nx

    X, Y, c, y, p_i, b, dx, dy, L = poisson_IG(nx, ny, xmax, xmin, ymax, ymin)
    p, l2_conv = poisson_2d(p_i.copy(), b, dx, dy, l2_target)

    p_an = p_analytical(X,Y,L)

    error[i] = L2_rel_error(p,p_an)
```

    Number of Jacobi iterations: 249
    Number of Jacobi iterations: 892
    Number of Jacobi iterations: 3125
    Number of Jacobi iterations: 10708

```python
pyplot.figure(figsize=(6,6))
pyplot.grid(True)
pyplot.xlabel(r'$n_x$', fontsize=18)
pyplot.ylabel(r'$L_2$-norm of the error', fontsize=18)
pyplot.loglog(nx_values, error, color='k', ls='--', lw=2, marker='o')
pyplot.axis('equal');
```

That looks pretty much second order!  Remember that the boundary conditions can adversely affect convergence, but Dirichlet boundaries are "exact" and will never impact your convergence.

## Final word

We have used the difference between two consecutive solutions in the iterative process as a way to indicate convergence. However, this is *not* in general the best idea. For some problems and some iterative methods, you could experience iterates *stagnating* but the solution *not converging*.

Convergence of an iterative solution of a system $A \mathbf{x} = \mathbf{b}$ means that:

\begin{equation}
\lim_{k \rightarrow \infty} \mathbf{x}^k = \mathbf{x}
\end{equation}

The error in the solution is actually $\mathbf{x}-\mathbf{x}^k$, but we're looking at $\mathbf{x}^{k+1}-\mathbf{x}^k$ for our exit criterion. They are not the same thing and the second could tend to zero (or machine precision) without the first being comparably small.

A discussion of better ways to apply stopping criteria for iterative methods is a more advanced topic than we want cover in this course module. Just keep this in mind as you continue your exploration of numerical methods in the future!

***

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, "r").read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
    margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h2 {
    font-family: 'Nixie One', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Nixie One', serif;
    margin-top:16px;
    font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Nixie One', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Nixie One', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
    font-family: "Source Code Pro";
    font-size: 90%;
}

.alert-box {
    padding:10px 10px 10px 36px;
    margin:5px;
}

.success {
    color:#666600;
    background:rgb(240,242,229);
}
</style>