# Teil 1 der Semesterarbeit: Lineare Transformationen

## Inhaltsverzeichnis
1. Theorie
   1.1 Lineare Abbildungen
   1.2 Matrizen
   1.3 Darstellung von linearen Abbildungen durch Matizen
   1.4 Die Anwendungen der linearen Transformationen
2. Code - Darstellung zweidimensionalen linearen Abbildungen
   2.1 Kreis
   2.2 Lineare Abbildungen
      2.2.1 Skalierung / Stauchen
      2.2.2 Scherung
      2.2.3 Drehung
      2.2.4 Spiegelung
      2.2.5 Projektion
      2.2.6 Komposition
3. Quellen
4. Collab

**Github Link für den Source Code**: https://github.com/greatmerlin/SA_Lineare_Transformationen

## 1. Theorie

### 1.1 Lineare Abbildungen

Im kartesischen Koordinaten System, Punkten können mit Hilfe von Funktionen in der zweiten Dimension dargestellt werden.
Nun werden wir schauen, wie wir Punkten darstellen können, die nichht das kartesische Koordinaten System als Basis haben, sondern ein beliebiges, nicht orthogonales System.

Die Koordinaten werden in Bezug auf Vektoren dargestellt, da ein Punkt soll gleich sein, wie sein Abbild in einem anderen Koordinatensystem.

Wenn wir ein Koordinatensystem in der Ebene haben, spielen die Vektoren $( 1, 0)^T$
und $(0, 1)^T$ eine besondere Rolle. Wenn wir die kennen, dann kennen wir
sowohl die Richtung der Achsen als auch ihren Massstab. Man nennt diese
Vektoren **kanonische Einheitsvektoren** und bezeichnet sie mit **e1 und e2**.

Zu einem beliebigen Punkt in der Ebene kann man dessen Koordinaten interpretieren als Faktoren, mit denen man die kanonischen Einheitsvektoren
multiplizieren muss, damit die Summe den Ortsvektor des Punktes ergibt:

$ $$\begin{pmatrix} x \\ y\\ \end{pmatrix}$$ =  $$\begin{pmatrix} 1 \\ 0\\ \end{pmatrix}$$ +  $$\begin{pmatrix} 0 \\ 1\\ \end{pmatrix}$$ =  x * $$\begin{pmatrix} 1 \\ 0\\ \end{pmatrix}$$ +  y * $$\begin{pmatrix} 0 \\ 1\\ \end{pmatrix}$$ $

Die Transformation von alten zu neuen Koordinatensystem nennt man **Lineare Abbildung**.

Lineare Abbildungen sind **strukturerhaltende Abbildungen zwischen Vektorräumen**, das heißt, sie erhalten die Addition und die skalare Multiplikation.

Sind $ V, W $ zwei Vektorräume (z. B. $ V = K^n $ und $ W = K^m $ ),
dann heißt eine Abbildung $ F : V → W $ eine **lineare Abbildung** (oder auch **lineare
Transformation**), wenn für alle Vektoren $ a, b ∈ V $ und alle Skalare $ k ∈ K $ gilt:

$ F(a + b) = F(a) + F(b) $,
$ F(k * a) = k*F(a) $.

Eine lineare Abbildung ist also mit Vektoraddition bzw. Multiplikation mit einem
Skalar verträglich in dem Sinn, dass die Reihenfolge Abbilden – Addieren (bzw.
Abbilden – Vielfaches bilden) vertauschbar ist.

Mann nennt eine Abbildung:
* **injektiv** wenn es zu jedem $ w ∈ W $ hochstens ein $ v ∈ V $ gibt.
* **surjektiv** wenn es zu jedem $ w ∈ W $ mindestens ein $ v ∈ V $ gibt.
* **bijektiv** wenn die Abbildung injektiv und surjektiv ist, es also zu jedem $ w ∈ W $ genau ein $ v ∈ V $ gibt.

Nun können wir die Verschiedene Menge veranschaulichen:

Drei wichtige Charakterisierungen der linearen Abbildung:
-  Eine Abbildung ist genau dann linear, wenn die Funktion von $ R^n nach R^m $ , die in der Form $ \vec{v} -> A * \vec{v} $ **durch eine Matrix** beschrieben wird.

Wie kommt man auf diese Formel?

Wenn nun ein Koordinatensystem mit neuen Einheitsvektoren b1 und b2 ver-
wendet werden soll, können diese als Vektorkombination der alten Vektoren
e1 und e2 dargestellt werden. Somit kann ein Punkt im neuen Koordina-
tensystem, beispielsweise P (1.5, 0.5) mit der genannten Formel in das alte
Koordinatensystem umgerechnet werden.

$ $$\begin{pmatrix} 1.5 \\ 0.5\\ \end{pmatrix}$$ =  $$\begin{pmatrix} 3 \\ 1\\ \end{pmatrix}$$ +  $$\begin{pmatrix} -1 \\ 2\\ \end{pmatrix}$$ =  1.5 * $$\begin{pmatrix} 3 \\ 1\\ \end{pmatrix}$$ +  0.5 * $$\begin{pmatrix} -1 \\ 2\\ \end{pmatrix}$$ = $$\begin{pmatrix} 4 \\ 2.5\\ \end{pmatrix}$$ $

und das kann auch als Matrix * Vektor dargestellt werden:

$ $$\begin{pmatrix} 4 \\ 2.5\\ \end{pmatrix}$$ = $$\begin{pmatrix} 3 & -1 \\ 1 & 2 \end{pmatrix}$$ * $$\begin{pmatrix} 1.5 \\ 0.5\\ \end{pmatrix}$$ $

so wir kommen auf die Formel:

$ \vec{v} → A * \vec{v} $

* Eine lineare Abbildung ist eine Funktion, die Punkte auf Punkte abbildet und dabei die folgenden drei Bedingungen erfüllt:
   – Eine Gerade wird entweder auf eine Gerade abgebildet, wobei die Streckenverhältnisse auf der Geraden erhalten bleiben,
   oder auf einen einzelnen Punkt.
   – Wenn zwei parallele Geraden auf zwei Geraden abgebildet werden, dann sind die Bildgeraden ebenfalls parallel.
   – Der Nullpunkt wird auf den Nullpunkt abgebildet.

* Eine lineare Abbildung ist eine Funktion f , die Vektoren auf Vektoren abbildet und für die $ f (v + w ) = f (v ) + f (w) $ sowie $ f (α · v ) = α · f ( v ) $
für beliebige Vektoren $ v $ und $ w $ sowie Skalare $ α $ gilt.

Als Beispiel, eine Visualisierung der Verträglichkeit mit der skalaren Multiplikation: Jede Skalierung $ λ v $ bleibt durch eine lineare Abbildung erhalten und es gilt $ f ( λ v ) = λ f ( v ) $.

### 1.2 Matrizen

Eine **Matrix** ist zunächst einmal nichts weiter als **ein rechteckiges Schema
von Zahlen**.
Die einzelnen Einträge nennt man wie üblich **Komponenten**.

$ A =  $$\begin{pmatrix} a1,1 & a1,2 & a1,3 \\ a2,1 & a2,2 & a2,3 \\ a3,1 & a3,2 & a3,3 \end{pmatrix}$$ $

In den meisten Programmiersprachen steht der Datentyp **array** (engl. für Feld, Anordnung) zur
Verfügung. In einem array können mehrere gleichartige Elemente zusammengefasst werden, auf die
mithilfe von Indizes zugegriffen wird. Hat jedes Element **einen Index**, so entspricht der array einem
**Vektor**; wird **jedes Element** durch **zwei Indizes** angegeben, so führt uns das auf den mathematischen
Begriff einer **Matrix**.

```python
# mit simpy kann eine Matrix leicht definiert und dargestellt werden, ein Beispiel:
from sympy import *
A = Matrix( [ [4, 5, 0], [-2, -2, 8], [-1, 4, 3] ] )
A
```

    Matrix([
    [ 4,  5, 0],
    [-2, -2, 8],
    [-1,  4, 3]])

### 1.3 Darstellung von linearen Abbildungen durch Matizen

Im endlichdimensionalen sind lineare Abbildungen **eng Matrizen verknüpft**: Die Anwendung einer Matrix auf einen Vektor ist eine lineare Abbildung und nach einer geeigneten Basiswahl **lässt sich jede lineare Abbildung durch eine Matrix ausdrücken**.

Ein Beispiel, eine Lineare Abbildung von $R^2$ nach $R^2$ lassen sich so hinschreiben:

$ $$\begin{pmatrix} x \\ y\\ \end{pmatrix}$$ →  $$\begin{pmatrix} ax & by \\ cx & dy\\ \end{pmatrix}$$ = $$\begin{pmatrix} a & b \\ c & d\\ \end{pmatrix}$$ * $$\begin{pmatrix} x \\ y\\ \end{pmatrix}$$ $

Zwei Beispiele sind:
i) wenn a = b = c = d = 1 dann $ \vec{x} → \vec{0} $
ii) wenn a = d = 1 && b = c = 0 dann $ \vec{x} → \vec{x} $  (identisch)

Sind $V$ und $W$ endlichdimensionale Vektorräume über dem Körper $K$, dann kann jede lineare Abbildung $f:V→W$, als Matrix $A$ dargestellt werden.

### 1.4 Die Anwendungen der linearen Transformationen (Abbildungen)

* Die einfachste Form der linearen Abbildung ist eine **Skalierung**

Die Skalierung ist die einfachste Form einer linearen Abbildung.
Man kann z.B. längs einer Achse strecken (zB $x$):

$ A = $$\begin{pmatrix} 1 & 0 \\ 0 & 1\\ \end{pmatrix}$$ $ →  $ A = $$\begin{pmatrix} 2 & 0 \\ 0 & 1\\ \end{pmatrix}$$ $

* **Stauchen**
Ein Skalierungsform, wo der $y$ Achse gestreckt wird:

$ B = $$\begin{pmatrix} 1 & 0 \\ 0 & 0.6\\ \end{pmatrix}$$ $

* Gleichzeitig in beide Richtungen **strecken und/oder stauchen**

$ C = $$\begin{pmatrix} 2 & 0 \\ 0 & 0.6\\ \end{pmatrix}$$ $

* **Scherung**

Hier bleiben die y-Koordinaten der Punkte gleich, während die x-Koordinaten
sich ändern. Die Punkte werden also parallel zur $x$-Achse verschoben.

$ D = $$\begin{pmatrix} 1 & 1.2 \\ 0 & 1\\ \end{pmatrix}$$ $

* **Drehung**

Die Matrix wird gedreht.
Eingaben mit einem positiven Winkel gegen den Uhrzeiger gedreht werden. Negative Winkel drehen sich in Uhrzeigerrichtung.

$ E = $$\begin{pmatrix} cos(a) & -sin(a) \\ sin(a) & cos(a)\\ \end{pmatrix}$$ $

* **Spiegelung**

Es wird an der Geraden gespiegelt, die durch den Nullpunkt geht
und einen Winkel von φ zur waagerechten Achse hat.

$ F = $$\begin{pmatrix} cos(2a) & sin(2a) \\ sin(2a) & -cos(2a)\\ \end{pmatrix}$$ $

* **Projektion**

In der Projektion fehlt jeweils entweder die X-Komponente oder die Y-Komponente komplett und die Punkte werden jeweils auf die Achsen abgebildet. Man sieht buchstäblich “ihre Schatten” auf der jeweiligen Achse.

$ F = $$\begin{pmatrix} 1 & 0 \\ 0 & 0\\ \end{pmatrix}$$ $

* **Komposition**

Die Komposition sind mehrere aufeinanderfolgende lineare Abbildungen.
Beispielsweise zuerst eine X-Skalierung und danach ein Drehen.

$ f o g = f(g(x)) $

## 2. Code - Darstellung zweidimensionalen linearen Abbildungen

```python
# Importieren Numpy und Matplotlib
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

# Vorbereirungscode für das alte und neue Koordinatenssystem (siehe 4. Collab)

def ticks_Ko_System(x,y):
    x_ticks = []
    y_ticks = []

    for x_tick in range(x[0], x[1] + 1):
        x_ticks.append(x_tick)

    for y_tick in range(y[0], y[1] + 1):
        y_ticks.append(y_tick)

    return (x_ticks, y_ticks)

def grid_Linien_Fugure(x_lim, y_lim):
    plt.figure(figsize=(10,10))

    plt.xlim(x_lim[0], x_lim[1])
    plt.ylim(y_lim[0], y_lim[1])

    x_ticks, y_ticks = ticks_Ko_System(x_lim, y_lim)
    plt.xticks(x_ticks)
    plt.yticks(y_ticks)

    plt.grid(True)

def grid_malen(e1, e2, x_lim, y_lim, color='green', line_width=1):

    A = np.array([e1, e2]) # Lineare Abbildung/transformations- Matrix

    x = []
    y = []

    for x_val in range(x_lim[0], x_lim[1] + 1):
        x.append(x_val)

    for y_val in range(y_lim[0], y_lim[1] + 1):
        y.append(y_val)

    # Senkrechte Linien
    for i in y:

        line_x = []
        line_y = []

        for j in x:
            v = np.array([x[j],y[i]]) # Erzeuge vector v von a Paar von x und y Koordinaten
            res = A @ v # mache die Linear Abbildung
            line_x.append(res[0]) # Addiere transformierten x Wert
            line_y.append(res[1]) # Addiere transformierten y Wert

        plt.plot(line_x, line_y, color=color, linewidth=line_width)

    # Waagerechte Linien
    for i in x:

        line_x = []
        line_y = []

        for j in y:
            v = np.array([x[i], y[j]])
            res = A @ v
            line_x.append(res[0])
            line_y.append(res[1])

        plt.plot(line_x, line_y, color=color, linewidth=line_width)
```

### 2.1 Kreis

```python
# Kreis Definieren und Darstellen

def kreisLinAb(e1, e2):

    # Radius des Kreises
    R = 1

    # Zahl der "Ecken" (Einheitskreis durch ein reguläres Vieleck (z.B. mit 500 Ecken) ersetzen)
    n = 500

    #Theta Winkel läuft zwischen 0 und 2π durch
    theta = np.linspace(0, 2 * np.pi, n)

    # Die Koordinaten einer Ecke, angenommen Xstart = 0 und Ystart = 0
    x = R * np.cos(theta)
    y = R * np.sin(theta)

    M = np.array([[e1], [e2]])

    for i in range(0,len(theta)):
        v = np.array([x[i],y[i]])
        linAb = M @ v # Lineare Abbildung
        x[i] = linAb[0] # alten x Wert mit dem neuen Wert tauschen (transformed)
        y[i] = linAb[1]

    # Darstellen den Kreis

    plt.plot(x, y, linewidth = 3.0)
    plt.show()
```

```python
e1 = np.array([1, 0])
e2 = np.array([0, 1])

x_lim = [-5,5]
y_lim = [-5,5]
setup_plot_figure(x_lim, y_lim)
grid_malen(e1, e2, x_lim, y_lim)
kreisLinAb(e1, e2)
```

### 2.2 Lineare Abbildungen

#### 2.2.1 Skalierung / Stauchen

```python
def skalieren(Vektor, Faktor, y_axis=False):
    e1 = np.array([1,0])
    e2 = np.array([0,1])

    if y_axis == True:
        e2 = e2 * Faktor
    else:
        e1 = e1 * Faktor

    A = np.array([e1, e2]) # erzeuge eine Lineare Transf Matrix
    linAbb = A @ Vektor # Lineare Abbildung
    return linAbb
```

```python
skal1 = skalieren(e1, 3) # skaliere den Vektor e1 in x Achse mit 3
skal2 = skalieren(e2, 5, y_axis=True) # skaliere den Vektor e1 in x Achse mit 5

grid_Linien_Fugure(x_lim, y_lim)
grid_malen(skal1, skal2, x_lim, y_lim)
kreisLinAb(skal1, skal2)
```

#### 2.2.2 Scherung

```python
def scherung(Vektor, Faktor, y_axis=False):
    e1 = np.array([1,0], dtype = np.float32) # Must be of type float, because if not no floating point factors are possible
    e2 = np.array([0,1], dtype = np.float32) # Must be of type float, because if not no floating point factors are possible

    if y_axis == False:
        e2[0] = e2[0] + Faktor
    else:
        e1[1] = e1[1] + Faktor

    A = np.array([e1, e2]) # erzeuge eine Lineare Trans Matrix
    linAbb = A @ Vektor # Lineare Abbildung
    return linAbb
```

```python
sche1 = scherung(e1, 2) # Scherung Vektor e1 mit 2
sche2 = scherung(e2, 4, y_axis=True) # Scherung Vektor e2 mit 4

grid_Linien_Fugure(x_lim, y_lim)
grid_malen(sche1, sche2, x_lim, y_lim)
kreisLinAb(sche1, sche2)
```

#### 2.2.3 Drehung

Um die Drehung besser Darstellen zu können, verwenden wir eine Hilfe-Funktion, die die andere Ebene zeigt

```python
def drehen(Vektor, theta_grad, im_uhrzeigersinn = False):
    theta = np.radians(theta_grad) # Grad in Bogenmass umwandeln

    if im_uhrzeigersinn == True:
        theta = theta * -1 # Vorzeichen umkehren

    e1 = np.array([np.cos(theta), np.sin(theta)])
    e2 = np.array([-np.sin(theta), np.cos(theta)])

    A = np.array([e1, e2])
    linAbb = A @ Vektor
    return linAbb
```

```python
dreh1 = drehen(e1, 60) # Drehen Sie den Vektor um 60 Grad gegen den Uhrzeigersinn
dreh2 = drehen(e2, 60)

grid_Linien_Fugure(x_lim, y_lim)
grid_malen(dreh1, dreh2, x_lim, y_lim)
kreisLinAb(dreh1, dreh2)
```

#### 2.2.4 Spiegelung

```python
def spiegelung(Vektor, theta_grad_refl):
    theta = np.radians(theta_grad_refl) # Grad in Bogenmass umwandeln

    e1 = np.array([np.cos(2 * theta), np.sin(2 * theta)])
    e2 = np.array([np.sin(2 * theta), -np.cos(2 * theta)])

    A = np.array([e1, e2])
    linAbb = A @ Vektor
    return linAbb
```

```python
# Für eine bessere Visualisierung
def malen_Hilfe_Punkte():
    punkt = np.array([1, 2])
    spiegel_punkt = spiegelung(punkt, theta_grad_refl)
    plt.plot(punkt[0], punkt[1], 'ro') # Roter Punk im alten System
    plt.plot(spiegel_punkt[0], spiegel_punkt[1], 'mo') # Lila Punkt im neuen System

```

```python
def malen_refl_Achsen(theta_grad_refl):
    theta = np.radians(theta_grad_refl)
    x = []
    for i in range(x_lim[0], x_lim[1] + 1):
        x.append(i)

    y = [(np.sin(theta) / np.cos(theta)) * x_val for x_val in x]

    plt.plot(x, y, color = 'black', linewidth=2)
```

```python
theta_grad_refl = 45

spiegel1 = spiegelung(e1, theta_grad_refl) # Reflektiere den Vektor auf die 45-Grad-Achse
spiegel2 = spiegelung(e2, theta_grad_refl)

grid_Linien_Fugure(x_lim, y_lim)
malen_Hilfe_Punkte()
malen_refl_Achsen(theta_grad_refl)
grid_malen(spiegel1, spiegel2, x_lim, y_lim)
kreisLinAb(spiegel1, spiegel2)

```

#### 2.2.5 Projektion

```python
def projektion(vector, y_projection=False):
    e1 = np.array([1,0])
    e2 = np.array([0,1])

    if y_projection == False:
        e2[1] = 0
    else:
        e1[0] = 0

    A = np.array([e1, e2])
    linAbb = A @ vector
    return linAbb
```

```python
proj1 = projektion(e1, False)
proj2 = projektion(e2, False)

grid_Linien_Fugure(x_lim, y_lim)
grid_malen(proj1, proj2, x_lim, y_lim)
kreisLinAb(proj1, proj2)
```

#### 2.2.6 Komposition

##### Beispiel: Skalierung und Drehen

```python
skal_dreh_1 = drehen(skalieren(e1, 1, y_axis=False), 60, im_uhrzeigersinn = True)
skal_dreh_2 = drehen(skalieren(e2, 3, y_axis=True), 60, im_uhrzeigersinn = True)

grid_Linien_Fugure(x_lim, y_lim)
grid_malen(skal_dreh_1, skal_dreh_2, x_lim, y_lim)
kreisLinAb(skal_dreh_1, skal_dreh_2)
```

## 3. Quellen

* Teschl, G. and Teschl, S., 2013. Mathematik Für Informatiker. 4th ed. Berlin: Springer Vieweg.
* Weitz, E., 2018. Konkrete Mathematik (Nicht Nur) Für Informatiker. Wiesbaden, Germany: Springer.
* De.wikipedia.org. 2020. Lineare Abbildung. [online] Available at: <https://de.wikipedia.org/wiki/Lineare_Abbildung> [Accessed 13 September 2020].

## 4. Collab

Für die Implementierung der Methoden **ticks_Ko_System(), grid_Linien_Fugure(), grid_malen, malen_Hilfe_Punkte()** und **malen_refl_Achsen()** möchte ich mich bei meinem Kollegen **Thierry Girod** für seine Unterstützung ganz herzlich bedanken.


***NOTE:*** I've run this notebook with the correction to sympy's trig simplification routines found in [this pull request](https://github.com/sympy/sympy/pull/17067), which has not yet made it into a released version.  I just ran `python -c "import sympy; print(sympy.__file__)"` to find where on my system the actual files are, then edited `.../simplify/fu.py` as given in the PR.

```python
import csv
import sympy
from sympy import sin, cos
from sympy.parsing.mathematica import mathematica
from sympy.physics.quantum.spin import Rotation
from sympy.abc import _clash

import numpy as np
import quaternion
import spherical_functions as sf
```

# Mathematica documentation

The Mathematica documentation for `WignerD` says

* `WignerD[{j, m_1, m_2}], psi, theta, phi]` gives the Wigner D-function $D^j_{m_1, m_2}(\psi, \theta, \phi)$.
* The Wigner D-function $D^j_{m_1, m_2}$ gives the matrix element of a rotation operator parametrized by Euler angles in a $2 j+1$-dimensional unitary representation of a rotation group when parameters $j, m_1, m_2$ are physical, i.e. all integers or half-integers such that $-j \leq m_1,m_2 \leq j$.

* The Wolfram Language uses phase conventions where $D^j_{m_1, m_2}(\psi, \theta, \phi) = e^{i m_1 \psi + i m_2 \phi} D^j_{m_1, m_2}(0, \theta, 0)$.

* `WignerD[{j, m1, m2}, psi, theta, phi] == (-1)^(m1 - m2) Conjugate[WignerD[{j, -m1, -m2}, psi, theta, phi]`

* `WignerD[{j, m1, m2}, psi, theta, phi] == (-1)^(m1 - m2) WignerD[{j, m2, m1}, phi, theta, psi]`

There are no more specifics about what the Euler angles mean in this function's documentation, but the documentation for `EulerMatrix[{alpha, beta, gamma}]` says that it "gives the Euler 3D rotation matrix formed by rotating by $\alpha$ around the current $z$ axis, then by $\beta$ around the current $y$ axis, and then by $\gamma$ around the current $z$ axis."  This is ambiguous, but
we later find that `EulerMatrix[{alpha, beta, gamma}, {a, b, c}]` is equivalent to $R_{\alpha, a} R_{\beta, b} R_{\gamma, c}$.  Evidently, "current" refers to the rotating body axes, and so `EulerMatrix[{alpha, beta, gamma}]` is what I would write in quaternion form as
\begin{equation}
  e^{\alpha \hat{z}/2} e^{\beta \hat{y}/2} e^{\gamma \hat{z}/2} = e^{\gamma \hat{z}''/2} e^{\beta \hat{y}'/2} e^{\alpha \hat{z}/2}
\end{equation}

I've created a CSV file with the analytic expressions for `j` from 0 through 5, using this code:
```mathematica
SetDirectory[NotebookDirectory[]];
Export[
  "conventions_mathematica.csv",
  Flatten[
   Table[{j, m1, m2, ToString[WignerD[{j, m1, m2}, psi, theta, phi], InputForm]},
     {j, 0, 5}, {m1, -j, j}, {m2, -j, j} ], 2],
  TableHeadings -> {"j", "m1", "m2", "WignerD[{j, m1, m2}, psi, theta, phi]"}
];
```
I'll be comparing these expressions to SymPy's, and then evaluating them to compare to the results from `spherical_functions`.

# SymPy documentation

The SymPy documentation is unclear and a little self-contradictory.  The main docstring for `sympy.physics.quantum.spin.Rotation` says that it

> Defines the rotation operator in terms of the Euler angles defined by the z-y-z convention for a passive transformation. That is the coordinate axes are rotated first about the z-axis, giving the new x'-y'-z' axes. Then this new coordinate system is rotated about the new y'-axis, giving new x''-y''-z'' axes. Then this new coordinate system is rotated about the z''-axis. Conventions follow those laid out in Varshalovich.
* `alpha`: First Euler Angle
* `beta`: Second Euler angle
* `gamma`: Third Euler angle

The docstring for `sympy.physics.quantum.spin.WignerD` says

> The Wigner D-function gives the matrix elements of the rotation operator in the jm-representation. For the Euler angles $\alpha$, $\beta$, $\gamma$, the D-function is defined such that:
\begin{equation}
  \left\langle j,m| \mathcal{R}(\alpha, \beta, \gamma ) |j',m' \right \rangle
  = \delta_{jj'} D(j, m, m', \alpha, \beta, \gamma)
\end{equation}
Where the rotation operator is as defined by the Rotation class.
>
> The Wigner D-function defined in this way gives:
\begin{equation}
  D(j, m, m', \alpha, \beta, \gamma) = e^{-i m \alpha} d(j, m, m', \beta) e^{-i m' \gamma}
\end{equation}
Where `d` is the Wigner small-d function, which is given by `Rotation.d`.
>
> The Wigner small-d function gives the component of the Wigner D-function that is determined by the second Euler angle. That is the Wigner D-function is:
\begin{equation}
  D(j, m, m', \alpha, \beta, \gamma) = e^{-i m \alpha} d(j, m, m', \beta) e^{-i m' \gamma}
\end{equation}
Where `d` is the small-d function. The Wigner D-function is given by `Rotation.D`.
* `j`: Total angular momentum
* `m`: Eigenvalue of angular momentum along axis after rotation
* `mp`: Eigenvalue of angular momentum along rotated axis

Again, this is all pretty ambiguous, regarding exactly which angle is supposed to go with which rotation, but my best guess is that it looks like this:
\begin{equation}
  e^{\gamma \hat{z}''/2} e^{\beta \hat{y}'/2} e^{\alpha \hat{z}/2} = e^{\alpha \hat{z}/2} e^{\beta \hat{y}/2} e^{\gamma \hat{z}/2},
\end{equation}
which is precisely the same as my interpretation of Mathematica's convention.

Here's the notation SymPy uses for the D matrices:

```python
Rotation.D(j, m, mp, alpha, beta, gamma)
```

$\displaystyle D^{j}_{m,{m}'}\left(\alpha,\beta,\gamma\right)$

# Compare `quaternion`'s Euler angles to Mathematica's

Although the Mathematica documentation doesn't *explicitly* relate its `WignerD` and `EulerMatrix` functions, I think enough of Mathematica to guess that they at least use consistent conventions.  And `spherical_functions` explicitly takes a `quaternion` object, so to the extent that I use Euler angles at all, we can stay consistent in this way.

So first, we check the rotation matrix that comes out of [`quaternion` via Euler angles](https://github.com/moble/quaternion/blob/0897de81504be8db0aed0f47fb3b62ca164bcc82/__init__.py#L481-L484):

```python
class QuaternionFromEuler(object):
    def __init__(self, alpha, beta, gamma):
        # This is essentially copied from the quaternion code
        self.w = cos(beta/2)*cos((alpha+gamma)/2)
        self.x = -sin(beta/2)*sin((alpha-gamma)/2)
        self.y = sin(beta/2)*cos((alpha-gamma)/2)
        self.z = cos(beta/2)*sin((alpha+gamma)/2)
q = QuaternionFromEuler(psi, theta, phi)

sympy.Matrix([
    [simplify(1 - 2*(q.y**2 + q.z**2)),  simplify(2*(q.x*q.y - q.z*q.w)),    simplify(2*(q.x*q.z + q.y*q.w))],
    [simplify(2*(q.x*q.y + q.z*q.w)),    simplify(1 - 2*(q.x**2 + q.z**2)),  simplify(2*(q.y*q.z - q.x*q.w))],
    [simplify(2*(q.x*q.z - q.y*q.w)),    simplify(2*(q.y*q.z + q.x*q.w)),    simplify(1 - 2*(q.x**2 + q.y**2))]
])
```

$\displaystyle \left[\begin{matrix}- \sin{\left(\phi \right)} \sin{\left(\psi \right)} + \cos{\left(\phi \right)} \cos{\left(\psi \right)} \cos{\left(\theta \right)} & - \sin{\left(\phi \right)} \cos{\left(\psi \right)} \cos{\left(\theta \right)} - \sin{\left(\psi \right)} \cos{\left(\phi \right)} & \sin{\left(\theta \right)} \cos{\left(\psi \right)}\\\sin{\left(\phi \right)} \cos{\left(\psi \right)} + \sin{\left(\psi \right)} \cos{\left(\phi \right)} \cos{\left(\theta \right)} & - \sin{\left(\phi \right)} \sin{\left(\psi \right)} \cos{\left(\theta \right)} + \cos{\left(\phi \right)} \cos{\left(\psi \right)} & \sin{\left(\psi \right)} \sin{\left(\theta \right)}\\- \sin{\left(\theta \right)} \cos{\left(\phi \right)} & \sin{\left(\phi \right)} \sin{\left(\theta \right)} & \cos{\left(\theta \right)}\end{matrix}\right]$

This is precisely the same matrix as Mathematica returns from `EulerMatrix[{ψ, θ, ϕ}]`, which would suggest to me that my Euler conventions are the same as Mathematica's.

# Compare Mathematica's expressions to SymPy's

```python
j, m1, m2 = sympy.symbols('j, m1, m2', integer=True)
#psi, theta, phi = sympy.symbols('psi, theta, phi', real=True)
```

```python
with open('conventions_mathematica.csv', 'r') as csvfile:
    reader = csv.reader(csvfile)
    header = next(reader, None)
    print('Mathematica header:', ', '.join(header))
    mathematica_wignerD = {
        tuple(int(s) for s in row[:3]): sympy.sympify(mathematica(row[3]))
        for row in reader
    }
```

    Mathematica header: j, m1, m2, WignerD[{j, m1, m2}, psi, theta, phi]

Unfortunately, I can't get `sympify` to correctly use `locals`, so I have to just grab all the symbols that it created in the previous cell, as follows:

```python
free_symbols = list(set(symbol for jm1m2 in mathematica_wignerD for symbol in mathematica_wignerD[jm1m2].free_symbols))
```

```python
sorted(free_symbols, key=lambda s: str(s))
```

    [phi, psi, theta]

```python
phi, psi, theta = sorted(free_symbols, key=lambda s: str(s))
```

```python
j, m, mp = sympy.symbols('j, m, mprime', integer=True)
alpha, beta, gamma = sympy.symbols('alpha, beta, gamma', real=True)
```

As always, SymPy isn't good enough at simplifying trig functions, so I have to jump through some extra hoops:

```python
half_angle_replacements = ([]
    +[(sin(theta/2)**n, ((1-cos(theta))/2)**(n//2)) for n in [2, 4, 6, 8]]
    +[(cos(theta/2)**n, ((1+cos(theta))/2)**(n//2)) for n in [2, 4, 6, 8]]
    +[(sin(theta/2)*cos(theta/2), sin(theta)/2)]
    #+[(sin(3*theta/2), 3*sin(theta/2)-4*sin(theta/2)**3)]
    #+[(sin(5*theta/2), 5*cos(theta/2)**4*sin(theta/2)-10*cos(theta/2)**2*sin(theta/2)**3+sin(theta/2)**5)]
)

def simplify(difference):
    from sympy import trigsimp, expand
    difference = trigsimp(expand(sympy.simplify(difference), trig=True))
    difference = sympy.simplify(difference.subs(half_angle_replacements, simultaneous=True))
    return difference
```

Finally, we can go through and check each and every expression (even though we could have assumed some symmetries to skip certain combinations) to ensure that the Mathematica expression returned by
```
WignerD[{j, m1, m2}, psi, theta, phi]
```
is identical to the SymPy expression returned by
```
Rotation.D(j, m1, m2, -psi, -theta, -phi)
```

```python
for j, m1, m2 in mathematica_wignerD:
    mathematica_value = sympy.expand(sympy.trigsimp(sympy.simplify(mathematica_wignerD[(j, m1, m2)])), trig=True)
    sympy_value = sympy.expand(sympy.trigsimp(sympy.simplify(Rotation.D(j, m1, m2, -psi, -theta, -phi).doit())), trig=True)
    ratio = sympy.simplify(mathematica_value/sympy_value)
    mathematica_value, sympy_value = sympy.fraction(ratio)
    mathematica_value = sympy.simplify(sympy.simplify(mathematica_value.subs(half_angle_replacements)).subs(half_angle_replacements))
    sympy_value = sympy.simplify(sympy.simplify(sympy_value.subs(half_angle_replacements)).subs(half_angle_replacements))
    difference = simplify(mathematica_value - sympy_value)
    print('Checking (j, m1, m2) = ({0}, {1}, {2})'.format(j, m1, m2))
    if difference:
        display(mathematica_value, sympy_value, difference)
        print()
```

    Checking (j, m1, m2) = (0, 0, 0)
    Checking (j, m1, m2) = (1, -1, -1)
    Checking (j, m1, m2) = (1, -1, 0)
    Checking (j, m1, m2) = (1, -1, 1)
    Checking (j, m1, m2) = (1, 0, -1)
    Checking (j, m1, m2) = (1, 0, 0)
    Checking (j, m1, m2) = (1, 0, 1)
    Checking (j, m1, m2) = (1, 1, -1)
    Checking (j, m1, m2) = (1, 1, 0)
    Checking (j, m1, m2) = (1, 1, 1)
    Checking (j, m1, m2) = (2, -2, -2)
    Checking (j, m1, m2) = (2, -2, -1)
    Checking (j, m1, m2) = (2, -2, 0)
    Checking (j, m1, m2) = (2, -2, 1)
    Checking (j, m1, m2) = (2, -2, 2)
    Checking (j, m1, m2) = (2, -1, -2)
    Checking (j, m1, m2) = (2, -1, -1)
    Checking (j, m1, m2) = (2, -1, 0)
    Checking (j, m1, m2) = (2, -1, 1)
    Checking (j, m1, m2) = (2, -1, 2)
    Checking (j, m1, m2) = (2, 0, -2)
    Checking (j, m1, m2) = (2, 0, -1)
    Checking (j, m1, m2) = (2, 0, 0)
    Checking (j, m1, m2) = (2, 0, 1)
    Checking (j, m1, m2) = (2, 0, 2)
    Checking (j, m1, m2) = (2, 1, -2)
    Checking (j, m1, m2) = (2, 1, -1)
    Checking (j, m1, m2) = (2, 1, 0)
    Checking (j, m1, m2) = (2, 1, 1)
    Checking (j, m1, m2) = (2, 1, 2)
    Checking (j, m1, m2) = (2, 2, -2)
    Checking (j, m1, m2) = (2, 2, -1)
    Checking (j, m1, m2) = (2, 2, 0)
    Checking (j, m1, m2) = (2, 2, 1)
    Checking (j, m1, m2) = (2, 2, 2)
    Checking (j, m1, m2) = (3, -3, -3)
    Checking (j, m1, m2) = (3, -3, -2)
    Checking (j, m1, m2) = (3, -3, -1)
    Checking (j, m1, m2) = (3, -3, 0)
    Checking (j, m1, m2) = (3, -3, 1)
    Checking (j, m1, m2) = (3, -3, 2)
    Checking (j, m1, m2) = (3, -3, 3)
    Checking (j, m1, m2) = (3, -2, -3)
    Checking (j, m1, m2) = (3, -2, -2)
    Checking (j, m1, m2) = (3, -2, -1)
    Checking (j, m1, m2) = (3, -2, 0)
    Checking (j, m1, m2) = (3, -2, 1)
    Checking (j, m1, m2) = (3, -2, 2)
    Checking (j, m1, m2) = (3, -2, 3)
    Checking (j, m1, m2) = (3, -1, -3)
    Checking (j, m1, m2) = (3, -1, -2)
    Checking (j, m1, m2) = (3, -1, -1)
    Checking (j, m1, m2) = (3, -1, 0)
    Checking (j, m1, m2) = (3, -1, 1)
    Checking (j, m1, m2) = (3, -1, 2)
    Checking (j, m1, m2) = (3, -1, 3)
    Checking (j, m1, m2) = (3, 0, -3)
    Checking (j, m1, m2) = (3, 0, -2)
    Checking (j, m1, m2) = (3, 0, -1)
    Checking (j, m1, m2) = (3, 0, 0)
    Checking (j, m1, m2) = (3, 0, 1)
    Checking (j, m1, m2) = (3, 0, 2)
    Checking (j, m1, m2) = (3, 0, 3)
    Checking (j, m1, m2) = (3, 1, -3)
    Checking (j, m1, m2) = (3, 1, -2)
    Checking (j, m1, m2) = (3, 1, -1)
    Checking (j, m1, m2) = (3, 1, 0)
    Checking (j, m1, m2) = (3, 1, 1)
    Checking (j, m1, m2) = (3, 1, 2)
    Checking (j, m1, m2) = (3, 1, 3)
    Checking (j, m1, m2) = (3, 2, -3)
    Checking (j, m1, m2) = (3, 2, -2)
    Checking (j, m1, m2) = (3, 2, -1)
    Checking (j, m1, m2) = (3, 2, 0)
    Checking (j, m1, m2) = (3, 2, 1)
    Checking (j, m1, m2) = (3, 2, 2)
    Checking (j, m1, m2) = (3, 2, 3)
    Checking (j, m1, m2) = (3, 3, -3)
    Checking (j, m1, m2) = (3, 3, -2)
    Checking (j, m1, m2) = (3, 3, -1)
    Checking (j, m1, m2) = (3, 3, 0)
    Checking (j, m1, m2) = (3, 3, 1)
    Checking (j, m1, m2) = (3, 3, 2)
    Checking (j, m1, m2) = (3, 3, 3)
    Checking (j, m1, m2) = (4, -4, -4)
    Checking (j, m1, m2) = (4, -4, -3)
    Checking (j, m1, m2) = (4, -4, -2)
    Checking (j, m1, m2) = (4, -4, -1)
    Checking (j, m1, m2) = (4, -4, 0)
    Checking (j, m1, m2) = (4, -4, 1)
    Checking (j, m1, m2) = (4, -4, 2)
    Checking (j, m1, m2) = (4, -4, 3)
    Checking (j, m1, m2) = (4, -4, 4)
    Checking (j, m1, m2) = (4, -3, -4)
    Checking (j, m1, m2) = (4, -3, -3)
    Checking (j, m1, m2) = (4, -3, -2)
    Checking (j, m1, m2) = (4, -3, -1)
    Checking (j, m1, m2) = (4, -3, 0)
    Checking (j, m1, m2) = (4, -3, 1)
    Checking (j, m1, m2) = (4, -3, 2)
    Checking (j, m1, m2) = (4, -3, 3)
    Checking (j, m1, m2) = (4, -3, 4)
    Checking (j, m1, m2) = (4, -2, -4)
    Checking (j, m1, m2) = (4, -2, -3)
    Checking (j, m1, m2) = (4, -2, -2)
    Checking (j, m1, m2) = (4, -2, -1)
    Checking (j, m1, m2) = (4, -2, 0)
    Checking (j, m1, m2) = (4, -2, 1)
    Checking (j, m1, m2) = (4, -2, 2)
    Checking (j, m1, m2) = (4, -2, 3)
    Checking (j, m1, m2) = (4, -2, 4)
    Checking (j, m1, m2) = (4, -1, -4)
    Checking (j, m1, m2) = (4, -1, -3)
    Checking (j, m1, m2) = (4, -1, -2)
    Checking (j, m1, m2) = (4, -1, -1)
    Checking (j, m1, m2) = (4, -1, 0)
    Checking (j, m1, m2) = (4, -1, 1)
    Checking (j, m1, m2) = (4, -1, 2)
    Checking (j, m1, m2) = (4, -1, 3)
    Checking (j, m1, m2) = (4, -1, 4)
    Checking (j, m1, m2) = (4, 0, -4)
    Checking (j, m1, m2) = (4, 0, -3)
    Checking (j, m1, m2) = (4, 0, -2)
    Checking (j, m1, m2) = (4, 0, -1)
    Checking (j, m1, m2) = (4, 0, 0)
    Checking (j, m1, m2) = (4, 0, 1)
    Checking (j, m1, m2) = (4, 0, 2)
    Checking (j, m1, m2) = (4, 0, 3)
    Checking (j, m1, m2) = (4, 0, 4)
    Checking (j, m1, m2) = (4, 1, -4)
    Checking (j, m1, m2) = (4, 1, -3)
    Checking (j, m1, m2) = (4, 1, -2)
    Checking (j, m1, m2) = (4, 1, -1)
    Checking (j, m1, m2) = (4, 1, 0)
    Checking (j, m1, m2) = (4, 1, 1)
    Checking (j, m1, m2) = (4, 1, 2)
    Checking (j, m1, m2) = (4, 1, 3)
    Checking (j, m1, m2) = (4, 1, 4)
    Checking (j, m1, m2) = (4, 2, -4)
    Checking (j, m1, m2) = (4, 2, -3)
    Checking (j, m1, m2) = (4, 2, -2)
    Checking (j, m1, m2) = (4, 2, -1)
    Checking (j, m1, m2) = (4, 2, 0)
    Checking (j, m1, m2) = (4, 2, 1)
    Checking (j, m1, m2) = (4, 2, 2)
    Checking (j, m1, m2) = (4, 2, 3)
    Checking (j, m1, m2) = (4, 2, 4)
    Checking (j, m1, m2) = (4, 3, -4)
    Checking (j, m1, m2) = (4, 3, -3)
    Checking (j, m1, m2) = (4, 3, -2)
    Checking (j, m1, m2) = (4, 3, -1)
    Checking (j, m1, m2) = (4, 3, 0)
    Checking (j, m1, m2) = (4, 3, 1)
    Checking (j, m1, m2) = (4, 3, 2)
    Checking (j, m1, m2) = (4, 3, 3)
    Checking (j, m1, m2) = (4, 3, 4)
    Checking (j, m1, m2) = (4, 4, -4)
    Checking (j, m1, m2) = (4, 4, -3)
    Checking (j, m1, m2) = (4, 4, -2)
    Checking (j, m1, m2) = (4, 4, -1)
    Checking (j, m1, m2) = (4, 4, 0)
    Checking (j, m1, m2) = (4, 4, 1)
    Checking (j, m1, m2) = (4, 4, 2)
    Checking (j, m1, m2) = (4, 4, 3)
    Checking (j, m1, m2) = (4, 4, 4)
    Checking (j, m1, m2) = (5, -5, -5)
    Checking (j, m1, m2) = (5, -5, -4)
    Checking (j, m1, m2) = (5, -5, -3)
    Checking (j, m1, m2) = (5, -5, -2)
    Checking (j, m1, m2) = (5, -5, -1)
    Checking (j, m1, m2) = (5, -5, 0)
    Checking (j, m1, m2) = (5, -5, 1)
    Checking (j, m1, m2) = (5, -5, 2)
    Checking (j, m1, m2) = (5, -5, 3)
    Checking (j, m1, m2) = (5, -5, 4)
    Checking (j, m1, m2) = (5, -5, 5)
    Checking (j, m1, m2) = (5, -4, -5)
    Checking (j, m1, m2) = (5, -4, -4)
    Checking (j, m1, m2) = (5, -4, -3)
    Checking (j, m1, m2) = (5, -4, -2)
    Checking (j, m1, m2) = (5, -4, -1)
    Checking (j, m1, m2) = (5, -4, 0)
    Checking (j, m1, m2) = (5, -4, 1)
    Checking (j, m1, m2) = (5, -4, 2)
    Checking (j, m1, m2) = (5, -4, 3)
    Checking (j, m1, m2) = (5, -4, 4)
    Checking (j, m1, m2) = (5, -4, 5)
    Checking (j, m1, m2) = (5, -3, -5)
    Checking (j, m1, m2) = (5, -3, -4)
    Checking (j, m1, m2) = (5, -3, -3)
    Checking (j, m1, m2) = (5, -3, -2)
    Checking (j, m1, m2) = (5, -3, -1)
    Checking (j, m1, m2) = (5, -3, 0)
    Checking (j, m1, m2) = (5, -3, 1)
    Checking (j, m1, m2) = (5, -3, 2)
    Checking (j, m1, m2) = (5, -3, 3)
    Checking (j, m1, m2) = (5, -3, 4)
    Checking (j, m1, m2) = (5, -3, 5)
    Checking (j, m1, m2) = (5, -2, -5)
    Checking (j, m1, m2) = (5, -2, -4)
    Checking (j, m1, m2) = (5, -2, -3)
    Checking (j, m1, m2) = (5, -2, -2)
    Checking (j, m1, m2) = (5, -2, -1)
    Checking (j, m1, m2) = (5, -2, 0)
    Checking (j, m1, m2) = (5, -2, 1)
    Checking (j, m1, m2) = (5, -2, 2)
    Checking (j, m1, m2) = (5, -2, 3)
    Checking (j, m1, m2) = (5, -2, 4)
    Checking (j, m1, m2) = (5, -2, 5)
    Checking (j, m1, m2) = (5, -1, -5)
    Checking (j, m1, m2) = (5, -1, -4)
    Checking (j, m1, m2) = (5, -1, -3)
    Checking (j, m1, m2) = (5, -1, -2)
    Checking (j, m1, m2) = (5, -1, -1)
    Checking (j, m1, m2) = (5, -1, 0)
    Checking (j, m1, m2) = (5, -1, 1)
    Checking (j, m1, m2) = (5, -1, 2)
    Checking (j, m1, m2) = (5, -1, 3)
    Checking (j, m1, m2) = (5, -1, 4)
    Checking (j, m1, m2) = (5, -1, 5)
    Checking (j, m1, m2) = (5, 0, -5)
    Checking (j, m1, m2) = (5, 0, -4)
    Checking (j, m1, m2) = (5, 0, -3)
    Checking (j, m1, m2) = (5, 0, -2)
    Checking (j, m1, m2) = (5, 0, -1)
    Checking (j, m1, m2) = (5, 0, 0)
    Checking (j, m1, m2) = (5, 0, 1)
    Checking (j, m1, m2) = (5, 0, 2)
    Checking (j, m1, m2) = (5, 0, 3)
    Checking (j, m1, m2) = (5, 0, 4)
    Checking (j, m1, m2) = (5, 0, 5)
    Checking (j, m1, m2) = (5, 1, -5)
    Checking (j, m1, m2) = (5, 1, -4)
    Checking (j, m1, m2) = (5, 1, -3)
    Checking (j, m1, m2) = (5, 1, -2)
    Checking (j, m1, m2) = (5, 1, -1)
    Checking (j, m1, m2) = (5, 1, 0)
    Checking (j, m1, m2) = (5, 1, 1)
    Checking (j, m1, m2) = (5, 1, 2)
    Checking (j, m1, m2) = (5, 1, 3)
    Checking (j, m1, m2) = (5, 1, 4)
    Checking (j, m1, m2) = (5, 1, 5)
    Checking (j, m1, m2) = (5, 2, -5)
    Checking (j, m1, m2) = (5, 2, -4)
    Checking (j, m1, m2) = (5, 2, -3)
    Checking (j, m1, m2) = (5, 2, -2)
    Checking (j, m1, m2) = (5, 2, -1)
    Checking (j, m1, m2) = (5, 2, 0)
    Checking (j, m1, m2) = (5, 2, 1)
    Checking (j, m1, m2) = (5, 2, 2)
    Checking (j, m1, m2) = (5, 2, 3)
    Checking (j, m1, m2) = (5, 2, 4)
    Checking (j, m1, m2) = (5, 2, 5)
    Checking (j, m1, m2) = (5, 3, -5)
    Checking (j, m1, m2) = (5, 3, -4)
    Checking (j, m1, m2) = (5, 3, -3)
    Checking (j, m1, m2) = (5, 3, -2)
    Checking (j, m1, m2) = (5, 3, -1)
    Checking (j, m1, m2) = (5, 3, 0)
    Checking (j, m1, m2) = (5, 3, 1)
    Checking (j, m1, m2) = (5, 3, 2)
    Checking (j, m1, m2) = (5, 3, 3)
    Checking (j, m1, m2) = (5, 3, 4)
    Checking (j, m1, m2) = (5, 3, 5)
    Checking (j, m1, m2) = (5, 4, -5)
    Checking (j, m1, m2) = (5, 4, -4)
    Checking (j, m1, m2) = (5, 4, -3)
    Checking (j, m1, m2) = (5, 4, -2)
    Checking (j, m1, m2) = (5, 4, -1)
    Checking (j, m1, m2) = (5, 4, 0)
    Checking (j, m1, m2) = (5, 4, 1)
    Checking (j, m1, m2) = (5, 4, 2)
    Checking (j, m1, m2) = (5, 4, 3)
    Checking (j, m1, m2) = (5, 4, 4)
    Checking (j, m1, m2) = (5, 4, 5)
    Checking (j, m1, m2) = (5, 5, -5)
    Checking (j, m1, m2) = (5, 5, -4)
    Checking (j, m1, m2) = (5, 5, -3)
    Checking (j, m1, m2) = (5, 5, -2)
    Checking (j, m1, m2) = (5, 5, -1)
    Checking (j, m1, m2) = (5, 5, 0)
    Checking (j, m1, m2) = (5, 5, 1)
    Checking (j, m1, m2) = (5, 5, 2)
    Checking (j, m1, m2) = (5, 5, 3)
    Checking (j, m1, m2) = (5, 5, 4)
    Checking (j, m1, m2) = (5, 5, 5)

All cases show agreement.

I'm not quite sure how to interpret this weird sign difference.  Flipping the signs is one of the things you do when inverting a rotation, but you also flip the order of the angles.  This could essentially be done here as well if we also flip the order of `m1` and `m2` — except that we need an additional factor of $(-1)^{m_1+m_2}$.  So we could think of this as saying that one of these provides the D matrix for the *inverse* rotation of the other, and they swap the order of the `m` arguments, and there's a (Condon-Shortley) phase difference.

I'll just quickly verify that `Rotation.D` actually satisfies this symmetry:

```python
for j in range(6):
    for m1 in range(-j, j+1):
        for m2 in range(-j, j+1):
            #print(j, m1, m2)
            difference = sympy.simplify(Rotation.D(j, m1, m2, -psi, -theta, -phi).doit()
                                        - (-1)**(m1+m2)*Rotation.D(j, m2, m1, -phi, -theta, -psi).doit())
            if difference:
                display(difference)
```

So another way to say this is that SymPy takes the inverse rotation, and returns the transpose with that weird phase.

# Compare Mathematica to `spherical_functions`

I find both Mathematica's and SymPy's descriptions to be ambiguous, but it looks like Mathematica's is closer to my thinking — except that other places in the documentation make me think that their Condon-Shortley phases are weird, so I'll play around with that until I get some agreement.  I'll check by simply evaluating on random numbers.

```python
np.random.seed(1234)
for _ in range(100):  # Test for 100 sets of random Euler angles
    ψ, θ, ϕ = np.random.rand(3) * np.array([2*np.pi, np.pi, 2*np.pi])
    for j, m1, m2 in mathematica_wignerD:
        mathematica_value = mathematica_wignerD[(j, m1, m2)].subs({psi: ψ, theta: θ, phi: ϕ}).evalf()
        spherical_functions_value = (-1)**(m1+m2) * sf.Wigner_D_element(quaternion.from_euler_angles(ψ, θ, ϕ), j, m1, m2)
        diff = abs(mathematica_value - spherical_functions_value)
        if diff > 3e-13:
            print(j, m1, m2, ψ, θ, ϕ, diff)
```

As expected, the only difference is in the overall phase.  In short, we have numerical equality between Mathematica's expression
```
WignerD[{j, m1, m2}, psi, theta, phi]
```
and `spherical_function`'s expression
```
(-1)**(m1+m2) * sf.Wigner_D_element(quaternion.from_euler_angles(psi, theta, phi), j, m1, m2)
```
So, the main conclusion is this:
> The Wigner $\mathfrak{D}$ functions contained in Mathematica and `spherical_functions` agree except for an overall phase factor of $(-1)^{m_1 + m_2}$.


# Ikeda barge

# Purpose
Ikeda has proposed a formula to calcualte the eddy damping for barges:
$$ B_{E}=\left(\frac{2}{\pi}\right) \rho L d^{4}\left(H_{0}^{2}+1-\frac{O G}{d}\right)\left(H_{0}^{2}+\left(1-\frac{O G}{d}\right)^{2}\right) R_{0} \omega $$

# Methodology
Investigate if this formulat makes any sense to the KVLCC2

# Setup

```python
# %load imports.py
"""
These is the standard setup for the notebooks.
"""

%matplotlib inline
%load_ext autoreload
%autoreload 2

from jupyterthemes import jtplot
jtplot.style(theme='onedork', context='notebook', ticks=True, grid=False)

import pandas as pd
pd.options.display.max_rows = 999
pd.options.display.max_columns = 999
pd.set_option("display.max_columns", None)
import numpy as np
import os
import matplotlib.pyplot as plt
from collections import OrderedDict
#plt.style.use('paper')

#import data
import copy
from mdldb.run import Run

from sklearn.pipeline import Pipeline
from rolldecayestimators.transformers import CutTransformer, LowpassFilterDerivatorTransformer, ScaleFactorTransformer, OffsetTransformer
from rolldecayestimators.direct_estimator_cubic import EstimatorQuadraticB, EstimatorCubic
from rolldecayestimators.ikeda_estimator import IkedaQuadraticEstimator
import rolldecayestimators.equations as equations
import rolldecayestimators.lambdas as lambdas
from rolldecayestimators.substitute_dynamic_symbols import lambdify
import rolldecayestimators.symbols as symbols
import sympy as sp

from sympy.physics.vector.printing import vpprint, vlatex
from IPython.display import display, Math, Latex

from sklearn.metrics import r2_score
from src.data import database
from mdldb import tables
import shipflowmotionshelpers.shipflowmotionshelpers as helpers

```

```python
from rolldecayestimators import ikeda_barge
```

```python
db = database.get_db()
run = db.session.query(Run).get(21338)
```

```python
scale_factor = run.model.scale_factor
w = 2.4621496310208086/np.sqrt(scale_factor)
draught = (run.loading_condition.TA + run.loading_condition.TF)/2
OG = run.loading_condition.kg - draught
phi_as = np.deg2rad(np.linspace(0,10,100))
beam = run.ship.beam
rho = 1000
g = 9.81
parameters = {
    'rho' : rho,
    'lpp' : run.ship.lpp,
    'd' : draught,
    'beam' : beam,
    'OG' : OG,
    'phi_a' : phi_as,
    'w' : w,
}

df = pd.DataFrame(index=phi_as)
df['B_E'] = ikeda_barge.eddy(**parameters)
Disp = run.loading_condition.Volume
df['B_E_hat'] = lambdas.B_to_hat_lambda(B=df['B_E'], Disp=Disp, beam=beam, g=g, rho=rho)

```

```python
fig,ax=plt.subplots()
df.plot(y='B_E_hat', ax=ax)
```


## Histograms of Oriented Gradients (HOG)

As we saw with the ORB algorithm, we can use keypoints in images to do keypoint-based matching to detect objects in images. These type of algorithms work great when you want to detect objects that have a lot of consistent internal features that are not affected by the background. For example, these algorithms work well for facial detection because faces have a lot of consistent internal features that don’t get affected by the image background, such as the eyes, nose, and mouth. However, these type of algorithms don’t work so well when attempting to do more general object recognition, say for example, pedestrian detection in images. The reason is that people don’t have consistent internal features, like faces do, because the body shape and style of every person is different (see Fig. 1). This means that every person is going to have a different set of internal features, and so we need something that can more generally describe a person.

Fig. 1. - Pedestrians.

One option is to try to detect pedestrians by their contours instead.  Detecting objects in images by their contours (boundaries) is very challenging because we have to deal with the difficulties brought about by the contrast between the background and the foreground. For example, suppose you wanted to detect a pedestrian in an image that is walking in front of a white building and she is wearing a white coat and black pants (see Fig. 2). We can see in Fig. 2, that since the background of the image is mostly white, the black pants are going to have a very high contrast, but the coat, since it is white as well, is going to have very low contrast. In this case, detecting the edges of pants is going to be easy but detecting the edges of the coat is going to be very difficult. This is where **HOG** comes in. HOG stands for **Histograms of Oriented Gradients** and it was first introduced by Navneet Dalal and Bill Triggs in 2005.

Fig. 2. - High and Low Contrast.

The HOG algorithm works by creating histograms of the distribution of gradient orientations in an image and then normalizing them in a very special way. This special normalization is what makes HOG so effective at detecting the edges of objects even in cases where the contrast is very low. These normalized histograms are put together into a feature vector, known as the HOG descriptor, that can be used to train a machine learning algorithm, such as a Support Vector Machine (SVM), to detect objects in images based on their boundaries (edges). Due to its great success and reliability, HOG has become one of the most widely used algorithms in computer vison for object detection.

In this notebook, you will learn:

* How the HOG algorithm works
* How to use OpenCV to create a HOG descriptor
* How to visualize the HOG descriptor.

# The HOG Algorithm

As its name suggests, the HOG algorithm, is based on creating histograms from the orientation of image gradients. The HOG algorithm is implemented in a series of steps:

1. Given the image of particular object, set a detection window (region of interest) that covers the entire object in the image (see Fig. 3).

2. Calculate the magnitude and direction of the gradient for each individual pixel in the detection window.

3. Divide the detection window into connected *cells* of pixels, with all cells being of the same size (see Fig. 3). The size of the cells is a free parameter and it is usually chosen so as to match the scale of the features that want to be detected. For example, in a 64 x 128 pixel detection window, square cells 6 to 8 pixels wide are suitable for detecting human limbs.

4. Create a Histogram for each cell, by first grouping the gradient directions of all pixels in each cell into a particular number of orientation (angular) bins; and then adding up the gradient magnitudes of the gradients in each angular bin (see Fig. 3). The number of bins in the histogram is a free parameter and it is usually set to 9 angular bins.

5. Group adjacent cells into *blocks* (see Fig. 3). The number of cells in each block is a free parameter and all blocks must be of the same size. The distance between each block (known as the stride) is a free parameter but it is usually set to half the block size, in which case you will get overlapping blocks (*see video below*).  The HOG algorithm has been shown empirically to work better with overlapping blocks.

6. Use the cells contained within each block to normalize the cell histograms in that block (see Fig. 3). If you have overlapping blocks this means that most cells will be normalized with respect to different blocks (*see video below*). Therefore, the same cell may have several different normalizations.

7. Collect all the normalized histograms from all the blocks into a single feature vector called the HOG descriptor.

8. Use the resulting HOG descriptors from many images of the same type of object to train a machine learning algorithm, such as an SVM, to detect those type of objects in images. For example, you could use the HOG descriptors from many images of pedestrians to train an SVM to detect pedestrians in images. The training is done with both positive a negative examples of the object you want detect in the image.

9. Once the SVM has been trained, a sliding window approach is used to try to detect and locate objects in images.  Detecting an object in the image entails finding the part of the image that looks similar to the HOG pattern learned by the SVM.

Fig. 3. - HOG Diagram.

Vid. 1. - HOG Animation.

# Why The HOG Algorithm Works

As we learned above, HOG creates histograms by adding the magnitude of the gradients in particular orientations in localized portions of the image called *cells*.  By doing this we guarantee that stronger gradients will contribute more to the magnitude of their respective angular bin, while the effects of weak and randomly oriented gradients resulting from noise are minimized. In this manner the histograms tell us the dominant gradient orientation of each cell.

### Dealing with contrast

Now, the magnitude of the dominant orientation can vary widely due to variations in local illumination and the contrast between the background and the foreground.

To account for the background-foreground contrast differences, the HOG algorithm tries to detect edges locally.  In order to do this, it defines groups of cells, called **blocks**, and normalizes the histograms using this local group of cells. By normalizing locally, the HOG algorithm can detect the edges in each block very reliably; this is called **block normalization**.

In addition to using block normalization, the HOG algorithm also uses overlapping blocks to increase its performance. By using overlapping blocks, each cell contributes several independent components to the final HOG descriptor, where each component corresponds to a cell being normalized with respect to a different block. This may seem redundant but, it has been shown empirically that by normalizing each cell several times with respect to different local blocks, the performance of the HOG algorithm increases dramatically.

### Loading Images and Importing Resources

The first step in building our HOG descriptor is to load the required packages into Python and to load our image.

We start by using OpenCV to load an image of a triangle tile. Since, the `cv2.imread()` function loads images as BGR we will convert our image to RGB so we can display it with the correct colors. As usual we will convert our BGR image to Gray Scale for analysis.

```python
import cv2
import numpy as np
import matplotlib.pyplot as plt

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Load the image
image = cv2.imread('./images/triangle_tile.jpeg')

# Convert the original image to RGB
original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Convert the original image to gray scale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Print the shape of the original and gray scale images
print('The original image has shape: ', original_image.shape)
print('The gray scale image has shape: ', gray_image.shape)

# Display the images
plt.subplot(121)
plt.imshow(original_image)
plt.title('Original Image')
plt.subplot(122)
plt.imshow(gray_image, cmap='gray')
plt.title('Gray Scale Image')
plt.show()
```

# Creating The HOG Descriptor

We will be using OpenCV’s `HOGDescriptor` class to create the HOG descriptor. The parameters of the HOG descriptor are setup using the `HOGDescriptor()` function. The parameters of the `HOGDescriptor()` function and their default values are given below:

`cv2.HOGDescriptor(win_size = (64, 128),
                  block_size = (16, 16),
                  block_stride = (8, 8),
                  cell_size = (8, 8),
                  nbins = 9,
                  win_sigma = DEFAULT_WIN_SIGMA,
                  threshold_L2hys = 0.2,
                  gamma_correction = true,
                  nlevels = DEFAULT_NLEVELS)`

Parameters:

* **win_size** – *Size*
Size of detection window in pixels (*width, height*). Defines the region of interest. Must be an integer multiple of cell size.

* **block_size** – *Size*
Block size in pixels (*width, height*). Defines how many cells are in each block. Must be an integer multiple of cell size and it must be smaller than the detection window. The smaller the block the finer detail you will get.

* **block_stride** – *Size*
Block stride in pixels (*horizontal, vertical*). It must be an integer multiple of cell size. The `block_stride` defines the distance between adjecent blocks, for example, 8 pixels horizontally and 8 pixels vertically. Longer `block_strides` makes the algorithm run faster (because less blocks are evaluated) but the algorithm may not perform as well.

* **cell_size** – *Size*
Cell size in pixels (*width, height*). Determines the size fo your cell. The smaller the cell the finer detail you will get.

* **nbins** – *int*
Number of bins for the histograms. Determines the number of angular bins used to make the histograms. With more bins you capture more gradient directions. HOG uses unsigned gradients, so the angular bins will have values between 0 and 180 degrees.

* **win_sigma** – *double*
Gaussian smoothing window parameter. The performance of the HOG algorithm can be improved by smoothing the pixels near the edges of the blocks by applying a Gaussian spatial window to each pixel before computing the histograms.

* **threshold_L2hys** – *double*
L2-Hys (Lowe-style clipped L2 norm) normalization method shrinkage. The L2-Hys method is used to normalize the blocks and it consists of an L2-norm followed by clipping and a renormalization. The clipping limits the maximum value of the descriptor vector for each block to have the value of the given threshold (0.2 by default). After the clipping the descriptor vector is renormalized as described in *IJCV*, 60(2):91-110, 2004.

* **gamma_correction** – *bool*
Flag to specify whether the gamma correction preprocessing is required or not. Performing gamma correction slightly increases the performance of the HOG algorithm.

* **nlevels** – *int*
Maximum number of detection window increases.

As we can see, the  `cv2.HOGDescriptor()`function supports a wide range of parameters. The first few arguments (`block_size, block_stride, cell_size`, and `nbins`) are probably the ones you are most likely to change. The other parameters can be safely left at their default values and you will get good results.

In the code below, we will use the `cv2.HOGDescriptor()`function to set the cell size, block size, block stride, and the number of bins for the histograms of the HOG descriptor. We will then use `.compute(image)`method to compute the HOG descriptor (feature vector) for the given `image`.

```python
# Specify the parameters for our HOG descriptor

# Cell Size in pixels (width, height). Must be smaller than the size of the detection window
# and must be chosen so that the resulting Block Size is smaller than the detection window.
cell_size = (6, 6)

# Number of cells per block in each direction (x, y). Must be chosen so that the resulting
# Block Size is smaller than the detection window
num_cells_per_block = (2, 2)

# Block Size in pixels (width, height). Must be an integer multiple of Cell Size.
# The Block Size must be smaller than the detection window
block_size = (num_cells_per_block[0] * cell_size[0],
              num_cells_per_block[1] * cell_size[1])

# Calculate the number of cells that fit in our image in the x and y directions
x_cells = gray_image.shape[1] // cell_size[0]
y_cells = gray_image.shape[0] // cell_size[1]

# Horizontal distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (x_cells - num_cells_per_block[0]) / h_stride = integer.
h_stride = 1

# Vertical distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (y_cells - num_cells_per_block[1]) / v_stride = integer.
v_stride = 1

# Block Stride in pixels (horizantal, vertical). Must be an integer multiple of Cell Size
block_stride = (cell_size[0] * h_stride, cell_size[1] * v_stride)

# Number of gradient orientation bins
num_bins = 9

# Specify the size of the detection window (Region of Interest) in pixels (width, height).
# It must be an integer multiple of Cell Size and it must cover the entire image. Because
# the detection window must be an integer multiple of cell size, depending on the size of
# your cells, the resulting detection window might be slightly smaller than the image.
# This is perfectly ok.
win_size = (x_cells * cell_size[0] , y_cells * cell_size[1])

# Print the shape of the gray scale image for reference
print('\nThe gray scale image has shape: ', gray_image.shape)
print()

# Print the parameters of our HOG descriptor
print('HOG Descriptor Parameters:\n')
print('Window Size:', win_size)
print('Cell Size:', cell_size)
print('Block Size:', block_size)
print('Block Stride:', block_stride)
print('Number of Bins:', num_bins)
print()

# Set the parameters of the HOG descriptor using the variables defined above
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)

# Compute the HOG Descriptor for the gray scale image
hog_descriptor = hog.compute(gray_image)
```

# Number of Elements In The HOG Descriptor

The resulting HOG Descriptor (feature vector), contains the normalized histograms from all cells from all blocks in the detection window concatenated in one long vector. Therefore, the size of the HOG feature vector will be given by the total number of blocks in the detection window, multiplied by the number of cells per block, times the number of orientation bins:

\begin{equation}
\mbox{total_elements} = (\mbox{total_number_of_blocks})\mbox{ } \times \mbox{ } (\mbox{number_cells_per_block})\mbox{ } \times \mbox{ } (\mbox{number_of_bins})
\end{equation}

If we don’t have overlapping blocks (*i.e.* the `block_stride`equals the `block_size`), the total number of blocks can be easily calculated by dividing the size of the detection window by the block size. However, in the general case we have to take into account the fact that we have overlapping blocks. To find the total number of blocks in the general case (*i.e.* for any `block_stride` and `block_size`), we can use the formula given below:

\begin{equation}
\mbox{Total}_i = \left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right)\left( \frac{\mbox{window_size}_i}{\mbox{block_size}_i} \right) - \left [\left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right) - 1 \right]; \mbox{  for  } i = x,y
\end{equation}

Where Total$_x$, is the total number of blocks along the width of the detection window, and Total$_y$, is the total number of blocks along the height of the detection window. This formula for Total$_x$ and Total$_y$, takes into account the extra blocks that result from overlapping. After calculating Total$_x$ and Total$_y$, we can get the total number of blocks in the detection window by multiplying Total$_x$  $\times$ Total$_y$. The above formula can be simplified considerably because the `block_size`, `block_stride`, and `window_size`are all defined in terms of the `cell_size`. By making all the appropriate substitutions and cancelations the above formula reduces to:

\begin{equation}
\mbox{Total}_i = \left(\frac{\mbox{cells}_i - \mbox{num_cells_per_block}_i}{N_i}\right) + 1\mbox{  }; \mbox{  for  } i = x,y
\end{equation}

Where cells$_x$ is the total number of cells along the width of the detection window, and cells$_y$, is the total number of cells along the height of the detection window. And $N_x$ is the horizontal block stride in units of `cell_size` and $N_y$ is the vertical block stride in units of `cell_size`.

Let's calculate what the number of elements for the HOG feature vector should be and check that it matches the shape of the HOG Descriptor calculated above.

```python
# Calculate the total number of blocks along the width of the detection window
tot_bx = np.uint32(((x_cells - num_cells_per_block[0]) / h_stride) + 1)

# Calculate the total number of blocks along the height of the detection window
tot_by = np.uint32(((y_cells - num_cells_per_block[1]) / v_stride) + 1)

# Calculate the total number of elements in the feature vector
tot_els = (tot_bx) * (tot_by) * num_cells_per_block[0] * num_cells_per_block[1] * num_bins

# Print the total number of elements the HOG feature vector should have
print('\nThe total number of elements in the HOG Feature Vector should be: ',
      tot_bx, 'x',
      tot_by, 'x',
      num_cells_per_block[0], 'x',
      num_cells_per_block[1], 'x',
      num_bins, '=',
      tot_els)

# Print the shape of the HOG Descriptor to see that it matches the above
print('\nThe HOG Descriptor has shape:', hog_descriptor.shape)
print()
```

# Visualizing The HOG Descriptor

We can visualize the HOG Descriptor by plotting the histogram associated with each cell as a collection of vectors. To do this, we will plot each bin in the histogram as a single vector whose magnitude is given by the height of the bin and its orientation is given by the angular bin that its associated with. Since any given cell might have multiple histograms associated with it, due to the overlapping blocks, we will choose to average all the histograms for each cell to produce a single histogram for each cell.

OpenCV has no easy way to visualize the HOG Descriptor, so we have to do some manipulation first in order to visualize it. We will start by reshaping the HOG Descriptor in order to make our calculations easier.  We will then compute the average histogram of each cell and finally we will convert the histogram bins into vectors. Once we have the vectors, we plot the corresponding vectors for each cell in an image.

The code below produces an interactive plot so that you can interact with the figure. The figure contains:
* the grayscale image,
* the HOG Descriptor (feature vector),
* a zoomed-in portion of the HOG Descriptor, and
* the histogram of the selected cell.

**You can click anywhere on the gray scale image or the HOG Descriptor image to select a particular cell**. Once you click on either image a *magenta* rectangle will appear showing the cell you selected. The Zoom Window will show you a zoomed in version of the HOG descriptor around the selected cell; and the histogram plot will show you the corresponding histogram for the selected cell. The interactive window also has buttons at the bottom that allow for other functionality, such as panning, and giving you the option to save the figure if desired. The home button returns the figure to its default value.

**NOTE**: If you are running this notebook in the Udacity workspace, there is around a 2 second lag in the interactive plot. This means that if you click in the image to zoom in, it will take about 2 seconds for the plot to refresh.

```python
%matplotlib notebook

import copy
import matplotlib.patches as patches

# Set the default figure size
plt.rcParams['figure.figsize'] = [9.8, 9]

# Reshape the feature vector to [blocks_y, blocks_x, num_cells_per_block_x, num_cells_per_block_y, num_bins].
# The blocks_x and blocks_y will be transposed so that the first index (blocks_y) referes to the row number
# and the second index to the column number. This will be useful later when we plot the feature vector, so
# that the feature vector indexing matches the image indexing.
hog_descriptor_reshaped = hog_descriptor.reshape(tot_bx,
                                                 tot_by,
                                                 num_cells_per_block[0],
                                                 num_cells_per_block[1],
                                                 num_bins).transpose((1, 0, 2, 3, 4))

# Print the shape of the feature vector for reference
print('The feature vector has shape:', hog_descriptor.shape)

# Print the reshaped feature vector
print('The reshaped feature vector has shape:', hog_descriptor_reshaped.shape)

# Create an array that will hold the average gradients for each cell
ave_grad = np.zeros((y_cells, x_cells, num_bins))

# Print the shape of the ave_grad array for reference
print('The average gradient array has shape: ', ave_grad.shape)

# Create an array that will count the number of histograms per cell
hist_counter = np.zeros((y_cells, x_cells, 1))

# Add up all the histograms for each cell and count the number of histograms per cell
for i in range (num_cells_per_block[0]):
    for j in range(num_cells_per_block[1]):
        ave_grad[i:tot_by + i,
                 j:tot_bx + j] += hog_descriptor_reshaped[:, :, i, j, :]

        hist_counter[i:tot_by + i,
                     j:tot_bx + j] += 1

# Calculate the average gradient for each cell
ave_grad /= hist_counter

# Calculate the total number of vectors we have in all the cells.
len_vecs = ave_grad.shape[0] * ave_grad.shape[1] * ave_grad.shape[2]

# Create an array that has num_bins equally spaced between 0 and 180 degress in radians.
deg = np.linspace(0, np.pi, num_bins, endpoint = False)

# Each cell will have a histogram with num_bins. For each cell, plot each bin as a vector (with its magnitude
# equal to the height of the bin in the histogram, and its angle corresponding to the bin in the histogram).
# To do this, create rank 1 arrays that will hold the (x,y)-coordinate of all the vectors in all the cells in the
# image. Also, create the rank 1 arrays that will hold all the (U,V)-components of all the vectors in all the
# cells in the image. Create the arrays that will hold all the vector positons and components.
U = np.zeros((len_vecs))
V = np.zeros((len_vecs))
X = np.zeros((len_vecs))
Y = np.zeros((len_vecs))

# Set the counter to zero
counter = 0

# Use the cosine and sine functions to calculate the vector components (U,V) from their maginitudes. Remember the
# cosine and sine functions take angles in radians. Calculate the vector positions and magnitudes from the
# average gradient array
for i in range(ave_grad.shape[0]):
    for j in range(ave_grad.shape[1]):
        for k in range(ave_grad.shape[2]):
            U[counter] = ave_grad[i,j,k] * np.cos(deg[k])
            V[counter] = ave_grad[i,j,k] * np.sin(deg[k])

            X[counter] = (cell_size[0] / 2) + (cell_size[0] * i)
            Y[counter] = (cell_size[1] / 2) + (cell_size[1] * j)

            counter = counter + 1

# Create the bins in degress to plot our histogram.
angle_axis = np.linspace(0, 180, num_bins, endpoint = False)
angle_axis += ((angle_axis[1] - angle_axis[0]) / 2)

# Create a figure with 4 subplots arranged in 2 x 2
fig, ((a,b),(c,d)) = plt.subplots(2,2)

# Set the title of each subplot
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
c.set(title = 'Zoom Window', xlim = (0, 18), ylim = (0, 18), autoscale_on = False)
d.set(title = 'Histogram of Gradients')

# Plot the gray scale image
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)

# Plot the feature vector (HOG Descriptor)
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')

# Define function for interactive zoom
def onpress(event):

    #Unless the left mouse button is pressed do nothing
    if event.button != 1:
        return

    # Only accept clicks for subplots a and b
    if event.inaxes in [a, b]:

        # Get mouse click coordinates
        x, y = event.xdata, event.ydata

        # Select the cell closest to the mouse click coordinates
        cell_num_x = np.uint32(x / cell_size[0])
        cell_num_y = np.uint32(y / cell_size[1])

        # Set the edge coordinates of the rectangle patch
        edgex = x - (x % cell_size[0])
        edgey = y - (y % cell_size[1])

        # Create a rectangle patch that matches the the cell selected above
        rect = patches.Rectangle((edgex, edgey),
                                  cell_size[0], cell_size[1],
                                  linewidth = 1,
                                  edgecolor = 'magenta',
                                  facecolor='none')

        # A single patch can only be used in a single plot. Create copies
        # of the patch to use in the other subplots
        rect2 = copy.copy(rect)
        rect3 = copy.copy(rect)

        # Update all subplots
        a.clear()
        a.set(title = 'Gray Scale Image\n(Click to Zoom)')
        a.imshow(gray_image, cmap = 'gray')
        a.set_aspect(aspect = 1)
        a.add_patch(rect)

        b.clear()
        b.set(title = 'HOG Descriptor\n(Click to Zoom)')
        b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
        b.invert_yaxis()
        b.set_aspect(aspect = 1)
        b.set_facecolor('black')
        b.add_patch(rect2)

        c.clear()
        c.set(title = 'Zoom Window')
        c.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 1)
        c.set_xlim(edgex - cell_size[0], edgex + (2 * cell_size[0]))
        c.set_ylim(edgey - cell_size[1], edgey + (2 * cell_size[1]))
        c.invert_yaxis()
        c.set_aspect(aspect = 1)
        c.set_facecolor('black')
        c.add_patch(rect3)

        d.clear()
        d.set(title = 'Histogram of Gradients')
        d.grid()
        d.set_xlim(0, 180)
        d.set_xticks(angle_axis)
        d.set_xlabel('Angle')
        d.bar(angle_axis,
              ave_grad[cell_num_y, cell_num_x, :],
              180 // num_bins,
              align = 'center',
              alpha = 0.5,
              linewidth = 1.2,
              edgecolor = 'k')

        fig.canvas.draw()

# Create a connection between the figure and the mouse click
fig.canvas.mpl_connect('button_press_event', onpress)
plt.show()
```

# Understanding The Histograms

Let's take a look at a couple of snapshots of the above figure to see if the histograms for the selected cell make sense. Let's start looking at a cell that is inside a triangle and not near an edge:

Fig. 4. - Histograms Inside a Triangle.

In this case, since the triangle is nearly all of the same color there shouldn't be any dominant gradient in the selected cell. As we can clearly see in the Zoom Window and the histogram, this is indeed the case. We have many gradients but none of them clearly dominates over the other.

Now let’s take a look at a cell that is near a horizontal edge:

Fig. 5. - Histograms Near a Horizontal Edge.

Remember that edges are areas of an image where the intensity changes abruptly. In these cases, we will have a high intensity gradient in some particular direction. This is exactly what we see in the corresponding histogram and Zoom Window for the selected cell.  In the Zoom Window, we can see that the dominant gradient is pointing up, almost at 90 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 90-degree bin in the histogram to dominate strongly over the others. This is in fact what we see.

Now let’s take a look at a cell that is near a vertical edge:

Fig. 6. - Histograms Near a Vertical Edge.

In this case we expect the dominant gradient in the cell to be horizontal, close to 180 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 170-degree bin in the histogram to dominate strongly over the others. This is what we see in the histogram but we also see that there is another dominant gradient in the cell, namely the one in the 10-degree bin. The reason for this, is because the HOG algorithm is using unsigned gradients, which means 0 degrees and 180 degrees are considered the same. Therefore, when the histograms are being created, angles between 160 and 180 degrees, contribute proportionally to both the 10-degree bin and the 170-degree bin. This results in there being two dominant gradients in the cell near the vertical edge instead of just one.

To conclude let’s take a look at a cell that is near a diagonal edge.

Fig. 7. - Histograms Near a Diagonal Edge.

To understand what we are seeing, let’s first remember that gradients have an *x*-component, and a *y*-component, just like vectors. Therefore, the resulting orientation of a gradient is going to be given by the vector sum of its components. For this reason, on vertical edges the gradients are horizontal, because they only have an x-component, as we saw in Figure 4. While on horizontal edges the gradients are vertical, because they only have a y-component, as we saw in Figure 3. Consequently, on diagonal edges, the gradients are also going to be diagonal because both the *x* and *y* components are non-zero.  Since the diagonal edges in the image are close to 45 degrees, we should expect to see a dominant gradient orientation in the 50-degree bin. This is in fact what we see in the histogram but, just like in Figure 4., we see there are two dominant gradients instead of just one. The reason for this is that when the histograms are being created, angles that are near the boundaries of bins, contribute proportionally to the adjacent bins.  For example, a gradient with an angle of 40 degrees, is right in the middle of the 30-degree and 50-degree bin. Therefore, the magnitude of the gradient is split evenly into the 30-degree and 50-degree bin. This results in there being two dominant gradients in the cell near the diagonal edge instead of just one.

Now that you know how HOG is implemented, in the workspace you will find a notebook named *Examples*. In there, you will be able set your own paramters for the HOG descriptor for various images. Have fun!


# Linear Regression

## Introduction

* 什麼是回歸，什麼是線性迴歸
* 估計方法
 Aatash Shah曾在他的文章中作过这样的定义：

> * “机器学习”是一种能够直接从数据中学习，而无需依赖规则编程的算法。

> * “建立统计模型”的意思是以数学方程式来表示数据变量间的关系。

實際上，機器學習的很多 fashion advanced 的算法都是基於線性回歸的，所以，在能夠跟進一步了解其他算法之前，我們需要好好了解一下甚麼是線性迴歸。

### 什麼是回歸

在现实世界中存在大量这样的情况：两个或多个变量之间有一些联系，但是没有确切关系（没有确切到可以严格决定的程度），例如：人的身高$X$和體重$Y$有關係，一般表現為$X$增大時$Y$也傾向於增大，但由$X$並不能嚴格決定$Y$；一種農作物的畝產量$Y$與其播種量$X_1$、施肥量$X_2$，等等有關，但是$X Series$並不能嚴格確定$Y$。

因此，根據上述例子，$Y$一般被稱為**因變量**或者回歸變量(regressand)，而$X$被稱為回歸量、自變量（independent variable, regressors)。

為什麼$X$不能嚴格決定$Y$？很簡單，因為自變量太多了，而我們是無法窮盡（大部分情況）這些自變量，而且如果窮盡了這些自變量，很容易發生「過擬合」的情況(overfitted)；另外，$X$是那些跟$Y$有關的變量，而非絕對的因果關係，如果從英文中，我們可以很容易理解這個「關係」：correlation & causality.

現在我們家者一個問題中有因變量$Y$以及自變量$X_1, X_2,\dots, X_p$.可以設想$Y$的值由兩部分構成，一部分由$X_1, X_2,\dots, X_p$的影響所導致，一部分是由未知因素導致，我們將第二部分歸咎為「誤差」(error)，這樣一來，我們就可以得到一個式子：
$$Y = f(X) + \epsilon \ \ \ \ X = (X_1, X_2,\dots, X_p), \ \ \epsilon = \text{error}$$
作為隨機誤差，我們要求它的期望為零：$\displaystyle E(\sum_i^p \epsilon_i) = 0$

### 線性迴歸 (Linear Regression)

在這裏，我們只討論回歸函數$f(x)$為線型函數的情況（包括可以轉化成線性函數的情況），我們稱其為：線性迴歸(Linear Regression),在這一節，我們只討論簡單線性迴歸，就是一元線性迴歸(Simple Linear Regression)，我們只討論還有一個自變量$X$（因變量只有也只會有一個$Y$）。

$\begin{align*}
    Y &=& \beta_0 + \beta_1 X + \epsilon \\
    E(\epsilon) = 0 \ \ \ Var(\epsilon) = \sigma^2 \\
 \end{align*}$
where:

* $x$: Independent variable(regressor);
* $Y$: Dependent variable(response);
* $\epsilon$: Random error;
* $\beta_0, \beta_1, \sigma^2$: Unknown parameters;
* $\sigma^2$: Random error variance

#### 估計方法 (Methods of Estimation) —— 點估計(Point Estimation)

我們對模型的變量$X, Y$進行了$n$次獨立觀察，得樣本：
$$(X_1, Y_1), (X_2, Y_2), \dots, (X_n, Y_n)$$

這組樣本：$y_i = \beta_0 + \beta_1 x_i + \epsilon_i, \ \ \ i = (1,\dots,n)$
我們可以將樣本帶入方程中得出$p$個式子，每一個式子裡都會有一個隨機誤差。

我們需要估計$\beta_0$和$\beta_1$

##### 最小二乘法 (Least Square Estimiation)

* 觀測點：$(X_1, Y_1), (X_2, Y_2), \dots, (X_n, Y_n)$
* 函數：$y_i \beta_0 + \beta_1 x_i + \epsilon_i , \ \ \ i = 1,\dots, n$
* $\epsilon_i, \dots, \epsilon_n$ are i.i.d, $\ E(\epsilon) = 0, \ \ Var(\epsilon_i) = \sigma^2$

我們想要估計的是$\beta_0, \beta_1$和$\sigma^2$，我們可以使用**最小二乘法**去進行估計。

* 殘差：$e_i = y_i - \hat{y_i}$;
* $Y$的估計值：$\hat{y_i} = \hat{beta_0} + \hat{beta_1}x_i$；
* $\beta_0$和$\beta_1$ 的估計值：$\hat{\beta_0}$ 和 $\hat{\beta_1}$.

使用最小二乘法的目的就是使得殘差最小化。

$$\displaystyle Q = \sum_{i=1}^n e_i^2 = \sum_{i=1}^n(y_i - \beta_0 - \beta_1 x_i)^2$$

For minimizeing $Q$ with respect to $\beta_0$ and $\beta_1$, take derivatives:

$\begin{align*}
    &\frac{\partial Q}{\partial \beta_0} = -2\sum_{i=1}^n (y_i - \beta_0 - \beta_1x_i) = 0 \\
    &\frac{\partial Q}{\partial \beta_1} = -2\sum_{i=1}^n (y_i -\beta_0 -\beta_1 x_i)x_i = 0 \\
    &S_{XX} = \sum_{i=1}^n (x_i - \bar{x})^2 = \sum_{i=1}^n x_i^2 - \frac{1}{n}(\sum_{i=1}^n x_i)^2 \\
    &S_{XY} = \sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y}) = \sum_{i=1}^n x_i y_i - \frac{1}{n}(\sum_{i=1}^n x_i)(\sum_{i=1}^n y_i) \\
  \end{align*}$
$\Rightarrow
    \begin{equation}
        \left\{
             \begin{array}{lr}
                 \hat{\beta_1} = S_{XY} / S_{XX} \\
                 \hat{\beta_0} = \bar{y} - \hat{\beta_1} \bar{x} \\
              \end{array}
        \right.
    \end{equation}
$

In this section(Using PY in Linear Regression), we will use `numpy`, `scipy.stats`, `matplotlib.pyplot` to do the regression analysis. You can also try `scikit-learn` by yourselves . If you want to know more about the `scikit-learn`, **please skim through its offical website:**  scikit-learn

```python
from scipy import stats
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

plt.scatter(x,y)

dataset= pd.read_csv("https://raw.githubusercontent.com/TerenceLiu98/Using-R-Series/master/data_set/student_scores.csv")
dataset.shape # dataset's shape
dataset.head() # the first five data of the dataset
dataset.describe() # to see some statistical details
```

```python
dataset.plot(x='Hours', y='Scores', style='o')
plt.title('Hours vs Percentage')
plt.xlabel('Hours Studied')
plt.ylabel('Percentage Score')
plt.show()
```

```python
b_1, b_0, r_value, p_value, std_err = stats.linregress(dataset.Hours,dataset.Scores)
```

```python
print('Linear regression using stats.linregress')
print('parameters: b_1=%.2f b_0=%.2f \nmodel parameters: r_value =%.2f, p_value =%.2f, std error= %.3f' % (b_1, b_0, r_value, p_value, std_err))
print('\n')
```

    Linear regression using stats.linregress
    parameters: b_1=9.78 b_0=2.48
    model parameters: r_value =0.98, p_value =0.00, std error= 0.453

```python
x = dataset.Hours
y = dataset.Scores
y_new = b_0 + b_1 * x

plt.figure(figsize=(4, 3))
ax = plt.axes()
ax.scatter(x, y, label = "original data")
ax.plot(x, y_new, "r-",label="regression line")

ax.set_xlabel('x')
ax.set_ylabel('y')

plt.legend(loc = 'best')

plt.show()
```

## 多元線性迴歸 (Multiple Linear Regression)

考慮完只有一個自變量$X$後，我們需要考慮有$p$個自變量$X_1, X_2, \dots $X_p$, 因變量依舊只有一個$Y$， 此時模型則變成了：

$$Y = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \dots + \epsilon$$

自變量增多以後，我們自然需要考慮：是否隨機這個問題，在這裡我們考慮自變量時非隨機的（不然就很難做了）。將所有的自變量看作一個矩陣，$\beta$看作一個向量，$Y$看作一個向量，$\epsilon$看作一個向量，這樣，我們就將模型向量化/矩陣化(matrixlize):

$$Y = X\beta + \epsilon$$

同樣的，我們可以重新考慮最小二乘法。

$\begin{align*}
    Q &= (X\beta - y)^T(X\beta - y) \\
      &\vdots \\
\end{align*}$

可以計算出：$\hat{\beta} = (X^T X)^{-1}X^T Y$

下面，我們用一個例子來解釋一下代碼部分：

下面是一個數據集，我們用多元線性迴歸來预测美国48个州的汽油消耗量(以百万加仑为单位)，其基础是汽油税(以美分为单位)、人均收入(以美元为单位)、铺设的公路(以英里为单位)以及拥有驾照的人口比例。

```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

dataset = pd.read_csv("https://raw.githubusercontent.com/TerenceLiu98/Using-R-Series/master/data_set/price.csv")
dataset.shape # dataset's shape
dataset.head() # the first five data of the dataset
```

      Index
      One
      Petrol tax (cents per gallon)
      Average income (dollars)
      Paved Highways (miles)
      Proportion of population with driver's licenses
      Consumption of petrol (millions of gallons)

      0
      1
      1
      9.0
      3571
      1976
      0.525
      541

      1
      2
      1
      9.0
      4092
      1250
      0.572
      524

      2
      3
      1
      9.0
      3865
      1586
      0.580
      561

      3
      4
      1
      7.5
      4870
      2351
      0.529
      414

      4
      5
      1
      8.0
      4399
      431
      0.544
      410

```python
dataset.describe() # to see some statistical details
```

      Index
      One
      Petrol tax (cents per gallon)
      Average income (dollars)
      Paved Highways (miles)
      Proportion of population with driver's licenses
      Consumption of petrol (millions of gallons)

      count
      48.00
      48.0
      48.000000
      48.000000
      48.000000
      48.000000
      48.000000

      mean
      24.50
      1.0
      7.668333
      4241.833333
      5565.416667
      0.570333
      576.770833

      std
      14.00
      0.0
      0.950770
      573.623768
      3491.507166
      0.055470
      111.885816

      min
      1.00
      1.0
      5.000000
      3063.000000
      431.000000
      0.451000
      344.000000

      25%
      12.75
      1.0
      7.000000
      3739.000000
      3110.250000
      0.529750
      509.500000

      50%
      24.50
      1.0
      7.500000
      4298.000000
      4735.500000
      0.564500
      568.500000

      75%
      36.25
      1.0
      8.125000
      4578.750000
      7156.000000
      0.595250
      632.750000

      max
      48.00
      1.0
      10.000000
      5342.000000
      17782.000000
      0.724000
      968.000000

```python
dataset.dtypes.index # get the row's name of the dataset, this can make next step easier
```

    Index(['Index', 'One', 'Petrol tax (cents per gallon)',
           'Average income (dollars)', 'Paved Highways (miles)',
           'Proportion of population with driver's licenses',
           'Consumption of petrol (millions of gallons)'],
          dtype='object')

```python
X = dataset[['Petrol tax (cents per gallon)',
       'Average income (dollars)', 'Paved Highways (miles)',
       "Proportion of population with driver's licenses"]]
y = dataset['Consumption of petrol (millions of gallons)']
```

為了訓練模型，我們使用 `fitted`：

```python
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
```

如一元線性迴歸一樣，在多元線性迴歸的情況下，回歸模型必須找到所有參數的最優係數：

```python
from sklearn.linear_model import LinearRegression
regressor = LinearRegression()
regressor.fit(X_train, y_train)

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

      Coefficient

      Petrol tax (cents per gallon)
      -40.016660

      Average income (dollars)
      -0.065413

      Paved Highways (miles)
      -0.004741

      Proportion of population with driver's licenses
      1341.862121

下面是應用模型進行預測：

```python
y_pred = regressor.predict(X_test)
```

```python
df = pd.DataFrame({'Actual': y_test, 'Predicted': y_pred})
df
```

      Actual
      Predicted

      29
      534
      469.391989

      4
      410
      545.645464

      26
      577
      589.668394

      30
      571
      569.730413

      32
      577
      649.774809

      37
      704
      646.631164

      34
      487
      511.608148

      40
      587
      672.475177

      7
      467
      502.074782

      10
      580
      501.270734

```python
from sklearn import metrics
print('Mean Absolute Error:', metrics.mean_absolute_error(y_test, y_pred))
print('Mean Squared Error:', metrics.mean_squared_error(y_test, y_pred))
print('Root Mean Squared Error:', np.sqrt(metrics.mean_squared_error(y_test, y_pred)))
```

    Mean Absolute Error: 56.82224747896468
    Mean Squared Error: 4666.344787588359
    Root Mean Squared Error: 68.31064915215167

```python

```

jupyterfiles/Using PY in Linear Regression .ipynb


```python
#!/usr/bin/env python
"""mass_radius_relations.ipynb: a notebook to calculate NS mass-radius relations as part of the 2019 CRAQ summerschool"""

__author__ = "Vanessa Graber"
__copyright__ = "Copyright 2019"
__credits__ = ["Vanessa Graber"]
__license__ = "MIT"
__maintainer__ = "Vanessa Graber"
__email__ = "vanessa.graber@mcgill.ca"
```

# Calculating Mass-radius Curves

In this exercise, we will solve the relativistic Tolman-Oppenheimer-Volkoff equation, which is obtained by solving Einstein's equation for a non-rotating, spherically symmetric, ideal fuid. Its solution descibes the global structure of a general relativistic, spherical object in hydrostatic equilibrium and is often used to obtain a simple model of neutron stars. Specifically, we can calculate characteristic mass-radius curves.

```python
import numpy as np
import scipy as sp
import matplotlib as mpl
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from matplotlib import rc
%matplotlib inline

# only comment out, when you have latex installed
# rc('text', usetex = True)
# rc('font', family = 'serif')
# mpl.rcParams['text.latex.preamble'] = [r"\usepackage{amsmath}"]
```

To calculate numerical estimates we use the following natural constants. Note that these are defined in SI-units, so we convert them to cgs units.

```python
from scipy.constants import pi, c, hbar, m_n, G
hbar = hbar*1e7
m_n = m_n*1e3
c = c *1e2
G = G*1e3
Msun = 1.98855e33
print(hbar, m_n, c, G)
```

    1.0545718001391126e-27 1.674927471e-24 29979245800.0 6.67408e-08

## TOV Equation

The TOV equation reads

$$
\frac{dP(r)}{dr} = - \frac{G}{r^2} \left[ \rho(r) + \frac{P(r)}{c^2} \right] \left[m(r) + 4 \pi r^3 \frac{P(r)}{c^2} \right] \left[ 1 - \frac{2 G m(r)}{c^2 r} \right]^{-1},
$$

where $G$ and $c$ represent the gravitational constant and speed of light, respectively. Moreover, $r$ denotes the radial coordinate and $\rho(r)$ and $P(r)$ the mass density and pressure at position $r$, respectively. Finally, $m(r)$ is the gravitational mass (i.e. the mass measured by the gravitational field felt by a distant observer), which satisfies $m(0) = 0$ and is obtained by solving the continuity equation

$$
\frac{dm(r)}{dr}=4 \pi \rho(r) r^2 \hspace{1cm} \rightarrow \hspace{1cm}
M = m(R) = \int_0^{R} 4 \pi \rho(r) r^2 \, d r ,
$$

with the stellar radius $r=R$. To solve the TOV equation and determine the structure of a simplified neutron star, we also need to provide an equation of state, relating pressure and density of the object, i.e. $P = P(\rho)$.  In the following, we will solve the TOV equation for a simplified equation of state.

## Two-component polytrope

As a simple neutron star model, we consider a sphere composed by non-interacting, degenerate neutrons which are characterised by two polytropic equations of state, i.e. one for the crust and one for the core with the boundary located at $\rho_{\rm b} \sim 3 \times 10^{14} \, \text{g} \, \text{cm}^{-3}$, i.e.

$$
P = \left\{ \begin{array}{ll} K_{\rm crust} \, \rho^{\gamma_{\rm crust}} & \text{for   } \rho < \rho_{\rm b}, \\ K_{\rm core} \, \rho^{\gamma_{\rm core}} & \text{for   } \rho > \rho_{\rm b}. \end{array} \right.
$$

We assume that the crust is pure degenerate neutron matter, described by the non-relativistic EoS

$$
P_{\rm crust} =  \frac{(3 \pi^2)^{2/3}}{5} \frac{\hbar^2}{m_n^{8/3}} \, \rho^{5/3}.
$$

For the core, we will try two different equations of state with different polytropic indices ($\gamma = 1 + 1/n$):

\begin{align}
&\text{stiff with} \hspace{0.5cm} \gamma_{\rm stiff} = 3, \hspace{0.5cm} n_{\rm stiff} = 1/2,\\[1.3ex]
&\text{soft with} \hspace{0.5cm} \gamma_{\rm soft} = 5/2, \hspace{0.5cm} n_{\rm soft} = 2/3 .
\end{align}

We also require continuity of the pressure at the crust-core interface, which gives the proportionality constant for the core EoS:

$$
K_{\rm core} = K_{\rm crust} \, \rho_{\rm b}^{\gamma_{\rm crust}-\gamma_{\rm core}}.
$$

```python
gamma_crust = 5.0 / 3.0
gamma_core_stiff = 3.0
gamma_core_soft = 2.5
K_crust = (hbar**2. * (3.0 * pi**2.)**(2. / 3.)) / (5 * m_n**(8. / 3.))
```

```python
rho_b = 3e14
P_b = K_crust*rho_b**gamma_crust
```

We need both $P(\rho)$ and the inverse $\rho(P)$, so we define two separate functions (negative densities/pressures are unphysical)

```python
def eos(dens, gamma_core):
    """Equation of state taking the density and the adiabatic
    index for the core EoS and returns the pressure"""

    K_core = K_crust * rho_b**(gamma_crust - gamma_core)

    if dens <= 0:
        return 0
    if dens < rho_b:
        return K_crust * dens**gamma_crust
    else:
        return K_core * dens**gamma_core
```

```python
def eos_inverse(pres, gamma_core):
    """Inverse equation of state taking the pressure and the adiabatic
    index for the core EoS and returns the density"""

    K_core = K_crust * rho_b**(gamma_crust - gamma_core)

    if pres <= 0:
        return 0
    if pres < P_b:
        return (pres / K_crust)**(1.0 / gamma_crust)
    else:
        return (pres / K_core)**(1.0 / gamma_core)
```

As a check, compare

```python
print(P_b)
print(eos(rho_b,gamma_core_stiff))
print(rho_b)
print(eos_inverse(P_b,gamma_core_stiff))
```

    7.233398298443051e+33
    7.233398298443053e+33
    300000000000000.0
    299999999999999.44

## Numerical solution

### Differential equations

We can write the two first order differential equations in a compact form by introducing the vector $\mathbf{y}=(P,m)^T$:

$$
\frac{d \mathbf{y}}{d r} = \frac{d}{d r} \left( \begin{array}{c} P \\ m \end{array} \right) = \left( \begin{array}{c} - \frac{G}{r^2} \left[ \rho(r) + \frac{P(r)}{c^2} \right] \left[m(r) + 4 \pi r^3 \frac{P(r)}{c^2} \right] \left[ 1 - \frac{2 G m(r)}{c^2 r} \right]^{-1} \\ 4 \pi \rho(r) r^2 \end{array} \right).
$$

```python
def TOV(y,radius,gamma_core):
    """TOV equations"""

    P = y[0]
    m = y[1]
    rho = eos_inverse(P,gamma_core)

    dPdr = -G * (rho + P / c**2.) * (m + 4.0 * pi * radius**3. * P / c**2.) / (radius * (radius - 2.0 * G * m / c**2.))
    dmdr = 4.0 * pi * radius**2. * rho
    dydr = np.array([dPdr, dmdr])

    return dydr
```

To integrate this system with respect to the radial variable $r$, we have to specify the mass and pressure at $r=0$. To do this, either a central density or pressure (as both are related by the EoS) can be chosen. We will calculate the star's mass and radius for a typical range of central densities.

### Numerical integration

To solve the initial value problem use `odeint`:

```python
dr = 1e2
r = np.arange(dr, 2e6, dr)
```

```python
def solve_TOV(radius, dens_central, gamma_core):
    """Function takes a radius array, central density and the adiabatic
    index for the core EoS and solves the TOV equation"""

    # setting the initial values at r=0
    P0 = eos(dens_central, gamma_core)
    m0 = 4.0 * pi * radius[0]**3. * dens_central / 3.0
    y0 = np.array([P0, m0])

    # adjust the maximum step size for integration
    y = odeint(TOV, y0, radius, args=(gamma_core,), mxstep=500000)

    # remove negative pressures, i.e. truncate at stellar surface
    y = y[y[:, 0] > 0]
    return y
```

```python
print(solve_TOV(r, 1e15, gamma_core_stiff))
```

    [[2.67903641e+35 4.18879020e+21]
     [2.67903630e+35 3.35103215e+22]
     [2.67903613e+35 1.13097333e+23]
     ...
     [6.31361227e+26 4.35145980e+33]
     [2.03926321e+26 4.35145982e+33]
     [2.45261288e+25 4.35145984e+33]]

### Pressure and mass behaviour

Plot the results as a function of radius to compare soft and stiff core equations of state for a central density $\rho_c = 10^{15} \, \text{g} \, \text{cm}^{-3}$. For convencience express the radial coordinate in kilometers and introduce the dimensionless quantities $\bar{P} = P / P_c$ ($P_c$ is the central pressure) and $\bar{M} = M/M_{\odot}$.

```python
P_c_stiff = eos(1e15, gamma_core_stiff)
Pbar_stiff = solve_TOV(r,1e15, gamma_core_stiff)[:,0] / (P_c_stiff)
P_c_soft = eos(1e15, gamma_core_soft)
Pbar_soft = solve_TOV(r,1e15, gamma_core_soft)[:,0]/(P_c_soft)
rbar = r/1e5
```

```python
fig = plt.figure()
fig.set_size_inches(8,8)
mpl.rcParams.update({'font.size': 22})
colour = ('red','blue','green','yellow','brown','purple')
plt.title(r'TOV solution for $\rho_c = 10^{15} \,$g$\,$cm$^{-3}$', fontsize = 22)

plt.plot(rbar[:len(Pbar_stiff)], Pbar_stiff, '.', color='blue', label=r"stiff EoS, $\gamma=3$")
plt.plot(rbar[:len(Pbar_soft)], Pbar_soft, '.', color='red', label=r"soft EoS, $\gamma=5/2$")

plt.xlim(0,15)
plt.ylim(0,1)
plt.xlabel(r'$r$ in km')
plt.ylabel(r'$\bar{P}(r)$')
plt.grid()

plt.legend(loc = 1);
plt.show()
```

```python
Mbar_stiff = solve_TOV(r, 1e15, gamma_core_stiff)[:,1] / Msun
Mbar_soft = solve_TOV(r, 1e15, gamma_core_soft)[:,1] / Msun
```

```python
fig=plt.figure()
fig.set_size_inches(8,8)
mpl.rcParams.update({'font.size': 22})
colour = ('red','blue','green','yellow','brown','purple')
plt.title(r'TOV solution for $\rho_c = 10^{15} \,$g$\,$cm$^{-3}$', fontsize = 22)

plt.plot(rbar[:len(Mbar_stiff)], Mbar_stiff, '.', color='blue', label=r"stiff EoS, $\gamma=3$")
plt.plot(rbar[:len(Mbar_soft)], Mbar_soft, '.', color='red', label=r"soft EoS, $\gamma=5/2$")

plt.xlim(0,15)
plt.ylim(0,2.5)
plt.xlabel(r'$r$ in km')
plt.ylabel(r'$\bar{M}(r)$')
plt.grid()

plt.legend(loc = 2);
plt.show()
```

### Mass-radius relations

Determine $R$, i.e. the surface of the star, as the entry in the $r$ array, where density and pressure drop below zero and we have cut off our solution $\mathbf{y}$.

```python
def EoS_parameters(radius, dens_central, gamma_core):
    """Function takes a central density and the adiabatic index for
    the core EoS and returns the star's mass and radius"""

    y = solve_TOV(radius, dens_central, gamma_core)
    M_star = y[-1 ,1]
    R_star = radius[len(y)-1]

    return M_star / Msun, R_star / 1e5
```

```python
print(EoS_parameters(r, 3e15, gamma_core_stiff))
```

    (2.6622117693286715, 10.539)

Determine mass-radius relations $M(R)$ for our stiff and soft core equations of state by selecting a range of central densities (start slightly above the boundary value $\rho_{\rm boundary} \sim 3 \times 10^{14} \, \text{g} \, \text{cm}^{-3}$):

```python
rho_c = np.logspace(14.5, 17, num=150)
```

```python
R_stiff = np.zeros(len(rho_c))
M_stiff = np.zeros(len(rho_c))
R_soft = np.zeros(len(rho_c))
M_soft = np.zeros(len(rho_c))

for i, rho in enumerate(rho_c):
    M_stiff[i], R_stiff[i] = EoS_parameters(r, rho, gamma_core_stiff)
    M_soft[i], R_soft[i] = EoS_parameters(r, rho, gamma_core_soft)
```

```python
fig = plt.figure()
fig.set_size_inches(8,8)
mpl.rcParams.update({'font.size': 22})
colour = ('red','blue','green','yellow','brown','purple')
plt.title(r'EoS $M(R)$ for two different adiabatic indices', fontsize=22)

plt.plot(R_stiff,M_stiff, '.', color='blue', label=r"stiff EoS, $\gamma=3$")
plt.plot(R_soft,M_soft, '.', color='red', label=r"soft EoS, $\gamma=5/2$")

plt.xlim(6,18)
plt.ylim(0.5,3)
plt.xlabel(r'$R$ in km')
plt.ylabel(r'$M$ in $M_{\odot}$')
plt.grid()

plt.legend(loc=3);
plt.show()
```

As expected, the softer EoS describes smaller maximum masses as well as smaller masses for a specific radii than the stiffer EoS.