```python
from sympy.physics.matrices import mgamma
import sympy.abc as greek
from sympy import *

init_printing()
```

**Shut up and calculate.** - *Fedor Herbut* quoting *Niels Bohr*.

```python
def commutator(a, b, p = -1):
    return a * b + p * b * a
```

Applying *commutator(mgamma(mu), mgamma(nu), p = 1)* to, for $\mu, \nu = 0, 1, 2, 3$ it is easily be verified that ($3^{rd}$ problem):

\begin{equation}
\{\gamma_{\mu}, \gamma_{\nu}\} = 2 g_{\mu \nu}
\end{equation}

Function *mgamma_2(mu, nu, gamma)* returns $\gamma^{\mu \nu}$, defined as:

\begin{equation}
\gamma^{\mu \nu} = \frac{1}{2}[\gamma^{\mu}, \gamma^{\nu}]
\end{equation}

Function *mgamma_3(mu, nu, rho, gamma)* returns $\gamma^{\mu \nu \rho}$, defined as:

\begin{equation}
\gamma^{\mu \nu \rho} = \frac{1}{2}[\gamma^{\mu \nu}, \gamma^{\rho}]
\end{equation}

Function *mgamma_4(mu, nu, rho, sigma, gamma)* returns $\gamma^{\mu \nu \rho \sigma}$, defined as:

\begin{equation}
\gamma^{\mu \nu \rho \sigma} = \frac{1}{2}[\gamma^{\mu \nu \rho}, \gamma^{\sigma}]
\end{equation}

Function *mgamma_n(indices, order, gamma)* returns general tensor of arbitrary order defined recursively.

Here argument *gamma* can specify any matrix to evaluate *mgamma_n* functions. Particularly, they can be $\gamma^{\mu}$ (as set by default), $\gamma_{\mu}$, or any function of single number.

```python
def mgamma_2(mu, nu, gamma = mgamma):
    return (gamma(mu) * gamma(nu) - gamma(nu) * gamma(mu)) / 2

def mgamma_3(mu, nu, rho, gamma = mgamma):
    return (mgamma_2(mu, nu, gamma = gamma) * gamma(rho) - gamma(rho) * mgamma_2(mu, nu, gamma = gamma)) / 2

def mgamma_4(mu, nu, rho, sigma, gamma = mgamma):
    return (mgamma_3(mu, nu, rho, gamma = gamma) * gamma(sigma) - \
            gamma(sigma) * mgamma_3(mu, nu, rho, gamma = gamma)) / 2

def mgamma_n(indices, order, gamma = mgamma):
    if order == 1:
        return gamma(indices[0])
    else:
        return commutator(mgamma_n(indices[1:], order-1, gamma = mgamma), gamma(indices[0]))/2

def mgamma_(mu):
    if mu == 0:
        return mgamma(0)
    else:
        return -mgamma(mu)
```

## Statement of problem

With tensors defined as above, find contraction:

\begin{equation}
\gamma^{\mu \nu \rho \sigma} \gamma_{\rho \sigma}
\end{equation}

Let:

\begin{equation}
G^{\mu \nu \rho \sigma}_{\alpha \beta} = \gamma^{\mu \nu \rho \sigma} \gamma_{\alpha \beta}
\end{equation}

```python
def G(mu, nu, rho, sigma, alpha, beta):
    return mgamma_4(mu, nu, rho, sigma) * mgamma_2(alpha, beta, gamma = mgamma_)

#More general tensor than G

def G_(upper, lower):
    return mgamma_n(upper, len(upper)) * mgamma_n(lower, len(lower), gamma = mgamma_)

def zeroMatrix(n, m):
    return Matrix([[0 for i in range(m)] for j in range(n)])
```

```python
def contractor(mu, nu):
    tensor = zeroMatrix(4, 4)

    for rho in range(4):
        for sigma in range(4):
            tensor += G(mu, nu, rho, sigma, rho, sigma)

    return tensor

```

```python
for mu in range(4):
    for nu in range(4):
        print('mu = ', mu, ' nu = ', nu)
        pprint(contractor(mu, nu))
```

    mu =  0  nu =  0
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  0  nu =  1
    ⎡0   0   0   -4⎤
    ⎢              ⎥
    ⎢0   0   -4  0 ⎥
    ⎢              ⎥
    ⎢0   -4  0   0 ⎥
    ⎢              ⎥
    ⎣-4  0   0   0 ⎦
    mu =  0  nu =  2
    ⎡ 0     0    0    4⋅ⅈ⎤
    ⎢                    ⎥
    ⎢ 0     0   -4⋅ⅈ   0 ⎥
    ⎢                    ⎥
    ⎢ 0    4⋅ⅈ   0     0 ⎥
    ⎢                    ⎥
    ⎣-4⋅ⅈ   0    0     0 ⎦
    mu =  0  nu =  3
    ⎡0   0  -4  0⎤
    ⎢            ⎥
    ⎢0   0  0   4⎥
    ⎢            ⎥
    ⎢-4  0  0   0⎥
    ⎢            ⎥
    ⎣0   4  0   0⎦
    mu =  1  nu =  0
    ⎡0  0  0  4⎤
    ⎢          ⎥
    ⎢0  0  4  0⎥
    ⎢          ⎥
    ⎢0  4  0  0⎥
    ⎢          ⎥
    ⎣4  0  0  0⎦
    mu =  1  nu =  1
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  1  nu =  2
    ⎡4⋅ⅈ   0     0    0  ⎤
    ⎢                    ⎥
    ⎢ 0   -4⋅ⅈ   0    0  ⎥
    ⎢                    ⎥
    ⎢ 0    0    4⋅ⅈ   0  ⎥
    ⎢                    ⎥
    ⎣ 0    0     0   -4⋅ⅈ⎦
    mu =  1  nu =  3
    ⎡0  -4  0  0 ⎤
    ⎢            ⎥
    ⎢4  0   0  0 ⎥
    ⎢            ⎥
    ⎢0  0   0  -4⎥
    ⎢            ⎥
    ⎣0  0   4  0 ⎦
    mu =  2  nu =  0
    ⎡ 0    0     0   -4⋅ⅈ⎤
    ⎢                    ⎥
    ⎢ 0    0    4⋅ⅈ   0  ⎥
    ⎢                    ⎥
    ⎢ 0   -4⋅ⅈ   0    0  ⎥
    ⎢                    ⎥
    ⎣4⋅ⅈ   0     0    0  ⎦
    mu =  2  nu =  1
    ⎡-4⋅ⅈ   0    0     0 ⎤
    ⎢                    ⎥
    ⎢ 0    4⋅ⅈ   0     0 ⎥
    ⎢                    ⎥
    ⎢ 0     0   -4⋅ⅈ   0 ⎥
    ⎢                    ⎥
    ⎣ 0     0    0    4⋅ⅈ⎦
    mu =  2  nu =  2
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  2  nu =  3
    ⎡ 0   4⋅ⅈ   0    0 ⎤
    ⎢                  ⎥
    ⎢4⋅ⅈ   0    0    0 ⎥
    ⎢                  ⎥
    ⎢ 0    0    0   4⋅ⅈ⎥
    ⎢                  ⎥
    ⎣ 0    0   4⋅ⅈ   0 ⎦
    mu =  3  nu =  0
    ⎡0  0   4  0 ⎤
    ⎢            ⎥
    ⎢0  0   0  -4⎥
    ⎢            ⎥
    ⎢4  0   0  0 ⎥
    ⎢            ⎥
    ⎣0  -4  0  0 ⎦
    mu =  3  nu =  1
    ⎡0   4  0   0⎤
    ⎢            ⎥
    ⎢-4  0  0   0⎥
    ⎢            ⎥
    ⎢0   0  0   4⎥
    ⎢            ⎥
    ⎣0   0  -4  0⎦
    mu =  3  nu =  2
    ⎡ 0    -4⋅ⅈ   0     0  ⎤
    ⎢                      ⎥
    ⎢-4⋅ⅈ   0     0     0  ⎥
    ⎢                      ⎥
    ⎢ 0     0     0    -4⋅ⅈ⎥
    ⎢                      ⎥
    ⎣ 0     0    -4⋅ⅈ   0  ⎦
    mu =  3  nu =  3
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦

### Solution

Since $G^{\mu \nu \rho \sigma}_{\rho \sigma}$ is second rank tensor, and as many physics book authors just use guessing method to solve some problems, we will guess the solution to be $g \cdot \gamma^{\mu \nu}$, where $g$ is constant to be determined.

```python
def guess():
    for mu in range(4):
        for nu in range(4):
            print('mu = ', mu, ' nu = ', nu)
            pprint(contractor(mu, nu))
            pprint(mgamma_2(mu, nu))
```

```python
guess()
```

    mu =  0  nu =  0
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  0  nu =  1
    ⎡0   0   0   -4⎤
    ⎢              ⎥
    ⎢0   0   -4  0 ⎥
    ⎢              ⎥
    ⎢0   -4  0   0 ⎥
    ⎢              ⎥
    ⎣-4  0   0   0 ⎦
    ⎡0  0  0  1⎤
    ⎢          ⎥
    ⎢0  0  1  0⎥
    ⎢          ⎥
    ⎢0  1  0  0⎥
    ⎢          ⎥
    ⎣1  0  0  0⎦
    mu =  0  nu =  2
    ⎡ 0     0    0    4⋅ⅈ⎤
    ⎢                    ⎥
    ⎢ 0     0   -4⋅ⅈ   0 ⎥
    ⎢                    ⎥
    ⎢ 0    4⋅ⅈ   0     0 ⎥
    ⎢                    ⎥
    ⎣-4⋅ⅈ   0    0     0 ⎦
    ⎡0  0   0  -ⅈ⎤
    ⎢            ⎥
    ⎢0  0   ⅈ  0 ⎥
    ⎢            ⎥
    ⎢0  -ⅈ  0  0 ⎥
    ⎢            ⎥
    ⎣ⅈ  0   0  0 ⎦
    mu =  0  nu =  3
    ⎡0   0  -4  0⎤
    ⎢            ⎥
    ⎢0   0  0   4⎥
    ⎢            ⎥
    ⎢-4  0  0   0⎥
    ⎢            ⎥
    ⎣0   4  0   0⎦
    ⎡0  0   1  0 ⎤
    ⎢            ⎥
    ⎢0  0   0  -1⎥
    ⎢            ⎥
    ⎢1  0   0  0 ⎥
    ⎢            ⎥
    ⎣0  -1  0  0 ⎦
    mu =  1  nu =  0
    ⎡0  0  0  4⎤
    ⎢          ⎥
    ⎢0  0  4  0⎥
    ⎢          ⎥
    ⎢0  4  0  0⎥
    ⎢          ⎥
    ⎣4  0  0  0⎦
    ⎡0   0   0   -1⎤
    ⎢              ⎥
    ⎢0   0   -1  0 ⎥
    ⎢              ⎥
    ⎢0   -1  0   0 ⎥
    ⎢              ⎥
    ⎣-1  0   0   0 ⎦
    mu =  1  nu =  1
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  1  nu =  2
    ⎡4⋅ⅈ   0     0    0  ⎤
    ⎢                    ⎥
    ⎢ 0   -4⋅ⅈ   0    0  ⎥
    ⎢                    ⎥
    ⎢ 0    0    4⋅ⅈ   0  ⎥
    ⎢                    ⎥
    ⎣ 0    0     0   -4⋅ⅈ⎦
    ⎡-ⅈ  0  0   0⎤
    ⎢            ⎥
    ⎢0   ⅈ  0   0⎥
    ⎢            ⎥
    ⎢0   0  -ⅈ  0⎥
    ⎢            ⎥
    ⎣0   0  0   ⅈ⎦
    mu =  1  nu =  3
    ⎡0  -4  0  0 ⎤
    ⎢            ⎥
    ⎢4  0   0  0 ⎥
    ⎢            ⎥
    ⎢0  0   0  -4⎥
    ⎢            ⎥
    ⎣0  0   4  0 ⎦
    ⎡0   1  0   0⎤
    ⎢            ⎥
    ⎢-1  0  0   0⎥
    ⎢            ⎥
    ⎢0   0  0   1⎥
    ⎢            ⎥
    ⎣0   0  -1  0⎦
    mu =  2  nu =  0
    ⎡ 0    0     0   -4⋅ⅈ⎤
    ⎢                    ⎥
    ⎢ 0    0    4⋅ⅈ   0  ⎥
    ⎢                    ⎥
    ⎢ 0   -4⋅ⅈ   0    0  ⎥
    ⎢                    ⎥
    ⎣4⋅ⅈ   0     0    0  ⎦
    ⎡0   0  0   ⅈ⎤
    ⎢            ⎥
    ⎢0   0  -ⅈ  0⎥
    ⎢            ⎥
    ⎢0   ⅈ  0   0⎥
    ⎢            ⎥
    ⎣-ⅈ  0  0   0⎦
    mu =  2  nu =  1
    ⎡-4⋅ⅈ   0    0     0 ⎤
    ⎢                    ⎥
    ⎢ 0    4⋅ⅈ   0     0 ⎥
    ⎢                    ⎥
    ⎢ 0     0   -4⋅ⅈ   0 ⎥
    ⎢                    ⎥
    ⎣ 0     0    0    4⋅ⅈ⎦
    ⎡ⅈ  0   0  0 ⎤
    ⎢            ⎥
    ⎢0  -ⅈ  0  0 ⎥
    ⎢            ⎥
    ⎢0  0   ⅈ  0 ⎥
    ⎢            ⎥
    ⎣0  0   0  -ⅈ⎦
    mu =  2  nu =  2
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  2  nu =  3
    ⎡ 0   4⋅ⅈ   0    0 ⎤
    ⎢                  ⎥
    ⎢4⋅ⅈ   0    0    0 ⎥
    ⎢                  ⎥
    ⎢ 0    0    0   4⋅ⅈ⎥
    ⎢                  ⎥
    ⎣ 0    0   4⋅ⅈ   0 ⎦
    ⎡0   -ⅈ  0   0 ⎤
    ⎢              ⎥
    ⎢-ⅈ  0   0   0 ⎥
    ⎢              ⎥
    ⎢0   0   0   -ⅈ⎥
    ⎢              ⎥
    ⎣0   0   -ⅈ  0 ⎦
    mu =  3  nu =  0
    ⎡0  0   4  0 ⎤
    ⎢            ⎥
    ⎢0  0   0  -4⎥
    ⎢            ⎥
    ⎢4  0   0  0 ⎥
    ⎢            ⎥
    ⎣0  -4  0  0 ⎦
    ⎡0   0  -1  0⎤
    ⎢            ⎥
    ⎢0   0  0   1⎥
    ⎢            ⎥
    ⎢-1  0  0   0⎥
    ⎢            ⎥
    ⎣0   1  0   0⎦
    mu =  3  nu =  1
    ⎡0   4  0   0⎤
    ⎢            ⎥
    ⎢-4  0  0   0⎥
    ⎢            ⎥
    ⎢0   0  0   4⎥
    ⎢            ⎥
    ⎣0   0  -4  0⎦
    ⎡0  -1  0  0 ⎤
    ⎢            ⎥
    ⎢1  0   0  0 ⎥
    ⎢            ⎥
    ⎢0  0   0  -1⎥
    ⎢            ⎥
    ⎣0  0   1  0 ⎦
    mu =  3  nu =  2
    ⎡ 0    -4⋅ⅈ   0     0  ⎤
    ⎢                      ⎥
    ⎢-4⋅ⅈ   0     0     0  ⎥
    ⎢                      ⎥
    ⎢ 0     0     0    -4⋅ⅈ⎥
    ⎢                      ⎥
    ⎣ 0     0    -4⋅ⅈ   0  ⎦
    ⎡0  ⅈ  0  0⎤
    ⎢          ⎥
    ⎢ⅈ  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  ⅈ⎥
    ⎢          ⎥
    ⎣0  0  ⅈ  0⎦
    mu =  3  nu =  3
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦

It seems reasonable to guess that $g = -4$. Lets verify that.

```python
def verify_1():
    for mu in range(4):
        for nu in range(4):
            print('mu = ', mu, ' nu = ', nu)
            pprint(contractor(mu, nu) + 4 * mgamma_2(mu, nu))

def verify_2():
    for mu in range(4):
        for nu in range(4):
            print('mu = ', mu, ' nu = ', nu, contractor(mu, nu) == -4 * mgamma_2(mu, nu))
```

```python
verify_1()
```

    mu =  0  nu =  0
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  0  nu =  1
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  0  nu =  2
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  0  nu =  3
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  1  nu =  0
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  1  nu =  1
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  1  nu =  2
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  1  nu =  3
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  2  nu =  0
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  2  nu =  1
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  2  nu =  2
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  2  nu =  3
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  3  nu =  0
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  3  nu =  1
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  3  nu =  2
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦
    mu =  3  nu =  3
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦

```python
verify_2()
```

    mu =  0  nu =  0 True
    mu =  0  nu =  1 True
    mu =  0  nu =  2 True
    mu =  0  nu =  3 True
    mu =  1  nu =  0 True
    mu =  1  nu =  1 True
    mu =  1  nu =  2 True
    mu =  1  nu =  3 True
    mu =  2  nu =  0 True
    mu =  2  nu =  1 True
    mu =  2  nu =  2 True
    mu =  2  nu =  3 True
    mu =  3  nu =  0 True
    mu =  3  nu =  1 True
    mu =  3  nu =  2 True
    mu =  3  nu =  3 True

So, solution is:

\begin{equation}
\gamma^{\mu \nu \rho \sigma} \gamma_{\rho \sigma} = -4 \gamma^{\mu \nu}
\end{equation}

Other reasonable guesses might be $\gamma^{\nu \mu}$, which is just $-\gamma^{\mu \nu}$. On the other hand, $\gamma^{\mu}\gamma^{\nu}$ might also be intuitive guess, but can easily be verified that it is not correct. For example, take $\mu = \nu$, we know ($3^{rd}$ problem) that $\{\gamma^{\mu}, \gamma^{\nu}\} = 2g^{\mu\nu}$, so:

```python
pprint(mgamma(0) * mgamma(0))
pprint(contractor(0, 0))
```

    ⎡1  0  0  0⎤
    ⎢          ⎥
    ⎢0  1  0  0⎥
    ⎢          ⎥
    ⎢0  0  1  0⎥
    ⎢          ⎥
    ⎣0  0  0  1⎦
    ⎡0  0  0  0⎤
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎢0  0  0  0⎥
    ⎢          ⎥
    ⎣0  0  0  0⎦

To conclude, this guessing method might not be best choice all the time, but having a way to evaluate all components of given tensor is deffinitely useful. :)


# NRPy+'s Reference Metric Interface

## Author: Zach Etienne
### Formatting improvements courtesy Brandon Clark

### NRPy+ Source Code for this module: [reference_metric.py](../edit/reference_metric.py)

## Introduction:
### Why use a reference metric? Benefits of choosing the best coordinate system for the problem

When solving a partial differential equation on the computer, it is useful to first pick a coordinate system well-suited to the geometry of the problem. For example, if we are modeling a spherically-symmetric star, it would be hugely wasteful to model the star in 3-dimensional Cartesian coordinates ($x$,$y$,$z$). This is because in Cartesian coordinates, we would need to choose high sampling in all three Cartesian directions. If instead we chose to model the star in spherical coordinates  ($r$,$\theta$,$\phi$), so long as the star is centered at $r=0$, we would not need to model the star with more than one point in the $\theta$ and $\phi$ directions!

A similar argument holds for stars that are *nearly* spherically symmetric. Such stars may exhibit density distributions that vary slowly in $\theta$ and $\phi$ directions (e.g., isolated neutron stars or black holes). In these cases the number of points needed to sample the angular directions will still be much smaller than in the radial direction.

Thus choice of an appropriate reference metric may directly mitigate the [Curse of Dimensionality](https://en.wikipedia.org/wiki/Curse_of_dimensionality).

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

This notebook is organized as follow

1. [Step 1](#define_ref_metric): Defining a reference metric, [`reference_metric.py`](../edit/reference_metric.py)
1. [Step 2](#define_geometric): Defining geometric quantities, **`ref_metric__hatted_quantities()`**
1. [Step 3](#prescribed_ref_metric): Prescribed reference metrics in [`reference_metric.py`](../edit/reference_metric.py)
    1. [Step 3.a](#sphericallike): Spherical-like coordinate systems
        1. [Step 3.a.i](#spherical): **`reference_metric::CoordSystem = "Spherical"`**
        1. [Step 3.a.ii](#sinhspherical): **`reference_metric::CoordSystem = "SinhSpherical"`**
        1. [Step 3.a.iii](#sinhsphericalv2): **`reference_metric::CoordSystem = "SinhSphericalv2"`**
    1. [Step 3.b](#cylindricallike): Cylindrical-like coordinate systems
        1. [Step 3.b.i](#cylindrical): **`reference_metric::CoordSystem = "Cylindrical"`**
        1. [Step 3.b.ii](#sinhcylindrical): **`reference_metric::CoordSystem = "SinhCylindrical"`**
        1. [Step 3.b.iii](#sinhcylindricalv2): **`reference_metric::CoordSystem = "SinhCylindricalv2"`**
    1. [Step 3.c](#cartesianlike): Cartesian-like coordinate systems
        1. [Step 3.c.i](#cartesian): **`reference_metric::CoordSystem = "Cartesian"`**
        1. [Step 3.c.ii](#sinhcartesian): **`reference_metric::CoordSystem = "SinhCartesian"`**
    1. [Step 3.d](#prolatespheroidal): Prolate spheroidal coordinates
        1. [Step 3.d.i](#symtp): **`reference_metric::CoordSystem = "SymTP"`**
        1. [Step 3.d.ii](#sinhsymtp): **`reference_metric::CoordSystem = "SinhSymTP"`**
1. [Step 4](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Defining a reference metric, [`reference_metric.py`](../edit/reference_metric.py) \[Back to [top](#toc)\]
$$\label{define_ref_metric}$$

***Note that currently only orthogonal reference metrics of dimension 3 or fewer are supported. This can be extended if desired.***

NRPy+ assumes all curvilinear coordinate systems map directly from a uniform, Cartesian numerical grid with coordinates $(x,y,z)$=(`xx[0]`,`xx[1]`,`xx[2]`). Thus when defining reference metrics, all defined coordinate quantities must be in terms of the `xx[]` array. As we will see, this adds a great deal of flexibility

For example,  [**reference_metric.py**](../edit/reference_metric.py) requires that the *orthogonal coordinate scale factors* be defined. As described [here](https://en.wikipedia.org/wiki/Curvilinear_coordinates), the $i$th scale factor is the positive root of the metric element $g_{ii}$. In ordinary spherical coordinates $(r,\theta,\phi)$, with line element $ds^2 = g_{ij} dx^i dx^j = dr^2+ r^2 d \theta^2 + r^2 \sin^2\theta \ d\phi^2$, we would first define
* $r = xx_0$
* $\theta = xx_1$
* $\phi = xx_2$,

so that the scale factors are defined as
* `scalefactor_orthog[0]` = $1$
* `scalefactor_orthog[1]` = $r$
* `scalefactor_orthog[2]` = $r \sin \theta$

Here is the corresponding code:

```python
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import NRPy_param_funcs as par   # NRPy+: parameter interface
import reference_metric as rfm   # NRPy+: Reference metric support

r = rfm.xx[0]
th = rfm.xx[1]
ph = rfm.xx[2]

rfm.scalefactor_orthog[0] = 1
rfm.scalefactor_orthog[1] = r
rfm.scalefactor_orthog[2] = r*sp.sin(th)

# Notice that the scale factor will be given
#    in terms of the fundamental Cartesian
#    grid variables, and not {r,th,ph}:
print("r*sin(th) = "+str(rfm.scalefactor_orthog[2]))
```

    r*sin(th) = xx0*sin(xx1)

Next suppose we wish to modify our radial coordinate $r(xx_0)$ to be an exponentially increasing function, so that our numerical grid $(xx_0,xx_1,xx_2)$ will map to a spherical grid with radial grid spacing ($\Delta r$) that *increases* with $r$. Generally we will find it useful to define $r(xx_0)$ to be an odd function, so let's choose

$$r(xx_0) = a \sinh(xx_0/s),$$

where $a$ is an overall radial scaling factor, and $s$ denotes the scale (in units of $xx_0$) over which exponential growth will take place. In our implementation below, note that we use the relation

$$\sinh(x) = \frac{e^x - e^{-x}}{2},$$

as SymPy finds it easier to evaluate exponentials than hyperbolic trigonometric functions.

```python
a,s = sp.symbols('a s',positive=True)
xx0_rescaled = rfm.xx[0] / s
r = a*(sp.exp(xx0_rescaled) - sp.exp(-xx0_rescaled))/2

# Must redefine the scalefactors since 'r' has been updated!
rfm.scalefactor_orthog[0] = 1
rfm.scalefactor_orthog[1] = r
rfm.scalefactor_orthog[2] = r*sp.sin(th)

print(rfm.scalefactor_orthog[2])
```

    a*(exp(xx0/s) - exp(-xx0/s))*sin(xx1)/2

Often we will find it useful to also define the appropriate mappings from (`xx[0]`,`xx[1]`,`xx[2]`) to Cartesian coordinates (for plotting purposes) and ordinary spherical coordinates (e.g., in case initial data when solving a PDE are naturally written in spherical coordinates). For this purpose, reference_metric.py also declares lists **`xx_to_Cart[]`** and **`xxSph[]`**, which in this case are defined as

```python
rfm.xxSph[0] = r
rfm.xxSph[1] = th
rfm.xxSph[2] = ph

rfm.xx_to_Cart[0] = r*sp.sin(th)*sp.cos(ph)
rfm.xx_to_Cart[1] = r*sp.sin(th)*sp.sin(ph)
rfm.xx_to_Cart[2] = r*sp.cos(th)

# Here we show off SymPy's pretty_print()
#   and simplify() functions. Nice, no?
sp.pretty_print(sp.simplify(rfm.xx_to_Cart[0]))
```

                            ⎛xx₀⎞
    a⋅sin(xx₁)⋅cos(xx₂)⋅sinh⎜───⎟
                            ⎝ s ⎠

# Step 2: Define geometric quantities, `ref_metric__hatted_quantities()` \[Back to [top](#toc)\]
$$\label{define_geometric}$$

Once `scalefactor_orthog[]` has been defined, the function **`ref_metric__hatted_quantities()`** within [reference_metric.py](../edit/reference_metric.py) can be called to define a number of geometric quantities useful for solving PDEs in curvilinear coordinate systems.

Adopting the notation of [Baumgarte, Montero, Cordero-Carrión, and Müller, PRD 87, 044026 (2012)](https://arxiv.org/abs/1211.6632), geometric quantities related to the reference metric are named "hatted" quantities, . For example, the reference metric is defined as $\hat{g}_{ij}$=`ghatDD[i][j]`:

```python
rfm.ref_metric__hatted_quantities()

sp.pretty_print(sp.Matrix(rfm.ghatDD))
```

    ⎡1           0                         0              ⎤
    ⎢                                                     ⎥
    ⎢                     2                               ⎥
    ⎢      ⎛ xx₀    -xx₀ ⎞                                ⎥
    ⎢      ⎜ ───    ─────⎟                                ⎥
    ⎢    2 ⎜  s       s  ⎟                                ⎥
    ⎢   a ⋅⎝ℯ    - ℯ     ⎠                                ⎥
    ⎢0  ───────────────────                0              ⎥
    ⎢            4                                        ⎥
    ⎢                                                     ⎥
    ⎢                                          2          ⎥
    ⎢                           ⎛ xx₀    -xx₀ ⎞           ⎥
    ⎢                           ⎜ ───    ─────⎟           ⎥
    ⎢                         2 ⎜  s       s  ⎟     2     ⎥
    ⎢                        a ⋅⎝ℯ    - ℯ     ⎠ ⋅sin (xx₁)⎥
    ⎢0           0           ─────────────────────────────⎥
    ⎣                                      4              ⎦

In addition to $\hat{g}_{ij}$, **`ref_metric__hatted_quantities()`** also provides:
* The rescaling "matrix" `ReDD[i][j]`, used for separating singular (due to chosen coordinate system) pieces of smooth rank-2 tensor components from the smooth parts, so that the smooth parts can be used within temporal and spatial differential operators.
* Inverse reference metric: $\hat{g}^{ij}$=`ghatUU[i][j]`.
* Reference metric determinant: $\det\left(\hat{g}_{ij}\right)$=`detgammahat`.
* First and second derivatives of the reference metric: $\hat{g}_{ij,k}$=`ghatDD_dD[i][j][k]`; $\hat{g}_{ij,kl}$=`ghatDD_dDD[i][j][k][l]`
* Christoffel symbols associated with the reference metric, $\hat{\Gamma}^i_{jk}$ = `GammahatUDD[i][j][k]` and their first derivatives $\hat{\Gamma}^i_{jk,l}$ = `GammahatUDD_dD[i][j][k][l]`

For example, the Christoffel symbol $\hat{\Gamma}^{xx_1}_{xx_2 xx_2}=\hat{\Gamma}^1_{22}$ is given by `GammahatUDD[1][2][2]`:

```python
sp.pretty_print(sp.simplify(rfm.GammahatUDD[1][2][2]))
```

    -sin(2⋅xx₁)
    ────────────
         2

Given the trigonometric identity $2\sin(x)\cos(x) = \sin(2x)$, notice that the above expression is equivalent to Eq. 18 of [Baumgarte, Montero, Cordero-Carrión, and Müller, PRD 87, 044026 (2012)](https://arxiv.org/abs/1211.6632). This is expected since the sinh-radial spherical coordinate system is equivalent to ordinary spherical coordinates in the angular components.

# Step 3: Prescribed reference metrics in [`reference_metric.py`](../edit/reference_metric.py) \[Back to [top](#toc)\]
$$\label{prescribed_ref_metric}$$

One need not manually define scale factors or other quantities for reference metrics, as a number of prescribed reference metrics are already defined in [reference_metric.py](../edit/reference_metric.py). These can be accessed by first setting the parameter **reference_metric::CoordSystem** to one of the following, and then calling the function **`rfm.reference_metric()`**.

```python
import indexedexp as ixp    # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import grid as gri          # NRPy+: Functions having to do with numerical grids

# Step 0a: Initialize parameters
thismodule = __name__
par.initialize_param(par.glb_param("char", thismodule, "CoordSystem", "Spherical"))

# Step 0b: Declare global variables
xx = gri.xx
xx_to_Cart = ixp.zerorank1(DIM=4) # Must be set in terms of xx[]s
Cart_to_xx = ixp.zerorank1(DIM=4) # Must be set in terms of xx[]s
Cartx,Carty,Cartz = sp.symbols("Cartx Carty Cartz", real=True)
Cart = [Cartx,Carty,Cartz]
xxSph  = ixp.zerorank1(DIM=4) # Must be set in terms of xx[]s
scalefactor_orthog = ixp.zerorank1(DIM=4) # Must be set in terms of xx[]s
have_already_called_reference_metric_function = False

CoordSystem = par.parval_from_str("reference_metric::CoordSystem")
M_PI,M_SQRT1_2 = par.Cparameters("#define",thismodule,["M_PI","M_SQRT1_2"],"")

global xxmin
global xxmax

global UnitVectors
UnitVectors = ixp.zerorank2(DIM=3)
```

We will find the following plotting function useful for analyzing coordinate systems in which the radial coordinate is rescaled.

```python
def create_r_of_xx0_plots(CoordSystem, r_of_xx0,rprime_of_xx0):
    import matplotlib.pyplot as plt  # matplotlib: Python module specializing in plotting capabilities
    plt.clf()
    Nr = 20
    dxx0 = 1.0 / float(Nr)
    xx0s    = []
    rs      = []
    deltars = []
    rprimes = []
    for i in range(Nr):
        xx0 = (float(i) + 0.5)*dxx0
        xx0s.append(xx0)
        rs.append(     sp.sympify(str(r_of_xx0     ).replace("xx0",str(xx0))))
        rprimes.append(sp.sympify(str(rprime_of_xx0).replace("xx0",str(xx0))))
        if i>0:
            deltars.append(sp.log(rs[i]-rs[i-1],10))
        else:
            deltars.append(sp.log(2*rs[0],10))

    # fig, ax = plt.subplots()
    fig = plt.figure(figsize=(12,12)) # 8 in x 8 in

    ax = fig.add_subplot(221)
    ax.set_title('$r(xx_0)$ for '+CoordSystem,fontsize='x-large')
    ax.set_xlabel('$xx_0$',fontsize='x-large')
    ax.set_ylabel('$r(xx_0)$',fontsize='x-large')
    ax.plot(xx0s, rs, 'k.', label='Spacing between\nadjacent gridpoints')
    # legend = ax.legend(loc='lower right', shadow=True, fontsize='x-large')
    # legend.get_frame().set_facecolor('C1')

    ax = fig.add_subplot(222)
    ax.set_title('Grid spacing for '+CoordSystem,fontsize='x-large')
    ax.set_xlabel('$xx_0$',fontsize='x-large')
    ax.set_ylabel('$\log_{10}(\Delta r)$',fontsize='x-large')
    ax.plot(xx0s, deltars, 'k.', label='Spacing between\nadjacent gridpoints\nin $r(xx_0)$ plot')
    legend = ax.legend(loc='lower right', shadow=True, fontsize='x-large')
    legend.get_frame().set_facecolor('C1')

    ax = fig.add_subplot(223)
    ax.set_title('$r\'(xx_0)$ for '+CoordSystem,fontsize='x-large')
    ax.set_xlabel('$xx_0$',fontsize='x-large')
    ax.set_ylabel('$r\'(xx_0)$',fontsize='x-large')
    ax.plot(xx0s, rprimes, 'k.', label='Nr=96')
    # legend = ax.legend(loc='upper left', shadow=True, fontsize='x-large')
    # legend.get_frame().set_facecolor('C1')

    plt.tight_layout(pad=2)
    plt.show()
```

## Step 3.a: Spherical-like coordinate systems \[Back to [top](#toc)\]
$$\label{sphericallike}$$

### Step 3.a.i: **`reference_metric::CoordSystem = "Spherical"`** \[Back to [top](#toc)\]
$$\label{spherical}$$

Standard spherical coordinates, with $(r,\theta,\phi)=(xx_0,xx_1,xx_2)$

```python
if CoordSystem == "Spherical":
    # Adding assumption real=True can help simplify expressions involving xx[0] & xx[1] below.
    xx[0] = sp.symbols("xx0", real=True)
    xx[1] = sp.symbols("xx1", real=True)

    RMAX = par.Cparameters("REAL", thismodule, ["RMAX"],10.0)
    xxmin = [sp.sympify(0), sp.sympify(0), -M_PI]
    xxmax = [         RMAX,          M_PI,  M_PI]

    r  = xx[0]
    th = xx[1]
    ph = xx[2]

    Cart_to_xx[0] = sp.sqrt(Cartx ** 2 + Carty ** 2 + Cartz ** 2)
    Cart_to_xx[1] = sp.acos(Cartz / Cart_to_xx[0])
    Cart_to_xx[2] = sp.atan2(Carty, Cartx)

    xxSph[0] = r
    xxSph[1] = th
    xxSph[2] = ph

    # Now define xCart, yCart, and zCart in terms of x0,xx[1],xx[2].
    # Note that the relation between r and x0 is not necessarily trivial in SinhSpherical coordinates. See above.
    xx_to_Cart[0] = xxSph[0]*sp.sin(xxSph[1])*sp.cos(xxSph[2])
    xx_to_Cart[1] = xxSph[0]*sp.sin(xxSph[1])*sp.sin(xxSph[2])
    xx_to_Cart[2] = xxSph[0]*sp.cos(xxSph[1])

    scalefactor_orthog[0] = sp.diff(xxSph[0],xx[0])
    scalefactor_orthog[1] = xxSph[0]
    scalefactor_orthog[2] = xxSph[0]*sp.sin(xxSph[1])

    # Set the unit vectors
    UnitVectors = [[ sp.sin(xxSph[1])*sp.cos(xxSph[2]), sp.sin(xxSph[1])*sp.sin(xxSph[2]),  sp.cos(xxSph[1])],
                   [ sp.cos(xxSph[1])*sp.cos(xxSph[2]), sp.cos(xxSph[1])*sp.sin(xxSph[2]), -sp.sin(xxSph[1])],
                   [                 -sp.sin(xxSph[2]),                  sp.cos(xxSph[2]),  sp.sympify(0)   ]]
```

Now let's analyze $r(xx_0)$ for **"Spherical"** coordinates.

```python
%matplotlib inline

CoordSystem = "Spherical"
par.set_parval_from_str("reference_metric::CoordSystem",CoordSystem)
rfm.reference_metric()

RMAX     = 10.0
r_of_xx0      = sp.sympify(str(rfm.xxSph[0]                   ).replace("RMAX",str(RMAX)))
rprime_of_xx0 = sp.sympify(str(sp.diff(rfm.xxSph[0],rfm.xx[0])).replace("RMAX",str(RMAX)))

create_r_of_xx0_plots(CoordSystem, r_of_xx0,rprime_of_xx0)
```

### Step 3.a.ii: **`reference_metric::CoordSystem = "SinhSpherical"`** \[Back to [top](#toc)\]
$$\label{sinhspherical}$$

Spherical coordinates, but with $$r(xx_0) = \text{AMPL} \frac{\sinh\left(\frac{xx_0}{\text{SINHW}}\right)}{\sinh\left(\frac{1}{\text{SINHW}}\right)}.$$

SinhSpherical uses two parameters: `AMPL` and `SINHW`. `AMPL` sets the outer boundary distance; and `SINHW` sets the focusing of the coordinate points near $r=0$, where a small `SINHW` ($\sim 0.125$) will greatly focus the points near $r=0$ and a large `SINHW` will look more like an ordinary spherical polar coordinate system.

```python
if CoordSystem == "SinhSpherical":
    xxmin = [sp.sympify(0), sp.sympify(0), -M_PI]
    xxmax = [sp.sympify(1),          M_PI,  M_PI]

    AMPL, SINHW = par.Cparameters("REAL",thismodule,["AMPL","SINHW"],[10.0,0.2])
    # Set SinhSpherical radial coordinate by default; overwrite later if CoordSystem == "SinhSphericalv2".
    r = AMPL * (sp.exp(xx[0] / SINHW) - sp.exp(-xx[0] / SINHW)) / \
               (sp.exp(1 / SINHW) - sp.exp(-1 / SINHW))
    th = xx[1]
    ph = xx[2]

    Cart_to_xx[0] = SINHW*sp.asinh(sp.sqrt(Cartx ** 2 + Carty ** 2 + Cartz ** 2)*sp.sinh(1/SINHW)/AMPL)
    Cart_to_xx[1] = sp.acos(Cartz / sp.sqrt(Cartx ** 2 + Carty ** 2 + Cartz ** 2))
    Cart_to_xx[2] = sp.atan2(Carty, Cartx)

    xxSph[0] = r
    xxSph[1] = th
    xxSph[2] = ph

    # Now define xCart, yCart, and zCart in terms of x0,xx[1],xx[2].
    # Note that the relation between r and x0 is not necessarily trivial in SinhSpherical coordinates. See above.
    xx_to_Cart[0] = xxSph[0]*sp.sin(xxSph[1])*sp.cos(xxSph[2])
    xx_to_Cart[1] = xxSph[0]*sp.sin(xxSph[1])*sp.sin(xxSph[2])
    xx_to_Cart[2] = xxSph[0]*sp.cos(xxSph[1])

    scalefactor_orthog[0] = sp.diff(xxSph[0],xx[0])
    scalefactor_orthog[1] = xxSph[0]
    scalefactor_orthog[2] = xxSph[0]*sp.sin(xxSph[1])

    # Set the unit vectors
    UnitVectors = [[ sp.sin(xxSph[1])*sp.cos(xxSph[2]), sp.sin(xxSph[1])*sp.sin(xxSph[2]),  sp.cos(xxSph[1])],
                   [ sp.cos(xxSph[1])*sp.cos(xxSph[2]), sp.cos(xxSph[1])*sp.sin(xxSph[2]), -sp.sin(xxSph[1])],
                   [                 -sp.sin(xxSph[2]),                  sp.cos(xxSph[2]),  sp.sympify(0)   ]]
```

Now we explore $r(xx_0)$ for `SinhSpherical` assuming `AMPL=10.0` and `SINHW=0.2`:

```python
%matplotlib inline

CoordSystem = "SinhSpherical"
par.set_parval_from_str("reference_metric::CoordSystem",CoordSystem)
rfm.reference_metric()

AMPL     = 10.0
SINHW    = 0.2
r_of_xx0      = sp.sympify(str(rfm.xxSph[0]                   ).replace("AMPL",str(AMPL)).replace("SINHW",str(SINHW)))
rprime_of_xx0 = sp.sympify(str(sp.diff(rfm.xxSph[0],rfm.xx[0])).replace("AMPL",str(AMPL)).replace("SINHW",str(SINHW)))

create_r_of_xx0_plots(CoordSystem, r_of_xx0,rprime_of_xx0)
```

### Step 3.a.iii: **`reference_metric::CoordSystem = "SinhSphericalv2"`** \[Back to [top](#toc)\]
$$\label{sinhsphericalv2}$$

The same as SinhSpherical coordinates, but with an additional `AMPL*const_dr*xx_0` term:
$$r(xx_0) = \text{AMPL} \left[\text{const_dr}\ xx_0 + \frac{\sinh\left(\frac{xx_0}{\text{SINHW}}\right)}{\sinh\left(\frac{1}{\text{SINHW}}\right)}\right].$$

```python
if CoordSystem == "SinhSphericalv2":
    # SinhSphericalv2 adds the parameter "const_dr", which allows for a region near xx[0]=0 to have
    # constant radial resolution of const_dr, provided the sinh() term does not dominate near xx[0]=0.
    xxmin = [sp.sympify(0), sp.sympify(0), -M_PI]
    xxmax = [sp.sympify(1),          M_PI,  M_PI]

    AMPL, SINHW = par.Cparameters("REAL",thismodule,["AMPL","SINHW"],[10.0,0.2])
    const_dr = par.Cparameters("REAL",thismodule,["const_dr"],0.0625)

    r = AMPL*( const_dr*xx[0] + (sp.exp(xx[0] / SINHW) - sp.exp(-xx[0] / SINHW)) /
               (sp.exp(1 / SINHW) - sp.exp(-1 / SINHW)) )
    th = xx[1]
    ph = xx[2]

    # NO CLOSED-FORM EXPRESSION FOR RADIAL INVERSION.
    # Cart_to_xx[0] = "NewtonRaphson"
    # Cart_to_xx[1] = sp.acos(Cartz / sp.sqrt(Cartx ** 2 + Carty ** 2 + Cartz ** 2))
    # Cart_to_xx[2] = sp.atan2(Carty, Cartx)

    xxSph[0] = r
    xxSph[1] = th
    xxSph[2] = ph

    # Now define xCart, yCart, and zCart in terms of x0,xx[1],xx[2].
    # Note that the relation between r and x0 is not necessarily trivial in SinhSpherical coordinates. See above.
    xx_to_Cart[0] = xxSph[0]*sp.sin(xxSph[1])*sp.cos(xxSph[2])
    xx_to_Cart[1] = xxSph[0]*sp.sin(xxSph[1])*sp.sin(xxSph[2])
    xx_to_Cart[2] = xxSph[0]*sp.cos(xxSph[1])

    scalefactor_orthog[0] = sp.diff(xxSph[0],xx[0])
    scalefactor_orthog[1] = xxSph[0]
    scalefactor_orthog[2] = xxSph[0]*sp.sin(xxSph[1])

    # Set the unit vectors
    UnitVectors = [[ sp.sin(xxSph[1])*sp.cos(xxSph[2]), sp.sin(xxSph[1])*sp.sin(xxSph[2]),  sp.cos(xxSph[1])],
                   [ sp.cos(xxSph[1])*sp.cos(xxSph[2]), sp.cos(xxSph[1])*sp.sin(xxSph[2]), -sp.sin(xxSph[1])],
                   [                 -sp.sin(xxSph[2]),                  sp.cos(xxSph[2]),  sp.sympify(0)   ]]
```

Now we explore $r(xx_0)$ for `SinhSphericalv2` assuming `AMPL=10.0`, `SINHW=0.2`, and `const_dr=0.05`. Notice that the `const_dr` term significantly increases the grid spacing near $xx_0=0$ relative to `SinhSpherical` coordinates.

```python
%matplotlib inline

CoordSystem = "SinhSphericalv2"
par.set_parval_from_str("reference_metric::CoordSystem",CoordSystem)
rfm.reference_metric()

AMPL     = 10.0
SINHW    = 0.2
const_dr = 0.05
r_of_xx0      = sp.sympify(str(rfm.xxSph[0]                   ).replace("AMPL",str(AMPL)).replace("SINHW",str(SINHW)).replace("const_dr",str(const_dr)))
rprime_of_xx0 = sp.sympify(str(sp.diff(rfm.xxSph[0],rfm.xx[0])).replace("AMPL",str(AMPL)).replace("SINHW",str(SINHW)).replace("const_dr",str(const_dr)))

create_r_of_xx0_plots(CoordSystem, r_of_xx0,rprime_of_xx0)
```

## Step 3.b: Cylindrical-like coordinate systems \[Back to [top](#toc)\]
$$\label{cylindricallike}$$

### Step 3.b.i: **`reference_metric::CoordSystem = "Cylindrical"`** \[Back to [top](#toc)\]
$$\label{cylindrical}$$

Standard cylindrical coordinates, with $(\rho,\phi,z)=(xx_0,xx_1,xx_2)$

```python
if CoordSystem == "Cylindrical":
    # Assuming the cylindrical radial coordinate
    #   is positive makes nice simplifications of
    #   unit vectors possible.
    xx[0] = sp.symbols("xx0", real=True)

    RHOMAX,ZMIN,ZMAX = par.Cparameters("REAL",thismodule,["RHOMAX","ZMIN","ZMAX"],[10.0,-10.0,10.0])
    xxmin = [sp.sympify(0), -M_PI, ZMIN]
    xxmax = [       RHOMAX,  M_PI, ZMAX]

    RHOCYL = xx[0]
    PHICYL = xx[1]
    ZCYL   = xx[2]

    Cart_to_xx[0] = sp.sqrt(Cartx ** 2 + Carty ** 2)
    Cart_to_xx[1] = sp.atan2(Carty, Cartx)
    Cart_to_xx[2] = Cartz

    xx_to_Cart[0] = RHOCYL*sp.cos(PHICYL)
    xx_to_Cart[1] = RHOCYL*sp.sin(PHICYL)
    xx_to_Cart[2] = ZCYL

    xxSph[0] = sp.sqrt(RHOCYL**2 + ZCYL**2)
    xxSph[1] = sp.acos(ZCYL / xxSph[0])
    xxSph[2] = PHICYL

    scalefactor_orthog[0] = sp.diff(RHOCYL,xx[0])
    scalefactor_orthog[1] = RHOCYL
    scalefactor_orthog[2] = sp.diff(ZCYL,xx[2])

    # Set the unit vectors
    UnitVectors = [[ sp.cos(PHICYL), sp.sin(PHICYL), sp.sympify(0)],
                   [-sp.sin(PHICYL), sp.cos(PHICYL), sp.sympify(0)],
                   [ sp.sympify(0),  sp.sympify(0),  sp.sympify(1)]]
```

Next let's plot **"Cylindrical"** coordinates.

```python
%matplotlib inline

import numpy as np                  # NumPy: A numerical methods module for Python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

R = np.linspace(0, 2, 24)
h = 2
u = np.linspace(0,  2*np.pi, 24)

x = np.outer(R, np.cos(u))
y = np.outer(R, np.sin(u))
z = h * np.outer(np.ones(np.size(u)), np.ones(np.size(u)))

r = np.arange(0,2,0.25)
theta = 2*np.pi*r*0

fig = plt.figure(figsize=(12,12)) # 8 in x 8 in
fig, (ax1, ax2) = plt.subplots(1, 2)

ax1 = plt.axes(projection='polar')

ax1.set_rmax(2)

ax1.set_rgrids(r,labels=[])

thetas = np.linspace(0,360,24, endpoint=True)
ax1.set_thetagrids(thetas,labels=[])

# ax.grid(True)
ax1.grid(True,linewidth='1.0')
ax1.set_title("Top Down View")
plt.show()

ax2 = plt.axes(projection='3d', xticklabels=[], yticklabels=[], zticklabels=[])
#ax2.plot_surface(x,y,z, alpha=.75, cmap = 'viridis') # z in case of disk which is parallel to XY plane is constant and you can directly use h

x=np.linspace(-2, 2, 100)
z=np.linspace(-2, 2, 100)
Xc, Zc=np.meshgrid(x, z)
Yc = np.sqrt(4-Xc**2)

rstride = 10
cstride = 10
ax2.plot_surface(Xc, Yc, Zc, alpha=1.0, rstride=rstride, cstride=cstride, cmap = 'viridis')
ax2.plot_surface(Xc, -Yc, Zc, alpha=1.0, rstride=rstride, cstride=cstride, cmap = 'viridis')
ax2.set_title("Standard Cylindrical Grid in 3D")
ax2.grid(False)
plt.axis('off')

plt.show()
```

### Step 3.b.ii" **`reference_metric::CoordSystem = "SinhCylindrical"`** \[Back to [top](#toc)\]
$$\label{sinhcylindrical}$$

Cylindrical coordinates, but with
$$\rho(xx_0) = \text{AMPLRHO} \frac{\sinh\left(\frac{xx_0}{\text{SINHWRHO}}\right)}{\sinh\left(\frac{1}{\text{SINHWRHO}}\right)}$$
and
$$z(xx_2) = \text{AMPLZ} \frac{\sinh\left(\frac{xx_2}{\text{SINHWZ}}\right)}{\sinh\left(\frac{1}{\text{SINHWZ}}\right)}$$

```python
if CoordSystem == "SinhCylindrical":
    # Assuming the cylindrical radial coordinate
    #   is positive makes nice simplifications of
    #   unit vectors possible.
    xx[0] = sp.symbols("xx0", real=True)

    xxmin = [sp.sympify(0), -M_PI, sp.sympify(-1)]
    xxmax = [sp.sympify(1),  M_PI, sp.sympify(+1)]

    AMPLRHO, SINHWRHO, AMPLZ, SINHWZ = par.Cparameters("REAL",thismodule,
                                                       ["AMPLRHO","SINHWRHO","AMPLZ","SINHWZ"],
                                                       [     10.0,       0.2,   10.0,    0.2])

    # Set SinhCylindrical radial & z coordinates by default; overwrite later if CoordSystem == "SinhCylindricalv2".
    RHOCYL = AMPLRHO * (sp.exp(xx[0] / SINHWRHO) - sp.exp(-xx[0] / SINHWRHO)) / (sp.exp(1 / SINHWRHO) - sp.exp(-1 / SINHWRHO))
    # phi coordinate remains unchanged.
    PHICYL = xx[1]
    ZCYL   = AMPLZ   * (sp.exp(xx[2] / SINHWZ)   - sp.exp(-xx[2] / SINHWZ))   / (sp.exp(1 / SINHWZ)   - sp.exp(-1 / SINHWZ))
    Cart_to_xx[0] = SINHWRHO*sp.asinh(sp.sqrt(Cartx ** 2 + Carty ** 2)*sp.sinh(1/SINHWRHO)/AMPLRHO)
    Cart_to_xx[1] = sp.atan2(Carty, Cartx)
    Cart_to_xx[2] = SINHWZ*sp.asinh(Cartz*sp.sinh(1/SINHWZ)/AMPLZ)

    xx_to_Cart[0] = RHOCYL*sp.cos(PHICYL)
    xx_to_Cart[1] = RHOCYL*sp.sin(PHICYL)
    xx_to_Cart[2] = ZCYL

    xxSph[0] = sp.sqrt(RHOCYL**2 + ZCYL**2)
    xxSph[1] = sp.acos(ZCYL / xxSph[0])
    xxSph[2] = PHICYL

    scalefactor_orthog[0] = sp.diff(RHOCYL,xx[0])
    scalefactor_orthog[1] = RHOCYL
    scalefactor_orthog[2] = sp.diff(ZCYL,xx[2])

    # Set the unit vectors
    UnitVectors = [[ sp.cos(PHICYL), sp.sin(PHICYL), sp.sympify(0)],
                   [-sp.sin(PHICYL), sp.cos(PHICYL), sp.sympify(0)],
                   [ sp.sympify(0),  sp.sympify(0),  sp.sympify(1)]]
```

Next let's plot **"SinhCylindrical"** coordinates.

```python
fig=plt.figure()

plt.clf()

fig = plt.figure()

ax = plt.subplot(1,1,1, projection='polar')

ax.set_rmax(2)

Nr = 20
xx0s = np.linspace(0,2,Nr, endpoint=True) + 1.0/(2.0*Nr)

rs = []
AMPLRHO = 1.0
SINHW = 0.4
for i in range(Nr):
    rs.append(AMPLRHO * (np.exp(xx0s[i] / SINHW) - np.exp(-xx0s[i] / SINHW)) / \
                        (np.exp(1.0 / SINHW) - np.exp(-1.0 / SINHW)))

ax.set_rgrids(rs,labels=[])

thetas = np.linspace(0,360,25, endpoint=True)
ax.set_thetagrids(thetas,labels=[])

# ax.grid(True)
ax.grid(True,linewidth='1.0')

plt.show()

```

### Step 3.b.iii: **`reference_metric::CoordSystem = "SinhCylindricalv2"`** \[Back to [top](#toc)\]
$$\label{sinhcylindricalv2}$$

Cylindrical coordinates, but with
$$\rho(xx_0) = \text{AMPLRHO} \left[\text{const_drho}\ xx_0 + \frac{\sinh\left(\frac{xx_0}{\text{SINHWRHO}}\right)}{\sinh\left(\frac{1}{\text{SINHWRHO}}\right)}\right]$$
and
$$z(xx_2) = \text{AMPLZ} \left[\text{const_dz}\ xx_2 + \frac{\sinh\left(\frac{xx_2}{\text{SINHWZ}}\right)}{\sinh\left(\frac{1}{\text{SINHWZ}}\right)}\right]$$

```python
if CoordSystem == "SinhCylindricalv2":
    # Assuming the cylindrical radial coordinate
    #   is positive makes nice simplifications of
    #   unit vectors possible.
    xx[0] = sp.symbols("xx0", real=True)

    # SinhCylindricalv2 adds the parameters "const_drho", "const_dz", which allows for regions near xx[0]=0
    # and xx[2]=0 to have constant rho and z resolution of const_drho and const_dz, provided the sinh() terms
    # do not dominate near xx[0]=0 and xx[2]=0.
    xxmin = [sp.sympify(0), -M_PI, sp.sympify(-1)]
    xxmax = [sp.sympify(1),  M_PI, sp.sympify(+1)]
    AMPLRHO, SINHWRHO, AMPLZ, SINHWZ = par.Cparameters("REAL",thismodule,
                                                       ["AMPLRHO","SINHWRHO","AMPLZ","SINHWZ"],
                                                       [     10.0,       0.2,   10.0,    0.2])
    const_drho, const_dz = par.Cparameters("REAL",thismodule,["const_drho","const_dz"],[0.0625,0.0625])

    RHOCYL = AMPLRHO * ( const_drho*xx[0] + (sp.exp(xx[0] / SINHWRHO) - sp.exp(-xx[0] / SINHWRHO)) / (sp.exp(1 / SINHWRHO) - sp.exp(-1 / SINHWRHO)) )
    PHICYL = xx[1]
    ZCYL   = AMPLZ   * ( const_dz  *xx[2] + (sp.exp(xx[2] / SINHWZ  ) - sp.exp(-xx[2] / SINHWZ  )) / (sp.exp(1 / SINHWZ  ) - sp.exp(-1 / SINHWZ  )) )

    # NO CLOSED-FORM EXPRESSION FOR RADIAL OR Z INVERSION.
    # Cart_to_xx[0] = "NewtonRaphson"
    # Cart_to_xx[1] = sp.atan2(Carty, Cartx)
    # Cart_to_xx[2] = "NewtonRaphson"

    xx_to_Cart[0] = RHOCYL*sp.cos(PHICYL)
    xx_to_Cart[1] = RHOCYL*sp.sin(PHICYL)
    xx_to_Cart[2] = ZCYL

    xxSph[0] = sp.sqrt(RHOCYL**2 + ZCYL**2)
    xxSph[1] = sp.acos(ZCYL / xxSph[0])
    xxSph[2] = PHICYL

    scalefactor_orthog[0] = sp.diff(RHOCYL,xx[0])
    scalefactor_orthog[1] = RHOCYL
    scalefactor_orthog[2] = sp.diff(ZCYL,xx[2])

    # Set the unit vectors
    UnitVectors = [[ sp.cos(PHICYL), sp.sin(PHICYL), sp.sympify(0)],
                   [-sp.sin(PHICYL), sp.cos(PHICYL), sp.sympify(0)],
                   [ sp.sympify(0),  sp.sympify(0),  sp.sympify(1)]]
```

For example, let's set up **`SinhCylindricalv2`** coordinates and output the Christoffel symbol $\hat{\Gamma}^{xx_2}_{xx_2 xx_2}$, or more simply $\hat{\Gamma}^2_{22}$:

```python
par.set_parval_from_str("reference_metric::CoordSystem","SinhCylindricalv2")

rfm.reference_metric()

sp.pretty_print(sp.simplify(rfm.GammahatUDD[2][2][2]))
```

                             ⎛ 2⋅xx₂     ⎞    1
                             ⎜ ──────    ⎟  ──────
                             ⎜ SINHWZ    ⎟  SINHWZ
                            -⎝ℯ       - 1⎠⋅ℯ
    ────────────────────────────────────────────────────────────────────────
           ⎛                  ⎛   2       ⎞   xx₂     ⎛ 2⋅xx₂     ⎞    1   ⎞
           ⎜                  ⎜ ──────    ⎟  ──────   ⎜ ──────    ⎟  ──────⎟
           ⎜                  ⎜ SINHWZ    ⎟  SINHWZ   ⎜ SINHWZ    ⎟  SINHWZ⎟
    SINHWZ⋅⎝- SINHWZ⋅const_dz⋅⎝ℯ       - 1⎠⋅ℯ       - ⎝ℯ       + 1⎠⋅ℯ      ⎠

As we will soon see, defining these "hatted" quantities will be quite useful when expressing hyperbolic ([wave-equation](https://en.wikipedia.org/wiki/Wave_equation)-like) PDEs in non-Cartesian coordinate systems.

## Step 3.c: Cartesian-like coordinate systems \[Back to [top](#toc)\]
$$\label{cartesianlike}$$

### Step 3.c.i: **`reference_metric::CoordSystem = "Cartesian"`** \[Back to [top](#toc)\]
$$\label{cartesian}$$

Standard Cartesian coordinates, with $(x,y,z)=$ `(xx0,xx1,xx2)`

```python
if CoordSystem == "Cartesian":
    xmin, xmax, ymin, ymax, zmin, zmax = par.Cparameters("REAL",thismodule,
                                                         ["xmin","xmax","ymin","ymax","zmin","zmax"],
                                                         [ -10.0,  10.0, -10.0,  10.0, -10.0,  10.0])
    xxmin = ["xmin", "ymin", "zmin"]
    xxmax = ["xmax", "ymax", "zmax"]

    xx_to_Cart[0] = xx[0]
    xx_to_Cart[1] = xx[1]
    xx_to_Cart[2] = xx[2]

    xxSph[0] = sp.sqrt(xx[0] ** 2 + xx[1] ** 2 + xx[2] ** 2)
    xxSph[1] = sp.acos(xx[2] / xxSph[0])
    xxSph[2] = sp.atan2(xx[1], xx[0])

    Cart_to_xx[0] = Cartx
    Cart_to_xx[1] = Carty
    Cart_to_xx[2] = Cartz

    scalefactor_orthog[0] = sp.sympify(1)
    scalefactor_orthog[1] = sp.sympify(1)
    scalefactor_orthog[2] = sp.sympify(1)

    # Set the transpose of the matrix of unit vectors
    UnitVectors = [[sp.sympify(1), sp.sympify(0), sp.sympify(0)],
                   [sp.sympify(0), sp.sympify(1), sp.sympify(0)],
                   [sp.sympify(0), sp.sympify(0), sp.sympify(1)]]
```

```python
%matplotlib inline

import numpy as np               # NumPy: A numerical methods module for Python
import matplotlib.pyplot as plt  # matplotlib: Python module specializing in plotting capabilities

plt.clf()

fig = plt.figure()
ax = fig.gca()
Nx = 16
ax.set_xticks(np.arange(0, 1., 1./Nx))
ax.set_yticks(np.arange(0, 1., 1./Nx))

for tick in ax.get_xticklabels():
    tick.set_rotation(60)

# plt.scatter(x, y)
ax.set_aspect('equal')

plt.grid()

# plt.savefig("Cartgrid.png",dpi=300)
plt.show()
# plt.close(fig)
```

### Step 3.c.ii: **`reference_metric::CoordSystem = "SinhCartesian"`** \[Back to [top](#toc)\]
$$\label{sinhcartesian}$$

In this coordinate system, all three coordinates behave like the $z$-coordinate in SinhCylindrical coordinates, i.e.

$$
\begin{align}
x(xx_0) &= \text{AMPLXYZ} \left[\frac{\sinh\left(\frac{xx_0}{\text{SINHWXYZ}}\right)}{\sinh\left(\frac{1}{\text{SINHWXYZ}}\right)}\right]\ ,\\
y(xx_1) &= \text{AMPLXYZ} \left[\frac{\sinh\left(\frac{xx_1}{\text{SINHWXYZ}}\right)}{\sinh\left(\frac{1}{\text{SINHWXYZ}}\right)}\right]\ ,\\
z(xx_2) &= \text{AMPLXYZ} \left[\frac{\sinh\left(\frac{xx_2}{\text{SINHWXYZ}}\right)}{\sinh\left(\frac{1}{\text{SINHWXYZ}}\right)}\right]
\end{align}
$$

```python
if CoordSystem == "SinhCartesian":
    # SinhCartesian coordinates allows us to push the outer boundary of the
    # computational domain a lot further away, while keeping reasonably high
    # resolution towards the center of the computational grid.

    # Set default values for min and max (x,y,z)
    xxmin = [sp.sympify(-1), sp.sympify(-1), sp.sympify(-1)]
    xxmax = [sp.sympify(+1), sp.sympify(+1), sp.sympify(+1)]

    # Declare basic parameters of the coordinate system and their default values
    AMPLXYZ, SINHWXYZ = par.Cparameters("REAL", thismodule,
                                        ["AMPLXYZ", "SINHWXYZ"],
                                        [     10.0,        0.2])

    # Compute (xx_to_Cart0,xx_to_Cart1,xx_to_Cart2) from (xx0,xx1,xx2)
    for ii in [0, 1, 2]:
        xx_to_Cart[ii] = AMPLXYZ*(sp.exp(xx[ii]/SINHWXYZ) - sp.exp(-xx[ii]/SINHWXYZ))/(sp.exp(1/SINHWXYZ) - sp.exp(-1/SINHWXYZ))

    # Compute (r,th,ph) from (xx_to_Cart2,xx_to_Cart1,xx_to_Cart2)
    xxSph[0] = sp.sqrt(xx_to_Cart[0] ** 2 + xx_to_Cart[1] ** 2 + xx_to_Cart[2] ** 2)
    xxSph[1] = sp.acos(xx_to_Cart[2] / xxSph[0])
    xxSph[2] = sp.atan2(xx_to_Cart[1], xx_to_Cart[0])

    # Compute (xx0,xx1,xx2) from (Cartx,Carty,Cartz)
    Cart_to_xx[0] = SINHWXYZ*sp.asinh(Cartx*sp.sinh(1/SINHWXYZ)/AMPLXYZ)
    Cart_to_xx[1] = SINHWXYZ*sp.asinh(Carty*sp.sinh(1/SINHWXYZ)/AMPLXYZ)
    Cart_to_xx[2] = SINHWXYZ*sp.asinh(Cartz*sp.sinh(1/SINHWXYZ)/AMPLXYZ)

    # Compute scale factors
    scalefactor_orthog[0] = sp.diff(xx_to_Cart[0], xx[0])
    scalefactor_orthog[1] = sp.diff(xx_to_Cart[1], xx[1])
    scalefactor_orthog[2] = sp.diff(xx_to_Cart[2], xx[2])

    # Set the transpose of the matrix of unit vectors
    UnitVectors = [[sp.sympify(1), sp.sympify(0), sp.sympify(0)],
                   [sp.sympify(0), sp.sympify(1), sp.sympify(0)],
                   [sp.sympify(0), sp.sympify(0), sp.sympify(1)]]

```

```python
%matplotlib inline

import numpy as np               # NumPy: A numerical methods module for Python
import matplotlib.pyplot as plt  # matplotlib: Python module specializing in plotting capabilities

plt.clf()

fig = plt.figure(dpi=160)
ax = fig.gca()

# Set plot title
ax.set_title(r"$z=0$ slice of the 3D grid")

# Set SINH parameters. Here we assume:
#
# AMPLX  = AMPLY  = SINHA
# SINHWX = SINHWY = SINHW
SINHA = 10.0
SINHW = 0.45

# Set number of points. We assume the same point
# distribution along the (x,y)-directions
Nxxs = 24
xxis = np.linspace(-1,1,Nxxs, endpoint=True)

# Compute axis ticks by evaluating x and y using SinhCartesian coordinates
axis_ticks = []
for i in range(Nxxs):
    axis_ticks.append(SINHA * (np.exp(xxis[i] / SINHW) - np.exp(-xxis[i] / SINHW)) / \
                        (np.exp(1.0 / SINHW) - np.exp(-1.0 / SINHW)))

# Set the axis ticks
ax.set_xticks(axis_ticks)
ax.set_yticks(axis_ticks)

# Set x and y labels. Initialize array with empty strings
labelsx = ["" for i in range(Nxxs)]
labelsy = ["" for i in range(Nxxs)]

# Set x_min and x_max tick label
labelsx[0] = r"-AMPLX"
labelsx[-1] = r"AMPLX"

# Set y_min and y_max tick label
labelsy[0] = r"-AMPLY"
labelsy[-1] = r"AMPLY"

# Set tick labels
ax.set_xticklabels(labelsx)
ax.set_yticklabels(labelsy)

# Rotate x labels by 60 degrees
for tick in ax.get_xticklabels():
    tick.set_rotation(60)

# Draw the x=0 and y=0 ticklabel
ax.text(0,-11,"0",ha="center",va="center")
ax.text(-11,0,"0",ha="center",va="center")

# plt.scatter(x, y)
ax.set_aspect('equal')

plt.grid(color='black',linewidth=0.3)

plt.show()
# plt.savefig("Cartgrid.png",dpi=400)
# plt.close(fig)
```

## Step 3.d: [Prolate spheroidal](https://en.wikipedia.org/wiki/Prolate_spheroidal_coordinates)-like coordinate systems \[Back to [top](#toc)\]
$$\label{prolatespheroidal}$$

### Step 3.d.i: **`reference_metric::CoordSystem = "SymTP"`** \[Back to [top](#toc)\]
$$\label{symtp}$$

Symmetric TwoPuncture coordinates, with $(\rho,\phi,z)=(xx_0\sin(xx_1), xx_2, \sqrt{xx_0^2 + \text{bScale}^2}\cos(xx_1))$

```python
if CoordSystem == "SymTP":

    var1, var2= sp.symbols('var1 var2',real=True)
    bScale, AW, AMAX, RHOMAX, ZMIN, ZMAX = par.Cparameters("REAL",thismodule,
                                                           ["bScale","AW","AMAX","RHOMAX","ZMIN","ZMAX"],
                                                           [0.5,     0.2,   10.0,    10.0, -10.0,  10.0])

    # Assuming xx0, xx1, and bScale
    #   are positive makes nice simplifications of
    #   unit vectors possible.
    xx[0],xx[1] = sp.symbols("xx0 xx1", real=True)

    xxmin = [sp.sympify(0), sp.sympify(0),-M_PI]
    xxmax = [         AMAX,          M_PI, M_PI]

    AA = xx[0]

    if CoordSystem == "SinhSymTP":
        AA = (sp.exp(xx[0]/AW)-sp.exp(-xx[0]/AW))/2

    var1 = sp.sqrt(AA**2 + (bScale * sp.sin(xx[1]))**2)
    var2 = sp.sqrt(AA**2 + bScale**2)

    RHOSYMTP = AA*sp.sin(xx[1])
    PHSYMTP = xx[2]
    ZSYMTP = var2*sp.cos(xx[1])

    xx_to_Cart[0] = AA  *sp.sin(xx[1])*sp.cos(xx[2])
    xx_to_Cart[1] = AA  *sp.sin(xx[1])*sp.sin(xx[2])
    xx_to_Cart[2] = ZSYMTP

    xxSph[0] = sp.sqrt(RHOSYMTP**2 + ZSYMTP**2)
    xxSph[1] = sp.acos(ZSYMTP / xxSph[0])
    xxSph[2] = PHSYMTP

    rSph  = sp.sqrt(Cartx ** 2 + Carty ** 2 + Cartz ** 2)
    thSph = sp.acos(Cartz / rSph)
    phSph = sp.atan2(Carty, Cartx)

    # Mathematica script to compute Cart_to_xx[]
    #             AA = x1;
    #             var2 = Sqrt[AA^2 + bScale^2];
    #             RHOSYMTP = AA*Sin[x2];
    #             ZSYMTP = var2*Cos[x2];
    #             Solve[{rSph == Sqrt[RHOSYMTP^2 + ZSYMTP^2],
    #                    thSph == ArcCos[ZSYMTP/Sqrt[RHOSYMTP^2 + ZSYMTP^2]],
    #                    phSph == x3},
    #                   {x1, x2, x3}]
    Cart_to_xx[0] = sp.sqrt(-bScale**2 + rSph**2 +
                            sp.sqrt(bScale**4 + 2*bScale**2*rSph**2 + rSph**4 -
                                    4*bScale**2*rSph**2*sp.cos(thSph)**2))*M_SQRT1_2 # M_SQRT1_2 = 1/sqrt(2); define this way for UnitTesting

    # The sign() function in the following expression ensures the correct root is taken.
    Cart_to_xx[1] = sp.acos(sp.sign(Cartz)*(
                              sp.sqrt(1 + rSph**2/bScale**2 -
                                      sp.sqrt(bScale**4 + 2*bScale**2*rSph**2 + rSph**4 -
                                              4*bScale**2*rSph**2*sp.cos(thSph)**2)/bScale**2)*M_SQRT1_2)) # M_SQRT1_2 = 1/sqrt(2); define this way for UnitTesting

    Cart_to_xx[2] = phSph
```

### Step 3.d.ii: **`reference_metric::CoordSystem = "SinhSymTP"`** \[Back to [top](#toc)\]
$$\label{sinhsymtp}$$

Symmetric TwoPuncture coordinates, but with $$xx_0 \to \sinh(xx_0/\text{AW})$$

```python
if CoordSystem == "SinhSymTP":

    var1, var2= sp.symbols('var1 var2',real=True)
    bScale, AW, AMAX, RHOMAX, ZMIN, ZMAX = par.Cparameters("REAL",thismodule,
                                                           ["bScale","AW","AMAX","RHOMAX","ZMIN","ZMAX"],
                                                           [0.5,     0.2,   10.0,    10.0, -10.0,  10.0])

    # Assuming xx0, xx1, and bScale
    #   are positive makes nice simplifications of
    #   unit vectors possible.
    xx[0],xx[1] = sp.symbols("xx0 xx1", real=True)

    xxmin = [sp.sympify(0), sp.sympify(0),-M_PI]
    xxmax = [         AMAX,          M_PI, M_PI]

    AA = xx[0]

    if CoordSystem == "SinhSymTP":
        # With xxmax[0] == AMAX, sinh(xx0/AMAX) will evaluate to a number between 0 and 1.
        #   Similarly, sinh(xx0/(AMAX*SINHWAA)) / sinh(1/SINHWAA) will also evaluate to a number between 0 and 1.
        #   Then AA = AMAX*sinh(xx0/(AMAX*SINHWAA)) / sinh(1/SINHWAA) will evaluate to a number between 0 and AMAX.
        AA = AMAX * (sp.exp(xx[0] / (AMAX*SINHWAA)) - sp.exp(-xx[0] / (AMAX*SINHWAA))) / (sp.exp(1 / SINHWAA) - sp.exp(-1 / AMAX))

    var1 = sp.sqrt(AA**2 + (bScale * sp.sin(xx[1]))**2)
    var2 = sp.sqrt(AA**2 + bScale**2)

    RHOSYMTP = AA*sp.sin(xx[1])
    PHSYMTP = xx[2]
    ZSYMTP = var2*sp.cos(xx[1])

    xx_to_Cart[0] = AA  *sp.sin(xx[1])*sp.cos(xx[2])
    xx_to_Cart[1] = AA  *sp.sin(xx[1])*sp.sin(xx[2])
    xx_to_Cart[2] = ZSYMTP

    xxSph[0] = sp.sqrt(RHOSYMTP**2 + ZSYMTP**2)
    xxSph[1] = sp.acos(ZSYMTP / xxSph[0])
    xxSph[2] = PHSYMTP

    scalefactor_orthog[0] = sp.diff(AA,xx[0]) * var1 / var2
    scalefactor_orthog[1] = var1
    scalefactor_orthog[2] = AA * sp.sin(xx[1])

    # Set the transpose of the matrix of unit vectors
    UnitVectors = [[sp.sin(xx[1]) * sp.cos(xx[2]) * var2 / var1,
                    sp.sin(xx[1]) * sp.sin(xx[2]) * var2 / var1,
                    AA * sp.cos(xx[1]) / var1],
                   [AA * sp.cos(xx[1]) * sp.cos(xx[2]) / var1,
                    AA * sp.cos(xx[1]) * sp.sin(xx[2]) / var1,
                        -sp.sin(xx[1]) * var2 / var1],
                   [-sp.sin(xx[2]), sp.cos(xx[2]), sp.sympify(0)]]
```

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

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

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-Reference_Metric")
```

    Created Tutorial-Reference_Metric.tex, and compiled LaTeX file to PDF file
        Tutorial-Reference_Metric.pdf


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c) 2019 Daniel Koehn, based on (c)2018 L.A. Barba, G.F. Forsyth [CFD Python](https://github.com/barbagroup/CFDPython#cfd-python), (c)2014 L.A. Barba, I. Hawke, B. Knaepen [Practical Numerical Methods with Python](https://github.com/numerical-mooc/numerical-mooc#practical-numerical-methods-with-python), also under CC-BY.

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

<link href="https://fonts.googleapis.com/css?family=Merriweather:300,300i,400,400i,700,700i,900,900i" rel='stylesheet' >
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,700,700i" rel='stylesheet' >
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' >
<style>

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

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

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

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

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

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

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

div.text_cell_render{
    font-family: 'Source Sans Pro', sans-serif;
    line-height: 140%;
    font-size: 110%;
    width:680px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Merriweather', serif;
    font-style:regular;
    font-weight: bold;
    font-size: 250%;
    line-height: 100%;
    color: #004065;
    margin-bottom: 1em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Merriweather', serif;
    font-weight: bold;
    font-size: 180%;
    line-height: 100%;
    color: #0096d6;
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Merriweather', serif;
	font-size: 150%;
    margin-top:12px;
    margin-bottom: 3px;
    font-style: regular;
    color: #008367;
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Merriweather', serif;
    font-weight: 300;
    font-size: 100%;
    line-height: 120%;
    text-align: left;
    width:500px;
    margin-top: 1em;
    margin-bottom: 2em;
    margin-left: 80pt;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Source Sans Pro', sans-serif;
    font-weight: regular;
    font-size: 130%;
    color: #e31937;
    font-style: italic;
    margin-bottom: .5em;
    margin-top: 1em;
    display: block;
}

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

    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/

    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>

# 1D Heat Conduction Example II: Cooling of a half-space

In this exercise we will focus on another problem in geophysics, which can be described by the 1D heat conduction equation: the cooling of a half-space. By solving this problem, we can understand the generation and cooling of the oceanic lithosphere on earth.

## Cooling of the Oceanic Lithosphere

The rigid outer shell of the earth, the [lithosphere](https://en.wikipedia.org/wiki/Lithosphere), is generated at the [**mid-ocean ridges (MOR)**](https://en.wikipedia.org/wiki/Mid-ocean_ridge) from molten mantle material. Due to [ridge push](https://en.wikipedia.org/wiki/Ridge_push) and [slab pull](https://en.wikipedia.org/wiki/Slab_pull) the new oceanic lithosphere is spreading away from the MORs.

.
#### Figure 1. Generation and cooling of the oceanic lithosphere.

Neglecting heat transport by advection and restrict heat conduction to the vertical direction $z$, we can describe the cooling of the oceanic lithosphere by the 1D heat conduction equation:

$$
\begin{equation}
\frac{\partial T}{\partial t} = \alpha \frac{\partial^2 T}{\partial z^2} \notag
\end{equation}
$$

Here, $\alpha$ is the thermal diffusivity and temperature $T$.

Let's take a deeper look into the problem.

##### Exercise 1

Lets assume that we have a partially molten half-space at the MOR with a constant temperature $T_{init} = 1300^oC$ down to a depth $z_1 = 250\;km$. The half-space is cooled by the lower surface temperature $T_{surf}=20^oC$ at $z_0 = 0\; km$.

Additionally, the [thermal diffusivity](http://en.wikipedia.org/wiki/Thermal_diffusivity) of the mantle rock is $\alpha=1\times10^{-6} {\rm m}^2/{\rm s}$. How will the temperature evolve in the lithosphere and upper mantle as a function of time? We can compare the numerical solution of the problem with an analytical solution:

\begin{equation}
T(z,t) = T_{init} + (T_{surf} - T_{init})\; \text{erfc}\biggl(\frac{z}{2\sqrt{\alpha t}}\biggr) \tag{1}
\end{equation}

where $\text{erfc}$ denotes the **Complementary Error Function**. The `erfc` can be computed using the `SciPy` package. For more details of the implementation, I refer to Prof. Ecosia.

In this exercise, we want to compute the temperature profiles $T(z,t)$ of the cooled half-space after $t=10\;\text{Ma}$, $t=60\;\text{Ma}$ and $t=100\;\text{Ma}$, respectively.

As usual, start by importing some libraries and setting up the discretization.

```python
import numpy
from matplotlib import pyplot
from scipy import special
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
```

We'll begin by defining a few parameters ...

- depth of the half-space $L = 250\; \text{km}$
- a spatial grid with $nz = 100\; \text{points}$ in z-direction
- thermal diffusivity $\alpha = 1\times10^{-6}\; m^2/s$
- initial temperature of the half-space $T_{init} = 1300^oC$
- surface temperature $T_{surf}=20^oC$

```python
# Set parameters
L =      # DEFINE DEPTH OF HALF-SPACE HERE [m]!
nz =     # DEFINE NUMBER OF GRIDPOINTS NZ HERE!
dz = L / (nz - 1)  # spatial distance between two consecutive locations [m]
alpha =    #  DEFINE THERMAL DIFFUSIVITY OF ROCK HERE [m^2/s]!

# Define the depth locations [m]
z = numpy.linspace(0.0, L, num=nz)

# Set the initial temperature in the half-space
T_init =  # INITIAL TEMPERATURE OF THE MOLTEN HALF-SPACE [°C]
T0 = numpy.ones(nz)
T0 = T0 * T_init

# COOL THE HALF-SPACE BY SETTING THE TEMPERATURE AT THE FIRST GRIDPOINT
# TO THE SURFACE TEMPERATURE AS DIRICHLET BOUNDARY CONDITION
T_surf =   # SURFACE TEMPERATURE [°C]
T0[0] = T_surf
```

To solve the 1D heat conduction equation, we are using the forward-time, centered-space **FTCS** discretization. You should work it out on a piece of paper yourself (if you can't do it without looking it up, it means you need to do this more!).

$$
\begin{equation}
\frac{T_{i}^{n+1}-T_{i}^{n}}{\Delta t}=\alpha\frac{T_{i+1}^{n}-2T_{i}^{n}+T_{i-1}^{n}}{\Delta z^2} \notag
\end{equation}
$$

To obtain the temperature at the next time step, $T^{n+1}_i$, from the known information at the current time step, we compute

$$
\begin{equation}
T_{i}^{n+1}=T_{i}^{n}+\frac{\alpha\Delta t}{\Delta z^2}(T_{i+1}^{n}-2T_{i}^{n}+T_{i-1}^{n}) \notag
\end{equation}
$$

In the [first lesson of module 5](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/02_spacetime/02_03_1DDiffusion.ipynb), we discretized the diffusion equation  with a forward-time, centered-space scheme, subject to the following stability constraint:

$$
\begin{equation}
\alpha \frac{\Delta t}{(\Delta z)^2} \leq \frac{1}{2} \notag
\end{equation}
$$

The following function already implements this numerical scheme:

```python
def ftcs_cool_lith(T0, nt, nz, dt, dz, alpha):
    """
    Computes and returns the depth-temperature distrubtion (geotherm)
    for the half-space cooling problem, according to a provided number
    of time steps, a given initial temperature and thermal diffusivity.
    The diffusion equation is integrated using forward differencing in
    time and central differencing in space.

    Parameters
    ----------
    T0 : numpy.ndarray
        The initial temperature as a 1D array of floats.
    nt : integer
        The number of time steps to compute.
    nz : integer
        The number of spatial grid points.
    dt : float
        The time-step size to integrate.
    dz : float
        The distance between two consecutive locations.
    alpha : float
        The thermal diffusivity of the rock.

    Returns
    -------
    T : numpy.ndarray
        The geotherm as a 1D array of floats.
    """

    T = T0.copy()
    sigma = alpha * dt / dz**2
    for n in range(nt): # loop over time steps
        Tn = T.copy()
        for i in range(1,nz-1): # loop over spatial grid
            T[i] = (Tn[i] + sigma * (Tn[i+1] - 2.0 * Tn[i] + Tn[i-1]))
    return T
```

We are all set to run! Lets compute the temperature profile after **10 Ma years**.
First, you have to define the maximum cooling time **Tmax in Million years [Ma]**. To get correct results, do not forget to convert the units of Tmax from [Ma] to [s].

Next, we define a time step `dt` that satisfies the stability constraint and calculate the number of time steps `nt` for the given `Tmax` and `dt`. Compute the temperature profile after 10 Ma years using the FD code `T10Ma` and the analytical solution `T10Ma_an`

```python
# DEFINE MAXIMUM COOLING TIME in [Ma]
Tmax =

# CONVERT Tmax UNITS [Ma] -> [s] HERE!
Tmax =

# Set the time-step size based on CFL limit.
sigma = 0.5
dt = sigma * dz**2 / alpha  # time-step size
nt = (int)(Tmax/dt)         # number of time steps

# Compute the temperature profile in the lithosphere and asthenosphere
T10Ma = ftcs_cool_lith(T0, nt, nz, dt, dz, alpha)

# COMPUTE ANALYTICAL SOLUTION EQ.(1) HERE!
T10Ma_an =
```

Compute the temperature profile after **60 Ma years**

```python
# DEFINE MAXIMUM COOLING TIME in [Ma]
Tmax =

# CONVERT Tmax UNITS [Ma] -> [s] HERE!
Tmax =

# Set the time-step size based on CFL limit.
sigma = 0.5
dt = sigma * dz**2 / alpha  # time-step size
nt = (int)(Tmax/dt)         # number of time steps

# Compute the temperature profile in the lithosphere and asthenosphere
T60Ma = ftcs_cool_lith(T0, nt, nz, dt, dz, alpha)

# COMPUTE ANALYTICAL SOLUTION EQ.(1) HERE!
T60Ma_an =
```

Compute the temperature profile after **100 Ma years**

```python
# DEFINE MAXIMUM COOLING TIME in [Ma]
Tmax =

# CONVERT Tmax UNITS [Ma] -> [s] HERE!
Tmax =

# Set the time-step size based on CFL limit.
sigma = 0.5
dt = sigma * dz**2 / alpha  # time-step size
nt = (int)(Tmax/dt)         # number of time steps

# Compute the temperature profile in the lithosphere and asthenosphere
T100Ma = ftcs_cool_lith(T0, nt, nz, dt, dz, alpha)

# COMPUTE ANALYTICAL SOLUTION EQ.(1) HERE!
T100Ma_an =
```

Plot the initial and different temperature profiles of the numerical and analytical solutions. Describe, compare and interpret the results ...

```python
# Plot the temperature profiles of the half-space after Tmax = 10, 60, 100 Ma
pyplot.figure(figsize=(8.0, 6.0))
pyplot.ylabel('Depth [km]')
pyplot.xlabel('Temperature [C]')
pyplot.grid()

# Initial temperature profile
pyplot.plot(T0, z/1000, color='C6', linestyle='-', linewidth=3, label='Tmax = 0 Ma')

# Temperature profile after 10 Ma
pyplot.plot(T10Ma, z/1000, color='C0', linestyle='-', linewidth=3, label='Tmax = 10 Ma')
pyplot.plot(T10Ma_an, z/1000, color='C1', linestyle='--', label='Tmax_an = 10 Ma', linewidth=2)

# Temperature profile after 60 Ma
pyplot.plot(T60Ma, z/1000, color='C2', linestyle='-', linewidth=3, label='Tmax = 60 Ma')
pyplot.plot(T60Ma_an, z/1000, color='C3', linestyle='--', label='Tmax_an = 60 Ma', linewidth=2)

# Temperature profile after 100 Ma
pyplot.plot(T100Ma, z/1000, color='C4', linestyle='-', linewidth=3, label='Tmax = 100 Ma')
pyplot.plot(T100Ma_an, z/1000, color='C5', linestyle='--', label='Tmax_an = 100 Ma', linewidth=2)

pyplot.legend()
pyplot.ylim(0.0, L/1000)
pyplot.xlim(100.0, 1500.0)
pyplot.gca().invert_yaxis()
```

##### Exercise 2

The boundary between the rigid [lithosphere](https://en.wikipedia.org/wiki/Lithosphere) and the partially molten [asthenosphere](https://en.wikipedia.org/wiki/Asthenosphere)
is defined by the $1100^oC$ isotherm. Modify the code `ftcs_lith_thickness` below to compute the thickness of the lithosphere at each time step and return the lithospheric thickness `d_lith` and times `t`

```python
def ftcs_lith_thickness(T0, nt, nz, dt, dz, alpha):
    """
    Computes and returns the depth-temperature distrubtion (geotherm)
    for the half-space cooling problem, according to a provided number
    of time steps, a given initial temperature and thermal diffusivity.
    The diffusion equation is integrated using forward differencing in
    time and central differencing in space.

    Parameters
    ----------
    T0 : numpy.ndarray
        The initial temperature as a 1D array of floats.
    nt : integer
        The number of time steps to compute.
    nz : integer
        The number of spatial grid points.
    dt : float
        The time-step size to integrate.
    dz : float
        The distance between two consecutive locations.
    alpha : float
        The thermal diffusivity of the rock.

    Returns
    -------
    d_lith : numpy.ndarray
        The lithosphere thickness as a 1D array of floats.
    t : numpy.ndarray
        Time as a 1D array of floats.
    """

    T = T0.copy()
    sigma = alpha * dt / dz**2
    for n in range(nt): # loop over time steps
        Tn = T.copy()

        for i in range(1,nz-1): # loop over spatial grid
            T[i] = (Tn[i] + sigma * (Tn[i+1] - 2.0 * Tn[i] + Tn[i-1]))

        # ESTIMATE THE LITHOSPHERE THICKNESS BASED ON THE 1100°C ISOTHERM HERE!
        TLAB = 1100.

    return d_lith, t
```

Compute the evolution of the lithosphere thickness up to Tmax = 100 Ma

```python
# Compute lithosphere thickness for Tmax = 100 Ma
Tmax =

# CONVERT Tmax UNITS [Ma] -> [s] HERE!
Tmax =

# Set the time-step size based on CFL limit.
sigma = 0.5
dt = sigma * dz**2 / alpha  # time-step size
nt = (int)(Tmax/dt)         # number of time steps

# initialize arrays for lithosphere thickness d_lith and time t
d_lith = numpy.zeros(nt)
t = numpy.zeros(nt)

# Compute the thickness of the lithosphere d_lith at times t
d_lith, t = ftcs_lith_thickness(T0, nt, nz, dt, dz, alpha)
```

Plot lithosphere depth over time. The thickness of the oceanic lithosphere can be approximated as a thermal boundary layer that thickens with the square root of time

\begin{equation}
h \approx 2 \sqrt{\alpha t} \tag{2}
\end{equation}

Here, $h$ denotes the thickness of the oceanic mantle lithosphere, $\alpha$ is the thermal diffusivity and $t$ is the age of the given part of the lithosphere. Compare and discuss the thickness of the lithosphere based on the FD solution $d_{lith}$ and approximation $h$ from eq. (2)

```python
# Plot the lithosphere thickness over time
pyplot.figure(figsize=(8.0, 6.0))
pyplot.ylabel('Lithosphere Thickness [km]')
pyplot.xlabel('Time [Ma]')
pyplot.grid()

# PLOT LITHOSPHERE THICKNESS OVER TIME FROM FD MODELLING HERE!
pyplot.plot(t, d_lith, color='C0', linestyle='-', linewidth=3, label='FD-based dlith')

# COMPUTE LITHOSPHERE THICKNESS OVER TIME FROM EQ.(2) HERE!
h =

# PLOT LITHOSPHERE THICKNESS OVER TIME FROM EQ.(2) HERE!
pyplot.plot(t, h, color='C1', linestyle='-', linewidth=3, label='analytical-based h')

pyplot.legend()
```

## What we learned:

- How to solve the cooling of a half-space problem using the finite-difference and analytical solution of the 1D heat conduction equation
- Estimation of the lithosphere thickness based on the $1100°C$ isotherm


# Gasoline Blending

The task is to determine the most profitable blend of gasoline products from given set of refinery streams.

```python
%matplotlib inline

import shutil
import sys
import os.path

if not shutil.which("pyomo"):
    !pip install -q pyomo
    assert(shutil.which("pyomo"))

if not (shutil.which("cbc") or os.path.isfile("cbc")):
    if "google.colab" in sys.modules:
        !apt-get install -y -qq coinor-cbc
    else:
        try:
            !conda install -c conda-forge coincbc
        except:
            pass

assert(shutil.which("cbc") or os.path.isfile("cbc"))

import pandas as pd
import pyomo.environ as pyomo
```

    Collecting package metadata (current_repodata.json): done
    Solving environment: -

## Gasoline Product Specifications

The gasoline products include regular and premium gasoline. In addition to the current price, the specifications include

* **octane** the minimum road octane number.  Road octane is the computed as the average of the Research Octane Number (RON) and Motor Octane Number (MON).
* **Reid Vapor Pressure** Upper and lower limits are specified for the Reid vapor pressure. The Reid vapor pressure is the absolute pressure exerted by the liquid at 100°F.
* **benzene** the maximum volume percentage of benzene allowed in the final product. Benzene helps to increase octane rating, but is also a treacherous environmental contaminant.

```python
products = {
    'Regular'        : {'price': 2.75, 'octane': 87, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
    'Premium'        : {'price': 2.85, 'octane': 91, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
}

print(pd.DataFrame.from_dict(products).T)
```

## Stream Specifications

A typical refinery produces many intermediate streams that can be incorporated in a blended gasoline product. Here we provide data on seven streams that include:

* **Butane** n-butane is a C4 product stream produced from the light components of the crude being processed by the refinery. Butane is a highly volatile of gasoline.
* **LSR** Light straight run naptha is a 90°F to 190°F cut from the crude distillation column primarily consisting of straight chain C5-C6 hydrocarbons.
* **Isomerate** is the result of isomerizing LSR to produce branched molecules that results in higher octane number.
* **Reformate** is result of catalytic reforming heavy straight run napthenes to produce a high octane blending component, as well by-product hydrogen used elsewhere in the refinery for hydro-treating.
* **Reformate LB** is a is a low benzene variant of reformate.
* **FCC Naphta** is the product of a fluidized catalytic cracking unit designed to produce gasoline blending components from long chain hydrocarbons present in the crude oil being processed by the refinery.
* **Alkylate** The alkylation unit reacts iso-butane with low-molecular weight alkenes to produce a high octane blending component for gasoline.

The stream specifications include research octane and motor octane numbers for each blending component, the Reid vapor pressure, the benzene content, cost, and availability (in gallons per day). The road octane number is computed as the average of the RON and MON.

```python
streams = {
    'Butane'       : {'RON': 93.0, 'MON': 92.0, 'RVP': 54.0, 'benzene': 0.00, 'cost': 0.85, 'avail': 30000},
    'LSR'          : {'RON': 78.0, 'MON': 76.0, 'RVP': 11.2, 'benzene': 0.73, 'cost': 2.05, 'avail': 35000},
    'Isomerate'    : {'RON': 83.0, 'MON': 81.1, 'RVP': 13.5, 'benzene': 0.00, 'cost': 2.20, 'avail': 0},
    'Reformate'    : {'RON':100.0, 'MON': 88.2, 'RVP':  3.2, 'benzene': 1.85, 'cost': 2.80, 'avail': 60000},
    'Reformate LB' : {'RON': 93.7, 'MON': 84.0, 'RVP':  2.8, 'benzene': 0.12, 'cost': 2.75, 'avail': 0},
    'FCC Naphtha'  : {'RON': 92.1, 'MON': 77.1, 'RVP':  1.4, 'benzene': 1.06, 'cost': 2.60, 'avail': 70000},
    'Alkylate'     : {'RON': 97.3, 'MON': 95.9, 'RVP':  4.6, 'benzene': 0.00, 'cost': 2.75, 'avail': 40000},
}

# calculate road octane as (R+M)/2
for s in streams.keys():
    streams[s]['octane'] = (streams[s]['RON'] + streams[s]['MON'])/2

# display feed information
print(pd.DataFrame.from_dict(streams).T)
```

## Blending Model

This simplified blending model assumes the product attributes can be computed as linear volume weighted averages of the component properties. Let the decision variable $x_{s,p} \geq 0$ be the volume, in gallons, of blending component $s \in S$ used in the final product $p \in P$.

The objective is maximize profit, which is the difference between product revenue and stream costs.

\begin{align}
\mbox{profit} & = \max_{x_{s,p}}\left( \sum_{p\in P} \mbox{Price}_p \sum_{s\in S} x_{s,p}
- \sum_{s\in S} \mbox{Cost}_s \sum_{p\in P} x_{s,p}\right)
\end{align}
or
\begin{align}
\mbox{profit} & = \max_{x_{s,p}} \sum_{p\in P}  \sum_{s\in S} x_{s,p}\mbox{Price}_p - \max_{x_{s,p}} \sum_{p\in P}  \sum_{s\in S} x_{s,p}\mbox{Cost}_s
\end{align}

The blending constraint for octane can be written as

\begin{align}
\frac{\sum_{s \in S} x_{s,p} \mbox{Octane}_s}{\sum_{s \in S} x_{s,p}} & \geq \mbox{Octane}_p & \forall p \in P
\end{align}

where $\mbox{Octane}_s$ refers to the octane rating of stream $s$, whereas $\mbox{Octane}_p$ refers to the octane rating of product $p$. Multiplying through by the denominator, and consolidating terms gives

\begin{align}
\sum_{s \in S} x_{s,p}\left(\mbox{Octane}_s - \mbox{Octane}_p\right) & \geq  0 & \forall p \in P
\end{align}

The same assumptions and development apply to the benzene constraint

\begin{align}
\sum_{s \in S} x_{s,p}\left(\mbox{Benzene}_s - \mbox{Benzene}_p\right) & \leq  0 & \forall p \in P
\end{align}

Reid vapor pressure, however, follows a somewhat different mixing rule.  For the Reid vapor pressure we have

\begin{align}
\sum_{s \in S} x_{s,p}\left(\mbox{RVP}_s^{1.25} - \mbox{RVP}_{min,p}^{1.25}\right) & \geq  0 & \forall p \in P \\
\sum_{s \in S} x_{s,p}\left(\mbox{RVP}_s^{1.25} - \mbox{RVP}_{max,p}^{1.25}\right) & \leq  0 & \forall p \in P
\end{align}

This model is implemented in the following cell.

```python
import pyomo.environ as pyomo

# create model
m = pyomo.ConcreteModel()

# create decision variables
S = streams.keys()
P = products.keys()
m.x = pyomo.Var(S,P, domain=pyomo.NonNegativeReals)

# objective
revenue = sum(sum(m.x[s,p]*products[p]['price'] for s in S) for p in P)
cost = sum(sum(m.x[s,p]*streams[s]['cost'] for s in S) for p in P)
m.profit = pyomo.Objective(expr = revenue - cost, sense=pyomo.maximize)

# constraints
m.cons = pyomo.ConstraintList()
for s in S:
    m.cons.add(sum(m.x[s,p] for p in P) <= streams[s]['avail'])
for p in P:
    m.cons.add(sum(m.x[s,p]*(streams[s]['octane'] -    products[p]['octane'])       for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['RVP']**1.25 - products[p]['RVPmin']**1.25) for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['RVP']**1.25 - products[p]['RVPmax']**1.25) for s in S) <= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['benzene'] -   products[p]['benzene'])      for s in S) <= 0)

# solve
solver = pyomo.SolverFactory('cbc')
solver.solve(m)

# display results
vol = sum(m.x[s,p]() for s in S for p in P)
print("Total Volume =", round(vol, 1), "gallons.")
print("Total Profit =", round(m.profit(), 1), "dollars.")
print("Profit =", round(100*m.profit()/vol,1), "cents per gallon.")
```

## Display Results

### Results for each Stream

```python
stream_results = pd.DataFrame()
for s in S:
    for p in P:
        stream_results.loc[s,p] = round(m.x[s,p](), 1)
    stream_results.loc[s,'Total'] = round(sum(m.x[s,p]() for p in P), 1)
    stream_results.loc[s,'Available'] = streams[s]['avail']

stream_results['Unused (Slack)'] = stream_results['Available'] - stream_results['Total']
print(stream_results)
```

### Results for each Product

```python
product_results = pd.DataFrame()
for p in P:
    product_results.loc[p,'Volume'] = round(sum(m.x[s,p]() for s in S), 1)
    product_results.loc[p,'octane'] = round(sum(m.x[s,p]()*streams[s]['octane'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
    product_results.loc[p,'RVP'] = round((sum(m.x[s,p]()*streams[s]['RVP']**1.25 for s in S)
                                            /product_results.loc[p,'Volume'])**0.8, 1)
    product_results.loc[p,'benzene'] = round(sum(m.x[s,p]()*streams[s]['benzene'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
print(product_results)
```

## Exercise 1.

The marketing team says there is an opportunity to create a mid-grade gasoline product with a road octane number of 89 that would sell for $2.82/gallon, and with all other specifications the same. Could an additional profit be created?

Create a new cell (or cells) below to compute a solution to this exercise.

```python
products = {
    'Regular'        : {'price': 2.75, 'octane': 87, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
    'Midgrade'       : {'price': 2.82, 'octane': 89, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
    'Premium'        : {'price': 2.85, 'octane': 91, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
}

print(pd.DataFrame.from_dict(products).T)

streams = {
    'Butane'       : {'RON': 93.0, 'MON': 92.0, 'RVP': 54.0, 'benzene': 0.00, 'cost': 0.85, 'avail': 30000},
    'LSR'          : {'RON': 78.0, 'MON': 76.0, 'RVP': 11.2, 'benzene': 0.73, 'cost': 2.05, 'avail': 35000},
    'Isomerate'    : {'RON': 83.0, 'MON': 81.1, 'RVP': 13.5, 'benzene': 0.00, 'cost': 2.20, 'avail': 0},
    'Reformate'    : {'RON':100.0, 'MON': 88.2, 'RVP':  3.2, 'benzene': 1.85, 'cost': 2.80, 'avail': 60000},
    'Reformate LB' : {'RON': 93.7, 'MON': 84.0, 'RVP':  2.8, 'benzene': 0.12, 'cost': 2.75, 'avail': 0},
    'FCC Naphtha'  : {'RON': 92.1, 'MON': 77.1, 'RVP':  1.4, 'benzene': 1.06, 'cost': 2.60, 'avail': 70000},
    'Alkylate'     : {'RON': 97.3, 'MON': 95.9, 'RVP':  4.6, 'benzene': 0.00, 'cost': 2.75, 'avail': 40000},
}

# calculate road octane as (R+M)/2
for s in streams.keys():
    streams[s]['octane'] = (streams[s]['RON'] + streams[s]['MON'])/2

# display feed information
print(pd.DataFrame.from_dict(streams).T)

# create model
m = pyomo.ConcreteModel()

# create decision variables
S = streams.keys()
P = products.keys()
m.x = pyomo.Var(S,P, domain=pyomo.NonNegativeReals)

# objective
revenue = sum(sum(m.x[s,p]*products[p]['price'] for s in S) for p in P)
cost = sum(sum(m.x[s,p]*streams[s]['cost'] for s in S) for p in P)
m.profit = pyomo.Objective(expr = revenue - cost, sense=pyomo.maximize)

# constraints
m.cons = pyomo.ConstraintList()
for s in S:
    m.cons.add(sum(m.x[s,p] for p in P) <= streams[s]['avail'])
for p in P:
    m.cons.add(sum(m.x[s,p]*(streams[s]['octane'] -    products[p]['octane'])       for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['RVP']**1.25 - products[p]['RVPmin']**1.25) for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['RVP']**1.25 - products[p]['RVPmax']**1.25) for s in S) <= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['benzene'] -   products[p]['benzene'])      for s in S) <= 0)

# solve
solver = pyomo.SolverFactory('cbc')
solver.solve(m)
print("Profit = $", round(m.profit(),2))

stream_results = pd.DataFrame()
for s in S:
    for p in P:
        stream_results.loc[s,p] = round(m.x[s,p](), 1)
    stream_results.loc[s,'Total'] = round(sum(m.x[s,p]() for p in P), 1)
    stream_results.loc[s,'Available'] = streams[s]['avail']

stream_results['Unused (Slack)'] = stream_results['Available'] - stream_results['Total']
print(stream_results)

product_results = pd.DataFrame()
for p in P:
    product_results.loc[p,'Volume'] = round(sum(m.x[s,p]() for s in S), 1)
    product_results.loc[p,'octane'] = round(sum(m.x[s,p]()*streams[s]['octane'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
    product_results.loc[p,'RVP'] = round((sum(m.x[s,p]()*streams[s]['RVP']**1.25 for s in S)
                                            /product_results.loc[p,'Volume'])**0.8, 1)
    product_results.loc[p,'benzene'] = round(sum(m.x[s,p]()*streams[s]['benzene'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
print(product_results)

vol = sum(m.x[s,p]() for s in S for p in P)
print("Total Profit =", round(m.profit(), 1), "dollars.")
print("Total Volume =", round(vol, 1), "gallons.")
print("Profit =", round(100*m.profit()/vol,1), "cents per gallon.")
```

## Exercise 2.

New environmental regulations have reduced the allowable benzene levels from 1.1 vol% to 0.62 vol%, and the maximum Reid vapor pressure from 15.0 to 9.0.

Compared to the base case (i.e., without the midgrade product), how does this change profitability?

```python
products = {
    'Regular'        : {'price': 2.75, 'octane': 87, 'RVPmin': 0.0, 'RVPmax': 9.0, 'benzene': 0.62},
    'Premium'        : {'price': 2.85, 'octane': 91, 'RVPmin': 0.0, 'RVPmax': 9.0, 'benzene': 0.62},
}

print(pd.DataFrame.from_dict(products).T)

streams = {
    'Butane'       : {'RON': 93.0, 'MON': 92.0, 'RVP': 54.0, 'benzene': 0.00, 'cost': 0.85, 'avail': 30000},
    'LSR'          : {'RON': 78.0, 'MON': 76.0, 'RVP': 11.2, 'benzene': 0.73, 'cost': 2.05, 'avail': 35000},
    'Isomerate'    : {'RON': 83.0, 'MON': 81.1, 'RVP': 13.5, 'benzene': 0.00, 'cost': 2.20, 'avail': 0},
    'Reformate'    : {'RON':100.0, 'MON': 88.2, 'RVP':  3.2, 'benzene': 1.85, 'cost': 2.80, 'avail': 60000},
    'Reformate LB' : {'RON': 93.7, 'MON': 84.0, 'RVP':  2.8, 'benzene': 0.12, 'cost': 2.75, 'avail': 0},
    'FCC Naphtha'  : {'RON': 92.1, 'MON': 77.1, 'RVP':  1.4, 'benzene': 1.06, 'cost': 2.60, 'avail': 70000},
    'Alkylate'     : {'RON': 97.3, 'MON': 95.9, 'RVP':  4.6, 'benzene': 0.00, 'cost': 2.75, 'avail': 40000},
}

# calculate road octane as (R+M)/2
for s in streams.keys():
    streams[s]['octane'] = (streams[s]['RON'] + streams[s]['MON'])/2

# display feed information
print(pd.DataFrame.from_dict(streams).T)

# create model
m = pyomo.ConcreteModel()

# create decision variables
S = streams.keys()
P = products.keys()
m.x = pyomo.Var(S,P, domain=pyomo.NonNegativeReals)

# objective
revenue = sum(sum(m.x[s,p]*products[p]['price'] for s in S) for p in P)
cost = sum(sum(m.x[s,p]*streams[s]['cost'] for s in S) for p in P)
m.profit = pyomo.Objective(expr = revenue - cost, sense=pyomo.maximize)

# constraints
m.cons = pyomo.ConstraintList()
for s in S:
    m.cons.add(sum(m.x[s,p] for p in P) <= streams[s]['avail'])
for p in P:
    m.cons.add(sum(m.x[s,p]*(streams[s]['octane'] -    products[p]['octane'])       for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['RVP']**1.25 - products[p]['RVPmin']**1.25) for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['RVP']**1.25 - products[p]['RVPmax']**1.25) for s in S) <= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['benzene'] -   products[p]['benzene'])      for s in S) <= 0)

# solve
solver = pyomo.SolverFactory('cbc')
solver.solve(m)
print("Profit = $", round(m.profit(),2))

stream_results = pd.DataFrame()
for s in S:
    for p in P:
        stream_results.loc[s,p] = round(m.x[s,p](), 1)
    stream_results.loc[s,'Total'] = round(sum(m.x[s,p]() for p in P), 1)
    stream_results.loc[s,'Available'] = streams[s]['avail']

stream_results['Unused (Slack)'] = stream_results['Available'] - stream_results['Total']
print(stream_results)

product_results = pd.DataFrame()
for p in P:
    product_results.loc[p,'Volume'] = round(sum(m.x[s,p]() for s in S), 1)
    product_results.loc[p,'octane'] = round(sum(m.x[s,p]()*streams[s]['octane'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
    product_results.loc[p,'RVP'] = round((sum(m.x[s,p]()*streams[s]['RVP']**1.25 for s in S)
                                            /product_results.loc[p,'Volume'])**0.8, 1)
    product_results.loc[p,'benzene'] = round(sum(m.x[s,p]()*streams[s]['benzene'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
print(product_results)

vol = sum(m.x[s,p]() for s in S for p in P)
print("Total Profit =", round(m.profit(), 1), "dollars.")
print("Total Volume =", round(vol, 1), "gallons.")
print("Profit =", round(100*m.profit()/vol,1), "cents per gallon.")
```

## Exercise 3.

Given the new product specifications in Exercise 2, let's consider using different refinery streams. In place of Reformate, the refinery could produce Reformate LB. (That is, one or the other of the two streams could be 60000 gallons per day, but not both).  Same for LSR and Reformate.  How should the refinery be operated to maximize profitability?

```python
products = {
    'Regular'        : {'price': 2.75, 'octane': 87, 'RVPmin': 0.0, 'RVPmax': 9.0, 'benzene': 0.62},
    'Premium'        : {'price': 2.85, 'octane': 91, 'RVPmin': 0.0, 'RVPmax': 9.0, 'benzene': 0.62},
}

print(pd.DataFrame.from_dict(products).T)

streams = {
    'Butane'       : {'RON': 93.0, 'MON': 92.0, 'RVP': 54.0, 'benzene': 0.00, 'cost': 0.85, 'avail': 30000},
    'LSR'          : {'RON': 78.0, 'MON': 76.0, 'RVP': 11.2, 'benzene': 0.73, 'cost': 2.05, 'avail': 35000},
    'Isomerate'    : {'RON': 83.0, 'MON': 81.1, 'RVP': 13.5, 'benzene': 0.00, 'cost': 2.20, 'avail': 0},
    'Reformate'    : {'RON':100.0, 'MON': 88.2, 'RVP':  3.2, 'benzene': 1.85, 'cost': 2.80, 'avail': 0},
    'Reformate LB' : {'RON': 93.7, 'MON': 84.0, 'RVP':  2.8, 'benzene': 0.12, 'cost': 2.75, 'avail': 60000},
    'FCC Naphtha'  : {'RON': 92.1, 'MON': 77.1, 'RVP':  1.4, 'benzene': 1.06, 'cost': 2.60, 'avail': 70000},
    'Alkylate'     : {'RON': 97.3, 'MON': 95.9, 'RVP':  4.6, 'benzene': 0.00, 'cost': 2.75, 'avail': 40000},
}

# calculate road octane as (R+M)/2
for s in streams.keys():
    streams[s]['octane'] = (streams[s]['RON'] + streams[s]['MON'])/2

# display feed information
print(pd.DataFrame.from_dict(streams).T)

# create model
m = pyomo.ConcreteModel()

# create decision variables
S = streams.keys()
P = products.keys()
m.x = pyomo.Var(S,P, domain=pyomo.NonNegativeReals)

# objective
revenue = sum(sum(m.x[s,p]*products[p]['price'] for s in S) for p in P)
cost = sum(sum(m.x[s,p]*streams[s]['cost'] for s in S) for p in P)
m.profit = pyomo.Objective(expr = revenue - cost, sense=pyomo.maximize)

# constraints
m.cons = pyomo.ConstraintList()
for s in S:
    m.cons.add(sum(m.x[s,p] for p in P) <= streams[s]['avail'])
for p in P:
    m.cons.add(sum(m.x[s,p]*(streams[s]['octane'] -    products[p]['octane'])       for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['RVP']**1.25 - products[p]['RVPmin']**1.25) for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['RVP']**1.25 - products[p]['RVPmax']**1.25) for s in S) <= 0)
    m.cons.add(sum(m.x[s,p]*(streams[s]['benzene'] -   products[p]['benzene'])      for s in S) <= 0)

# solve
solver = pyomo.SolverFactory('cbc')
solver.solve(m)
print("Profit = $", round(m.profit(),2))

stream_results = pd.DataFrame()
for s in S:
    for p in P:
        stream_results.loc[s,p] = round(m.x[s,p](), 1)
    stream_results.loc[s,'Total'] = round(sum(m.x[s,p]() for p in P), 1)
    stream_results.loc[s,'Available'] = streams[s]['avail']

stream_results['Unused (Slack)'] = stream_results['Available'] - stream_results['Total']
print(stream_results)

product_results = pd.DataFrame()
for p in P:
    product_results.loc[p,'Volume'] = round(sum(m.x[s,p]() for s in S), 1)
    product_results.loc[p,'octane'] = round(sum(m.x[s,p]()*streams[s]['octane'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
    product_results.loc[p,'RVP'] = round((sum(m.x[s,p]()*streams[s]['RVP']**1.25 for s in S)
                                            /product_results.loc[p,'Volume'])**0.8, 1)
    product_results.loc[p,'benzene'] = round(sum(m.x[s,p]()*streams[s]['benzene'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
print(product_results)

vol = sum(m.x[s,p]() for s in S for p in P)
print("Total Profit =", round(m.profit(), 1), "dollars.")
print("Total Volume =", round(vol, 1), "gallons.")
print("Profit =", round(100*m.profit()/vol,1), "cents per gallon.")
```