# Multi-Qubit System Measurements Tutorial Workbook

**What is this workbook?**
A workbook is a collection of problems, accompanied by solutions to them.
The explanations focus on the logical steps required to solve a problem; they illustrate the concepts that need to be applied to come up with a solution to the problem, explaining the mathematical steps required.

Note that a workbook should not be the primary source of knowledge on the subject matter; it assumes that you've already read a tutorial or a textbook and that you are now seeking to improve your problem-solving skills. You should attempt solving the tasks of the respective kata first, and turn to the workbook only if stuck. While a textbook emphasizes knowledge acquisition, a workbook emphasizes skill acquisition.

This workbook describes the solutions to the problems offered in the [Multi-Qubit System Measurements tutorial](./MultiQubitSystemMeasurements.ipynb).
Since the tasks are offered as programming problems, the explanations also cover some elements of Q# that might be non-obvious for a first-time user.

**What you should know for this workbook**

You should be familiar with the following concepts before tackling the Single-Qubit System Measurements tutorial (and this workbook):
1. Basic linear algebra
2. Single and multi-qubit systems
3. Single and multi-qubit gates
$\newcommand{\ket}[1]{\left|#1\right>}$
$\newcommand{\bra}[1]{\left<#1\right|}$

### Exercise 1: Multi-qubit measurement outcome probabilities I
Suppose that a two-qubit system is known to be in the following state:
$$\ket \psi =  \frac{1}{3}\ket {00} + \frac{2}{3} \ket {01} + \frac{2}{3}\ket {11}$$.

If all the qubits are measured simultaneously in the computational basis, what are the outcome probabilities?

### Solution
The wavefunction $|\psi\rangle$ is normalized, since $\left(\frac{1}{3}\right)^2 + \left(\frac{2}{3}\right)^2 + \left(\frac{2}{3}\right)^2 = 1$. Hence, the probabilities of measuring each of the computational basis states is simply the square of the absolute value of the corresponding coefficients. That is, the probabilities of measuring $00$, $01$ and $11$ are $\frac{1}{9}$, $\frac{4}{9}$ and $\frac{4}{9}$ respectively.

[Return to exercise 1 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-1:-Multi-qubit-measurement-outcome-probabilities-I)

### Exercise 2: Multi-qubit measurement outcome probabilities II
Suppose that a two-qubit system is known to be in the following state:
$$\ket \psi =  \frac{2}{3}\ket {00} + \frac{1}{3} \ket {01} + \frac{2}{3}\ket {11}$$.

If all the qubits are measured simultaneously in the Pauli X basis, i.e. in the $\{ \ket{++}, \ket{+-}, \ket{-+}, \ket{--}\}$ basis, what are the outcome probabilities?

### Solution
Using the expressions $|0\rangle = \frac{1}{\sqrt{2}} \left( |+\rangle + |-\rangle \right)$ and $|1\rangle = \frac{1}{\sqrt{2}} \left( |+\rangle - |-\rangle \right)$, we first express $|\psi\rangle$ in the Pauli X basis. This gives us
$$
|\psi \rangle = \frac{5}{6} |++\rangle - \frac{1}{6} |+-\rangle + \frac{1}{2} |-+\rangle + \frac{1}{6} |--\rangle.
$$
Thus, the probabilities of measuring each of the four basis vectors is given by

    <col width=150>
    <col width=150>

        Measurement outcome
        Probability of outcome

        $++$
        $\left( \frac{5}{6}\right)^2 = \frac{25}{36}$

        $+-$
        $\left( -\frac{1}{6}\right)^2 = \frac{1}{36}$

        $-+$
        $\left( \frac{1}{2}\right)^2 = \frac{1}{4}$

        $++$
        $\left( \frac{1}{6}\right)^2 = \frac{1}{36}$

[Return to exercise 2 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-2:-Multi-qubit-measurement-outcome-probabilities-II)

### Exercise 3: Identify computational basis states

**Input**: An array of qubits which is guaranteed to be in a computational basis state.

**Output**: An array of `Result` values, with the same length as the number of qubits, specifying the computational basis state corresponding to the input state. For example, if the input qubit wavefunction is set to $\ket{001}$, the output should be the array `[Zero, Zero, One]`.

### Solution

First, we measure both qubits in the input array and store the result in `m1` and `m2`. We can decode these results like this:
- `m1` is $|0\rangle$ and `m2` is $|0\rangle$: we return $0\cdot2+0 = 0$
- `m1` is $|0\rangle$ and `m2` is $|1\rangle$: we return $0\cdot2+1 = 1$
- `m1` is $|1\rangle$ and `m2` is $|0\rangle$: we return $1\cdot2+0 = 2$
- `m1` is $|1\rangle$ and `m2` is $|1\rangle$: we return $1\cdot2+1 = 3$

In other words, we treat the measurement results as the binary notation of the return value in [big endian notation](../MultiQubitSystems/MultiQubitSystems.ipynb#Endianness).

```qsharp
%kata T1_BasisStateMeasurement

operation BasisStateMeasurement (qs : Qubit[]) : Int {
    // Measurement on the first qubit gives the higher bit of the answer, on the second - the lower
    let m1 = M(qs[0]) == Zero ? 0 | 1;
    let m2 = M(qs[1]) == Zero ? 0 | 1;
    return m1 * 2 + m2;
}
```

[Return to exercise 3 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-3:-Identify-computational-basis-states)

### Exercise 4: Partial measurement probabilities for the Hardy state

Consider a 2-qubit system which is in the state $\ket \psi = \frac{1}{\sqrt{12}} \big(3|00\rangle + |01\rangle + |10\rangle + |11\rangle\big)$.

If only the first qubit is measured in the computational basis what are the probabilities of the outcomes, and the post-measurement states of the system?

### Solution

A measurement of the first qubit in the state $0$ corresponds to the projection operator $P_0|0\rangle\langle 0| \otimes \mathbb{1}$. A few lines of algebra gives us
\begin{align}
\frac{P_0 \ket{\psi}}{|P_0 \ket{\psi}|} &= \frac{1}{\sqrt{10}} \left( 3\ket{00} + \ket{01}\right);\\
\text{and }|P_0 \ket{\psi}|^2 &= \frac{5}{6}.
\end{align}
Similarly, $P_1 = |1\rangle \langle 1 | \otimes \mathbb{1}$ is the projector corresponding to a measurement outcome of 1 for the first qubit. Operating $P_1$ on $\ket{\psi}$ gives us
\begin{align}
\frac{P_1 \ket{\psi}}{|P_1 \ket{\psi}|} &= \frac{1}{\sqrt{2}} \left(\ket{10} + \ket{11}\right);\\
\text{and }|P_1 \ket{\psi}|^2 &= \frac{1}{6}.
\end{align}

    <col width=150>
    <col width=150>
    <col width=150>

        Measurement outcome
        Probability of outcome
        Post-measurement state

        $0$
        $\frac{5}{6}$
        $\frac{1}{\sqrt{10}} \left( 3\ket{00} + \ket{01}\right)$

        $1$
        $\frac{1}{6}$
        $\frac{1}{\sqrt{2}} \left(\ket{10} + \ket{11}\right)$

[Return to exercise 4 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-4:-Partial-measurement-probabilities-for-the-Hardy-state)

### Exercise 5: Distinguish orthogonal states using partial measurements

**Input**: A 3-qubit system which is guaranteed to be in either the $\ket{++-}$ state, or the $\ket{---}$ state.

**Output**:
- 0 if the qubit is in the $\ket{++-}$ state,
- 1 if the qubit is in the $\ket{---}$ state.

The state of the qubits at the end of the operation should be the same as the initial state.

### Solution
Since both the states correspond to the first qubit being in states $\ket +$ and $\ket -$ respectively, it is sufficient to measure only the first qubit in the Pauli X basis in order to distinguish the two states. Furthermore, the measurement does will not result in a change in the state of the qubit, since the post-projection state is identical to the original state in either case.

```qsharp
%kata T2_IsPlusPlusMinus

operation IsPlusPlusMinus (qs : Qubit[]) : Int {
    return Measure([PauliX], [qs[0]]) == Zero  ? 0 | 1;
}
```

[Return to exercise 5 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-5:-Distinguish-orthogonal-states-using-partial-measurements)

### Exercise 6: Sequential measurements on an entangled state and a separable state
Consider two two-qubit states:
- The Bell state $|\Phi^{+}\rangle = \frac{1}{\sqrt{2}} \big (|00\rangle + |11\rangle\big)$.
- A state $\ket \Theta = \frac{1}{2} \big( \ket{00} + \ket{01} + \ket{10} + \ket{11} \big)$.

For both the states, consider a measurement on the first qubit, followed by a measurement on the second second qubit (both in the computational basis). For which state can we expect the measurement outcomes to be correlated? Verify by calculating the sequential measurement probabilities explicitly for both the states.

### Solution
- *The Bell state*: If the first qubit is measured to be in the state $\ket 0$, a subsequent measurement on the second qubit *always* results in an outcome of $0$, with probabilitiy $1$. Similarly, if the outcome of the measurement on the first qubit is $1$, then the second qubit measurement always results in $1$. Thus, sequential measurements are perfectly *correlated*.
- *Separable state* $\ket \Theta$: Irrespective of whether the first qubit measurement outcome is $0$ of $1$ (each of which occurs with a probability of $0.5$), a subsequent measurement on the second qubit results in an outcome of $0$ or $1$ (both with a probabilitiy of $0.5$). Thus, sequential measurements are perfectly *uncorrelated*.

This aligns with the fact that the Bell state is entangled, while the $\ket{\Theta}$ is not entangled, it is separable and can be expressed as $\ket \Theta = \ket + \otimes \ket +$.

[Return to exercise 6 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-6:-Sequential-measurements-on-an-entangled-state-and-a-separable-state)

### Exercise 7: State selection using partial measurements

**Input**:
1. A 2-qubit system which is known to be in a state $|\psi\rangle$, given by
$$
|\psi\rangle = \frac{1}{\sqrt{2}} |0\rangle \otimes ( a |0\rangle + b|1\rangle) + \frac{1}{\sqrt{2}} |1\rangle \otimes (b|0\rangle + a |1\rangle),
$$
where the constants $a$ and $b$ satisfying $|a|^2 + |b|^2 = 1$ are not provided.
2. An integer $i$ which is either $0$ or $1$.

**Output**:
- If $i$ equals 0, the state of the state of the second qubit should be $a|0\rangle + b|1\rangle$
- If $i$ equals 1, the state of the second qubit should be $b|0\rangle + a |1\rangle$.

The state of the first qubit at the end does not matter.

### Solution
We note that if we measure the first qubit in the computational basis, then an outcome of $0$ collapses the second qubit to the state $a\ket 0 + b \ket 1$, while an outcome of $1$ collapses the second qubit to the state $b\ket 0 + a \ket 1$.

Thus, if $i=0$ and we measure $0$ or if $i=1$ and we measure $1$, then post-measurement, the second qubit will be in the desired state. On the other hand, we $i=1$ and we measure $0$, or if we $i=0$ and we measure $1$, then the post-measurement state of the second qubit can be acted upon by the Pauli $X$ gate in order to obtain the desired state.

```qsharp
%kata T3_StateSelction

operation StateSelction(qs : Qubit[], i : Int) : Unit {
    if (i == 0) {
        if (M(qs[0]) == One){
            // apply the X gate to the second qubit
            X(qs[1]);
        }
    } else {
        if (M(qs[0]) == Zero){
            // apply the X gate to the second qubit only
            X(qs[1]);
        }
    }
}
```

[Return to exercise 7 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-7:-State-selection-using-partial-measurements)

### Exercise 8: State preparation using partial measurements

**Input:** Two qubits (in an array) which are in the state $\ket{00}$.

**Goal:**  Modify the qubits to the state $\frac{1}{\sqrt{3}} \big(|00\rangle + |01\rangle + |10\rangle\big)$ using post-selection.

### Solution
While it is possible to prepare the state $\ket \psi$ directly using unitary rotations, it is simpler to use post-selection for preparing it. Here, we describe the procedure in more detail below.

Initially we will prepare an equal superposition of all basis states corresponding to the first two qubits by applying the **H** gate to each of them:
$$\frac{1}{2} \big(|00\rangle + |01\rangle + |10\rangle + |11\rangle\big) \ket 0$$

This state is a superposition of the state we want to prepare, and the $|11\rangle$ state that we would like to discard. We can do exactly that by performing the right measurement on the system.

Now, we want to separate the first three basis states from the last one and to store this separation in the extra qubit.
For example, we can keep the state of the extra qubit $|0\rangle$ for the basis states that we want to keep, and switch it to $|1\rangle$ for the basis states that we would like to discard.
A **CCNOT** gate can be used to accomplish this, with the first two qubits used as control qubits and the extra qubit as target.
When the gate is applied, the state of the extra qubit will only change to $|1\rangle$ if both control qubits are in the $|11\rangle$ state, which marks exactly the state that we want to discard:

$$\text{CCNOT}\frac{1}{2} \big(|00\textbf{0}\rangle + |01\textbf{0}\rangle + |10\textbf{0}\rangle + |11\textbf{0}\rangle\big) =
\frac{1}{2}\big(|00\rangle + |01\rangle + |10\rangle \big) \otimes |\textbf{0}\rangle + \frac{1}{2}|11\rangle \otimes |\textbf{1}\rangle $$

Finally we measure just the extra qubit; this causes a partial collapse of the system to the state defined by the measurement result:
* If the result is $|0\rangle$, the system collapses to a state that is a linear combination of basis states which had the extra qubit in state $|0\rangle$, i.e., the two qubits end up in the target state $\frac{1}{\sqrt3}\big(|00\rangle + |01\rangle + |10\rangle\big)$.
* If the result is $|1\rangle$, the system collapses to a state $|11\rangle$, so our goal is not achieved. The good thing is, this only happens in 25% of the cases, and we can just reset our qubits to the $|00\rangle$ state and try again.

> Q# has a built-in [repeat-until-success (RUS) loop](https://docs.microsoft.com/en-us/quantum/user-guide/using-qsharp/control-flow#repeat-until-success-loop), which comes in handy in this case.
> * We will describe the main operations (applying **H** and **CCNOT** gates and the measurement) in the `repeat` part of the loop, which specifies its body.
> * `until` section specifies the condition which will break the loop. In this case the result of the measurement needs to be `Zero` to indicate our success.
> * Finally, the `fixup` section allows us to clean up the results of the loop body execution before trying again if the success criteria is not met. In this case we reset the first two qubits back to the $|00\rangle$ state.

This technique is sometimes called post-selection.

```qsharp
%kata T4_PostSelection

open Microsoft.Quantum.Measurement;

operation PostSelection (qs : Qubit[]) : Unit {
    // Initialize the extra qubit
    use anc = Qubit();
    // Using the repeat-until-success pattern to prepare the right state
    repeat {
        ApplyToEach(H, qs);
        Controlled X(qs, anc);
        let res = MResetZ(anc);
    }
    until (res == Zero)
    fixup {
        ResetAll(qs);
    }
}
```

[Return to exercise 8 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-8:-State-preparation-using-partial-measurements)

### Exercise 9: Two qubit parity Measurement

**Inputs**: Two qubits stored in an array which are guaranteed to be either in an unknown superposition of the states $|00\rangle$ and $|11\rangle$ or in a superposition of states $|01\rangle$ and $|10\rangle$.

**Output**: 0 if qubits were in the first superposition, 1 if they were in the second superposition.
*The state of the qubits at the end of the operation should be the same as the starting state.*

### Solution

If we were not asked to maintain the state of the qubits, one approach would be to measure both the qubits separately in the computational basis, and check if the result is the same for both the measurements. If the result for the two measurements are equal, the input state must have been a superposition of $\ket{00}$ and $\ket{11}$, while different measurement outcomes will imply that the input state must have been a superposition of $\ket{01}$ and $\ket{10}$. However, in this process, we will lose the information about the original superposition states: a state $\alpha |00\rangle + \beta |11\rangle$ will collapse to either $|00\rangle$ or $|11\rangle$, and we won't be able to recover the information about the coefficients $\alpha$ and $\beta$.

We need to measure *parity* of the state without collapsing it all the way to the basis states. [Pauli measurements](https://docs.microsoft.com/en-us/quantum/concepts/pauli-measurements) can be used for joint measurements involving multiple qubits. For this task we apply the `PauliZ` matrix on both qubits.

A joint measurement using `PauliZ` can be thought as projecting the measured state to one of the two eigenspaces of `PauliZ` with $+1$ and $-1$ as the corresponding eigenvalues. The measurement returns `Zero` if the measured state is projected to the space with an eigenvalue of $+1$, and a result of `One` if projected to the space with an eigenvalue of $-1$.

To solidify this intuition, let us see what happens to various two-qubit states after joint measurement. The `PauliZ` matrix for two qubits is denoted by $Z \otimes Z$:

$$Z \otimes Z = \begin{bmatrix}
    1 & 0 & 0 & 0 \\
    0 & -1 & 0 & 0 \\
    0 & 0 & -1 & 0 \\
    0 & 0 & 0 & 1 \\
\end{bmatrix}$$

When this transformation is applied to a basis state $|00\rangle$, we get

$$\begin{bmatrix}
    1 & 0 & 0 & 0 \\
    0 & -1 & 0 & 0 \\
    0 & 0 & -1 & 0 \\
    0 & 0 & 0 & 1 \\
\end{bmatrix}
\begin{bmatrix} 1 \\ 0 \\ 0 \\ 0 \\ \end{bmatrix} =
\begin{bmatrix} 1 \\ 0 \\ 0 \\ 0 \\ \end{bmatrix}$$

Comparing this to the characteristic equation for eigenvectors of $Z \otimes Z$ given by
$ Z \otimes Z |\psi\rangle = \lambda |\psi\rangle$,
it is easy to see that $|00\rangle$ belongs to the $+1$ eigenspace, hence the $Z \otimes Z$ measurement will return `Zero` and leave the state unchanged.

Similarly, it can easily be verified that $|11\rangle$ also belongs to $+1$ eigenspace, while $|01\rangle$ and $|10\rangle$ belong to the $-1$ eigenspace.

Now, what happens if we apply a $Z \otimes Z$ measurement to a superposition state $\alpha |00\rangle + \beta |11\rangle$? We can see that

$$\begin{bmatrix}
    1 & 0 & 0 & 0 \\
    0 & -1 & 0 & 0 \\
    0 & 0 & -1 & 0 \\
    0 & 0 & 0 & 1 \\
\end{bmatrix}
\begin{bmatrix} \alpha \\ 0 \\ 0 \\ \beta \\ \end{bmatrix} =
\begin{bmatrix} \alpha \\ 0 \\ 0 \\ \beta \\ \end{bmatrix}$$

So this state also belongs to the $+1$ eigenspace, and measuring it will return `Zero` and leave the state unchanged. Similarly, we can verify that an $\alpha |01\rangle + \beta |10\rangle$ state belongs to the $-1$ eigenspace, and measuring it will return `One` without changing the state.

Hence, based on the return value from a `PauliZ` joint measurement, we can recognize which of the superposition states we were given while preserving the initial superposition state.

In Q#, the operation [`Measure`](https://docs.microsoft.com/en-us/qsharp/api/qsharp/microsoft.quantum.intrinsic.measure) can be used to measure multiple qubits using an array of [Pauli](https://docs.microsoft.com/en-us/quantum/user-guide/language/types?#primitive-types) constants that define the basis for measurement.

(This solution can also be used in task 1.)

```qsharp
%kata T5_ParityMeasurement

operation ParityMeasurement (qs : Qubit[]) : Int {
    return Measure([PauliZ, PauliZ], qs) == Zero ? 0 | 1;
}
```

[Return to exercise 9 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-9:-Two-qubit-parity-Measurement)

### Exercise 10: Parity measurement in different basis

Consider a system which is in a state $\alpha |00\rangle + \beta |01\rangle + \beta |10\rangle + \alpha |11\rangle$.

What are the possible outcomes and their associated probabilities, if a measurement in an $XX$ Pauli measurement is done?

## Solution

The first step towards identifying the outcomes and their probabilities for joint measurements is to identify the eigenvectors corresponding to eigenvalues $\pm1$ of the Pauli operator. We note that since $X\ket{\pm}= \pm\ket{\pm}$, we have
\begin{align}
XX \ket{++} &= \ket{++},& XX \ket{--} = \ket{--};\\
XX \ket{+-} &= -\ket{+-},& XX \ket{-+} = -\ket{-+}.
\end{align}
Thus, the $XX$ operator measures the parity in the Hadamard, or the $\ket{\pm}$ basis (i.e. it distinguishes basis states with an even number of $+$'s, from basis states which have an odd number of $+$'s).

To elaborate, the projector corresponding to a result of `Zero` is given by $P_{+1} = \ket{++}\bra{++} + \ket{--}\bra{--}$, while the projector corresponding to a result of `One` is given by $P_{-1} = \ket{+-}\bra{+-} + \ket{-+}\bra{-+}$. Then, we note that $P_{+1}$ annihilates states with odd parity, while leaving states with even parity unaffected. That is,
\begin{align}
P_{+1} ( \gamma \ket{++} + \delta \ket{--} ) &= ( \gamma \ket{++} + \delta \ket{--} )\\
P_{+1} ( \mu \ket{-+} + \nu \ket{+-} ) &= 0,
\end{align}
for arbitrary values of the constants. Similarly, $P_{-1}$ annihilates states with even parity, while leaving states with odd parity unaffected.

Now we express the given state in the Hadamard basis. We note that it is possible to go from the computational basis to the Hadamard basis using the following relations
$$
\ket{0} = \frac{1}{\sqrt{2}} \left( \ket{+} + \ket{-} \right)\\
\ket{1} = \frac{1}{\sqrt{2}} \left( \ket{+} - \ket{-} \right).
$$
Using these, we obtain
$$ \alpha |00\rangle + \beta |01\rangle + \beta |10\rangle + \alpha |11\rangle = (\alpha + \beta) |++\rangle + (\alpha - \beta) |--\rangle.$$
Thus, this state has an even parity in the Hadamard basis. It follows that an $XX$ Pauli measurement will result in the outcome `Zero` with probability 1, leaving the state unchanged after the measurement.

[Return to exercise 10 of the Multi-Qubit System Measurements tutorial.](./MultiQubitSystemMeasurements.ipynb#Exercise-10:-Parity-measurement-in-different-basis)


# 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, and Leo Werneck
### 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).

**Notebook Status:**  Validated

**Validation Notes:** 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); ([**NRPy+ Tutorial module reviewing mathematical formulation and equations solved**](Tutorial-ADM_Initial_Data-TOV.ipynb)); ([**start-to-finish NRPy+ Tutorial module demonstrating that initial data satisfy Hamiltonian constraint**](Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.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 set up 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 as follows, with links to the relevant NRPy+ tutorial notebooks listed at each step:

1. Allocate memory for gridfunctions, including temporary storage for the Method of Lines time integration [(**NRPy+ tutorial on NRPy+ Method of Lines algorithm**)](Tutorial-Method_of_Lines-C_Code_Generation.ipynb).
1. Set gridfunction values to initial data
    * [**NRPy+ tutorial on TOV initial data**](Tutorial-ADM_Initial_Data-TOV.ipynb)
    * [**NRPy+ tutorial on validating TOV initial data**](Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.ipynb).
1. Evaluate the Hamiltonian constraint violation
    * [**NRPy+ tutorial on BSSN constraints**](Tutorial-BSSN_constraints.ipynb)
1. Repeat above steps at two numerical resolutions to confirm convergence of Hamiltonian constraint violation to zero.

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

This notebook 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): `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 notebook to $\LaTeX$-formatted PDF file

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

```python
# Step P1: Import needed NRPy+ core modules:
from outputC import lhrh,outCfunction,outputC  # NRPy+: Core C code output module
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import finite_difference as fin  # NRPy+: Finite difference C code generation module
import grid as gri               # NRPy+: Functions having to do with numerical grids
import indexedexp as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import reference_metric as rfm   # NRPy+: Reference metric support
import cmdline_helper as cmd     # NRPy+: Multi-platform Python command-line interface
import shutil, os, sys           # Standard Python modules for multiplatform OS-level functions

# Step P2: Create C code output directory:
Ccodesdir = os.path.join("TOVID_Ccodes/")
# First remove C code output directory if it exists
# Courtesy https://stackoverflow.com/questions/303200/how-do-i-remove-delete-a-folder-that-is-not-empty
# !rm -r ScalarWaveCurvilinear_Playground_Ccodes
shutil.rmtree(Ccodesdir, ignore_errors=True)
# Then create a fresh directory
cmd.mkdir(Ccodesdir)

# Step P3: Create executable output directory:
outdir = os.path.join(Ccodesdir,"output/")
cmd.mkdir(outdir)

# Step 1: Set the spatial dimension parameter
#         to three this time, and then read
#         the parameter as DIM.
par.set_parval_from_str("grid::DIM",3)
DIM = par.parval_from_str("grid::DIM")

# Step 2: Set some core parameters, including CoordSystem MoL timestepping algorithm,
#                                 FD order, floating point precision, and CFL factor:
# Choices are: Spherical, SinhSpherical, SinhSphericalv2, Cylindrical, SinhCylindrical,
#              SymTP, SinhSymTP
CoordSystem     = "Spherical"

# Step 2.a: Set defaults for Coordinate system parameters.
#           These are perhaps the most commonly adjusted parameters,
#           so we enable modifications at this high level.

# domain_size     = 7.5 # SET BELOW BASED ON TOV STELLAR RADIUS

# sinh_width sets the default value for:
#   * SinhSpherical's params.SINHW
#   * SinhCylindrical's params.SINHW{RHO,Z}
#   * SinhSymTP's params.SINHWAA
sinh_width      = 0.4 # If Sinh* coordinates chosen

# sinhv2_const_dr sets the default value for:
#   * SinhSphericalv2's params.const_dr
#   * SinhCylindricalv2's params.const_d{rho,z}
sinhv2_const_dr = 0.05# If Sinh*v2 coordinates chosen

# SymTP_bScale sets the default value for:
#   * SinhSymTP's params.bScale
SymTP_bScale    = 0.5 # If SymTP chosen

# Step 2.b: Set the order of spatial finite difference derivatives;
#           and the core data type.
FD_order  = 4        # Finite difference order: even numbers only, starting with 2. 12 is generally unstable
REAL      = "double" # Best to use double here.

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

# Step 4: Set the finite differencing order to FD_order (set above).
par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER", FD_order)

# Step 5: Set the direction=2 (phi) axis to be the symmetry axis; i.e.,
#         axis "2", corresponding to the i2 direction.
#         This sets all spatial derivatives in the phi direction to zero.
par.set_parval_from_str("indexedexp::symmetry_axes","2")

# Step 6: The MoLtimestepping interface is only used for memory allocation/deallocation
import MoLtimestepping.C_Code_Generation as MoL
from MoLtimestepping.RK_Butcher_Table_Dictionary import Butcher_dict
RK_method = "Euler" # DOES NOT MATTER; Again MoL interface is only used for memory alloc/dealloc.
RK_order  = Butcher_dict[RK_method][1]
cmd.mkdir(os.path.join(Ccodesdir,"MoLtimestepping/"))
MoL.MoL_C_Code_Generation(RK_method, RHS_string      = "", post_RHS_string = "",
    outdir = os.path.join(Ccodesdir,"MoLtimestepping/"))

# Step 7: Polytropic EOS setup
# For EOS_type, choose either "SimplePolytrope" or "PiecewisePolytrope"
EOS_type = "SimplePolytrope"
# If "PiecewisePolytrope" is chosen as EOS_type, you
#   must also choose the name of the EOS, which can
#   be any of the following:
# 'PAL6', 'SLy', 'APR1', 'APR2', 'APR3', 'APR4',
# 'FPS', 'WFF1', 'WFF2', 'WFF3', 'BBB2', 'BPAL12',
# 'ENG', 'MPA1', 'MS1', 'MS2', 'MS1b', 'PS', 'GS1',
# 'GS2', 'BGN1H1', 'GNH3', 'H1', 'H2', 'H3', 'H4',
# 'H5', 'H6', 'H7', 'PCL2', 'ALF1', 'ALF2', 'ALF3',
# 'ALF4'
EOS_name = 'SLy' # <-- IGNORED IF EOS_type is not PiecewisePolytrope.
```

# 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
##########################
# Polytropic EOS example #
##########################
import TOV.Polytropic_EOSs as ppeos

if EOS_type == "SimplePolytrope":
    # Set neos = 1 (single polytrope)
    neos = 1

    # Set rho_poly_tab (not needed for a single polytrope)
    rho_poly_tab = []

    # Set Gamma_poly_tab
    Gamma_poly_tab = [2.0]

    # Set K_poly_tab0
    K_poly_tab0 = 1. # ZACH NOTES: CHANGED FROM 100.

    # Set the eos quantities
    eos = ppeos.set_up_EOS_parameters__complete_set_of_input_variables(neos,rho_poly_tab,Gamma_poly_tab,K_poly_tab0)
    rho_baryon_central = 0.129285
elif EOS_type == "PiecewisePolytrope":
    eos = ppeos.set_up_EOS_parameters__Read_et_al_input_variables(EOS_name)
    rho_baryon_central=2.0
else:
    print("""Error: unknown EOS_type. Valid types are 'SimplePolytrope' and 'PiecewisePolytrope' """)
    sys.exit(1)

import TOV.TOV_Solver as TOV
M_TOV, R_Schw_TOV, R_iso_TOV = TOV.TOV_Solver(eos,
                                              outfile="outputTOVpolytrope.txt",
                                              rho_baryon_central=rho_baryon_central,
                                              return_M_RSchw_and_Riso = True,
                                              verbose = True)

# domain_size sets the default value for:
#   * Spherical's params.RMAX
#   * SinhSpherical*'s params.AMAX
#   * Cartesians*'s -params.{x,y,z}min & .{x,y,z}max
#   * Cylindrical's -params.ZMIN & .{Z,RHO}MAX
#   * SinhCylindrical's params.AMPL{RHO,Z}
#   * *SymTP's params.AMAX
domain_size = 2.0 * R_iso_TOV
```

    1256 1256 1256 1256 1256 1256
    Just generated a TOV star with
    * M        = 1.405030336771405e-01 ,
    * R_Schw   = 9.566044579232513e-01 ,
    * R_iso    = 8.100085557410308e-01 ,
    * M/R_Schw = 1.468768334847266e-01

## Step 2.a: Interpolate the TOV data file as needed to set up ADM spacetime quantities in spherical basis (for input into the `Converting_Numerical_ADM_Spherical_or_Cartesian_to_BSSNCurvilinear` module) and $T^{\mu\nu}$ in the chosen reference metric basis \[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 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}

```python
thismodule = "TOVID"
rbar,theta,rho,P,expnu,exp4phi = par.Cparameters("REAL",thismodule,
                                                 ["rbar","theta","rho","P","expnu","exp4phi"],1e300)
IDalpha = sp.sqrt(expnu)
gammaSphDD = ixp.zerorank2(DIM=3)
gammaSphDD[0][0] = exp4phi
gammaSphDD[1][1] = exp4phi*rbar**2
gammaSphDD[2][2] = exp4phi*rbar**2*sp.sin(theta)**2

T4SphUU = ixp.zerorank2(DIM=4)
T4SphUU[0][0] = rho/expnu
T4SphUU[1][1] = P/exp4phi
T4SphUU[2][2] = P/(exp4phi*rbar**2)
T4SphUU[3][3] = P/(exp4phi*rbar**2*sp.sin(theta)**2)
```

```python
expr_list = [IDalpha]
name_list = ["*alpha"]
for i in range(3):
    for j in range(i,3):
        expr_list.append(gammaSphDD[i][j])
        name_list.append("*gammaDD"+str(i)+str(j))

desc = """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."""
name = "ID_TOV_ADM_quantities"
outCparams = "preindent=1,outCverbose=False,includebraces=False"

outCfunction(
    outfile=os.path.join(Ccodesdir, name + ".h"), desc=desc, name=name,
    params="""  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""",
    body="""
      // Set trivial metric quantities:
      *KDD00 = *KDD01 = *KDD02 = 0.0;
      /**/     *KDD11 = *KDD12 = 0.0;
      /**/              *KDD22 = 0.0;
      *betaU0 = *betaU1 = *betaU2 = 0.0;
      *BU0 = *BU1 = *BU2 = 0.0;

      // Next set gamma_{ij} in spherical basis
      const REAL rbar  = xyz_or_rthph[0];
      const REAL theta = xyz_or_rthph[1];
      const REAL phi   = xyz_or_rthph[2];

      REAL rho,rho_baryon,P,M,expnu,exp4phi;
      TOV_interpolate_1D(rbar,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.rho_baryon_arr,other_inputs.P_arr,other_inputs.M_arr,
                         other_inputs.expnu_arr,other_inputs.exp4phi_arr,other_inputs.rbar_arr,
                         &rho,&rho_baryon,&P,&M,&expnu,&exp4phi);\n"""+
    outputC(expr_list,name_list, "returnstring",outCparams),
    opts="DisableCparameters")
```

    Output C function ID_TOV_ADM_quantities() to file TOVID_Ccodes/ID_TOV_ADM_quantities.h

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).

$$
{\rm Jac\_dUSph\_dDrfmUD[mu][nu]} = \frac{\partial x^\mu_{\rm Sph}}{\partial x^\nu_{\rm rfm}},
$$

via exact differentiation (courtesy SymPy), and the inverse Jacobian
$$
{\rm Jac\_dUrfm\_dDSphUD[mu][nu]} = \frac{\partial x^\mu_{\rm rfm}}{\partial x^\nu_{\rm Sph}},
$$

using NRPy+'s `generic_matrix_inverter3x3()` function. In terms of these, the transformation of BSSN tensors from Spherical to `"reference_metric::CoordSystem"` coordinates may be written:

$$
T^{\mu\nu}_{\rm rfm} =
\frac{\partial x^\mu_{\rm rfm}}{\partial x^\delta_{\rm Sph}}
\frac{\partial x^\nu_{\rm rfm}}{\partial x^\sigma_{\rm Sph}} T^{\delta\sigma}_{\rm Sph}
$$

```python
r_th_ph_or_Cart_xyz_oID_xx = []
CoordType_in = "Spherical"
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.

Jac4_dUSphorCart_dDrfmUD = ixp.zerorank2(DIM=4)
Jac4_dUSphorCart_dDrfmUD[0][0] = sp.sympify(1)
for i in range(DIM):
    for j in range(DIM):
        Jac4_dUSphorCart_dDrfmUD[i+1][j+1] = sp.diff(r_th_ph_or_Cart_xyz_oID_xx[i],rfm.xx[j])

Jac4_dUrfm_dDSphorCartUD, dummyDET = ixp.generic_matrix_inverter4x4(Jac4_dUSphorCart_dDrfmUD)

# Perform Jacobian operations on T^{mu nu} and gamma_{ij}
T4UU = ixp.register_gridfunctions_for_single_rank2("AUXEVOL","T4UU","sym01",DIM=4)

IDT4UU = ixp.zerorank2(DIM=4)
for mu in range(4):
    for nu in range(4):
        for delta in range(4):
            for sigma in range(4):
                IDT4UU[mu][nu] += \
                     Jac4_dUrfm_dDSphorCartUD[mu][delta]*Jac4_dUrfm_dDSphorCartUD[nu][sigma]*T4SphUU[delta][sigma]

lhrh_list = []
for mu in range(4):
    for nu in range(mu,4):
        lhrh_list.append(lhrh(lhs=gri.gfaccess("auxevol_gfs","T4UU"+str(mu)+str(nu)),rhs=IDT4UU[mu][nu]))

desc = """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."""
name = "ID_TOV_TUPMUNU_xx0xx1xx2"
outCparams = "preindent=1,outCverbose=False,includebraces=False"
outCfunction(
    outfile=os.path.join(Ccodesdir, name + ".h"), desc=desc, name=name,
    params="""const paramstruct *restrict params,REAL *restrict xx[3],
              const ID_inputs other_inputs,REAL *restrict auxevol_gfs""",
    body=outputC([rfm.xxSph[0],rfm.xxSph[1],rfm.xxSph[2]],
                 ["const REAL rbar","const REAL theta","const REAL ph"],"returnstring",
                 "CSE_enable=False,includebraces=False")+"""
      REAL rho,rho_baryon,P,M,expnu,exp4phi;
      TOV_interpolate_1D(rbar,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.rho_baryon_arr,other_inputs.P_arr,other_inputs.M_arr,
                         other_inputs.expnu_arr,other_inputs.exp4phi_arr,other_inputs.rbar_arr,
                         &rho,&rho_baryon,&P,&M,&expnu,&exp4phi);\n"""+
    fin.FD_outputC("returnstring",lhrh_list,params="outCverbose=False,includebraces=False").replace("IDX4","IDX4S"),
    loopopts="AllPoints,Read_xxs")
```

    Output C function ID_TOV_TUPMUNU_xx0xx1xx2() to file TOVID_Ccodes/ID_TOV_TUPMUNU_xx0xx1xx2.h

# Step 3: Convert ADM initial data to BSSN-in-curvilinear coordinates \[Back to [top](#toc)\]
$$\label{adm_id_spacetime}$$

This is an automated process, taken care of by [`BSSN.ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear`](../edit/BSSN.ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py), and documented [in this tutorial notebook](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",
                                                               Ccodesdir=Ccodesdir,loopopts="")
```

    Output C function ID_BSSN_lambdas() to file TOVID_Ccodes/ID_BSSN_lambdas.h
    Output C function ID_ADM_xx0xx1xx2_to_BSSN_xx0xx1xx2__ALL_BUT_LAMBDAs() to file TOVID_Ccodes/ID_ADM_xx0xx1xx2_to_BSSN_xx0xx1xx2__ALL_BUT_LAMBDAs.h
    Output C function ID_BSSN__ALL_BUT_LAMBDAs() to file TOVID_Ccodes/ID_BSSN__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 notebook](Tutorial-BSSN_constraints.ipynb)

```python
# Enable rfm_precompute infrastructure, which results in
#   BSSN RHSs that are free of transcendental functions,
#   even in curvilinear coordinates, so long as
#   ConformalFactor is set to "W" (default).
cmd.mkdir(os.path.join(Ccodesdir,"rfm_files/"))
par.set_parval_from_str("reference_metric::enable_rfm_precompute","True")
par.set_parval_from_str("reference_metric::rfm_precompute_Ccode_outdir",os.path.join(Ccodesdir,"rfm_files/"))

import BSSN.Enforce_Detgammabar_Constraint as EGC
enforce_detg_constraint_symb_expressions = EGC.Enforce_Detgammabar_Constraint_symb_expressions()

# Now register the Hamiltonian as a gridfunction.
H = gri.register_gridfunctions("AUX","H")
# Then define the Hamiltonian constraint and output the optimized C code.
import BSSN.BSSN_constraints as bssncon
import BSSN.BSSN_stress_energy_source_terms as Bsest
bssncon.BSSN_constraints(add_T4UUmunu_source_terms=False)
Bsest.BSSN_source_terms_for_BSSN_constraints(T4UU)
bssncon.H += Bsest.sourceterm_H

# Now that we are finished with all the rfm hatted
#           quantities in generic precomputed functional
#           form, let's restore them to their closed-
#           form expressions.
par.set_parval_from_str("reference_metric::enable_rfm_precompute","False") # Reset to False to disable rfm_precompute.
rfm.ref_metric__hatted_quantities()

desc="Evaluate the Hamiltonian constraint"
name="Hamiltonian_constraint"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   = """rfm_struct *restrict rfmstruct,const paramstruct *restrict params,
                  REAL *restrict in_gfs, REAL *restrict auxevol_gfs, REAL *restrict aux_gfs""",
    body     = fin.FD_outputC("returnstring",lhrh(lhs=gri.gfaccess("aux_gfs", "H"), rhs=bssncon.H),
                              params="outCverbose=False").replace("IDX4","IDX4S"),
    loopopts = "InteriorPoints,Enable_rfm_precompute")
```

    Output C function Hamiltonian_constraint() to file TOVID_Ccodes/Hamiltonian_constraint.h

## Step 4.b: Set up boundary condition functions for chosen singular, curvilinear coordinate system \[Back to [top](#toc)\]
$$\label{bc_functs}$$

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

```python
import CurviBoundaryConditions.CurviBoundaryConditions as cbcs
cbcs.Set_up_CurviBoundaryConditions(os.path.join(Ccodesdir,"boundary_conditions/"),Cparamspath=os.path.join("../"))
```

    Wrote to file "TOVID_Ccodes/boundary_conditions/parity_conditions_symbolic_dot_products.h"
    Evolved parity: ( aDD00:4, aDD01:5, aDD02:6, aDD11:7, aDD12:8, aDD22:9,
        alpha:0, betU0:1, betU1:2, betU2:3, cf:0, hDD00:4, hDD01:5, hDD02:6,
        hDD11:7, hDD12:8, hDD22:9, lambdaU0:1, lambdaU1:2, lambdaU2:3, trK:0,
        vetU0:1, vetU1:2, vetU2:3 )
    Auxiliary parity: ( H:0 )
    AuxEvol parity: ( T4UU00:0, T4UU01:1, T4UU02:2, T4UU03:3, T4UU11:4,
        T4UU12:5, T4UU13:6, T4UU22:7, T4UU23:8, T4UU33:9 )
    Wrote to file "TOVID_Ccodes/boundary_conditions/EigenCoord_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 notebook](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
# Set up the C function for the det(gammahat) = det(gammabar)
EGC.output_Enforce_Detgammabar_Constraint_Ccode(Ccodesdir,
                                                exprs=enforce_detg_constraint_symb_expressions)
```

    Output C function enforce_detgammabar_constraint() to file TOVID_Ccodes/enforce_detgammabar_constraint.h

## Step 4.d: Output C codes needed for declaring and setting Cparameters; also set `free_parameters.h` \[Back to [top](#toc)\]
$$\label{cparams_rfm_and_domainsize}$$

Based on declared NRPy+ Cparameters, first we generate `declare_Cparameters_struct.h`, `set_Cparameters_default.h`, and `set_Cparameters[-SIMD].h`.

Then we output `free_parameters.h`, which sets initial data parameters, as well as grid domain & reference metric parameters, applying `domain_size` and `sinh_width`/`SymTP_bScale` (if applicable) as set above

```python
# Step 3.d.i: Generate declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
par.generate_Cparameters_Ccodes(os.path.join(Ccodesdir))

# Step 3.d.ii: Set free_parameters.h
# Output to $Ccodesdir/free_parameters.h reference metric parameters based on generic
#    domain_size,sinh_width,sinhv2_const_dr,SymTP_bScale,
#    parameters set above.
rfm.out_default_free_parameters_for_rfm(os.path.join(Ccodesdir,"free_parameters.h"),
                                        domain_size,sinh_width,sinhv2_const_dr,SymTP_bScale)

# Step 3.d.iii: Generate set_Nxx_dxx_invdx_params__and__xx.h:
rfm.set_Nxx_dxx_invdx_params__and__xx_h(Ccodesdir)

# Step 3.d.iv: Generate xxCart.h, which contains xxCart() for
#               (the mapping from xx->Cartesian) for the chosen
#               CoordSystem:
rfm.xxCart_h("xxCart","./set_Cparameters.h",os.path.join(Ccodesdir,"xxCart.h"))

# Step 3.d.v: Generate declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
par.generate_Cparameters_Ccodes(os.path.join(Ccodesdir))
```

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

```python
# Part P0: Define REAL, set the number of ghost cells NGHOSTS (from NRPy+'s FD_CENTDERIVS_ORDER)

with open(os.path.join(Ccodesdir,"TOV_Playground_REAL__NGHOSTS.h"), "w") as file:
    file.write("""
// Part P0.a: Set the number of ghost cells, from NRPy+'s FD_CENTDERIVS_ORDER
#define NGHOSTS """+str(int(FD_order/2)+1)+"""
// Part P0.b: Set the numerical precision (REAL) to double, ensuring all floating point
//            numbers are stored to at least ~16 significant digits
#define REAL """+REAL+"""
// Part P0.c: Set TOV stellar parameters
#define TOV_Mass  """+str(M_TOV)+"""
#define TOV_Riso  """+str(R_iso_TOV)+"\n")
```

```python
%%writefile $Ccodesdir/TOV_Playground.c

// Step P0: Define REAL and NGHOSTS. This header is generated by NRPy+.
#include "TOV_Playground_REAL__NGHOSTS.h"

#include "rfm_files/rfm_struct__declare.h"

#include "declare_Cparameters_struct.h"

// Step P1: Import needed header files
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#ifndef M_PI
#define M_PI 3.141592653589793238462643383279502884L
#endif
#ifndef M_SQRT1_2
#define M_SQRT1_2 0.707106781186547524400844362104849039L
#endif

// Step P2: Declare the IDX4S(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_2NGHOSTS0 elements in memory. Similarly, consecutive values of
//           "k" are separated by Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1 in memory, etc.
#define IDX4S(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) + Nxx_plus_2NGHOSTS2 * (g) ) ) )
#define IDX4ptS(g,idx) ( (idx) + (Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2) * (g) )
#define IDX3S(i,j,k) ( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) ) ) )
#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++)
#define LOOP_ALL_GFS_GPS(ii) _Pragma("omp parallel for") \
  for(int (ii)=0;(ii)<Nxx_plus_2NGHOSTS_tot*NUM_EVOL_GFS;(ii)++)

// Step P3: Set UUGF and VVGF macros, as well as xxCart()
#include "boundary_conditions/gridfunction_defines.h"

// Step P4: Set xxCart(const paramstruct *restrict params,
//                     REAL *restrict xx[3],
//                     const int i0,const int i1,const int i2,
//                     REAL xCart[3]),
//           which maps xx->Cartesian via
//    {xx[0][i0],xx[1][i1],xx[2][i2]}->{xCart[0],xCart[1],xCart[2]}
#include "xxCart.h"

// Step P5: Defines set_Nxx_dxx_invdx_params__and__xx(const int EigenCoord, const int Nxx[3],
//                                       paramstruct *restrict params, REAL *restrict xx[3]),
//          which sets params Nxx,Nxx_plus_2NGHOSTS,dxx,invdx, and xx[] for
//          the chosen Eigen-CoordSystem if EigenCoord==1, or
//          CoordSystem if EigenCoord==0.
#include "set_Nxx_dxx_invdx_params__and__xx.h"

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

// Step P8: Include function for enforcing detgammabar constraint.
#include "enforce_detgammabar_constraint.h"

// Step P4: Declare initial data input struct:
//          stores data from initial data solver,
//          so they can be put on the numerical grid.
typedef struct __ID_inputs {
    REAL Rbar;
    int Rbar_idx;
    int interp_stencil_size;
    int numlines_in_file;
    REAL *r_Schw_arr,*rho_arr,*rho_baryon_arr,*P_arr,*M_arr,*expnu_arr,*exp4phi_arr,*rbar_arr;
} ID_inputs;

// Part P11: Declare all functions for setting up TOV 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_xx0xx1xx2.h"

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

// Step P10: Declare function necessary for setting up the initial data.
// Step P10.a: Define BSSN_ID() for BrillLindquist initial data

// Step P10.b: Set the generic driver function for setting up BSSN initial data
void initial_data(const paramstruct *restrict params,const bc_struct *restrict bcstruct,
                  const rfm_struct *restrict rfmstruct,
                  REAL *restrict xx[3], REAL *restrict auxevol_gfs, REAL *restrict in_gfs) {
#include "set_Cparameters.h"
        // Step 1: Set up TOV initial data
    // Step 1.a: Read TOV initial data from data file
    // 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 *rho_baryon_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
    // read_datafile__set_arrays() may be found in TOV/tov_interp.h
    if(read_datafile__set_arrays(in1Dpolytrope, r_Schw_arr,rho_arr,rho_baryon_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.rho_baryon_arr = rho_baryon_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 1.b: Interpolate data from data file to set BSSN gridfunctions
    ID_BSSN__ALL_BUT_LAMBDAs(params,xx,TOV_in, in_gfs);
    apply_bcs_curvilinear(params, bcstruct, NUM_EVOL_GFS, evol_gf_parity, in_gfs);
    enforce_detgammabar_constraint(rfmstruct, params,                   in_gfs);
    ID_BSSN_lambdas(params, xx, in_gfs);
    apply_bcs_curvilinear(params, bcstruct, NUM_EVOL_GFS, evol_gf_parity, in_gfs);
    enforce_detgammabar_constraint(rfmstruct, params,                   in_gfs);

    ID_TOV_TUPMUNU_xx0xx1xx2(params,xx,TOV_in,auxevol_gfs);

    free(rbar_arr);
    free(rho_arr);
    free(rho_baryon_arr);
    free(P_arr);
    free(M_arr);
    free(expnu_arr);
}

// Step P11: Declare function for evaluating Hamiltonian constraint (diagnostic)
#include "Hamiltonian_constraint.h"

// main() function:
// Step 0: Read command-line input, set up grid structure, allocate memory for gridfunctions, set up coordinates
// Step 1: Set up initial data to an exact solution
// Step 2: Start the timer, for keeping track of how fast the simulation is progressing.
// Step 3: Integrate the initial data forward in time using the chosen RK-like Method of
//         Lines timestepping algorithm, and output periodic simulation diagnostics
// Step 3.a: Output 2D data file periodically, for visualization
// Step 3.b: Step forward one timestep (t -> t+dt) in time using
//           chosen RK-like MoL timestepping algorithm
// Step 3.c: If t=t_final, output conformal factor & Hamiltonian
//           constraint violation to 2D data file
// Step 3.d: Progress indicator printing to stderr
// Step 4: Free all allocated memory
int main(int argc, const char *argv[]) {
    paramstruct params;
#include "set_Cparameters_default.h"

    // Step 0a: Read command-line input, error out if nonconformant
    if((argc != 4) || atoi(argv[1]) < NGHOSTS || atoi(argv[2]) < NGHOSTS || atoi(argv[3]) < 2 /* FIXME; allow for axisymmetric sims */) {
        fprintf(stderr,"Error: Expected three command-line arguments: ./BrillLindquist_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...
    const int Nxx[3] = { atoi(argv[1]), atoi(argv[2]), atoi(argv[3]) };
    if(Nxx[0]%2 != 0 || Nxx[1]%2 != 0 || Nxx[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);
    }

    // Step 0c: Set free parameters, overwriting Cparameters defaults
    //          by hand or with command-line input, as desired.
#include "free_parameters.h"

   // Step 0d: Uniform coordinate grids are stored to *xx[3]
    REAL *xx[3];
    // Step 0d.i: Set bcstruct
    bc_struct bcstruct;
    {
        int EigenCoord = 1;
        // Step 0d.ii: Call set_Nxx_dxx_invdx_params__and__xx(), which sets
        //             params Nxx,Nxx_plus_2NGHOSTS,dxx,invdx, and xx[] for the
        //             chosen Eigen-CoordSystem.
        set_Nxx_dxx_invdx_params__and__xx(EigenCoord, Nxx, &params, xx);
        // Step 0d.iii: Set Nxx_plus_2NGHOSTS_tot
#include "set_Cparameters-nopointer.h"
        const int Nxx_plus_2NGHOSTS_tot = Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2;
        // Step 0e: Find ghostzone mappings; set up bcstruct
#include "boundary_conditions/driver_bcstruct.h"
        // Step 0e.i: Free allocated space for xx[][] array
        for(int i=0;i<3;i++) free(xx[i]);
    }

    // Step 0f: Call set_Nxx_dxx_invdx_params__and__xx(), which sets
    //          params Nxx,Nxx_plus_2NGHOSTS,dxx,invdx, and xx[] for the
    //          chosen (non-Eigen) CoordSystem.
    int EigenCoord = 0;
    set_Nxx_dxx_invdx_params__and__xx(EigenCoord, Nxx, &params, xx);

    // Step 0g: Set all C parameters "blah" for params.blah, including
    //          Nxx_plus_2NGHOSTS0 = params.Nxx_plus_2NGHOSTS0, etc.
#include "set_Cparameters-nopointer.h"
    const int Nxx_plus_2NGHOSTS_tot = Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2;

    // Step 0j: Error out if the number of auxiliary gridfunctions outnumber evolved gridfunctions.
    //              This is a limitation of the RK method. You are always welcome to declare & allocate
    //              additional gridfunctions by hand.
    if(NUM_AUX_GFS > NUM_EVOL_GFS) {
        fprintf(stderr,"Error: NUM_AUX_GFS > NUM_EVOL_GFS. Either reduce the number of auxiliary gridfunctions,\n");
        fprintf(stderr,"       or allocate (malloc) by hand storage for *diagnostic_output_gfs. \n");
        exit(1);
    }

    // Step 0k: Allocate memory for gridfunctions
#include "MoLtimestepping/RK_Allocate_Memory.h"
    REAL *restrict auxevol_gfs = (REAL *)malloc(sizeof(REAL) * NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS_tot);

    // Step 0l: Set up precomputed reference metric arrays
    // Step 0l.i: Allocate space for precomputed reference metric arrays.
#include "rfm_files/rfm_struct__malloc.h"

    // Step 0l.ii: Define precomputed reference metric arrays.
    {
    #include "set_Cparameters-nopointer.h"
    #include "rfm_files/rfm_struct__define.h"
    }

    // Step 1: Set up initial data to an exact solution
    initial_data(&params,&bcstruct, &rfmstruct, xx, auxevol_gfs, y_n_gfs);

    // 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_curvilinear(&params, &bcstruct, NUM_EVOL_GFS,evol_gf_parity, y_n_gfs);
    enforce_detgammabar_constraint(&rfmstruct, &params, y_n_gfs);

    // Evaluate Hamiltonian constraint violation
    Hamiltonian_constraint(&rfmstruct, &params, y_n_gfs,auxevol_gfs, diagnostic_output_gfs);
    char filename[100];
    sprintf(filename,"out%d.txt",Nxx[0]);
    FILE *out2D = fopen(filename, "w");
    LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS0-NGHOSTS,
                NGHOSTS,Nxx_plus_2NGHOSTS1-NGHOSTS,
                NGHOSTS,Nxx_plus_2NGHOSTS2-NGHOSTS) {
        REAL xx0 = xx[0][i0];
        REAL xx1 = xx[1][i1];
        REAL xx2 = xx[2][i2];
        REAL xCart[3];
        xxCart(&params,xx,i0,i1,i2,xCart);
        int idx = IDX3S(i0,i1,i2);
        fprintf(out2D,"%e %e %e %e\n",xCart[1]/TOV_Mass,xCart[2]/TOV_Mass, y_n_gfs[IDX4ptS(CFGF,idx)],
                log10(fabs(diagnostic_output_gfs[IDX4ptS(HGF,idx)])));
    }
    fclose(out2D);

    // Step 4: Free all allocated memory
#include "rfm_files/rfm_struct__freemem.h"
#include "boundary_conditions/bcstruct_freemem.h"
#include "MoLtimestepping/RK_Free_Memory.h"
    free(auxevol_gfs);
    for(int i=0;i<3;i++) free(xx[i]);

    return 0;
}
```

    Writing TOVID_Ccodes//TOV_Playground.c

```python
import cmdline_helper as cmd
cmd.C_compile(os.path.join(Ccodesdir,"TOV_Playground.c"), "TOV_Playground")
cmd.delete_existing_files("out96.txt")
cmd.Execute("TOV_Playground", "96 96 2", "out96.txt")
```

    Compiling executable...
    (EXEC): Executing `gcc -Ofast -fopenmp -march=native -funroll-loops TOVID_Ccodes/TOV_Playground.c -o TOV_Playground -lm`...
    (BENCH): Finished executing in 1.614800214767456 seconds.
    Finished compilation.
    (EXEC): Executing `taskset -c 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 ./TOV_Playground 96 96 2`...
    (BENCH): Finished executing in 0.21306824684143066 seconds.

# 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 = 7.5
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("Neutron Star: log10( max(1e-6,Energy 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
# Now rerun TOV_Playground with twice lower resolution.
cmd.delete_existing_files("out48.txt")
cmd.Execute("TOV_Playground", "48 48 2", "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 notebook 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
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data")
```

    Created Tutorial-Start_to_Finish-BSSNCurvilinear-
        Setting_up_TOV_initial_data.tex, and compiled LaTeX file to PDF file
        Tutorial-Start_to_Finish-BSSNCurvilinear-
        Setting_up_TOV_initial_data.pdf


```python
import ipywidgets as widgets
from ipywidgets import BoundedFloatText, HBox, VBox, Button, Label, interactive
from IPython.display import display, clear_output, Javascript
import matplotlib.pyplot as plt
import numpy as np

% matplotlib inline
```

# Równanie stężenia przy wlewie dokomparmentowym

\begin{equation}
\Large
c(t) = \frac{q}{k \cdot V} (1 - e^{-kt})
\end{equation}

## Stała prędkość wlewu

$$
\Large
q = 1 ml / min
$$
## Dane zmierzone na pacjencie

$$
\begin{array}{c|r|l}
& \large t [min] & \large c [mg/ml] \\
\hline
1 & 10 & 0.0019 \\
2 & 30 & 0.0052 \\
3 & 120 & 0.0140 \\
\end{array}
$$

## Szukane wartości

$$
\Large
k, V = ?
$$

# Zadanie: zminimalizować funkcję błędu kwadratowego

## Błąd średnio kwadratowy

\begin{equation}
\Large
Q(k, V) = \sum_{i=1}^{3} (c(t_i, k, V) - c_i) ^ 2
\end{equation}

$$
\Large
\min_{k, V} Q = ?
$$

```python
def c_t(t, q, k, v):
    return q * (1 - np.exp(-k * t)) / (k * v)

def t_c(c, q, k, v):
    return np.log(1 - c * k * v / q) / -k

def Q(c_i, t_i, q, k, v):
    return np.sum((c_t(t_i, q, k, v) - c_i) ** 2)

t_i = np.array([10., 30., 120.])
c_i = np.array([.0019, .0052, .0140])

c = .0052
t = 30
q = 1
k = .01
v = 5000
```

```python
kw = BoundedFloatText(value=k, min=.001, step=.001, description='k')
vw = BoundedFloatText(value=v, min=1, max=1e4, description='V [ml]')

def f_k_v(kk, vv):
    global k, v
    k = kk
    v = vv
    qq = Q(c_i, t_i, q, k, v)

    lab = Label(f'$$ \large Q({k:.3f}, {v}) = {qq:.12f} $$')
    display(lab)
    display(Javascript('IPython.notebook.execute_cells_below()'))

interactive(f_k_v, kk=kw, vv=vw)
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

```python
x = np.arange(400)
qw = BoundedFloatText(value=q, min=.1, step=.1, description='q [ml/min]')

def f_q(qq):
    global q
    q = qq
    y = c_t(x, q, k, v)

    fig, ax = plt.subplots()
    ax.set_title('Stężenie od czasu')
    ax.set_xlabel('czas [min]')
    ax.set_ylabel('stężenie [mg/ml]')
    for i, j in zip(t_i, c_i):
        ax.scatter(i, j, label=f'$t={i} min$')
    ax.legend()
    ax.plot(x, y)
    plt.show()
    display(Javascript('IPython.notebook.execute_cells_below()'))

interactive(f_q, qq=qw)
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

# Wybierz czas, by sprawdzić, jakie będzie stężenie

```python
tw = BoundedFloatText(value=t, min=0, max=100, description='t [min]')

def f_t(tt):
    global c, t
    t = tt
    c = c_t(t, q, k, v)

    lab = Label(f'$$ \large c(t={t:.0f} \space min) = {c:.4f} \space mg/ml $$')
    display(lab)
    display(Javascript('IPython.notebook.execute_cells_below()'))

interactive(f_t, tt=tw)
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

# Sprawdź, po jakim czasie, będzie żądane stężenie

```python
cw = BoundedFloatText(value=c, step=.0001, description='c [mg/ml]')

def f_c(cc):
    global c, t
    c = cc
    t = t_c(c, q, k, v)

    lab = Label(f'$$ \large c(t={t:.0f} \space min) = {c:.4f} \space mg/ml $$')
    display(lab)
    display(Javascript('IPython.notebook.execute_cells_below()'))

interactive(f_c, cc=cw)
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

```python
print(f'c={c}, t={t}, q={q}, k={k}, V={v}')
```

    c=0.0051836355863656425, t=30.0, q=1.0, k=0.01, V=5000.0


# Encontrando a velocidade de crescimento populacional em uma cidade

(Dublin, Irland)

### Referências:

http://www.scielo.br/scielo.php?script=sci_arttext&pid=S1516-73132011000400010
http://www.mat.uc.pt/~sandra/Ensino0809/2.s/Matem%C3%A1tica%20II(AP)/AulasTeoricas/AulasT(09-03-06).pdf
http://www.uel.br/projetos/matessencial/superior/pdfs/crescpop.pdf

```python
#Packs
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
```

## Parte 1: Coleta dos dados

```python
df = pd.read_html(r'https://www.worldometers.info/world-population/ireland-population/')[1]

#Font1: https://www.worldometers.info/world-population/ireland-population/
#Font2: http://worldpopulationreview.com/world-cities/dublin-population/
#Font3: http://population.city/ireland/dublin/
```

```python
df.to_csv('irland.csv')
```

```python
df
```

      Year
      Population
      Yearly % Change
      Yearly Change
      Migrants (net)
      Median Age
      Fertility Rate
      Density (P/Km²)
      Urban Pop %
      Urban Population
      Country's Share of World Pop
      World Population
      IrelandGlobal Rank

      0
      2019
      4882495
      1.32 %
      63805
      23604
      36.8
      1.94
      71
      62.9 %
      3073341
      0.06 %
      7713468100
      124

      1
      2018
      4818690
      1.38 %
      65411
      23604
      36.8
      1.94
      70
      63.0 %
      3034528
      0.06 %
      7631091040
      124

      2
      2017
      4753279
      1.22 %
      57500
      23604
      36.8
      1.94
      69
      63.1 %
      2997322
      0.06 %
      7547858925
      121

      3
      2016
      4695779
      0.93 %
      43354
      23604
      36.8
      1.94
      68
      63.1 %
      2964988
      0.06 %
      7464022049
      121

      4
      2015
      4652425
      0.43 %
      19621
      -22364
      36.5
      1.97
      68
      63.2 %
      2939375
      0.06 %
      7379797139
      121

      5
      2010
      4554321
      1.92 %
      82620
      39457
      34.4
      2.00
      66
      62.5 %
      2847514
      0.07 %
      6956823603
      120

      6
      2005
      4141223
      1.83 %
      71624
      40293
      33.2
      1.93
      60
      61.5 %
      2547885
      0.06 %
      6541907027
      123

      7
      2000
      3783103
      1.04 %
      38183
      16976
      31.8
      1.90
      55
      60.2 %
      2276738
      0.06 %
      6143493823
      123

      8
      1995
      3592186
      0.46 %
      16260
      -3075
      30.1
      1.96
      52
      58.9 %
      2116050
      0.06 %
      5744212979
      122

      9
      1990
      3510885
      -0.00 %
      -19
      -25189
      28.4
      2.26
      51
      57.9 %
      2031946
      0.07 %
      5327231061
      122

      10
      1985
      3510979
      0.73 %
      25245
      -12186
      26.9
      2.83
      51
      57.1 %
      2006170
      0.07 %
      4870921740
      118

      11
      1980
      3384756
      1.53 %
      49516
      10353
      26.3
      3.25
      49
      56.2 %
      1900678
      0.08 %
      4458003514
      114

      12
      1975
      3137176
      1.53 %
      45751
      8867
      26.1
      3.73
      46
      54.4 %
      1707788
      0.08 %
      4079480606
      113

      13
      1970
      2908421
      0.59 %
      16954
      -11456
      26.4
      3.86
      42
      52.0 %
      1512629
      0.08 %
      3700437046
      110

      14
      1965
      2823650
      0.23 %
      6451
      -34071
      30.2
      3.42
      41
      46.8 %
      1322242
      0.08 %
      3339583597
      95

      15
      1960
      2791397
      -0.58 %
      -16404
      -41448
      29.2
      3.58
      41
      45.7 %
      1275146
      0.09 %
      3034949748
      104

      16
      1955
      2873415
      -0.27 %
      -7853
      -34071
      30.2
      3.42
      42
      44.1 %
      1267440
      0.10 %
      2773019936
      95

```python
plt.figure(figsize=(10,4))
plt.plot(df.Year,df.Population, label='Total Population')
plt.plot(df.Year, df['Urban Population'], label='Urban Population')
plt.legend();
```

```python
#Criando uma coluna com o número de anos considerando 1955 como ano 0
df['Tempo'] = df['Year']-1955

#Colocando a população na escala de 'milhões de pessoas'
df['Population'] = df['Population']/1000000
```

```python
plt.figure(figsize=(10,4))
plt.scatter(df.Tempo,df.Population)
plt.title('Scatter plot do total de população (M) pelo número de anos passados');
```

```python
#Separando os dados
X = df[['Tempo']]
y = df[['Population']]
```

## Parte 2: Modelagem

```python
# Abordagem linear (Usando SKLEARN)
from sklearn.linear_model import LinearRegression
model = LinearRegression()

# Ajustando os Dados ao nosso Modelo
model.fit(X, y)

# Predizendo os dados para calcular o erro posteriormente
y_pred = model.predict(X)

# Pontuando o Modelo
from sklearn.metrics import r2_score, mean_squared_error

# Valor de R2 perto de 1 nos diz que é um bom modelo
print(f"R2 score: {r2_score(y_pred, y)}")

# MSE Score perto de 0 é um bom modelo
print(f"MSE score: {mean_squared_error(y_pred, y)}")
```

    R2 score: 0.9492800841417058
    MSE score: 0.027760185348142632

```python
#Coeficientes lineares

print('Coeficientes: \n', model.coef_)
print('Intercept: \n', model.intercept_)
```

    Coeficientes:
     [[0.03456945]]
    Intercept:
     [2.51116073]

```python
#Equação do modelo:

            #coeff_df = pd.DataFrame(regressor.coef_, X.columns, columns=['Coefficient'])

m = model.coef_[0]
b = model.intercept_
print('Equação do modelo:')
print('     y = {0} * x + {1}'.format(m.round(5), b.round(5)))
```

    Equação do modelo:
         y = [0.03457] * x + [2.51116]

```python
# plotamos os resultados
plt.scatter(X, y,  color='black')
plt.plot(X, y_pred, color='blue', linewidth=3);
```

```python
# Abordagem polinominal (USANDO NUMPY e printando com SYMPY)
#https://docs.scipy.org/doc/numpy/reference/generated/numpy.poly1d.html
x = np.array(X)
x = x.reshape(-1)
y = np.array(y)
y = y.reshape(-1)
```

```python
#Calculando para os graus de 1 até 7

l_grau = []
l_coef = []
l_r2 = []
l_MSE = []
l_models = []

for grau in range(1,8):
    p = np.poly1d(np.polyfit(x, y, grau))

    print('GRAU: \n \n \n', grau, '\n \n \n')

    #Coeficientes
    coef = p.coefficients
    print('Coeficientes: \n', coef)

    print('Equação do modelo: \n')
    print(' y = 0 ')
    for i, v in enumerate(coef[::-1]):
        print('     + {:.5f} * x^{} '.format(v, i))

    # Valor de R2 perto de 1 nos diz que é um bom modelo
    print(f"R2 score: {r2_score(p(x), y)}")

    # MSE Score perto de 0 é um bom modelo
    print(f"MSE score: {mean_squared_error(p(x), y)}")

    # plotamos os resultados
    plt.scatter(x, y,  color='black')
    plt.plot(x, p(x), color='blue', linewidth=3)
    plt.show()

    l_grau.append(grau)
    l_coef.append(coef)
    l_r2.append(r2_score(p(x), y))
    l_MSE.append(mean_squared_error(p(x), y))
    l_models.append(p)
```

```python
#Armazendando resultados em um DataFrame
avaliacao = pd.DataFrame({
    "graus": l_grau,
    "r2": l_r2,
    "MSE": l_MSE,
    "coef": l_coef,
    "models": l_models
})
print(avaliacao)

(1-avaliacao['r2']).plot()
avaliacao['MSE'].plot()
plt.legend();
```

### Removendo 40% dos dados para avaliar os modelos

```python
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    x, y, test_size=0.40, random_state=50)
```

```python
#Calculando para os graus de 1 até 7 e testando em novos dados

a_grau = []
a_coef = []
a_r2 = []
a_MSE = []
a_models = []

for grau in range(1,8):
    p = np.poly1d(np.polyfit(X_train, y_train, grau))

    print('GRAU: \n \n \n', grau, '\n \n \n')

    #Coeficientes
    coef = p.coefficients
    print('Coeficientes: \n', coef)

    print('Equação do modelo: \n')
    print(' y = 0 ')
    for i, v in enumerate(coef[::-1]):
        print('     + {:.5f} * x^{} '.format(v, i))

    # Valor de R2 perto de 1 nos diz que é um bom modelo
    print(f"R2 score: {r2_score(p(X_test), y_test)}")

    # MSE Score perto de 0 é um bom modelo
    print(f"MSE score: {mean_squared_error(p(X_test), y_test)}")

    # plotamos os resultados
    plt.scatter(x, y,  color='black')
    plt.plot(x, p(x), color='blue', linewidth=3)
    plt.show()

    a_grau.append(grau)
    a_coef.append(coef)
    a_r2.append(r2_score(p(X_test), y_test))
    a_MSE.append(mean_squared_error(p(X_test), y_test))
    a_models.append(p)
```

```python
#Armazendando resultados em um DataFrame
teste = pd.DataFrame({
    "graus": a_grau,
    "r2": a_r2,
    "MSE": a_MSE,
    "coef": a_coef,
    "models": a_models
})
print(teste)

(1-teste['r2']).plot()
teste['MSE'].plot()
plt.legend();
```

### Modelo escolhido: 4° Grau

## Parte 3: Diferenciação da função para encontrar a taxa de crescimento (velocidade)

```python
import sympy as sp
sp.init_printing(pretty_print=True)
```

```python
#Criando a expressão
y1 = l_models[3]
x=Symbol('x')
expr = y1(x)
expr
```

```python
#Expandindo a expressão
sp.expand(expr)
```

```python
#Derivada da expressão
diff = expr.diff()
diff
```

```python
#Expandindo a derivada
diff.expand()
```

### Criando um intervalo de tempo de 1955 (inicio dos dados) até 2020 (final) para usar o modelo e extimar os seguintes parâmetros:

* População
* Taxa de Crescimento

```python
X = [i for i in range(2021-1955)]
X
```

```python
pop = []
taxa = []
for i in X:
    pop.append(expr.subs(x, i))
    taxa.append(diff.subs(x,i)*1000000) #Reconvertendo para Milhões de pessoas
```

```python
#Plotando os dois parâmetros
plt.figure(figsize=(10,4))
plt.subplot(121)
plt.plot(pop)
plt.title('População')
plt.subplot(122)
plt.plot(taxa)
plt.title('Taxa de Crescimento');
plt.tight_layout()
```

```python
# retornando os anos
X = [i+1955 for i in X]
```

```python
# Criando um DataFrame dos resultados
resultado = pd.DataFrame({
    "Ano": X,
    "População": pop,
    "Taxa de Crescimento": taxa
})
resultado.head()
```

      Ano
      População
      Taxa de Crescimento

      0
      1955
      2.82635579481014
      -7410.27078733972

      1
      1956
      2.82047601339781
      -4378.40307461187

      2
      1957
      2.81754170263075
      -1518.39345238498

      3
      1958
      2.81738381255172
      1175.37434585904

      4
      1959
      2.81983890946998
      3708.51658663830

```python
#Calculando a diferença populacional e de taxa de crescimento por ano
resultado['Diff Pop'] = resultado["População"].diff(1)
resultado['Diff Taxa'] = resultado["Taxa de Crescimento"].diff(1)
```

```python
#Plotando os dois novos parâmetros
plt.figure(figsize=(10,4))
plt.subplot(121)
plt.plot(resultado['Diff Pop'])
plt.title('Diff Pop')
plt.subplot(122)
plt.plot(resultado['Diff Taxa'])
plt.title('Diff Taxa');
plt.tight_layout()
```

```python
# arredondando os valores
resultado.dropna(inplace=True)
resultado.População = resultado.População.astype('float', inplace=True)
resultado['Taxa de Crescimento'] = resultado['Taxa de Crescimento'].astype('float', inplace=True)
resultado['Diff Pop'] = resultado['Diff Pop'].astype('float', inplace=True)
resultado['Diff Taxa'] = resultado['Diff Taxa'].astype('float', inplace=True)
```

```python
import seaborn as sns
```

```python
#plotando todos em relação a todos
sns.pairplot(data=resultado, diag_kind='kde');
```

```python
#Resumo estatístico de todos os parâmetros
resultado.describe()
```

      Ano
      População
      Taxa de Crescimento
      Diff Pop
      Diff Taxa

      count
      65.00000
      65.000000
      65.000000
      65.000000
      65.000000

      mean
      1988.00000
      3.603696
      33926.431173
      0.033273
      1306.539917

      std
      18.90767
      0.642293
      18002.055304
      0.017922
      895.784151

      min
      1956.00000
      2.817384
      -4378.403075
      -0.005880
      315.830838

      25%
      1972.00000
      3.025396
      24577.841253
      0.024107
      500.047060

      50%
      1988.00000
      3.495013
      32542.642071
      0.032383
      1043.704340

      75%
      2004.00000
      4.077496
      42520.227037
      0.041953
      1946.802676

      max
      2020.00000
      4.989084
      77514.823808
      0.075822
      3355.343221

```python
#Qual o ano com a menor diferença na taxa de crescimento populacional em relação ao seu antecessor?
resultado[ resultado['Diff Taxa'] == resultado['Diff Taxa'].min()]
```

      Ano
      População
      Taxa de Crescimento
      Diff Pop
      Diff Taxa

      32
      1987
      3.462629
      32224.565061
      0.032067
      315.830838

### 1987


# Integrais de liña en 2 e 3 dimensións

O cálculo de integrais de liña son unha nova extensión das integrais usuais nunha única variable onde o dominio de integración era un segmento sobre o eixo de coordenadas. Agora, o dominio de integración será unha porción dunha liña (curva) nun plano bidimensional ou no espazo tridimendional. Para realizar estes cálculos, seguiremos usando o módulo **Sympy**. Ademais, destes cálculos integrais, tamén revisaremos como introducir a parametrización dunha curva, estudo que xa foi realizado na *práctica 4*.

Nesta práctica volveremos a usar o módulo **Sympy** tanto para calcular integrais como para representar as curvas que serán usadas como dominio de integración.

## Obxectivos

- Revisión da parametrización de liñas en 2D e 3D (**repaso**)
- Cálculo de integrais de liña

# Parametrización de liñas en 2D e 3D

Como xa fixemos en prácticas anteriores, para usar os módulos **Numpy**, **Matplotlib** e **Sympy**, o primeiro que debemos facer é importalos, e posto que usaremos este módulo usando a interface `pyplot` de **Matplotlib**, tamén haberá que importar esta.

```python
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import sympy as sp
%matplotlib inline
```

## Representación de curvas no plano (repaso)
Unha curva no plano pódese definir como un conxunto de puntos que satisface a relación:
$$
\text{Curva}=\{(x,y)\in\mathbb{R}^2:\ x=f(t),\ y=g(t)\text{ con }t\in\mathbb{R}\}=\{(f(t),g(t)):t\in\mathbb{R}\},
$$
onde $t$ é o que se denomina parámetro da representación da curva.

Por exemplo, para representar a curva
$$
x=\sin(t),\ y=\cos(3t),\ t\in[0,2\pi]
$$
empregaríase o comando `plot_parametric`:

```python
t = sp.symbols('t', real=True)
p = sp.plotting.plot_parametric(sp.sin(t), sp.cos(3*t), (t, 0, 2*sp.pi), show=False)
p.xlabel='x'
p.ylabel='y'
p.show()
```

## Representación de curvas no espazo tridimensional (repaso)
De forma análoga no espazo tridimensional, unha curva ven definida por tres funcións diferentes:
$$
\text{Curva}=\{(x,y,z)\in\mathbb{R}^3:\ x=f(t),\ y=g(t),\ z=h(t)\text{ con }t\in\mathbb{R}\}=\{(f(t),g(t),h(t)):\ t\in\mathbb{R}\},
$$
onde $t$ é o que se denomina parámetro da representación da curva.

Por exemplo, para representar graficamente a curva definida
$$
\begin{align*}
f(t)&=\cos(t),\\
g(t)&=\sin(t),\\
h(t)&=2t,
\end{align*}
$$
con $t\in[0,8\pi]$, usaremos a función `plot3d_parametric_line` como segue:

```python
t = sp.symbols('t', real=True)
xt = sp.cos(t)
yt = sp.sin(t)
zt = 2*t
p = sp.plotting.plot3d_parametric_line(xt, yt, zt, (t, 0, 8*sp.pi), show=False)
p.xlabel='x'
p.ylabel='y'
p.zlabel='z'
p.show()
```

### Exercicio 11.1
Calcula os vectores velocidade e aceleración sobre algúns puntos da espiral anterior. Representa graficamente estes vectores (velocidade en vermello e aceleración en azul usando **Matplotlib**). Que relación hai entre os dous campos de vectores?

```python
## O TEU CÓDIGO AQUÍ
```

# Cálculo de integrais de liña en 2D e 3D

O cálculo dunha integral de liña non deixa de ser unha integral calculada sobre unha única variable, neste caso sobre o parametro da parametrización usada para representar a liña (curva) sobre a que se integra. Máis precisamente, se a porción de curva ven dada por
$$
C=\{(x,y,z)\in\mathbb{R}^3:\ x=f(t),\ y=g(t),\ z=h(t)\text{ con }t\in[t_{0},t_{1}]\}
$$
onde $t$ é o que se denomina parámetro da representación da curva, entón
$$
\int_{C}F(x,y,z)d\ell=\int_{t_{0}}^{t_{1}}F(f(t),g(t),h(t))\sqrt{(f'(t))^2+(g'(t))^2+(h'(t))^2}dt.
$$
Unha expresión análoga sería válida para integrais de liña en dúas dimensións (onde se podería supoñer que $z=0$).

Vexamos cun exemplo como realizaríamos o cálculo da integral de liña no caso de
$$
\int_{C}2xy\,d\ell,
$$
onde $C=\{(x,y,z)\in\mathbb{R}^3: x=t,\ y=t^2,\ z=3t-2,\ t\in[0,1]\}$.

```python
x, y, z, t = sp.symbols('x y z t', real=True)
F = sp.Lambda((x,y,z), 2*x*y)
# Parametrización da curva
C = sp.Lambda(t,sp.Matrix((t,t**2,3*t-2)))
# Cálculo do vector velocidade
v = sp.diff(C(t),t)
display('Módulo de v:',v.norm())
# Calculo da integral de liña
I = sp.integrate(F(*C(t))*v.norm(),(t,0,1))
display('Integral de liña:', I)
```

    'Módulo de v:'

$\displaystyle \sqrt{4 t^{2} + 10}$

    'Integral de liña:'

$\displaystyle - \frac{14 \sqrt{14}}{15} + \frac{5 \sqrt{10}}{3}$

### Exercicio 11.2
Calcula a integral de liña
$$
\int_{C}(x^2-y+3z)d\ell,
$$
onde $C$ é o segmento recto de extremos $(0,0,0)$ e $(1,2,1)$.

```python
x, y, z, t = sp.symbols('x y z t', real=True)
F = sp.Lambda((x,y,z), x**2-y+3*z)
# Parametrización da curva
C = sp.Lambda(t,sp.Matrix((0+t*1,0+t*2,0+t*1)))
# Cálculo do vector velocidade
v = sp.diff(C(t),t)
display('Módulo de v:',v.norm())
# Calculo da integral de liña
I = sp.integrate(F(*C(t))*v.norm(),(t,0,1))
display('Integral de liña:', I)
```