# [Polytropic TOV](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation) Initial Data

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

## This module sets up initial data for a [TOV](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation) star in *spherical, isotropic coordinates*

**Module 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 [start-to-finish TOV module](Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.ipynb) for full test). Note that convergence at the surface of the star is 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)

[comment]: <> (Introduction: TODO)

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

This module is organized as follows:

1. [Step 1](#initializenrpy): **Initialize core Python/NRPy+ modules**
1. [Step 2](#polytropic_eoss): **Polytropic EOSs**
    1. [Step 2.a](#polytropic_eoss__continuity_of_pcold): *Continuity of $P_{\rm cold}$*
    1. [Step 2.b](#polytropic_eoss__p_poly_tab): *Computing $P_{j}$*
    1. [Step 2.c](#polytropic_eoss__parameters_from_input): *Setting up EOS parameters from user input*
    1. [Step 2.d](#polytropic_eoss__pcold): *Computing $P_{\rm cold}\left(\rho_{b}\right)$*
    1. [Step 2.e](#polytropic_eoss__rhob): *Computing $\rho_{b}\left(P_{\rm cold}\right)$*
    1. [Step 2.f](#polytropic_eoss__polytropic_index): *Determining the polytropic index*
        1. [Step 2.f.i](#polytropic_eoss__polytropic_index__from_rhob): From $\rho_{b}$
        1. [Step 2.f.ii](#polytropic_eoss__polytropic_index__from_pcold): From $P_{\rm cold}$
    1. [Step 2.g](#polytropic_eoss__simple_test): *Simple test of our functions*
    1. [Step 2.h](#polytropic_eoss__pcold_plot): *Visualizing $P_{\rm cold}\left(\rho_{b}\right)$*
1. [Step 3](#tov): **The TOV Equations**
1. [Step 4](#code_validation): **Code Validation against `TOV.TOV_Solver` NRPy+ module**
1. [Step 5](#latex_pdf_output): **Output this module to $\LaTeX$-formatted PDF**

# Step 1: Initialize core Python/NRPy+ modules \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

```python
# Step 1: Import needed Python/NRPy+ modules
import numpy as np
import scipy.integrate as si
import math
import sys
```

# Step 2: Polytropic EOSs \[Back to [top](#toc)\]
$$\label{polytropic_eoss}$$

## Step 2.a: Continuity of $P_{\rm cold}$ \[Back to [top](#toc)\]
$$\label{polytropic_eoss__continuity_of_pcold}$$

Consider a piecewise polytrope EOS of the form

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

The case of a single polytrope is given by the first EOS above, with no condition imposed on the value of $\rho$, i.e.

$$
\boxed{P_{\rm cold} = K_{0}\rho_{b}^{\Gamma_{0}} = K\rho_{b}^{\Gamma}}\ .
$$

Notice that we have the following sets of variables:

$$
\left\{\underbrace{\rho_{0},\rho_{1},\ldots,\rho_{N-2}}_{N-1\ {\rm values}}\right\}\ ;\
\left\{\underbrace{K_{0},K_{1},\ldots,K_{N-1}}_{N\ {\rm values}}\right\}\ ;\
\left\{\underbrace{\Gamma_{0},\Gamma_{1},\ldots,\Gamma_{N-1}}_{N\ {\rm values}}\right\}\ .
$$

Also, notice that $K_{0}$ and the entire sets $\left\{\rho_{0},\rho_{1},\ldots,\rho_{N-1}\right\}$ and $\left\{\Gamma_{0},\Gamma_{1},\ldots,\Gamma_{N}\right\}$ must be specified by the user. The values of $\left\{K_{1},\ldots,K_{N}\right\}$, on the other hand, are determined by imposing that $P_{\rm cold}$ be continuous, i.e.

$$
P_{\rm cold}\left(\rho_{0}\right) = K_{0}\rho_{0}^{\Gamma_{0}} = K_{1}\rho_{0}^{\Gamma_{1}} \implies
\boxed{K_{1} = K_{0}\rho_{0}^{\Gamma_{0}-\Gamma_{1}}}\ .
$$

Analogously,

$$
\boxed{K_{j} = K_{j-1}\rho_{j-1}^{\Gamma_{j-1}-\Gamma_{j}}\ ,\ j\in\left[1,N-1\right]}\ .
$$

Again, for the case of a single polytropic EOS, the set $\left\{\rho_{j}\right\}$ is empty and $\left\{\Gamma_{j},K_{j}\right\}\to \left\{\Gamma,K\right\}$.

Below we implement a function to set up $\left\{K_{j}\right\}$ for both single and piecewise polytropic EOSs, based on the last boxed equation above.

```python
# Function     : impose_continuity_on_P_cold()
# Author(s)    : Leo Werneck
# Description  : This function populates the array K_poly_tab
#                by demanding that P_cold be everywhere continuous
# Dependencies : none
#
# Inputs       : eos              - named tuple containing the following:
#                  neos           - number of EOSs to be used (single polytrope = 1)
#                  rho_poly_tab   - values of rho distinguish one EOS from the
#                                   other (not required for a single polytrope)
#                  Gamma_poly_tab - values of Gamma to be used within each EOS
#                  K_poly_tab     - uninitialized, see output variable below
#                  P_poly_tab     - uninitialized, see function
#                                   compute_P_poly_tab() below
#                K_poly_tab0      - value of K_poly_tab[0], for the first EOS
#
# Outputs      : eos.K_poly_tab   - values of K to be used within each EOS, determined
#                                   by imposing that P_cold be everywhere continuous

def impose_continuity_on_P_cold(eos,K_poly_tab0):

    # A piecewise polytropic EOS is given by
    # .--------------------------------------------------------------------------.
    # |      /     K_0 * rho^(Gamma_0)     ,                rho < rho_0 ;        |
    # |      |     K_1 * rho^(Gamma_1)     ,        rho_0 < rho < rho_1 ;        |
    # |      |          ...                                 ...                  |
    # | P = <      K_j * rho^(Gamma_j)     ,    rho_(j-1) < rho < rho_j ;        |
    # |      |          ...                                 ...                  |
    # |      | K_(n-2) * rho^(Gamma_(n-2)) , rho_(neos-3) < rho < rho_(neos-2) ; |
    # |      \ K_(n-1) * rho^(Gamma_(n-1)) ,                rho > rho_(neos-2) . |
    # .--------------------------------------------------------------------------.
    # Notice that the case of a single polytropic EOS corresponds to
    # the first EOS in the boxed equation above, with no condition on
    # rho. Thus we need only return K_poly_tab0.
    eos.K_poly_tab[0] = K_poly_tab0
    if eos.neos==1:
        return

    # For the case of a piecewise polytropic EOS, emanding that P_cold
    # be everywhere continuous results in the relation:
    # .-----------------------------------------------------.
    # | K_j = K_(j-1) * rho_(j-1)^( Gamma_(j-1) - Gamma_j ) |
    # .-----------------------------------------------------.
    for j in range(1,eos.neos):
        eos.K_poly_tab[j] = eos.K_poly_tab[j-1]*eos.rho_poly_tab[j-1]**(eos.Gamma_poly_tab[j-1]-eos.Gamma_poly_tab[j])

    return
```

## Step 2.b: Computing $P_{j}$ \[Back to [top](#toc)\]
$$\label{polytropic_eoss__p_poly_tab}$$

We now set a new set of quantities, $P_{\rm tab}$, which are used in a similar fashion to $\rho_{j}$, that is, to determine which EOS we should use. This quantity is defined as:

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

```python
# Function     : compute_P_poly_tab()
# Author(s)    : Leo Werneck
# Description  : This function populates the array eos.P_poly_tab,
#                used to distinguish which EOS we are using in the
#                case of a piecewise polytropic EOS
# Dependencies : none
#
# Inputs       : eos              - named tuple containing the following:
#                  neos           - number of EOSs to be used (single polytrope = 1)
#                  rho_poly_tab   - values of rho used to distinguish one EOS from
#                                   the other (not required for a single polytrope)
#                  Gamma_poly_tab - values of Gamma to be used within each EOS
#                  K_poly_tab     - value of K to be used within each EOS
#                  P_poly_tab     - uninitialized, see output variable below
#
# Outputs      : eos.P_poly_tab   - values of P used to distinguish one EOS from
#                                   the other (not required for a single polytrope)

def compute_P_poly_tab(eos):

    # We now compute the values of P_poly_tab that are used
    # to find the appropriate polytropic index and, thus,
    # EOS we must use.
    # First, if we have a single polytrope EOS, we need to
    # do nothing.
    if eos.neos==1:
        return

    # For the case of a piecewise polytropic EOS, we have
    # .---------------------------.
    # | P_j = K_j*rho_j^(Gamma_j) |
    # .---------------------------.
    for j in range(eos.neos-1):
        eos.P_poly_tab[j] = eos.K_poly_tab[j]*rho_poly_tab[j]**(Gamma_poly_tab[j])

    return
```

## Step 2.c: Setting up EOS parameters from user input \[Back to [top](#toc)\]
$$\label{polytropic_eoss__parameters_from_input}$$

We now implement a driver function to set up all polytropic EOS related quantities based on the input given by the user. From the given input set:

$$
\left\{n_{\rm eos}, \rho_{j}, \Gamma_{j}, K_{0}\right\}\ ,
$$

the code returns a "C like struct" (a [*named tuple*](https://docs.python.org/3/library/collections.html#collections.namedtuple)) containing

$$
\left\{n_{\rm eos}, \rho_{j}, \Gamma_{j}, K_{j}, P_{j}\right\}\ .
$$

```python
# Function     : set_single_or_piecewise_polytrope_EOS_parameters()
# Author(s)    : Leo Werneck
# Description  : This function determine all polytropic related
#                parameters from user input
# Dependencies : impose_continuity_on_P_cold()
#                compute_P_poly_tab()
#
# Inputs       : neos             - number of EOSs to be used (single polytrope = 1)
#                rho_poly_tab     - values of rho distinguish one EOS from the
#                                   other (not required for a single polytrope)
#                Gamma_poly_tab   - values of Gamma to be used within each EOS
#                K_poly_tab0      - value of K_poly_tab[0], for the first EOS
#
# Outputs      : eos              - named tuple containing the following:
#                  neos           - number of EOSs to be used (single polytrope = 1)
#                  rho_poly_tab   - values of rho used to distinguish one EOS from
#                                   the other (not required for a single polytrope)
#                  Gamma_poly_tab - values of Gamma to be used within each EOS
#                  K_poly_tab     - value of K to be used within each EOS
#                  P_poly_tab     - values of P used to distinguish one EOS from
#                                   the other (not required for a single polytrope)

def set_single_or_piecewise_polytrope_EOS_parameters(neos,rho_poly_tab,Gamma_poly_tab,K_poly_tab0):

    # Error check #1: Verify if the correct number of rho_poly_tab has been given by the user
    if (neos == 1):
        pass
    elif len(rho_poly_tab) != neos-1:
        print("Error: neos="+str(neos)+". Expected "+str(neos-1)+" values of rho_poly_tab, but "+str(len(rho_poly_tab))+" values were given.")
        sys.exit(1)

    # Error check #2: Verify if the correct number of Gamma_poly_tab has been given by the user
    if len(Gamma_poly_tab) != neos:
        print("Error: neos="+str(neos)+". Expected "+str(neos)+" values of Gamma_poly_tab, but "+str(len(Gamma_poly_tab))+" values were given.")
        sys.exit(2)

    # Create the arrays to store the values of K_poly_tab and eps_integ_const_tab
    K_poly_tab = [0 for i in range(neos)]
    P_poly_tab = [0 for i in range(neos-1)]

    # Create the EOS "struct" (named tuple)
    from collections import namedtuple
    eos_struct = namedtuple("eos_struct","neos rho_poly_tab Gamma_poly_tab K_poly_tab P_poly_tab")
    eos = eos_struct(neos,rho_poly_tab,Gamma_poly_tab,K_poly_tab,P_poly_tab)

    # Step 1: Determine K_poly_tab. For the details, please see the implementation
    #         of the function impose_continuity_on_P_cold() below.
    impose_continuity_on_P_cold(eos,K_poly_tab0)

    # Step 2: Determine eps_integ_const_tab. For the details, please see the
    #         implementation of the function impose_continuity_on_eps_cold() below.
    compute_P_poly_tab(eos)

    return eos
```

## Step 2.d: Computing $P_{\rm cold}\left(\rho_{b}\right)$ \[Back to [top](#toc)\]
$$\label{polytropic_eoss__pcold}$$

Then, let us compute $P_{\rm cold}$ for a polytropic EOS:

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

```python
# Function     : Polytrope_EOS__compute_P_cold_from_rhob()
# Author(s)    : Leo Werneck
# Description  : This function computes P_cold for a polytropic EOS
# Dependencies : polytropic_index_from_rhob()
#
# Inputs       : eos              - named tuple containing the following:
#                  neos           - number of EOSs to be used (single polytrope = 1)
#                  rho_poly_tab   - values of rho distinguish one EOS from the
#                                   other (not required for a single polytrope)
#                  Gamma_poly_tab - values of Gamma to be used within each EOS
#                  K_poly_tab     - value of K to be used within each EOS
#                  P_poly_tab     - values of P used to distinguish one EOS from
#                                   the other (not required for a single polytrope)
#                rho_baryon       - the value of rho for which we want to
#                                   compute P_cold
#
# Outputs      : P_cold           - for a single or piecewise polytropic EOS

def Polytrope_EOS__compute_P_cold_from_rhob(eos, rho_baryon):

    # Compute the polytropic index from rho_baryon
    j = polytropic_index_from_rhob(eos, rho_baryon)

    # Return the value of P_cold for a polytropic EOS
    # .--------------------------------.
    # | P_cold = K_j * rho_b^(Gamma_j) |
    # .--------------------------------.
    return eos.K_poly_tab[j]*rho_baryon**eos.Gamma_poly_tab[j]
```

## Step 2.e: Computing $\rho_{b}\left(P_{\rm cold}\right)$ \[Back to [top](#toc)\]
$$\label{polytropic_eoss__rhob}$$

Then, let us compute $\rho_{b}$ as a function of $P_{\rm cold}\equiv P$ for a polytropic EOS:

$$
\boxed{
\rho_{b} =
\left\{
\begin{matrix}
\left(\frac{P}{K_{0}}\right)^{1/\Gamma_{0}} & , & P \leq P_{0}\\
\left(\frac{P}{K_{1}}\right)^{1/\Gamma_{1}} & , & P_{0} \leq P \leq P_{1}\\
\vdots &  & \vdots\\
\left(\frac{P}{K_{j}}\right)^{1/\Gamma_{j}} & , & P_{j-1} \leq P \leq P_{j}\\
\vdots &  & \vdots\\
\left(\frac{P}{K_{N-2}}\right)^{1/\Gamma_{N-2}} & , & P_{N-3} \leq P \leq P_{N-2}\\
\left(\frac{P}{K_{N-1}}\right)^{1/\Gamma_{N-1}} & , & P \geq P_{N-2}
\end{matrix}
\right.
}\ .
$$

```python
# Function     : Polytrope_EOS__compute_rhob_from_P_cold()
# Author(s)    : Leo Werneck
# Description  : This function computes rho_b for a polytropic EOS
# Dependencies : polytropic_index_from_P()
#
# Inputs       : eos              - named tuple containing the following:
#                  neos           - number of EOSs to be used (single polytrope = 1)
#                  rho_poly_tab   - values of rho distinguish one EOS from the
#                                   other (not required for a single polytrope)
#                  Gamma_poly_tab - values of Gamma to be used within each EOS
#                  K_poly_tab     - value of K to be used within each EOS
#                  P_poly_tab     - values of P used to distinguish one EOS from
#                                   the other (not required for a single polytrope)
#                P                - the value of P for which we want to
#                                   compute rho_b
#
# Outputs      : rho_baryon       - for a single or piecewise polytropic EOS

def Polytrope_EOS__compute_rhob_from_P_cold(eos,P):

    # Compute the polytropic index from P
    j = polytropic_index_from_P(eos,P)

    # Return the value of rho_b for a polytropic EOS
    # .----------------------------------.
    # | rho_b = (P_cold/K_j)^(1/Gamma_j) |
    # .----------------------------------.
    return (P/eos.K_poly_tab[j])**(1.0/eos.Gamma_poly_tab[j])
```

## Step 2.f: Determining the polytropic index \[Back to [top](#toc)\]
$$\label{polytropic_eoss__polytropic_index}$$

## Step 2.f.i: From $\rho_{b}$ \[Back to [top](#toc)\]
$$\label{polytropic_eoss__polytropic_index__from_rhob}$$

The function below determines the polytropic index from a given value $\rho_{b} = \rho_{\rm in}$.

```python
# Function     : polytropic_index_from_rhob()
# Author(s)    : Leo Werneck and Zach Etienne
# Description  : This function computes P_cold for a polytropic EOS
# Dependencies : none
#
# Input(s)     : eos              - named tuple containing the following:
#                  neos           - number of EOSs to be used (single polytrope = 1)
#                  rho_poly_tab   - values of rho distinguish one EOS from the
#                                   other (not required for a single polytrope)
#                  Gamma_poly_tab - values of Gamma to be used within each EOS
#                  K_poly_tab     - value of K to be used within each EOS
#                  P_poly_tab     - values of P used to distinguish one EOS from
#                                   the other (not required for a single polytrope)
#                rho_in           - value of rho for which we compute the
#                                   polytropic index
#
# Output(s)    : polytropic index computed from rho_in

def polytropic_index_from_rhob(eos, rho_in):

    # Returns the value of the polytropic index based on rho_in
    polytropic_index = 0
    if not (eos.neos==1):
        for j in range(eos.neos-1):
            polytropic_index += (rho_in > eos.rho_poly_tab[j])

    return polytropic_index
```

## Step 2.f.ii: From $P_{\rm cold}$ \[Back to [top](#toc)\]
$$\label{polytropic_eoss__polytropic_index__from_pcold}$$

The function below determines the polytropic index from a given value $P_{\rm cold} = P$.

```python
# Function     : polytropic_index_from_P()
# Author(s)    : Leo Werneck and Zach Etienne
# Description  : This function computes P_cold for a polytropic EOS
# Dependencies : none
#
# Input(s)     : eos              - named tuple containing the following:
#                  neos           - number of EOSs to be used (single polytrope = 1)
#                  rho_poly_tab   - values of rho distinguish one EOS from the
#                                   other (not required for a single polytrope)
#                  Gamma_poly_tab - values of Gamma to be used within each EOS
#                  K_poly_tab     - value of K to be used within each EOS
#                  P_poly_tab     - values of P used to distinguish one EOS from
#                                   the other (not required for a single polytrope)
#                P_in             - value of P for which we compute the
#                                   polytropic index
#
# Output(s)    : polytropic index computed from P_in

def polytropic_index_from_P(eos, P_in):

    # Returns the value of the polytropic index based on P_in
    polytropic_index = 0
    if not (eos.neos==1):
        for j in range(eos.neos-1):
            polytropic_index += (P_in > eos.P_poly_tab[j])

    return polytropic_index
```

## Step 2.g: Simple test of our functions \[Back to [top](#toc)\]
$$\label{polytropic_eoss__simple_test}$$

We now want to test the functions we have implemented above. In order for us to work with realistic values (i.e. values actually used by researchers), we will implement a simple test using the values from [Table II in J.C. Read *et al.* (2008)](https://arxiv.org/pdf/0812.2163.pdf):

| $\rho_{i}$ | $\Gamma_{i}$ | $K_{\rm expected}$ |
|------------|--------------|--------------------|
|2.44034e+07 |   1.58425    |    6.80110e-09     |
|3.78358e+11 |   1.28733    |    1.06186e-06     |
|2.62780e+12 |   0.62223    |    5.32697e+01     |
|    $-$     |   1.35692    |    3.99874e-08     |

First, we have $n_{\rm eos} = 4$. Then, giving our function the values

$$
\begin{align}
\left\{\rho_{j}\right\}   &= \left\{\text{2.44034e+07},\text{3.78358e+11},\text{2.62780e+12}\right\}\ ,\\
\left\{\Gamma_{j}\right\} &= \left\{{\rm 1.58425},{\rm 1.28733},{\rm 0.62223},{\rm 1.35692}\right\}\ ,\\
K_{0} &= \text{6.80110e-09}\ ,
\end{align}
$$

we expect to obtain the values

$$
\begin{align}
K_{1} &= \text{1.06186e-06}\ ,\\
K_{2} &= \text{5.32697e+01}\ ,\\
K_{3} &= \text{3.99874e-08}\ .
\end{align}
$$

```python
# Number of equation of states (i.e. polytropes)
# to be used for the solution. The single-polytrope
# case corresponds to neos = 1
neos = 4

# User input #1: The values of rho_b that distinguish one EOS from the other
#                Values taken from Table II of J.C. Read et al. (2008)
#                       https://arxiv.org/pdf/0812.2163.pdf
rho_poly_tab = [2.44034e+07,3.78358e+11,2.62780e+12]

# User input #2: The values of Gamma to be used within each EOS
#                Values taken from Table II of J.C. Read et al. (2008)
#                       https://arxiv.org/pdf/0812.2163.pdf
Gamma_poly_tab = [1.58425,1.28733,0.62223,1.35692]

# User input #3: The value K_0, to be used within the *first* EOS. The other
#                values of K_j are determined by imposing that P be everywhere
#                continuous
#                Value taken from Table II of J.C. Read et al. (2008)
#                       https://arxiv.org/pdf/0812.2163.pdf
K_poly_tab0 = 6.80110e-09

# Set up EOS parameters
eos = set_single_or_piecewise_polytrope_EOS_parameters(neos,rho_poly_tab,Gamma_poly_tab,K_poly_tab0)

# Validade results obtained against the expected ones:
K_expected = [6.80110e-09,1.06186e-06,5.32697e+01,3.99874e-08]
for j in range(1,eos.neos):
    print("K_"+str(j)+": Expected - obtained = "+str(np.fabs(eos.K_poly_tab[j] - K_expected[j])))
```

    K_1: Expected - obtained = 1.5516672146434653e-11
    K_2: Expected - obtained = 0.005384583961500766
    K_3: Expected - obtained = 4.669172431909315e-12

## Step 2.h: Visualizing $P_{\rm cold}\left(\rho_{b}\right)$ \[Back to [top](#toc)\]
$$\label{polytropic_eoss__pcold_plot}$$

Now let us visualize our results by plotting $P_{\rm cold}$ as a function of $\rho_{b}$. We will make a plot that covers the entire range of $\left\{\rho_{j}\right\}$ so that we can visualize whether or not all of our EOSs are being used. We will also differenciate each EOS by color, ranging from colder regions (blue) to hotter regions (red).

```python
# Let us plot our piecewise polytropic so that we can see what is happening
# First set the number of points in the plot
n_plot = 1000

# Then split the plot by the number of EOSs used in the code
rho_b  = [0 for i in range(eos.neos)]
P_cold = [[0 for j in range(n_plot/eos.neos)] for i in range(eos.neos)]

# Then set the plotting limits for each region
lim = [eos.rho_poly_tab[0]/50.0,eos.rho_poly_tab[0],eos.rho_poly_tab[1],eos.rho_poly_tab[2],eos.rho_poly_tab[2]*30.0]

# Then populate the rho_b arrays
for j in range(eos.neos):
    rho_b[j] = np.linspace(lim[j],lim[j+1],n_plot/eos.neos)

# Finally, populate the P array
for i in range(eos.neos):
    for j in range(n_plot/4):
        P_cold[i][j] = Polytrope_EOS__compute_P_cold_from_rhob(eos, rho_b[i][j])

import matplotlib.pyplot as plt
colors = ['blue','green','orange','red']
for i in range(eos.neos):
    label=r'EOS #'+str(i+1)+r', parameters: $\left(\Gamma_'+str(i)+r',K_'+str(i)+r'\right)$'
    plt.plot(np.log10(rho_b[i]),np.log10(P_cold[i]),label=label,c=colors[i])
plt.legend()
plt.title(r"$\log_{10}\left(P_{\rm cold}\right)\times\log_{10}\left(\rho_{b}\right)$",fontsize=14)
plt.xlabel(r"$\log_{10}\left(\rho_{b}\right)$",fontsize=14)
plt.ylabel(r"$\log_{10}\left(P_{\rm cold}\right)$",fontsize=14)
plt.grid()
plt.savefig("P_cold__rho_b__piecewise_polytrope.png")
plt.close()

from IPython.display import Image
Image("P_cold__rho_b__piecewise_polytrope.png")
```

# Step 3: The TOV equations \[Back to [top](#toc)\]
$$\label{tov}$$

The [TOV line element](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation) in terms of the *Schwarzschild coordinate* $r$ 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,
$$
where $m(r)$ is the mass-energy enclosed at a given $r$, and is equal to the total star's mass outside the stellar radius $r=R$.

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

Setting components of the above line element equal to one another, we get (in $G=c=1$ units):

\begin{align}
r^2 &= e^{4\phi} \bar{r}^2 \implies e^{4\phi} = \frac{r^2}{\bar{r}^2} \\
\left(1 - \frac{2m}{r}\right)^{-1} dr^2 &= e^{4\phi} d\bar{r}^2 \\
\implies \frac{d\bar{r}(r)}{dr} &= \left(1 - \frac{2m}{r} \right)^{-1/2} \frac{\bar{r}(r)}{r}.
\end{align}

The TOV equations provide radial ODEs for the pressure and $\nu$ (from [the Wikipedia article on the TOV solution](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation)):

\begin{align}
\frac{dP}{dr} &= - \frac{1}{r} \left( \frac{\rho + P}{2} \right) \left(\frac{2 m}{r} + 8 \pi r^2 P\right) \left(1 - \frac{2 m}{r}\right)^{-1} \\
\frac{d \nu}{d r} &= \frac{1}{r}\left(1 - \frac{2 m}{r}\right)^{-1} \left(\frac{2 m}{r} + 8 \pi r^2 P\right) \\
\end{align}

Assuming a polytropic equation of state, which relates the pressure $P$ to the baryonic rest-mass density $\rho_B$,

$$
P(\rho_B) = K \rho_B^\Gamma,
$$
the specific internal energy will be given by
$$
\epsilon = \frac{P}{\rho_B (\Gamma - 1)},
$$

so the total mass-energy density $\rho$ is given by
$$
\rho = \rho_B (1 + \epsilon).
$$

Given this, the mass-energy $m(r)$ density is the solution to the ODE:
$$
\frac{m(r)}{dr} = 4\pi r^2 \rho(r)
$$

Thus the full set of ODEs that need to be solved is given by

$$
\boxed{
\begin{align}
\frac{dP}{dr} &= - \frac{1}{r} \left( \frac{\rho + P}{2} \right) \left(\frac{2 m}{r} + 8 \pi r^2 P\right) \left(1 - \frac{2 m}{r}\right)^{-1} \\
\frac{d \nu}{d r} &= \frac{1}{r}\left(1 - \frac{2 m}{r}\right)^{-1} \left(\frac{2 m}{r} + 8 \pi r^2 P\right) \\
\frac{m(r)}{dr} &= 4\pi r^2 \rho(r) \\
\frac{d\bar{r}(r)}{dr} &= \left(1 - \frac{2m}{r} \right)^{-1/2} \frac{\bar{r}(r)}{r}
\end{align}
}\ .
$$

The following code solves these equations, and was largely written by Phil Chang.

```python
# Step 2: The TOV equations

## TOV SOLVER FOR SINGLE AND PIECEWISE POLYTROPES
## Authors: Phil Chang, Zachariah B. Etienne, Leo Werneck

# Full documentation for this module may be found in the NRPy+ tutorial Jupyter notebook:
#  Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.ipynb

# Inputs:
# * Output data file name
# * rho_baryon_central, the central density of the TOV star.
# * n, the polytropic equation of state index. n=1 models cold, degenerate neutron star matter.
# * K_Polytrope, the polytropic constant.
# * Verbose output toggle (default = True)

# Output: An initial data file (default file name = "outputTOVpolytrope.txt") that well
#         samples the (spherically symmetric) solution both inside and outside the star.
#         It is up to the initial data module to perform the 1D interpolation to generate
#         the solution at arbitrary radius. The file has the following columns:
# Column 1: Schwarzschild radius
# Column 2: rho(r), *total* mass-energy density (as opposed to baryonic rest-mass density)
# Column 3: P(r), Pressure
# Column 4: m(r), mass enclosed
# Column 5: e^{nu(r)}, g_{tt}(r)
# Column 6: e^{4 phi(r)}, conformal factor g_{rr}(r)
# Column 7: rbar(r), Isotropic radius

# rbar refers to the isotropic radius, and
# R_Schw refers to the Schwarzschild radius

def TOV_rhs(r_Schw, y) :
# In \tilde units
#
    P    = y[0]
    m    = y[1]
    nu   = y[2]
    rbar = y[3]

    j = polytropic_index_from_P(eos,P)
    Gamma = Gamma_poly_tab[j]
    Gam1  = Gamma-1.0

    rho_baryon = Polytrope_EOS__compute_rhob_from_P_cold(eos,P)
    rho = rho_baryon + P/Gam1 # rho is the *total* mass-energy density!

    if( r_Schw < 1e-4 or m <= 0.):
        m = 4*math.pi/3. * rho*r_Schw**3
        dPdrSchw = -(rho + P)*(4.*math.pi/3.*r_Schw*rho + 4.*math.pi*r_Schw*P)/(1.-8.*math.pi*rho*r_Schw*r_Schw)
        drbardrSchw = 1./(1. - 8.*math.pi*rho*r_Schw*r_Schw)**0.5
    else:
        dPdrSchw = -(rho + P)*(m + 4.*math.pi*r_Schw**3*P)/(r_Schw*r_Schw*(1.-2.*m/r_Schw))
        drbardrSchw = 1./(1. - 2.*m/r_Schw)**0.5*rbar/r_Schw

    dmdrSchw  =  4.*math.pi*r_Schw*r_Schw*rho
    dnudrSchw = -2./(P + rho)*dPdrSchw
    return [dPdrSchw, dmdrSchw, dnudrSchw, drbardrSchw]

def integrateStar( eos, P, dumpData = False):
    integrator = si.ode(TOV_rhs).set_integrator('dop853')
    y0 = [P, 0., 0., 0.]
    integrator.set_initial_value(y0,0.)
    dr_Schw = 1e-5
    P = y0[0]

    PArr      = []
    r_SchwArr = []
    mArr      = []
    nuArr     = []
    rbarArr   = []

    r_Schw = 0.

    while integrator.successful() and P > 1e-9*y0[0] :
        P, m, nu, rbar = integrator.integrate(r_Schw + dr_Schw)
        r_Schw = integrator.t

        dPdrSchw, dmdrSchw, dnudrSchw, drbardrSchw = TOV_rhs( r_Schw+dr_Schw, [P,m,nu,rbar])
        dr_Schw = 0.1*min(abs(P/dPdrSchw), abs(m/dmdrSchw))
        dr_Schw = min(dr_Schw, 1e-2)
        PArr.append(P)
        r_SchwArr.append(r_Schw)
        mArr.append(m)
        nuArr.append(nu)
        rbarArr.append(rbar)

    M = mArr[-1]
    R_Schw = r_SchwArr[-1]

    # Apply integration constant to ensure rbar is continuous across TOV surface
    for ii in range(len(rbarArr)):
        rbarArr[ii] *= 0.5*(np.sqrt(R_Schw*(R_Schw - 2.0*M)) + R_Schw - M) / rbarArr[-1]

    nuArr_np = np.array(nuArr)
    # Rescale solution to nu so that it satisfies BC: exp(nu(R))=exp(nutilde-nu(r=R)) * (1 - 2m(R)/R)
    #   Thus, nu(R) = (nutilde - nu(r=R)) + log(1 - 2*m(R)/R)
    nuArr_np = nuArr_np - nuArr_np[-1] + math.log(1.-2.*mArr[-1]/r_SchwArr[-1])

    r_SchwArrExtend_np = 10.**(np.arange(0.01,5.0,0.01))*r_SchwArr[-1]

    r_SchwArr.extend(r_SchwArrExtend_np)
    mArr.extend(r_SchwArrExtend_np*0. + M)
    PArr.extend(r_SchwArrExtend_np*0.)
    exp2phiArr_np = np.append( np.exp(nuArr_np), 1. - 2.*M/r_SchwArrExtend_np)
    nuArr.extend(np.log(1. - 2.*M/r_SchwArrExtend_np))
    rbarArr.extend( 0.5*(np.sqrt(r_SchwArrExtend_np**2 - 2.*M*r_SchwArrExtend_np) + r_SchwArrExtend_np - M) )

    # Appending to a Python array does what one would reasonably expect.
    #   Appending to a numpy array allocates space for a new array with size+1,
    #   then copies the data over... over and over... super inefficient.
    r_SchwArr_np     = np.array(r_SchwArr)
    PArr_np          = np.array(PArr)
    rho_baryonArr_np = np.array(PArr)
    for j in range(len(PArr_np)):
        # Compute rho_b from P
        rho_baryonArr_np[j] = Polytrope_EOS__compute_rhob_from_P_cold(eos,PArr_np[j])

    mArr_np               = np.array(mArr)
    rbarArr_np            = np.array(rbarArr)
    confFactor_exp4phi_np = (r_SchwArr_np/rbarArr_np)**2

    # Compute the *total* mass-energy density (as opposed to the *baryonic* mass density)
    rhoArr_np = []
    for i in range(len(rho_baryonArr_np)):
        polytropic_index = 0
        if not (eos.neos==1):
            for i in range(eos.neos-1):
                polytropic_index += (PArr_np[j] > P_poly_tab[i])

        rhoArr_np.append(rho_baryonArr_np[i] + PArr_np[i]/(eos.Gamma_poly_tab[polytropic_index] - 1.))

    print(len(r_SchwArr_np),len(rhoArr_np),len(PArr_np),len(mArr_np),len(exp2phiArr_np))
    # Special thanks to Leonardo Werneck for pointing out this issue with zip()
    if sys.version_info[0] < 3:
        np.savetxt("outputTOVpolytrope.txt", zip(r_SchwArr_np,rhoArr_np,PArr_np,mArr_np,exp2phiArr_np,confFactor_exp4phi_np,rbarArr_np),
                   fmt="%.15e")
    else:
        np.savetxt("outputTOVpolytrope.txt", list(zip(r_SchwArr_np,rhoArr_np,PArr_np,mArr_np,exp2phiArr_np,confFactor_exp4phi_np,rbarArr_np)),
                   fmt="%.15e")

    return R_Schw, M

############################
# Single polytrope example #
############################
# 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 = set_single_or_piecewise_polytrope_EOS_parameters(neos,rho_poly_tab,Gamma_poly_tab,K_poly_tab0)

# Set initial condition (Pressure computed from central density)
rho_baryon_central  = 0.129285
P_initial_condition = Polytrope_EOS__compute_P_cold_from_rhob(eos, rho_baryon_central)

R_Schw_TOV,M_TOV = integrateStar(eos, P_initial_condition, True)
print("Just generated a TOV star with R_Schw = "+str(R_Schw_TOV)+" , M = "+str(M_TOV)+" , M/R_Schw = "+str(M_TOV/R_Schw_TOV)+" .")
```

    (1051, 1051, 1051, 1051, 1051)
    Just generated a TOV star with R_Schw = 0.956568142523 , M = 0.14050303285288188 , M/R_Schw = 0.1468824086931645 .

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

## Step 4.a: Code Validation against `TOV.TOV_Solver` NRPy+ module \[Back to [top](#toc)\]
$$\label{code_validation__single_polytrope_eos}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for these TOV initial data between

1. this tutorial and
2. the NRPy+ [TOV.TOV_Solver](../edit/TOV/TOV_Solver.py) module.

```python
# Step 3: Code Validation against TOV.TOV_Solver module

import filecmp

import TOV.TOV_Solver as TOV
TOV.TOV_Solver("outputTOVpolytrope-validation.txt",rho_baryon_central=0.129285, \
               rho_poly_tab=[],Gamma_poly_tab=[2.0], K_poly_tab0=1.0, \
               verbose = False)

if filecmp.cmp('outputTOVpolytrope.txt',
               'outputTOVpolytrope-validation.txt') == False:
    print("ERROR: TOV initial data test FAILED!")
    exit(1)
else:
    print("TOV initial data test PASSED.")
```

    (1051, 1051, 1051, 1051, 1051)
    TOV initial data test PASSED.

## Step 4.b: Code Validation against an external code \[Back to [top](#toc)\]
$$\label{code_validation__piecewise_polytrope_eos}$$

We now validate our results against [Joshua Faber's TOV solver](https://ccrg.rit.edu/~jfaber/BNSID/TOV/tov_solver.C), which uses a $n_{\rm eos}=7$ piecewise polytropic EOS. Faber's code uses the following parameters:

|  $\rho_{j}$  | $\Gamma_{j}$ |   $K_{j}$   |
|--------------|--------------|-------------|
| 2.440619e+07 |   1.58425    | 6.80110e-09 |
| 3.783555e+11 |   1.28733    |     $-$     |
| 2.627847e+12 |   0.62223    |     $-$     |
|  $\rho_{3}$  |   1.35692    |     $-$     |
| 5.011872e+14 | $\Gamma_{4}$ |     $-$     |
|    1e+15     | $\Gamma_{5}$ |     $-$     |
|     $-$      | $\Gamma_{6}$ |     $-$     |

The values of $\left\{\rho_{0},\rho_{1},\rho_{2},\Gamma_{0},\Gamma_{1},\Gamma_{2},\Gamma_{3},K_{0}\right\}$ are meant to match the values from [table II in Read *et al.*](https://arxiv.org/pdf/0812.2163.pdf). However Faber's code uses as input

$$
\left\{
\begin{align}
\log_{10}\left(\rho_{0}\right) &= 7.3875\ ,\\
\log_{10}\left(\rho_{1}\right) &= 11.5779\ ,\\
\log_{10}\left(\rho_{2}\right) &= 12.4196\ ,\\
\log_{10}\left(\rho_{4}\right) &= 14.7\ ,\\
\log_{10}\left(\rho_{5}\right) &= 15.0\ ,\\
\end{align}
\right.
$$

causing slight discrepancy between the table above and [table II in Read *et al.*](https://arxiv.org/pdf/0812.2163.pdf). For example, using $\rho_{0}$ from our table above, we have

$$
\log_{10}\left(\rho_{0}^{\rm Faber}\right) = 7.387499987892311\ ,
$$

while using $\rho_{0}$ from Read *et al.* yields

$$
\log_{10}\left(\rho_{0}^{\rm Readetal}\right) = 7.387450338567011\ ,
$$

i.e.

$$
\frac{\left|\log_{10}\left(\rho_{0}^{\rm Faber}\right) - \log_{10}\left(\rho_{0}^{\rm Readetal}\right)\right|}{\log_{10}\left(\rho_{0}^{\rm Faber}\right)} = 6.720720863798443\times10^{-6}
$$

We will therefore use the table above instead, as to produce results that more closely resemble those from Faber's code.

```python
########################################################
# Piecewise Polytrope EOS - TOV Solver Validation Test #
########################################################
# Initial data table - Following Joshua Faber's TOV solver
#  (https://ccrg.rit.edu/~jfaber/BNSID/TOV/tov_solver.C)
# .--------------.---------.-------------.
# | log10(rho_j) | Gamma_j |   $K_{j}$   |
# .--------------.---------.-------------.
# |    7.3875    | 1.58425 | 6.80110e-09 |
# |    11.5779   | 1.28733 |      -      |
# |    12.4196   | 0.62223 |      -      |
# | log10(rho_3) | 1.35692 |      -      |
# |     14.7     | Gamma_4 |      -      |
# |     15.0     | Gamma_5 |      -      |
# |       -      | Gamma_6 |      -      |
# .--------------.---------.-------------.

# Set neos to 7
neos = 7

# Set rho_poly_tab using the table above
# Notice that rho_3 is not yet set
rho_poly_tab    = [0 for i in range(neos-1)]
rho_poly_tab[0] = 10.0**(7.3875)
rho_poly_tab[1] = 10.0**(11.5779)
rho_poly_tab[2] = 10.0**(12.4196)
rho_poly_tab[4] = 10.0**(14.7)
rho_poly_tab[5] = 10.0**(15.0)

# Set Gamma_poly_tab using the table above
# Notice that Gamma_4, Gamma_5, and Gamma_6
# are not yet set
Gamma_poly_tab    = [0 for i in range(neos)]
Gamma_poly_tab[0] = 1.58425
Gamma_poly_tab[1] = 1.28733
Gamma_poly_tab[2] = 0.62223
Gamma_poly_tab[3] = 1.35692

# Set K_poly_tab0 according to the table above
K_poly_tab0 = 6.80110e-09
```

Faber's code then takes as input $\left\{\log_{10}\left(p_{1}\right),\Gamma_{4},\Gamma_{5},\Gamma_{6}\right\}$, where

$$
\log_{10}\left(p_{3}\right) \equiv K_{j} + \Gamma_{j}\log_{10}\left(\rho_{3}\right)\ .
$$

Thus, we have the EOS

$$
p_{1} = K_{3}\rho_{3}^{\Gamma_{3}} \implies \rho_{3} = \left(\frac{p_{1}}{K_{3}}\right)^{1/\Gamma_{3}}\ .
$$

Now we use

$$
\boxed{K_{3} = K_{2}\rho_{2}^{\Gamma_{2}-\Gamma_{3}}}\ .
$$

This means that our computation of the EOS parameters will have to be done by hand.

```python
# Set up K_poly_tab
# Add K_0 then compute {K_1,K_2,K_3}
K_poly_tab    = [0 for i in range(neos)]
K_poly_tab[0] = K_poly_tab0
for j in range(1,4):
    K_poly_tab[j] = K_poly_tab[j-1]*rho_poly_tab[j-1]**(Gamma_poly_tab[j-1] - Gamma_poly_tab[j])

# User input: log10(p_1), Gamma_4, Gamma_5, Gamma_6
# We will be using here the SLy EOS parameters found
# in table III of Read et al.
#      (https://arxiv.org/pdf/0812.2163.pdf)
c                 = 2.99                                        # Speed of light in cm/s
log10_of_p1       = 34.380                                      # log10(p1), with p1 in dyne/cm^2
log10_of_p1_cgs   = log10_of_p1 + 2.0*np.log10(c)
p1                = 10.0**log10_of_p1_cgs                       # SLy EOS parameter p1
rho_poly_tab[3]   = (p1/K_poly_tab[3])**(1.0/Gamma_poly_tab[3]) # Last value needed for rho_poly_tab
Gamma_poly_tab[4] = 3.005                                       # SLy EOS parameter Gamma_1
Gamma_poly_tab[5] = 2.988                                       # SLy EOS parameter Gamma_2
Gamma_poly_tab[6] = 2.851                                       # SLy EOS parameter Gamma_3

# Compute {K_4,K_5,K_6}
for j in range(4,neos):
    K_poly_tab[j] = K_poly_tab[j-1]*rho_poly_tab[j-1]**(Gamma_poly_tab[j-1] - Gamma_poly_tab[j])

# Initialize the P_poly_tab array
P_poly_tab = [0 for i in range(neos-1)]

# Set up the EOS "struct" (named tuple)
from collections import namedtuple
eos_struct = namedtuple("eos_struct","neos rho_poly_tab Gamma_poly_tab K_poly_tab P_poly_tab")
eos = eos_struct(neos,rho_poly_tab,Gamma_poly_tab,K_poly_tab,P_poly_tab)

# Compute {P_0,P_1,P_2,P_3,P_4,P_5}
compute_P_poly_tab(eos)
```

```python
# Expected value of rho_poly_tab
rho_expected   = [2.440619068041981e+07,3.783554551232308e+11,2.627846540838499e+12,1.462304063288540e+14,5.011872336272714e+14,1.000000000000000e+15]
Ptab_expected  = [3.437368046964257e+03,8.524200720315686e+08,2.846981588373988e+09,6.649729273215710e+11,2.693802277376512e+13,2.122099366611129e+14]
Pcgs_expected  = [3.536226035543711e+00,8.930653667543710e+00,9.454384658543711e+00,1.182280396446974e+01,1.343036571573900e+01,1.432676571573900e+01]
Gamma_expected = [1.584250000000000e+00,1.287330000000000e+00,6.222299999999999e-01,1.356920000000000e+00,3.005000000000000e+00,2.988000000000000e+00,2.851000000000000e+00]
Kpoly_expected = [6.801100000000000e-09,1.061880536433024e-06,5.327665511164587e+01,3.999272773574940e-08,1.806615432455483e-31,3.211927366633627e-31,3.645572300283066e-29]

for i in range(eos.neos-1):
    print("rho_diff_"+str(i)+" = "+str(np.fabs(rho_expected[i] - eos.rho_poly_tab[i])/rho_expected[i]))

for i in range(eos.neos-1):
    print("P_diff_"+str(i)+" = "+str(np.fabs(Ptab_expected[i] - eos.P_poly_tab[i])/Ptab_expected[i]))

for i in range(eos.neos):
    print("Gamma_diff_"+str(i)+" = "+str(np.fabs(Gamma_expected[i] - eos.Gamma_poly_tab[i])/Gamma_expected[i]))

for i in range(eos.neos):
    print("Kpoly_diff_"+str(i)+" = "+str(np.fabs(Kpoly_expected[i] - eos.K_poly_tab[i])/Kpoly_expected[i]))
```

    rho_diff_0 = 1.5263710536567174e-16
    rho_diff_1 = 0.0
    rho_diff_2 = 1.8581041259897853e-16
    rho_diff_3 = 2.1131283202035885e+17
    rho_diff_4 = 1.247038946855552e-16
    rho_diff_5 = 0.0
    P_diff_0 = 1.9844282515284264e-15
    P_diff_1 = 1.118784444075047e-15
    P_diff_2 = 1.339909355649788e-15
    P_diff_3 = 3.225064554989832e+23
    P_diff_4 = 1.0
    P_diff_5 = 1.0
    Gamma_diff_0 = 0.0
    Gamma_diff_1 = 0.0
    Gamma_diff_2 = 0.0
    Gamma_diff_3 = 0.0
    Gamma_diff_4 = 0.0
    Gamma_diff_5 = 0.0
    Gamma_diff_6 = 0.0
    Kpoly_diff_0 = 0.0
    Kpoly_diff_1 = 3.9883627121539447e-16
    Kpoly_diff_2 = 1.3336849587705835e-16
    Kpoly_diff_3 = 0.0
    Kpoly_diff_4 = 1.0
    Kpoly_diff_5 = 1.0
    Kpoly_diff_6 = 1.0

```python
eos.K_poly_tab
```

    [6.8011e-09,
     1.0618805364330245e-06,
     53.276655111645866,
     3.99927277357494e-08,
     2.0415487123835052e-33,
     3.6296081954241814e-33,
     4.119644558459321e-31]

```python
# Let us plot our piecewise polytropic so that we can see what is happening
# First set the number of points in the plot
n_plot = 100000

# Then split the plot by the number of EOSs used in the code
rho_b  = [0 for i in range(eos.neos)]
P_cold = [[0 for j in range(n_plot/eos.neos)] for i in range(eos.neos)]

# Then set the plotting limits for each region
lim = [eos.rho_poly_tab[0]/50.0,
       eos.rho_poly_tab[0],
       eos.rho_poly_tab[1],
       eos.rho_poly_tab[2],
       eos.rho_poly_tab[3],
       eos.rho_poly_tab[4],
       eos.rho_poly_tab[5],
       eos.rho_poly_tab[5]*30.0]

# Then populate the rho_b arrays
for j in range(eos.neos):
    rho_b[j] = np.linspace(lim[j],lim[j+1],n_plot/eos.neos)

# Finally, populate the P array
for i in range(eos.neos):
    for j in range(n_plot/eos.neos):
        P_cold[i][j] = Polytrope_EOS__compute_P_cold_from_rhob(eos, rho_b[i][j])

import matplotlib.pyplot as plt
f = plt.figure(figsize=(12,8))
colors = ['blue',
          'green',
          'yellow',
          'orange',
          'magenta',
          'pink',
          'red']
for i in range(eos.neos):
    label=r'EOS #'+str(i+1)+r', parameters: $\left(\Gamma_'+str(i)+r',K_'+str(i)+r'\right)$'
    plt.plot(np.log10(rho_b[i]),np.log10(P_cold[i]),label=label,c=colors[i])
plt.legend()
plt.title(r"$\log_{10}\left(P_{\rm cold}\right)\times\log_{10}\left(\rho_{b}\right)$",fontsize=14)
plt.xlabel(r"$\log_{10}\left(\rho_{b}\right)$",fontsize=14)
plt.ylabel(r"$\log_{10}\left(P_{\rm cold}\right)$",fontsize=14)
plt.grid()
plt.xlim(14.5,15.1)
plt.savefig("P_cold__rho_b__piecewise_polytrope.png")
plt.close()

from IPython.display import Image
Image("P_cold__rho_b__piecewise_polytrope.png")
```

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

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

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

    [NbConvertApp] Converting notebook Tutorial-ADM_Initial_Data-TOV.ipynb to latex
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    /Users/werneck/anaconda3/lib/python3.7/site-packages/nbconvert/utils/pandoc.py:52: RuntimeWarning: You are using an unsupported version of pandoc (2.2.3.2).
    Your version must be at least (1.12.1) but less than (2.0.0).
    Refer to http://pandoc.org/installing.html.
    Continuing with doubts...
      check_pandoc_version()
    [NbConvertApp] Support files will be in Tutorial-ADM_Initial_Data-TOV_files/
    [NbConvertApp] Making directory Tutorial-ADM_Initial_Data-TOV_files
    [NbConvertApp] Writing 97247 bytes to Tutorial-ADM_Initial_Data-TOV.tex
    This is pdfTeX, Version 3.14159265-2.6-1.40.19 (TeX Live 2018) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.19 (TeX Live 2018) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.19 (TeX Live 2018) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode


### Core collapse

Heat trasnfer due to self interactions can lead to core collapse of SIDM halos, particularly at high cross sections. This process can be accelerated if the halos exist in a tidal field (i.e. are subhalos).

To run this notebook, you'll need to install the software package pyHalo https://github.com/dangilman/pyHalo

```python
from sidmpy.CrossSections.power_law import PowerLaw
from sidmpy.CrossSections.tchannel import TChannel
from sidmpy.CrossSections.velocity_independent import VelocityIndependentCrossSection
from sidmpy.core_collapse_timescale import *
from sidmpy.Solver.util import nfw_velocity_dispersion, nfw_circular_velocity
from matplotlib import cm
import matplotlib.pyplot as plt

plt.rcParams['axes.linewidth'] = 2.5
plt.rcParams['xtick.major.width'] = 2.5
plt.rcParams['xtick.major.size'] = 8
plt.rcParams['xtick.minor.size'] = 5
plt.rcParams['ytick.major.width'] = 2.5
plt.rcParams['ytick.major.size'] = 8
plt.rcParams['ytick.minor.size'] = 4
plt.rcParams['ytick.labelsize'] = 15
plt.rcParams['xtick.labelsize'] = 15
```

## Timescales for subhalos and field halos

Eventually, all SIDM halos should undergoe core collapse. The timescale for this process depends on the strength of the interaction cross section and other processes like tidal stripping. Depending on these factors, it can be much longer than, comparable, or shorter than the age of the Universe.

We will use a characteristic timescale for core collapse

\begin{equation}
t_c = \frac{1}{3 \rho_s \langle \sigma\left(v\right) v \rangle }
\end{equation}

### Core collapse timescale as a function of halo mass and redshift.

#### Note: this cell requires the software package pyHalo

```python
from pyHalo.Halos.lens_cosmo import LensCosmo
import numpy as np
from sidmpy.Solver.util import nfw_velocity_dispersion, nfw_mass_from_velocity_dispersion
import matplotlib.pyplot as plt
from sidmpy.sidmpy import solve_rho_with_interpolation, solve_sigmav_with_interpolation
help(evolution_timescale_scattering_rate)
lc = LensCosmo()
t1 = []
t2 = []
t3 = []

kwargs_cross_section_1 = {'norm': 35., 'v_ref': 10}
kwargs_cross_section_2 = {'norm': 20., 'v_ref': 25}
kwargs_cross_section_3 = {'norm': 5., 'v_ref': 100}
cross_section_1 = TChannel(**kwargs_cross_section_1)
cross_section_2 = TChannel(**kwargs_cross_section_2)
cross_section_3 = TChannel(**kwargs_cross_section_3)
cmap = cm.gist_heat
z = 0.
m = np.logspace(6, 10, 100)

for mi in m:

    c = lc.NFW_concentration(mi, z, scatter=False)
    rhos, rs, _ = lc.NFW_params_physical(mi, c, z)
    v1 = solve_sigmav_with_interpolation(mi, z, 0., 'TCHANNEL', kwargs_cross_section_1)
    v2 = solve_sigmav_with_interpolation(mi, z, 0., 'TCHANNEL', kwargs_cross_section_2)
    v3 = solve_sigmav_with_interpolation(mi, z, 0., 'TCHANNEL', kwargs_cross_section_3)

    t0 = evolution_timescale_scattering_rate(rhos, v1, cross_section_1, rescale=1.)
    t1.append(t0)

    t0 = evolution_timescale_scattering_rate(rhos, v2, cross_section_2, rescale=1.)
    t2.append(t0)

    t0 = evolution_timescale_scattering_rate(rhos, v3, cross_section_3, rescale=1.)
    t3.append(t0)

fig = plt.figure(1)
fig.set_size_inches(6, 6)
ax = plt.subplot(111)
ax.plot(np.log10(m), t1, color='k', lw=5)
ax.plot(np.log10(m), t2, color=cmap(0.4), lw=5)
ax.plot(np.log10(m), t3, color=cmap(0.8), lw=5)
ax.set_xlabel(r'$M_{200} \left[M_{\odot}\right]$', fontsize=18)
ax.set_ylabel('evolution timescale '+r'$t_0 \ \left[\rm{Gyr}\right]$', fontsize=16)
ax.set_xticks([6, 7, 8, 9, 10])
ax.set_xticklabels([r'$10^{6}$', r'$10^{7}$', r'$10^{8}$', r'$10^{9}$', r'$10^{10}$'], fontsize=20)
ax.set_yticks([0., 0.5, 1., 1.5, 2, 2.5, 3])
ax.set_ylim(0, 3)
ax.set_xlim(6,10)
ax.legend(frameon=False, fontsize=14)
```

```python

```


```python
# Header starts here.
from sympy.physics.units import *
from sympy import *

# Rounding:
import decimal
from decimal import Decimal as DX
from copy import deepcopy
def iso_round(obj, pv, rounding=decimal.ROUND_HALF_EVEN):
    import sympy
    """
    Rounding acc. to DIN EN ISO 80000-1:2013-08
    place value = Rundestellenwert
    """
    assert pv in set([
        # place value   #  round to:
        1,              #  1
        0.1,            #  1st digit after decimal
        0.01,           #  2nd
        0.001,          #  3rd
        0.0001,         #  4th
        0.00001,        #  5th
        0.000001,       #  6th
        0.0000001,      #  7th
        0.00000001,     #  8th
        0.000000001,    #  9th
        0.0000000001,   # 10th
        ])
    objc = deepcopy(obj)
    try:
        tmp = DX(str(float(objc)))
        objc = tmp.quantize(DX(str(pv)), rounding=rounding)
    except:
        for i in range(len(objc)):
            tmp = DX(str(float(objc[i])))
            objc[i] = tmp.quantize(DX(str(pv)), rounding=rounding)
    return objc

# LateX:
kwargs = {}
kwargs["mat_str"] = "bmatrix"
kwargs["mat_delim"] = ""
# kwargs["symbol_names"] = {FB: "F^{\mathsf B}", }

# Units:
(k, M, G ) = ( 10**3, 10**6, 10**9 )
(mm, cm) = ( m/1000, m/100 )
Newton = kg*m/s**2
Pa     = Newton/m**2
MPa    = M*Pa
GPa    = G*Pa
kN     = k*Newton
deg    = pi/180

half = S(1)/2

# Header ends here.
#
# https://colab.research.google.com/github/kassbohm/wb-snippets/blob/master/ipynb/HTM_03/Selbst/4.2_cc.ipynb

# 1:
mu, l = var("mu, l")

(l1, l2, l3) = (2*l, l, 2*l/3)
(m1, m2, m3) = (mu*l1, mu*l2, mu*l3)
(xi1, xi2, xi3) = (m1*l1**2/12, m2*l2**2/12, m3*l3**2/12)

# 2:
IC1 = xi1 * Matrix([
    [1,0,0],
    [0,0,0],
    [0,0,1],
    ])

IC2 = xi2 * Matrix([
    [0,0,0],
    [0,1,0],
    [0,0,1],
    ])

IC3 = xi3 * Matrix([
    [1,0,0],
    [0,1,0],
    [0,0,0],
    ])

pprint("\nIC1, IC2, IC3 / ( μl³ ):")
for x in [IC1, IC2, IC3]:
    tmp = x / (mu*l**3)
    pprint(tmp)

# 3:
M, dx, dy, dz = var("M, dx, dy, dz")

(dx1, dy1, dz1) = (0, l1/2, 0)
(dx2, dy2, dz2) = (l2/2, l1, 0)
(dx3, dy3, dz3) = (l2, l1, l3/2)

tmp = Matrix([
    [dy**2 + dz**2,0,0],
    [0,dx**2 + dz**2,0],
    [0,0,dx**2 + dy**2],
    ])
tmp+= Matrix([
    [     0,-dx*dy,-dx*dz],
    [-dy*dx,     0,-dy*dz],
    [-dz*dx,-dz*dy,     0],
    ])
tmp*=M

# 4:
pprint("\nI01, I02, I03 / ( μl³ ):")
IO1 = IC1 + tmp.subs([(M,m1), (dx, dx1), (dy, dy1), (dz, dz1)])
IO2 = IC2 + tmp.subs([(M,m2), (dx, dx2), (dy, dy2), (dz, dz2)])
IO3 = IC3 + tmp.subs([(M,m3), (dx, dx3), (dy, dy3), (dz, dz3)])
for x in [IO1, IO2, IO3]:
    tmp = x / (mu*l**3)
    pprint(tmp)

# 5:
pprint("\nIO / ( μl³ ):")
IO = IO1 + IO2 + IO3
IO /= mu*l**3
pprint(IO)

# 6:
pprint("\nEigenvalues of IO / (kg m²):")
ev = IO.eigenvals()
for v in ev:
    # take real part only:
    tmp = re(v)
    # multiply with μl³:
    tmp *= 4*kg/meter * (4*meter/10)**3
    tmp /= kg *meter*meter
    tmp = iso_round(tmp,0.001)
    pprint(tmp)

pprint("\nb:")
b = Matrix([1, 2, S(2)/3])
b /= b.norm()
pprint(b)

pprint("\nIOb / ( μl³ ):")
tmp = IO*b
pprint(tmp)
pprint("\nJ / ( μl³ ):")
J = b.transpose()*IO*b
J = J[0]
pprint(J)
pprint("\nJ / (kg m²):")
tmp = J
# multiply with μl³:
tmp *= 4*kg/meter * (4*meter/10)**3
tmp /= kg * meter*meter
tmp = iso_round(tmp,0.0001)
pprint(tmp)

# IC1, IC2, IC3 / ( μl³ ):
# ⎡2/3  0   0 ⎤
# ⎢           ⎥
# ⎢ 0   0   0 ⎥
# ⎢           ⎥
# ⎣ 0   0  2/3⎦
# ⎡0   0     0  ⎤
# ⎢             ⎥
# ⎢0  1/12   0  ⎥
# ⎢             ⎥
# ⎣0   0    1/12⎦
# ⎡2/81   0    0⎤
# ⎢             ⎥
# ⎢ 0    2/81  0⎥
# ⎢             ⎥
# ⎣ 0     0    0⎦
#
# I01, I02, I03 / ( μl³ ):
# ⎡8/3  0   0 ⎤
# ⎢           ⎥
# ⎢ 0   0   0 ⎥
# ⎢           ⎥
# ⎣ 0   0  8/3⎦
# ⎡4   -1    0  ⎤
# ⎢             ⎥
# ⎢-1  1/3   0  ⎥
# ⎢             ⎥
# ⎣0    0   13/3⎦
# ⎡224             ⎤
# ⎢───   -4/3  -2/9⎥
# ⎢ 81             ⎥
# ⎢                ⎥
# ⎢       62       ⎥
# ⎢-4/3   ──   -4/9⎥
# ⎢       81       ⎥
# ⎢                ⎥
# ⎣-2/9  -4/9  10/3⎦
#
# IO / ( μl³ ):
# ⎡764             ⎤
# ⎢───   -7/3  -2/9⎥
# ⎢ 81             ⎥
# ⎢                ⎥
# ⎢       89       ⎥
# ⎢-7/3   ──   -4/9⎥
# ⎢       81       ⎥
# ⎢                ⎥
# ⎣-2/9  -4/9  31/3⎦
#
# Eigenvalues of IO / (kg m²):
# 0.119
# 2.659
# 2.563
#
# b:
# ⎡3/7⎤
# ⎢   ⎥
# ⎢6/7⎥
# ⎢   ⎥
# ⎣2/7⎦
#
# IOb / ( μl³ ):
# ⎡ 374 ⎤
# ⎢ ─── ⎥
# ⎢ 189 ⎥
# ⎢     ⎥
# ⎢-5/27⎥
# ⎢     ⎥
# ⎢ 52  ⎥
# ⎢ ──  ⎥
# ⎣ 21  ⎦
#
# J / ( μl³ ):
# 88
# ──
# 63
#
# J / (kg m²):
# 0.3576

```


# A comparison of Monte Carlo and polynomial chaos approaches for the sensitivity analysis of the $A1$, $B1$, $B2$ and $C2$ test functions

**Samuele Lo Piano**,
[s.lopiano@gmail.com](mailto:s.lopiano@gmail.com)

# The test functions

In two previous notebooks
[g_function](interactive_g_function.ipynb) and
[gstar_function](interactive_gstar_function.ipynb), the potential of the
polynomial chaos expansions (with the `chaospy` package) for sensitivity
analysis has been disclosed. Its performance has been benchmarked against the
Monte Carlo methods may be used to approximate the variance based Sobol
sensitivity indices.

In this notebook, the analysis is extended to the
other [Kucherenko's test
functions](https://www.sciencedirect.com/science/article/pii/S0951832010002437),
for which the usefulness of low-discrepancy sequences towards reaching a higher
convergence pace has been already showcased in [this
notebook](testfunction3.ipynb). We adopt herein the same nomenclature in the
interest of coherency. Functions A2, B3 and C1 are excluded from this notebook
as they have already been implicitly covered in the one dedicated to the
[g_function](interactive_g_function.ipynb). One can get these different cases by
simply tuning the additive $a_i$ coefficients.

$$
\begin{equation}
A1: f(X) = \sum_{j=1}^{k}
(-1)^j \, \prod_{l=1}^{j} x_l \label{eq:1} \tag{1}
\end{equation}
$$

$$
\begin{equation}
B1: f(X) = \prod_{j=1}^{k} \frac{k-{4x_j}}{k-0.5} \label{eq:2} \tag{2}
\end{equation}
$$

$$
\begin{equation}
B2: f(X) = \left(1+\frac{1}{k}\right)^k
\prod_{j=1}^{k} \sqrt{x_j} \label{eq:3} \tag{3}
\end{equation}
$$

$$
\begin{equation}
C2: f(X)
= 2^k
\prod_{j=1}^{k} x_j \label{eq:4} \tag{4}
\end{equation}
$$

All the input
factors $X_i$ are assumed to be uniformly distributed in the interval $[0,1]$.
The number of factors *k* here amounts to six, but it can be varied as the
reader pleases, although it should be never lower than three to produce a
meaningful sensitivity analysis.

Run the first cell to initialise plotting and
printing modules for later use (and system settings).

```
# ipython magic
%matplotlib notebook
%load_ext autoreload
%autoreload 2
import os, sys, inspect
# Use this if you want to include modules from a subfolder
cmd_subfolder = os.path.realpath(os.path.abspath(os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0],"python_source")))
if cmd_subfolder not in sys.path:
     sys.path.insert(0, cmd_subfolder)

%run matplotlib_header

from present_output import print_vectors_relerror, print_3vectors_relerror
```

As discussed in [the dedicated notebook on test
functions](testfunctions3.ipynb), for functions of type _A_ the sensitivity
indices of the parameters decrease across the series: every parameter is less
influential on the output variance than the previous. Functions of type _A_ have
been precisely designed to represent an asymmetric case of unequal importance
across parameters. Conversely, _B_ and _C_ functions are symmetric with all
parameters equally important. The former do no have significant higher-order
interactions across parameters, having therefore $S_j \approx S_{T_j}$. I.e.,
most of the variance can be explained with the individual parameters first-order
effects. This is not the case for _C_ type functions, for which higher-order
interactions play a prominent role and the inequality $S_{T_j} > S_j$ holds.
The
functions introduced in equations ([1](#eq:1)),  ([2](#eq:2)), ([3](#eq:3))
and
([4](#eq:4)) can be loaded from an external module. Non-standard modules
such as
`sobol_seq` and `chaospy` are required. However, they can be easily
installed by
typing
        `pip install sobol_seq`
from your terminal (Linux)
or from the
anaconda-prompt environment in case you use Anaconda package
manager.

```
# import modules
import numpy as np
import pandas as pd
import sobol_seq
from string import ascii_lowercase
from testfunctions import create_dict, A1, B1, B2, C2, AE_dic, AEF_dic
```

The analytical expression of the sensitivity indices can be analytically
computed for all the functions and can be adopted for benchmarking the
performance of the sensitivity indices computed.

As regards function
$A_1$,

$$
\begin{equation}
E_{k2} =
\frac{1}{6}\left(1-\left(\frac{1}{6}\right)^k\right)+\frac{4}{15}\left((-1)^{k+1}\left(\frac{1}{2}\right)^k+\left(\frac{1}{3}\right)^k\right)
\label{eq:5} \tag{5}
\end{equation}
$$

$$
\begin{equation}
V(k) =
\frac{1}{10}\left(\frac{1}{3}\right)^k+\frac{1}{18}-\frac{1}{9}\left(\frac{1}{2}\right)^{2k}+(-1)^{k+1}\left(\frac{2}{45}\right)\left(\frac{1}{2}\right)^k
\label{eq:6} \tag{6}
\end{equation}
$$

$$
\begin{equation}
E_j(j) =
\frac{1}{6}\left(1-\left(\frac{1}{3}\right)^j\right)+\frac{4}{15}\left((-1)^{j+1}\left(\frac{1}{2}\right)^j+\left(\frac{1}{3}\right)^j\right)
\
\label{eq:7} \tag{7}
\end{equation}
$$

$$
\begin{equation}
T_1 =
\frac{1}{2}\left(\frac{1}{3}^{j-1}\right)\left(1-\left(\frac{1}{3}\right)^{k-j}\right)
\
\label{eq:8} \tag{8}
\end{equation}
$$

$$
\begin{equation}
T_2 =
\frac{1}{2}\left(\frac{1}{3}^j-\frac{1}{3}^k\right) \
\label{eq:9} \tag{9}
\end{equation}
$$

$$
\begin{equation}
T_3 =
\frac{3}{5}\left(4\left(\frac{1}{3}\right)^{k+1}+(-1)^{j+k+1}\left(\frac{1}{2}\right)^{k-j+2}+\left(\frac{1}{3}\right)^{j+1}\right)
\
\label{eq:10} \tag{10}
\end{equation}
$$

$$
\begin{equation}
T_4 =
\frac{1}{5}\left((-1)^{j+2}\frac{1}{3}\left(\frac{1}{2}\right)^{j-2}-4\left(\frac{1}{3}\right)^{j+1}\right)
\
\label{eq:11} \tag{11}
\end{equation}
$$

$$
\begin{equation}
T_5 =
\frac{1}{5}\left((-1)^{k+1}\frac{1}{3}\left(\frac{1}{2}\right)^{k-2}+(-1)^{k+j}\left(\frac{1}{3}\right)^{j+1}\left(\frac{1}{2}\right)^{k-j-2}\right)
\
\label{eq:12} \tag{12}
\end{equation}
$$

From which one can derive the
expression for the total-order sensitivity indices $S_{T_j}$ as:

$$
\begin{equation}
S_{T_j}
= \frac{E_{k2}(k)-E_j(j)-\frac{1}{4}(T_1(j)-2T_2(j)+T_3(j)-T_4(j)-T_5(j))}{V(k)}
\
\label{eq:13} \tag{13}
\end{equation}
$$

As well as first order indices:

$$
\begin{equation}
S_j =
\frac{E_{k2}(k)-E_j(j)-\frac{1}{4}(T_1(j)-2T_2(j)+T_3(j)-T_4(j)-T_5(j))\left(12\left(1-\left(\frac{1}{2}\right)^k\right)^2\right)}{V(k)*27\left(\frac{1}{2}-\frac{4}{5}\left(\frac{-1}{2}\right)^k+\frac{3}{10}\left(\frac{1}{3}\right)^k\right)}
\
\label{eq:14} \tag{14}
\end{equation}
$$

The analytical expressions for the
other functions are luckily less complicated. For function $B_1$:

$$
\begin{equation}
p(j) =
12(k-0.5)^2
\label{eq:15} \tag{15}
\end{equation}
$$

$$
\begin{equation}
S_{T_j} =
\frac{(p(j)+1)^k}{(p(j)+1)((p(j)+1)^k-p(j)^k)}
\label{eq:16} \tag{16}
\end{equation}
$$

$$
\begin{equation}
S_j =
\frac{1}{(p(j)\left(1+\left(\frac{1}{p(j)}\right)^k\right)}
\label{eq:17}
\tag{17}
\end{equation}
$$

And for function $B_2$:

$$
\begin{equation}
S_{T_j} =
\frac{(k+1)^(2k-2)}{(k+1)^2k-(k^2+2k)^k}
\label{eq:18} \tag{18}
\end{equation}
$$

$$
\begin{equation}
S_j =
\frac{1}{(k^2+2k)\left(1+\left(\frac{1}{k^2+2k}\right)\right)^{k-1}}
\label{eq:19} \tag{19}
\end{equation}
$$

Finally, for function $C_2$:

$$
\begin{equation}
S_{T_j} = \frac{4^{k-1}}{4^k-3^k}
\label{eq:20} \tag{20}
\end{equation}
$$

## Comparison of the sensitivity indices computed with the
spectral expansions and the Monte Carlo simulations

In this section, we
benchmark the performance of the two approaches against the analytical values
dependent on the sample size explored. One can firstly select one of the four
test functions. This comparison has been undertaken for the
[g_function](interactive_g_function.ipynb) and
[gstar_function](interactive_gstar_function.ipynb) in dedicated notebooks.

```
def f(x):
    return x

import ipywidgets as widgets
chart = widgets.interactive(f, x=['A1','B1','B2','C2'])
display(chart)

k = 6
```

The number of parameters _k_ has been here set as three, but the interested
reader can vary it as s/he pleases. However, one should bear in mind a
meaningful sensitivity analysis needs at least three parameters.

```
f = eval(chart.children[0].value)
```

```
Siv = []
for ke in AEF_dic.keys(): #update the functions analytical values
    if ke[2:4] == chart.children[0].value:
        Siv.append(AEF_dic[ke])
Si = np.array(Siv)

STv = []
for kef in AE_dic.keys():
    if kef[2:4] == chart.children[0].value:
        STv.append(AE_dic[kef])
ST = np.array(STv)
```

```
# chaospy f-function with sliders
import chaospy as cp

if not 'jpdf' in globals():
    jpdf = cp.Iid(cp.Uniform(),k) #the joint pdf
    print('Create the joint pdf')

def update_f(**kwargs):
    NsPC=kwargs['NsPC']
    del kwargs['NsPC']
    NsMC=kwargs['NsMC']
    del kwargs['NsMC']

    polynomial_order=kwargs['polynomial_order']
    del kwargs['polynomial_order']

    prm_cat=int(len(kwargs)/k)
    prms=np.zeros((prm_cat,k))

    import re
    r = re.compile("([a-zA-Z]+)([0-9]+)")

    for key, value in kwargs.items(): #find indx and value for a_prms
        pre,post=r.match(key).groups()
        cat_idx=strings.index(pre)
        prms[cat_idx,int(post)]=value

    ## Monte Carlo update
    print('Number of samples for Monte Carlo: ', NsMC)
    X_mc=sobol_seq.i4_sobol_generate(2*k,NsMC)
    A = X_mc[:,:k] # Generate A and B matrices
    B = X_mc[:,k:]
    f_A_sample = f(pd.DataFrame(A)).values
    f_B_sample = f(pd.DataFrame(B)).values

    exp_mc = np.mean(f_A_sample)
    std_mc = np.std(f_A_sample,ddof=0)
    print("Statistics Monte Carlo\n")
    print('\n        E(Y)  |  std(Y) \n')
    print('mc  : {:2.5f} | {:2.5f}'.format(float(exp_mc), std_mc))

    A_mixed = []
    for j in range(k):
        A_mixed.append(A.copy())
        A_mixed[j][:,j] = B[:,j]

    f_A_mixed = np.array([f(pd.DataFrame(As)) for As in A_mixed])
    S_mc = np.array([np.mean(f_B_sample*(f_A_mixed[j]-f_A_sample))/np.var(f_A_sample,ddof=0) for j in range(k)])
    S_tmc = np.array([0.5*np.mean((f_A_mixed[j]-f_A_sample)**2)/np.var(f_A_sample,ddof=0) for j in range(k)])

    Xpc=sobol_seq.i4_sobol_generate(k,NsPC)
    print('Number of samples: ',NsPC)

    f_sample=f(pd.DataFrame(Xpc)).values

    poly = cp.orth_ttr(polynomial_order, jpdf)
    approx = cp.fit_regression(poly, Xpc.T, f_sample)

    exp_pc = cp.E(approx, jpdf)
    std_pc = cp.Std(approx, jpdf)
    print("Statistics polynomial chaos\n")
    print('\n        E(Y)  |  std(Y) \n')
    print('pc  : {:2.5f} | {:2.5f}'.format(float(exp_pc), std_pc))
    S_pc = cp.Sens_m(approx, cp.Iid(cp.Uniform(),k)) #Si from chaospy
    S_tpc = cp.Sens_t(approx, cp.Iid(cp.Uniform(),k)) #Total effect sensitivity index from chaospy

    row_labels= ['S_'+str(idx) for idx in range(k)]
    #col_labels=['Chaospy','Analytical','Error (%)']
    col_labels=['Monte Carlo','Err (%)','PolyChaos','Err (%)']

    print("\nFirst Order Indices")
#    print_vectors_relerror(S_pc,Si,col_labels,row_labels,[3,3,0])
    print_3vectors_relerror(S_mc,S_pc, Si, col_labels, row_labels, [3,0,3,0])

    print("\n\nTotal Effect Indices")
    row_labels= ['St_'+str(idx) for idx in range(k)]
#    print_vectors_relerror(S_tpc,ST,col_labels,row_labels,[3,3,0])
    print_3vectors_relerror(S_tmc,S_tpc, ST, col_labels, row_labels, [3,0,3,0])

my_sliders=[]

my_sliders.append(widgets.IntSlider(min=500,max=5100,step=250,value=500,description='NsPC')) #add slider for samples
my_sliders.append(widgets.IntSlider(min=500,max=5100,step=250,value=500,description='NsMC')) #add slider for samples
my_sliders.append(widgets.IntSlider(description='polynomial_order', min=1,max=6,value=4)) # add slider for polynomial order

slider_dict = {slider.description:slider for slider in my_sliders} #add the sliders in the dictionary

ui_left = widgets.VBox(my_sliders[0::3])
ui_mid  = widgets.VBox(my_sliders[1::3])
ui_right = widgets.VBox(my_sliders[2::3])
ui=widgets.HBox([ui_left,ui_mid,ui_right])

out=widgets.interactive_output(update_f, slider_dict)
display(ui,out)

# end chaospy G-function with sliders
```

What approach converges the quickest towards the analytical value? What indices
are the hardest to compute and why is this the case? Do you think this
occurrence may negatively impact on the quality of the analysis in a real case
study?


# Tire Swing Problem
<bk>
Meagan Rittmanic and Corinne Wilklow
<bk>
9 December 2018

```python
# Configure Jupyter so figures appear in the notebook
%matplotlib inline

# Configure Jupyter to display the assigned value after an assignment
%config InteractiveShell.ast_node_interactivity='last_expr_or_assign'

# import functions from the modsim.py module
from modsim import *

# import numpy
import numpy as np

# import LaTeX
from sympy import *

from sympy.printing import latex

def show(expr, show_latex=False):
    """Display a SymPy expression.

    expr: SymPy expression
    show_latex: boolean
    """
    if show_latex:
        print(latex(expr))
    return expr

init_printing()

#import pictures
import matplotlib.pyplot as plt

import matplotlib.image as mpimg

print('done')
```

    done

## What is the max force that someone can push a child on a tire swing before the child falls?

We modeled the system of a single child on a tire swing to determine the max force that someone can push the tire swing without the child falling. To do this we gathered average weight data and average grip strength data for children ages 6 to 12.

### Model

First, some equations that will be important to the implementation of our model:

$\frac{d\theta}{dt} = \omega $  Where $\theta$ is angular position, and $\omega$ is angular velocity.
<bk>
$\frac{d\omega}{dt} = \alpha = \frac{\omega^2}{2\theta} = \frac{d^2\theta}{dt^2} = \frac{\tau}{I_{system}}$ Where $\tau$ is the torque and $I_{system}$ is the rotational inertia of the system.
<bk>
$F_{hold} + F_{sf} = \frac{mv^2}{r} $ Where $F_{hold}$ is the force that the child holds on with, $F_{sf}$ is the force of static friction acting on the child and $\frac{mv^2}{r}$ is the centripetal force pulling the child off the tire swing.

Second, a schematic of our model:

```python
plt.figure(figsize=(5,5))

myImage = mpimg.imread("schematic.JPG")

myImage = plt.imshow(myImage)

plt.axis('off')

plt.show()
```

To implement our model, we started by importing the various units that we needed.

```python
m = UNITS.meter
s = UNITS.second
kg = UNITS.kilogram
degree = UNITS.degree
N = UNITS.newton
rad = UNITS.radian
```

radian

We created a params object to hold the parameters that we would need for our project, including units. Everything that was in units of Newtons we converted to kilograms * meters/second^2  because the library we are using has trouble with that conversion.
<bk>
Our data on the grip strength of children of various ages from [this study](https://ajot.aota.org/article.aspx?articleid=1884114), and the average mass of children of different ages comes from [here](https://www.disabled-world.com/calculators-charts/height-weight-teens.php). We found the coefficient of friction between the clothing of the child (fabric) and the tire (rubber) from [this study](http://www.ircobi.org/wordpress/downloads/irc16-asia/pdf-files/48.pdf), which claimed that the coefficient between fabric and rubber was approximately 1. We found that the mass of the tire swing was about 120 lbs, or 54.54 kg, from [here](http://www.energyjustice.net/files/tires/files/scrapchn.html). Our tire size data came from [this website](https://www.tirerack.com/tires/tiretech/techpage.jsp?techid=53), where we chose a one meter, because it is handy for physics calculations, and also because it is within the higher part of the range. We assumed the radius of the child to be a little less than our radius, so we estimated it to be about 0.09 m. We chose the initial duration of the push to be one full rotation of the tire swing.
<bk>
For our first run of our model, we also decided to use the average grip strength and mass of a 6-year-old child.

```python
params = Params(child_strength = 132 * kg*m/(s**2),
                mass_child = 16 * kg,
                mu_friction = 1,
                g = 9.81 * m/(s**2),
                mass_tire_swing = 54.54 * kg,
                radius_child = .09 * m,
                duration_push = np.pi*2 * rad,
                radius_inside_tire = 0.5 * m,
                radius_outside_tire = 1 *m,
                t_end = 30 * s)
```

      values

      child_strength
      132.0 kilogram * meter / second ** 2

      mass_child
      16 kilogram

      mu_friction
      1

      g
      9.81 meter / second ** 2

      mass_tire_swing
      54.54 kilogram

      radius_child
      0.09 meter

      duration_push
      6.283185307179586 radian

      radius_inside_tire
      0.5 meter

      radius_outside_tire
      1 meter

      t_end
      30 second

We then created our system, creating a state with $\theta$, or the angular position, and $\omega$, or angular velocity. We also calculated the force of friction based on the mass of our child, the value of the acceleration due to gravity, and the coefficient due to friction between our child and the tire. We took the average of the inner and outer radii of our tire to be the radius to the center of mass of our child from the center of rotation, the center of the tire swing. We also added the force of the starting push.

```python
def make_system(theta, omega, F_push, params):
    """Make a system object.

    params: Params object with angle, velocity, mu,
             duration, g, masses, radii, and t_end

    returns: System object
    """
    unpack(params)

    init = State (theta=theta * rad, omega=omega * rad/s)
    F_friction = mass_child * g * mu_friction
    radius_to_child = (radius_inside_tire + radius_outside_tire)/2
    F_push = F_push * kg *m/(s**2)

    return System(params, F_friction=F_friction, radius_to_child = radius_to_child, F_push = F_push, init=init)
```

```python
system = make_system(0, 0, 20, params)
```

      values

      child_strength
      132.0 kilogram * meter / second ** 2

      mass_child
      16 kilogram

      mu_friction
      1

      g
      9.81 meter / second ** 2

      mass_tire_swing
      54.54 kilogram

      radius_child
      0.09 meter

      duration_push
      6.283185307179586 radian

      radius_inside_tire
      0.5 meter

      radius_outside_tire
      1 meter

      t_end
      30 second

      F_friction
      156.96 kilogram * meter / second ** 2

      radius_to_child
      0.75 meter

      F_push
      20.0 kilogram * meter / second ** 2

      init
      theta               0 radian
omega    0.0 radi...

Next, we created a slope function, finding the change in theta and change in omega throughout the duration of the push.  In order to find alpha, our change in angular velocity, we used the equation $I = \tau\alpha$, with $I$ being rotational inertia of the system, $\tau$ being torque, and $\alpha$ being the change in rotational velocity. We found the rotational inertia of the tire-swing-child system by finding the sum of the rotational inertia of the tire swing and the rotational inertia of the child (which we approximated to be a cylinder) using the parallel axis theorem when finding the rotational inertia of the child because the child was not rotating about its center of mass.

```python
def slope_func(state, t, system):
    '''Make a slope function that
        takes into account the rotational inertia of both objects,
        applies a torque,
        and returns the change in theta and change in omega'''

    theta, omega = state
    unpack (system)

    Torque = radius_outside_tire * F_push

    I_tire = .5*mass_tire_swing*(radius_inside_tire**2 + radius_outside_tire**2)
    I_child = .5*mass_child*radius_child**2 + mass_child * (radius_to_child)**2
    I_system = I_tire + I_child

    alpha = Torque / I_system

    d_theta_dt = omega
    d_omega_dt = alpha

    return d_theta_dt, d_omega_dt
```

We made an event function to stop the push when the desired duration of the push was reached. For our first run, this duration was one full rotation.

```python
def event_func(state, t, system):

    '''Make an event function that stops the ODE solver from running when the push ends'''

    theta, omega = state
    unpack (system)

    return theta - duration_push
```

We created an error function to find the force of the push that would make the force of static friction between the child’s clothing and the tire added to the strength of the child holding onto the tire swing equal to $\frac{mv^2}{r}$, with $m$ being the mass of the child, $v$ being the linear velocity of the child, and $r$ being the radius between the center of rotation and the child. We used this equation because it is the version of Newton’s second law that can be applied to rotational kinematics, in which a force changes the direction of linear velocity as opposed to the magnitude of linear velocity. For each run of the error function, we made a system with the given force of the push, and then we ran our slope function through our ODE solver, returning the final value of rotational velocity. We then multiplied that value by the radius to the child to find our final linear velocity. Finally, we found the maximum strength that the child can exert to keep themselves on the swing by multiplying their grip strength by two hands. We chose to assume that their legs on the inside of the tire swing do not hold them on the swing because if the child’s hands slip, they have essentially fallen off the swing for our purposes.
<bk>
We ran our error function with a test force of 5 Newtons, printing $F_{push}$ and the final difference for each run of the function.

```python
def error_func(F_push, system):

    '''Define an error func that outputs the difference between the combined grip strength and frictional force
    holding child to the tire swing and total force acting on them'''

    system = make_system(0, 0, F_push, params)
    results, details = run_ode_solver(system, slope_func,
                                  events=event_func)
    omega_final = get_last_value(results.omega)

    v_final = omega_final*radius_to_child / s

    print (F_push)
    print (system.child_strength + system.F_friction - ( system.mass_child * v_final**2)/radius_to_child)

    return system.child_strength*2 + system.F_friction - ( system.mass_child * v_final**2)/radius_to_child
```

```python
error_func(5, system)
```

    5
    271.4874148339358 kilogram * meter / second ** 2

403.4874148339358 kilogram meter/second2

We ran fsolve using our error function, finding that, for a 6-year-old child and a push that lasts one whole rotation, the maximum average force one can push the swing at is 120.5 Newtons.

```python
fsolve(error_func, 5, system) * N
```

    5
    271.4874148339358 kilogram * meter / second ** 2
    [5]
    271.4874148339358 kilogram * meter / second ** 2
    [5.]
    271.4874148339358 kilogram * meter / second ** 2
    [5.]
    271.4874148339358 kilogram * meter / second ** 2
    [5.00000007]
    271.4874145735741 kilogram * meter / second ** 2
    [120.46303289]
    -132.00012031357875 kilogram * meter / second ** 2
    [120.46299846]
    -131.99999999999994 kilogram * meter / second ** 2

[120.46299846] newton

In order to further iterate on our system, we imported a Dataframe with data referring to child average mass and grip strength with respect to age.

```python
child_data = pd.read_csv('child_data.csv', index_col = 0)
```

      Mass
      Grip

      Age

      6
      15.873
      132.165

      8
      25.737
      165.985

      10
      31.973
      219.385

      12
      40.703
      246.975

We added units to the data, with kilograms as the unit of mass and Newtons (or kilograms * meters / second^2) as the unit of force.

```python
child_data.Mass.units = kg
child_data.Grip.units = kg * m / s**2
```

We created a second error function identical to our first except that it does not print the force of the push or the final difference.

```python
def error_func_2(F_push, system):

    '''Define an error func that outputs the difference between the combined grip strength and frictional force
    holding child to the tire swing and total force acting on them'''
    system = make_system(0, 0, F_push, params)

    results, details = run_ode_solver(system, slope_func,
                                  events=event_func)
    omega_final = get_last_value(results.omega)

    v_final = omega_final*radius_to_child / s

    return system.child_strength*2 + system.F_friction - ( system.mass_child * v_final**2)/radius_to_child
```

In order to run through the values for each age, we created an array with the four different ages we had access to. In the for loop, we made a new params object that changed the parameters so they aligned with the mass and grip strength of that age of child. We then made a system and ran our fsolve function, finding the age and consequent maximum force for each age.

```python
array = np.array([6, 8, 10, 12])
for Age in array:
    params = Params(params, child_strength = child_data.loc[Age]["Grip"] * kg*m/s**2, child_mass = child_data.loc[Age]["Mass"] * kg)
    system = make_system(0, 0, F_push, params)
    Force = fsolve(error_func_2, 300, system)
    print (Age, Force)
```

    6 [120.55743211]
    8 [139.91346883]
    10 [170.47563207]
    12 [186.26608307]

### Results
<bk>
We graphed our findings, modeling maximum force of push with respect to age for a push with the duration of one full rotation.

```python
Age = array
print(type(Age))
Force = np.array([120.56, 139.91, 170.47, 186.27])
print(type(Force))
plt.plot(Age, Force, 'o')
decorate(xlabel = 'Age (years)',
        ylabel = 'Force (Newtons)',
        title = 'Max Force Before Fall')
```

We then graphed our findings with respect to mass of child so that people who had unusually large or small children for that age might be able to more accurately define which maximum force of push they should use.

```python
Mass = np.array([15.87301587, 25.73696145, 31.97278912, 40.70294785])
plt.plot(Mass, Force, 'o')
decorate (title = 'Max Force Before Fall Based on Mass',
         xlabel = 'Mass of Child (kilograms)',
         ylabel = 'Force (Newtons)')
```

We then assumed a different scenario, taking into account how someone might push in a single, quicker push as opposed to a push that lasted an entire rotation. We ran the simulation again with the push lasting only a quarter rotation, more of a single, quick push as opposed to a continuous push.

```python
system.duration_push = np.pi/2
array = np.array([6, 8, 10, 12])
for Age in array:
    params = Params(params, child_strength = child_data.loc[Age]["Grip"] * kg*m/s**2,
                    child_mass = child_data.loc[Age]["Mass"] * kg,
                   duration_push = np.pi/2 * rad)
    system = make_system(0, 0, F_push, params)
    Force = fsolve(error_func_2, 300, system)
    print (Age, Force)
Age = array
Force = np.array([482.23, 559.65, 681.90, 745.06])
plt.plot(Age, Force, 'o')
decorate(xlabel = 'Age (years)',
        ylabel = 'Force (Newtons)',
        title = 'Max Force Before Fall based on Age')
```

We also graphed this data based on mass.

```python
Mass = np.array([15.87301587, 25.73696145, 31.97278912, 40.70294785])
plt.plot(Mass, Force, 'o')
decorate (title = 'Max Force Before Fall Based on Mass',
         xlabel = 'Mass of Child (kilograms)',
         ylabel = 'Force (Newtons)')
```

### Interpretation
<bk>
To get our information about perspective in reference to various weights, we used these sources:
<bk>
[20 lbs](http://shailah.blogspot.com/2011/11/random-things-that-weigh-20-lbs.html)
<bk>
[30 lbs](https://2fluffy2long.wordpress.com/what-things-weigh-weight-loss-reference/)
<bk>
[Converting Newtons to Pounds](http://www.kylesconverter.com/force/newtons-to-pounds--force)
<bk>
[Lager weights](http://www.ewtaz.com/images/uploads/average-weight-furniture-2.pdf)
<bk>
In order to make this data more accessible, 120.56 Newtons is 27.1 pounds of force, 139.91 Newtons is 31.45 pounds of force, 170.48 Newtons is 38.33 pounds of force, and 186.27 Newtons is 41.88 pounds of force.
<bk>
To put that in perspective, 20 pounds is about the weight of a small to medium sized dog, a regular car tire, a kitchen aid mixer, or a one-year-old. 30 pounds is about the weight of an average toddler or a cinder block. 36 pounds is about the size of a microwave. For a full rotation, a fairly strong push might send a kid falling off.
<bk>
For the second run, 482.23 Newtons is 108.41 pounds, 559.65 Newtons is about 125.81 pounds of force, 681.9 Newtons is 153.3 pounds of force, and 745.06 Newtons is 167.5 pounds.
<bk>
To put that in perspective, 80 pounds is about the weight of a medium-sized TV, 100 pounds is about the weight of a toilet, 150 pounds is about the weight of a 3-cushion sofa. For this short of a push, it would be very difficult to push hard enough to get a kid to fall off.

```python

```