# $p_t$, the tangential component of the momentum vector, up to and including 3.5 post-Newtonian order

## This notebook constructs the tangential component of the momentum vector

**Notebook Status:**  Validated

**Validation Notes:** All expressions in this notebook were transcribed twice by hand on separate occasions, and expressions were corrected as needed to ensure consistency with published work. Published work was cross-validated and typo(s) in published work were corrected. In addition, this tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation). **Additional validation tests may have been performed, but are as yet, undocumented.**

## Author: Zach Etienne

### This notebook exists as the following Python module:
1. [PN_p_t.py](../../edit/NRPyPN/PN_p_t.py)

### This notebook & corresponding Python module depend on the following NRPy+/NRPyPN Python modules:
1. [indexedexp.py](../../edit/indexedexp.py): [**documentation+tutorial**](../Tutorial-Indexed_Expressions.ipynb)
1. [NRPyPN_shortcuts.py](../../edit/NRPyPN/NRPyPN_shortcuts.py): [**documentation**](NRPyPN_shortcuts.ipynb)

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

1. Part 1: [$p_t$](#p_t), up to and including 3.5PN order, as derived in [Ramos-Buades, Husa, and Pratten (2018)](https://arxiv.org/abs/1810.00036)
1. Part 2: [Validation against second transcription and corresponding Python module](#code_validation)
1. Part 3: [Validation against trusted numerical values](#code_validationv2) (i.e., in Table V of [Ramos-Buades, Husa, and Pratten (2018)](https://arxiv.org/abs/1810.00036))
1. Part 4: [LaTeX PDF output](#latex_pdf_output): $\LaTeX$ PDF Output

# Part 1: $p_t$, up to and including 3.5PN order, as derived in [Ramos-Buades, Husa, and Pratten (2018)](https://arxiv.org/abs/1810.00036) \[Back to [top](#toc)\]
$$\label{p_t}$$

As described in the [nonspinning Hamiltonian notebook](PN-Hamiltonian-Nonspinning.ipynb), the basic physical system assumes two point particles of mass $m_1$ and $m_2$ with corresponding momentum vectors $\mathbf{P}_1$ and $\mathbf{P}_2$, and displacement vectors $\mathbf{X}_1$ and $\mathbf{X}_2$ with respect to the center of mass. Here we also consider the spin vectors of each point mass $\mathbf{S}_1$ and $\mathbf{S}_2$, respectively.

To reduce possibility of copying error, the equation for $p_t$ is taken directly from the arXiv LaTeX source code of Eq A2 in [Ramos-Buades, Husa, and Pratten (2018)](https://arxiv.org/abs/1810.00036), and only mildly formatted to (1) improve presentation in Jupyter notebooks, (2) to ensure some degree of consistency in notation across different terms in other NRPyPN notebooks, and (3) to correct any errors. In particular, the boxed negative sign at 2.5PN order ($a_5$ below) was missing in the original equation. We will later show that this negative sign is necessary for consistency with other expressions in the same paper, as well as with the expression up to 3PN order in [Healy, Lousto, Nakano, and Zlochower (2017)](https://arxiv.org/abs/1702.00872):

$$
p_t = \frac{q}{(1+q)^2}\frac{1}{r^{1/2}}\left(1 + \sum_{k=2}^7 \frac{a_k}{r^{k/2}}\right),
$$
where

\begin{align}
a_2 &= 2\\
a_3 &= \left[-\frac{3 \left(4 q^2+3 q\right) \chi _{2z}}{4 (q+1)^2}-\frac{3 (3 q+4) \chi _{1z}}{4 (q+1)^2}\right]\\
a_4 &= \left[ -\frac{3 q^2 \chi _{2x}^2}{2 (q+1)^2} +\frac{3 q^2 \chi _{2y}^2}{4 (q+1)^2}+\frac{3 q^2 \chi _{2z}^2}{4 (q+1)^2}
 +\frac{42 q^2+41 q+42}{16 (q+1)^2}-\frac{3 \chi _{1x}^2}{2 (q+1)^2} \right.\\
&\quad\quad \left. -\frac{3 q  \chi _{1x} \chi _{2x}}{(q+1)^2}+\frac{3 \chi _{1y}^2}{4 (q+1)^2}+\frac{3 q \chi _{1y}\chi _{2y}}{2 (q+1)^2}+\frac{3 \chi _{1z}^2}{4 (q+1)^2}+\frac{3 q \chi _{1z} \chi _{2z}}{2 (q+1)^2}\right]\\
a_5 &= \left[ \boxed{-1} \frac{\left(13 q^3+60 q^2+116 q+72\right) \chi _{1z}}{16 (q+1)^4}+\frac{\left(-72 q^4-116 q^3-60 q^2-13 q\right) \chi _{2z}}{16 (q+1)^4}  \right]\\
a_6 &= \left[\frac{\left(472 q^2-640\right) \chi _{1x}^2}{128 (q+1)^4} + \frac{\left(-512 q^2-640 q-64\right) \chi _{1y}^2}{128 (q+1)^4}+\frac{\left(-108 q^2+224 q+512\right) \chi _{1z}^2}{128 (q+1)^4}\right.\\
&\quad\quad \left.+\frac{\left(472 q^2-640 q^4\right) \chi _{2x}^2}{128 (q+1)^4}+\frac{\left(192 q^3+560 q^2+192 q\right) \chi _{1x} \chi _{2x}}{128 (q+1)^4} +\frac{\left(-864 q^3-1856 q^2-864 q\right) \chi _{1y} \chi _{2y}}{128 (q+1)^4}\right.\\
&\quad\quad \left.+\frac{\left(480 q^3+1064 q^2+480 q\right) \chi _{1z} \chi _{2z}}{128 (q+1)^4}+\frac{\left(-64 q^4-640 q^3-512 q^2\right) \chi _{2y}^2}{128 (q+1)^4}+\frac{\left(512 q^4+224 q^3-108 q^2\right) \chi _{2z}^2}{128 (q+1)^4} \right. \nonumber
\\
&\quad\quad\left.+\frac{480 q^4+163 \pi ^2 q^3-2636 q^3+326 \pi ^2 q^2-6128 q^2+163 \pi ^2 q-2636 q+480}{128 (q+1)^4} \right]\\
a_7 &= \left[ \frac{5 (4 q+1) q^3 \chi _{2 x}^2 \chi _{2 z}}{2 (q+1)^4}-\frac{5 (4 q+1) q^3 \chi _{2 y}^2 \chi _{2 z}}{8 (q+1)^4}-\frac{5 (4 q+1) q^3 \chi _{2 z}^3}{8 (q+1)^4}+\chi _{1x} \left(\frac{15 (2 q+1) q^2 \chi _{2 x} \chi _{2 z}}{4 (q+1)^4}+\frac{15 (q+2) q \chi _{2 x} \chi _{1z}}{4 (q+1)^4}\right)\right. \nonumber
\\
&\quad\quad \left.+\chi _{1y} \left(\frac{15 q^2 \chi _{2 y} \chi _{1z}}{4 (q+1)^4}+\frac{15 q^2 \chi _{2 y} \chi _{2 z}}{4 (q+1)^4}\right)+\chi _{1z} \left(\frac{15 q^2 (2 q+3) \chi _{2 x}^2}{4 (q+1)^4}-\frac{15 q^2 (q+2) \chi _{2 y}^2}{4 (q+1)^4}-\frac{15 q^2 \chi _{2 z}^2}{4 (q+1)^3} \right.\right. \nonumber
\\
&\quad\quad \left.\left. -\frac{103 q^5+145 q^4-27 q^3+252 q^2+670 q+348}{32 (q+1)^6}\right)-\frac{\left(348 q^5+670 q^4+252 q^3-27 q^2+145 q+103\right) q \chi _{2 z}}{32 (q+1)^6}\right.\nonumber
\\
&\quad\quad \left.+\chi _{1x}^2 \left(\frac{5 (q+4) \chi _{1z}}{2 (q+1)^4}+\frac{15 q (3 q+2) \chi _{2 z}}{4 (q+1)^4}\right)+\chi _{1y}^2 \left(-\frac{5 (q+4) \chi _{1z}}{8 (q+1)^4}-\frac{15 q (2 q+1) \chi _{2 z}}{4 (q+1)^4}\right)-\frac{15 q \chi _{1z}^2 \chi _{2 z}}{4 (q+1)^3}-\frac{5 (q+4) \chi _{1z}^3}{8 (q+1)^4} \right]
\end{align}

Let's divide and conquer, by tackling the coefficients one at a time:

\begin{align}
a_2 &= 2\\
a_3 &= \left[-\frac{3 \left(4 q^2+3 q\right) \chi _{2z}}{4 (q+1)^2}-\frac{3 (3 q+4) \chi _{1z}}{4 (q+1)^2}\right]\\
a_4 &= \left[ -\frac{3 q^2 \chi _{2x}^2}{2 (q+1)^2} +\frac{3 q^2 \chi _{2y}^2}{4 (q+1)^2}+\frac{3 q^2 \chi _{2z}^2}{4 (q+1)^2}
 +\frac{42 q^2+41 q+42}{16 (q+1)^2}-\frac{3 \chi _{1x}^2}{2 (q+1)^2} \right.\\
&\quad\quad \left. -\frac{3 q  \chi _{1x} \chi _{2x}}{(q+1)^2}+\frac{3 \chi _{1y}^2}{4 (q+1)^2}+\frac{3 q \chi _{1y}\chi _{2y}}{2 (q+1)^2}+\frac{3 \chi _{1z}^2}{4 (q+1)^2}+\frac{3 q \chi _{1z} \chi _{2z}}{2 (q+1)^2}\right]
\end{align}

```python
# Step 0: Add NRPy's directory to the path
# https://stackoverflow.com/questions/16780014/import-file-from-parent-directory
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import indexedexpNRPyPN as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
from NRPyPN_shortcuts import div # NRPyPN: shortcuts for e.g., vector operations

# Step 1: Construct terms a_2, a_3, and a_4, from
#  Eq A2 of Ramos-Buades, Husa, and Pratten (2018)
#    https://arxiv.org/abs/1810.00036
#  These terms have been independently validated
#    against the same terms in Eq 7 of
#  Healy, Lousto, Nakano, and Zlochower (2017)
#    https://arxiv.org/abs/1702.00872
def p_t__a_2_thru_a_4(m1,m2, chi1x,chi1y,chi1z, chi2x,chi2y,chi2z):
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    global a_2,a_3,a_4
    a_2 = 2
    a_3 = (-3*(4*q**2+3*q)*chi2z/(4*(q+1)**2) - 3*(3*q+4)*chi1z/(4*(q+1)**2))
    a_4 = (-3*q**2*chi2x**2/(2*(q+1)**2)
           +3*q**2*chi2y**2/(4*(q+1)**2)
           +3*q**2*chi2z**2/(4*(q+1)**2)
           +(+42*q**2 + 41*q + 42)/(16*(q+1)**2)
           -3*chi1x**2/(2*(q+1)**2)
           -3*q*chi1x*chi2x/(q+1)**2
           +3*chi1y**2/(4*(q+1)**2)
           +3*q*chi1y*chi2y/(2*(q+1)**2)
           +3*chi1z**2/(4*(q+1)**2)
           +3*q*chi1z*chi2z/(2*(q+1)**2))
```

```python
# Second version, for validation purposes only.
def p_t__a_2_thru_a_4v2(m1,m2, chi1x,chi1y,chi1z, chi2x,chi2y,chi2z):
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    global a_2v2,a_3v2,a_4v2
    # Validated against HLNZ2017 version
    a_2v2 = 2
    # Validated against HLNZ2017 version
    a_3v2 = (-(3*(4*q**2+3*q)*chi2z)/(4*(q+1)**2)-(3*(3*q+4)*chi1z)/(4*(q+1)**2))
    # Validated against HLNZ2017 version
    a_4v2 = -(3*q**2*chi2x**2)/(2*(q+1)**2)+(3*q**2*chi2y**2)/(4*(q+1)**2)+(3*q**2*chi2z**2)/(4*(q+1)**2)+(42*q**2+41*q+42)/(16*(q+1)**2)-(3*chi1x**2)/(2*(q+1)**2)-(3*q*chi1x*chi2x)/((q+1)**2)+(3*chi1y**2)/(4*(q+1)**2)+(3*q*chi1y*chi2y)/(2*(q+1)**2)+(3*chi1z**2)/(4*(q+1)**2)+(3*q*chi1z*chi2z)/(2*(q+1)**2)
```

Next, $a_5$ and $a_6$:

\begin{align}
a_5 &= \left[ \boxed{-1} \frac{\left(13 q^3+60 q^2+116 q+72\right) \chi _{1z}}{16 (q+1)^4}+\frac{\left(-72 q^4-116 q^3-60 q^2-13 q\right) \chi _{2z}}{16 (q+1)^4}  \right]\\
a_6 &= \left[\frac{\left(472 q^2-640\right) \chi _{1x}^2}{128 (q+1)^4} + \frac{\left(-512 q^2-640 q-64\right) \chi _{1y}^2}{128 (q+1)^4}+\frac{\left(-108 q^2+224 q+512\right) \chi _{1z}^2}{128 (q+1)^4}\right.\\
&\quad\quad \left.+\frac{\left(472 q^2-640 q^4\right) \chi _{2x}^2}{128 (q+1)^4}+\frac{\left(192 q^3+560 q^2+192 q\right) \chi _{1x} \chi _{2x}}{128 (q+1)^4} +\frac{\left(-864 q^3-1856 q^2-864 q\right) \chi _{1y} \chi _{2y}}{128 (q+1)^4}\right.\\
&\quad\quad \left.+\frac{\left(480 q^3+1064 q^2+480 q\right) \chi _{1z} \chi _{2z}}{128 (q+1)^4}+\frac{\left(-64 q^4-640 q^3-512 q^2\right) \chi _{2y}^2}{128 (q+1)^4}+\frac{\left(512 q^4+224 q^3-108 q^2\right) \chi _{2z}^2}{128 (q+1)^4} \right. \nonumber
\\
&\quad\quad\left.+\frac{480 q^4+163 \pi ^2 q^3-2636 q^3+326 \pi ^2 q^2-6128 q^2+163 \pi ^2 q-2636 q+480}{128 (q+1)^4} \right]\\
\end{align}

```python
# Construct terms a_5 and a_6, from
#  Eq A2 of Ramos-Buades, Husa, and Pratten (2018)
#    https://arxiv.org/abs/1810.00036
#  These terms have been independently validated
#    against the same terms in Eq 7 of
#  Healy, Lousto, Nakano, and Zlochower (2017)
#    https://arxiv.org/abs/1702.00872
#  and a sign error was corrected in the a_5
#  expression.
def p_t__a_5_thru_a_6(m1,m2, chi1x,chi1y,chi1z, chi2x,chi2y,chi2z, FixSignError=True):
    SignFix = sp.sympify(-1)
    if FixSignError == False:
        SignFix = sp.sympify(+1)
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    global a_5,a_6
    a_5 = (SignFix*(13*q**3 + 60*q**2 + 116*q + 72)*chi1z/(16*(q+1)**4)
           +(-72*q**4 - 116*q**3 - 60*q**2 - 13*q)*chi2z/(16*(q+1)**4))
    a_6 = (+(+472*q**2 - 640)*chi1x**2/(128*(q+1)**4)
           +(-512*q**2 - 640*q - 64)*chi1y**2/(128*(q+1)**4)
           +(-108*q**2 + 224*q +512)*chi1z**2/(128*(q+1)**4)
           +(+472*q**2 - 640*q**4)*chi2x**2/(128*(q+1)**4)
           +(+192*q**3 + 560*q**2 + 192*q)*chi1x*chi2x/(128*(q+1)**4)
           +(-864*q**3 -1856*q**2 - 864*q)*chi1y*chi2y/(128*(q+1)**4)
           +(+480*q**3 +1064*q**2 + 480*q)*chi1z*chi2z/(128*(q+1)**4)
           +( -64*q**4 - 640*q**3 - 512*q**2)*chi2y**2/(128*(q+1)**4)
           +(+512*q**4 + 224*q**3 - 108*q**2)*chi2z**2/(128*(q+1)**4)
           +(+480*q**4 + 163*sp.pi**2*q**3 - 2636*q**3 + 326*sp.pi**2*q**2 - 6128*q**2 + 163*sp.pi**2*q-2636*q+480)
            /(128*(q+1)**4))
```

```python
# Second version, for validation purposes only.
def p_t__a_5_thru_a_6v2(m1,m2, chi1x,chi1y,chi1z, chi2x,chi2y,chi2z, FixSignError=True):
    SignFix = sp.sympify(-1)
    if FixSignError == False:
        SignFix = sp.sympify(+1)
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    pi = sp.pi
    global a_5v2,a_6v2
    # Validated (separately) against HLNZ2017, as well as row 3 of Table V in RHP2018
    a_5v2 =  SignFix*((13*q**3+60*q**2+116*q+72)*chi1z)/(16*(q+1)**4)+((-72*q**4-116*q**3-60*q**2-13*q)*chi2z)/(16*(q+1)**4)
    # Validated (separately) against HLNZ2017 version
    a_6v2 = (+(+472*q**2         - 640)*chi1x**2/(128*(q+1)**4)
             +(-512*q**2 - 640*q -  64)*chi1y**2/(128*(q+1)**4)
             +(-108*q**2 + 224*q + 512)*chi1z**2/(128*(q+1)**4)
             +(+472*q**2         - 640*q**4)*chi2x**2/(128*(q+1)**4)
             +(+192*q**3 + 560*q**2 + 192*q)*chi1x*chi2x/(128*(q+1)**4)
             +(-864*q**3 -1856*q**2 - 864*q)*chi1y*chi2y/(128*(q+1)**4)
             +(+480*q**3 +1064*q**2 + 480*q)*chi1z*chi2z/(128*(q+1)**4)
             +(- 64*q**4 - 640*q**3 - 512*q**2)*chi2y**2/(128*(q+1)**4)
             +(+512*q**4 + 224*q**3 - 108*q**2)*chi2z**2/(128*(q+1)**4)
             +(+480*q**4 + 163*pi**2*q**3 - 2636*q**3 + 326*pi**2*q**2 - 6128*q**2 + 163*pi**2*q - 2636*q + 480)
               /(128*(q+1)**4))
```

Next we compare the expression for $a_5$ with Eq. 7 of [Healy, Lousto, Nakano, and Zlochower (2017)](https://arxiv.org/abs/1702.00872), as additional validation that there at least is a sign inconsistency:

To reduce possibility of copying error, the following equation for $a_5$ is taken directly from the arXiv LaTeX source code of Eq. 7 of [Healy, Lousto, Nakano, and Zlochower (2017)](https://arxiv.org/abs/1702.00872), and only mildly formatted to (1) improve presentation in Jupyter notebooks and (2) to ensure some degree of consistency in notation across different terms in other NRPyPN notebooks.

**Important: Note that [Healy, Lousto, Nakano, and Zlochower (2017)](https://arxiv.org/abs/1702.00872) adopts notation such that particle labels are interchanged: $1\leftrightarrow 2$, with respect to [Ramos-Buades, Husa, and Pratten (2018)](https://arxiv.org/abs/1810.00036)**

\begin{align}
a_5 &= + \left( -\frac{1}{16}\,{\frac {q \left( 72\,{q}^{3}+116\,{q}^{2}+60\,q+13 \right)
{\chi_{1z}}}{ \left( 1+q \right) ^{4}}}
-\frac{1}{16}\,{\frac { \left( 13\,{q}^{3}+60\,{q}^{2}+116\,q+72 \right) {\chi_{2z}}}
{ \left( 1+q \right) ^{4}}} \right)\\
\end{align}

```python
# Third version, for addtional validation.
def p_t__a_5_thru_a_6_HLNZ2017(m1,m2, chi1x,chi1y,chi1z, chi2x,chi2y,chi2z):
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    global a_5_HLNZ2017
    a_5_HLNZ2017 = (-div(1,16)*(q*(72*q**3 + 116*q**2 + 60*q + 13)*chi1z/(1+q)**4)
                    -div(1,16)*(  (13*q**3 +  60*q**2 +116*q + 72)*chi2z/(1+q)**4))
```

Finally, we validate that all 3 expressions for $a_5$ agree. (At the bottom, we confirm that all v2 expressions for $a_i$ match.)

```python
from NRPyPN_shortcuts import m1,m2, chi1U,chi2U # Import needed input variables

p_t__a_5_thru_a_6(         m1,m2, chi1U[0],chi1U[1],chi1U[2], chi2U[0],chi2U[1],chi2U[2])
p_t__a_5_thru_a_6v2(       m1,m2, chi1U[0],chi1U[1],chi1U[2], chi2U[0],chi2U[1],chi2U[2])
# Again, the particle labels are interchanged in Healy, Lousto, Nakano, and Zlochower (2017):
p_t__a_5_thru_a_6_HLNZ2017(m1,m2, chi2U[0],chi2U[1],chi2U[2], chi1U[0],chi1U[1],chi1U[2])

def error(varname):
    print("ERROR: When comparing Python module & notebook, "+varname+" was found not to match.")
    sys.exit(1)

if sp.simplify(a_5 - a_5v2)        != 0: error("a_5v2")
if sp.simplify(a_5 - a_5_HLNZ2017) != 0: error("a_5_HLNZ2017")
```

Finally $a_7$:

\begin{align}
a_7 &= \left[ \frac{5 (4 q+1) q^3 \chi _{2 x}^2 \chi _{2 z}}{2 (q+1)^4}-\frac{5 (4 q+1) q^3 \chi _{2 y}^2 \chi _{2 z}}{8 (q+1)^4}-\frac{5 (4 q+1) q^3 \chi _{2 z}^3}{8 (q+1)^4}+\chi _{1x} \left(\frac{15 (2 q+1) q^2 \chi _{2 x} \chi _{2 z}}{4 (q+1)^4}+\frac{15 (q+2) q \chi _{2 x} \chi _{1z}}{4 (q+1)^4}\right)\right. \nonumber
\\
&\quad\quad \left.+\chi _{1y} \left(\frac{15 q^2 \chi _{2 y} \chi _{1z}}{4 (q+1)^4}+\frac{15 q^2 \chi _{2 y} \chi _{2 z}}{4 (q+1)^4}\right)+\chi _{1z} \left(\frac{15 q^2 (2 q+3) \chi _{2 x}^2}{4 (q+1)^4}-\frac{15 q^2 (q+2) \chi _{2 y}^2}{4 (q+1)^4}-\frac{15 q^2 \chi _{2 z}^2}{4 (q+1)^3} \right.\right. \nonumber
\\
&\quad\quad \left.\left. -\frac{103 q^5+145 q^4-27 q^3+252 q^2+670 q+348}{32 (q+1)^6}\right)-\frac{\left(348 q^5+670 q^4+252 q^3-27 q^2+145 q+103\right) q \chi _{2 z}}{32 (q+1)^6}\right.\nonumber
\\
&\quad\quad \left.+\chi _{1x}^2 \left(\frac{5 (q+4) \chi _{1z}}{2 (q+1)^4}+\frac{15 q (3 q+2) \chi _{2 z}}{4 (q+1)^4}\right)+\chi _{1y}^2 \left(-\frac{5 (q+4) \chi _{1z}}{8 (q+1)^4}-\frac{15 q (2 q+1) \chi _{2 z}}{4 (q+1)^4}\right)-\frac{15 q \chi _{1z}^2 \chi _{2 z}}{4 (q+1)^3}-\frac{5 (q+4) \chi _{1z}^3}{8 (q+1)^4} \right]
\end{align}

```python
# Construct term a_7, from Eq A2 of
#  Ramos-Buades, Husa, and Pratten (2018)
#    https://arxiv.org/abs/1810.00036
def p_t__a_7(m1,m2, chi1x,chi1y,chi1z, chi2x,chi2y,chi2z):
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    global a_7
    a_7 = (+5*(4*q+1)*q**3*chi2x**2*chi2z/(2*(q+1)**4)
           -5*(4*q+1)*q**3*chi2y**2*chi2z/(8*(q+1)**4)
           -5*(4*q+1)*q**3*chi2z**3      /(8*(q+1)**4)
           +chi1x*(+15*(2*q+1)*q**2*chi2x*chi2z/(4*(q+1)**4)
                   +15*(1*q+2)*q   *chi2x*chi1z/(4*(q+1)**4))
           +chi1y*(+15*q**2*chi2y*chi1z/(4*(q+1)**4)
                   +15*q**2*chi2y*chi2z/(4*(q+1)**4))
           +chi1z*(+15*q**2*(2*q+3)*chi2x**2/(4*(q+1)**4)
                   -15*q**2*(  q+2)*chi2y**2/(4*(q+1)**4)
                   -15*q**2        *chi2z**2/(4*(q+1)**3)
                   -(103*q**5 + 145*q**4 - 27*q**3 + 252*q**2 + 670*q + 348)/(32*(q+1)**6))
           -(+348*q**5 + 670*q**4 + 252*q**3 - 27*q**2 + 145*q + 103)*q*chi2z/(32*(q+1)**6)
           +chi1x**2*(+5*(q+4)*chi1z/(2*(q+1)**4)
                      +15*q*(3*q+2)*chi2z/(4*(q+1)**4))
           +chi1y**2*(-5*(q+4)*chi1z/(8*(q+1)**4)
                      -15*q*(2*q+1)*chi2z/(4*(q+1)**4))
           -15*q*chi1z**2*chi2z/(4*(q+1)**3)
           -5*(q+4)*chi1z**3/(8*(q+1)**4))
```

```python
# Second version, for validation purposes only.
def p_t__a_7v2(m1,m2, chi1x,chi1y,chi1z, chi2x,chi2y,chi2z):
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    global a_7v2
    a_7v2  = (+5*(4*q+1)*q**3*chi2x**2*chi2z/(2*(q+1)**4)
          -5*(4*q+1)*q**3*chi2y**2*chi2z/(8*(q+1)**4)
          -5*(4*q+1)*q**3*chi2z**3/(8*(q+1)**4)
          +chi1x*(+(15*(2*q+1)*q**2*chi2x*chi2z)/(4*(q+1)**4)
                  +(15*(  q+2)*q   *chi2x*chi1z)/(4*(q+1)**4))
          +chi1y*(+(15*q**2*chi2y*chi1z)/(4*(q+1)**4)
                  +(15*q**2*chi2y*chi2z)/(4*(q+1)**4))
          +chi1z*(+(15*q**2*(2*q+3)*chi2x**2)/(4*(q+1)**4)
                  -(15*q**2*(  q+2)*chi2y**2)/(4*(q+1)**4)
                  -(15*q**2*        chi2z**2)/(4*(q+1)**3)
                  -(103*q**5+145*q**4-27*q**3+252*q**2+670*q+348)/(32*(q+1)**6))
          -(348*q**5+670*q**4+252*q**3-27*q**2+145*q+103)*q*chi2z/(32*(q+1)**6)
          +chi1x**2*(+5*(q+4)*chi1z/(2*(q+1)**4) + 15*q*(3*q+2)*chi2z/(4*(q+1)**4))
          +chi1y**2*(-5*(q+4)*chi1z/(8*(q+1)**4) - 15*q*(2*q+1)*chi2z/(4*(q+1)**4))
          -15*q*chi1z**2*chi2z/(4*(q+1)**3) - 5*(q+4)*chi1z**3/(8*(q+1)**4))
```

Putting it all together, recall that

$$
p_t = \frac{q}{(1+q)^2}\frac{1}{r^{1/2}}\left(1 + \sum_{k=2}^7 \frac{a_k}{r^{k/2}}\right),
$$
where $k/2$ is the post-Newtonian order.

```python
# Finally, sum the expressions for a_k to construct p_t as prescribed:
# p_t = q/(sqrt(r)*(1+q)^2) (1 + \sum_{k=2}^7 (a_k/r^{k/2}))
def f_p_t(m1,m2, chi1U,chi2U, r):
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    a = ixp.zerorank1(DIM=10)
    p_t__a_2_thru_a_4(m1,m2, chi1U[0],chi1U[1],chi1U[2], chi2U[0],chi2U[1],chi2U[2])
    a[2] = a_2
    a[3] = a_3
    a[4] = a_4
    p_t__a_5_thru_a_6(m1,m2, chi1U[0],chi1U[1],chi1U[2], chi2U[0],chi2U[1],chi2U[2])
    a[5] = a_5
    a[6] = a_6
    p_t__a_7(         m1,m2, chi1U[0],chi1U[1],chi1U[2], chi2U[0],chi2U[1],chi2U[2])
    a[7] = a_7
    global p_t
    p_t = 1 # Term prior to the sum in parentheses
    for k in range(8):
        p_t += a[k]/r**div(k,2)
    p_t *= q / (1+q)**2 * 1/r**div(1,2)
```

```python
# Second version, for validation purposes only.
def f_p_tv2(m1,m2, chi1U,chi2U, r):
    q = m2/m1 # It is assumed that q >= 1, so m2 >= m1.
    a = ixp.zerorank1(DIM=10)
    p_t__a_2_thru_a_4v2(m1,m2, chi1U[0],chi1U[1],chi1U[2], chi2U[0],chi2U[1],chi2U[2])
    a[2] = a_2v2
    a[3] = a_3v2
    a[4] = a_4v2
    p_t__a_5_thru_a_6v2(m1,m2, chi1U[0],chi1U[1],chi1U[2], chi2U[0],chi2U[1],chi2U[2])
    a[5] = a_5v2
    a[6] = a_6v2
    p_t__a_7v2(         m1,m2, chi1U[0],chi1U[1],chi1U[2], chi2U[0],chi2U[1],chi2U[2])
    a[7] = a_7v2
    global p_tv2
    p_tv2 = 1 # Term prior to the sum in parentheses
    for k in range(8):
        p_tv2 += a[k]/r**div(k,2)
    p_tv2 *= q / (1+q)**2 * 1/r**div(1,2)
```

# Part 2: Validation against second transcription and corresponding Python module \[Back to [top](#toc)\]
$$\label{code_validation}$$

As a code validation check, we verify agreement between
* the SymPy expressions transcribed from the cited published work on two separate occasions, and
* the SymPy expressions generated in this notebook, and the corresponding Python module.

```python
from NRPyPN_shortcuts import q, num_eval # Import needed input variable & numerical evaluation routine

f_p_t(m1,m2, chi1U,chi2U, q)

def error(varname):
    print("ERROR: When comparing Python module & notebook, "+varname+" was found not to match.")
    sys.exit(1)

# Validation against second transcription of the expressions:
f_p_tv2(m1,m2, chi1U,chi2U, q)
if sp.simplify(p_t - p_tv2) != 0: error("p_tv2")

# Validation against corresponding Python module:
import PN_p_t as pt
pt.f_p_t(m1,m2, chi1U,chi2U, q)

if sp.simplify(p_t - pt.p_t) != 0: error("pt.p_t")
print("ALL TESTS PASS")
```

    ALL TESTS PASS

# Part 3: Validation against trusted numerical values (i.e., in Table V of [Ramos-Buades, Husa, and Pratten (2018)](https://arxiv.org/abs/1810.00036)) \[Back to [top](#toc)\]
$$\label{code_validationv2}$$

```python
# Useful function for comparing published & NRPyPN results
def compare_pub_NPN(desc, pub,NPN,NPN_with_a5_chi1z_sign_error):
    print("##################################################")
    print("  "+desc)
    print("##################################################")
    print(str(pub) + "          <- Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018)")
    print(str(NPN) + " <- Result from NRPyPN")
    relerror = abs(pub-NPN)/pub
    resultstring = "Relative error between NRPyPN & published: "+str(relerror*100)+"%"
    if relerror > 1e-3:
        resultstring += " <--- NOT GOOD! (see explanation below)"
    else:
        resultstring += " <--- EXCELLENT AGREEMENT!"
    print(resultstring+"\n")
    print(str(NPN_with_a5_chi1z_sign_error) + " <- Result from NRPyPN, with chi1z sign error in a_5 expression.")
```

```python
# 1. Let's consider the case:
# * Mass ratio q=1, chi1=chi2=(0,0,0), radial separation r=12
pub_result = 0.850941e-1 # Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018) https://arxiv.org/abs/1810.00036
NPN_with_a5_chi1z_sign_error = 0.0850940927209620 # should be unaffected by sign error, as chi1z=0.
NPN_result = num_eval(p_t,
                      qmassratio =  1.0,  # must be >= 1
                      nr         = 12.0,  # Orbital separation
                      nchi1x     = +0.,
                      nchi1y     = +0.,
                      nchi1z     = +0.,
                      nchi2x     = +0.,
                      nchi2y     = +0.,
                      nchi2z     = +0.)
compare_pub_NPN("Case: q=1, nonspinning, initial separation 12",
                pub_result,NPN_result,NPN_with_a5_chi1z_sign_error)
```

    ##################################################
      Case: q=1, nonspinning, initial separation 12
    ##################################################
    0.0850941          <- Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018)
    0.0850940927209620 <- Result from NRPyPN
    Relative error between NRPyPN & published: 8.55410421798243e-6% <--- EXCELLENT AGREEMENT!

    0.085094092720962 <- Result from NRPyPN, with chi1z sign error in a_5 expression.

```python
# 2. Let's consider the case:
# * Mass ratio q=1.5, chi1= (0,0,-0.6); chi2=(0,0,0.6), radial separation r=10.8
pub_result = 0.868557e-1 # Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018) https://arxiv.org/abs/1810.00036
NPN_with_a5_chi1z_sign_error = 0.0867002374951143
NPN_result = num_eval(p_t,
                      qmassratio =  1.5,  # must be >= 1
                      nr         = 10.8,  # Orbital separation
                      nchi1x     = +0.,
                      nchi1y     = +0.,
                      nchi1z     = -0.6,
                      nchi2x     = +0.,
                      nchi2y     = +0.,
                      nchi2z     = +0.6)
compare_pub_NPN("Case: q=1.5, chi1z=-0.6, chi2z=0.6, initial separation 10.8",
                pub_result,NPN_result,NPN_with_a5_chi1z_sign_error)
```

    ##################################################
      Case: q=1.5, chi1z=-0.6, chi2z=0.6, initial separation 10.8
    ##################################################
    0.0868557          <- Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018)
    0.0868556558764586 <- Result from NRPyPN
    Relative error between NRPyPN & published: 5.08009738035946e-5% <--- EXCELLENT AGREEMENT!

    0.0867002374951143 <- Result from NRPyPN, with chi1z sign error in a_5 expression.

```python
# 3. Let's consider the case:
# * Mass ratio q=4, chi1= (0,0,-0.8); chi2=(0,0,0.8), radial separation r=11
pub_result = 0.559207e-1 # Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018) https://arxiv.org/abs/1810.00036
NPN_with_a5_chi1z_sign_error = 0.0557629777874552
NPN_result = num_eval(p_t,
                      qmassratio =  4.0,  # must be >= 1
                      nr         = 11.0,  # Orbital separation
                      nchi1x     = +0.,
                      nchi1y     = +0.,
                      nchi1z     = -0.8,
                      nchi2x     = +0.,
                      nchi2y     = +0.,
                      nchi2z     = +0.8)
compare_pub_NPN("Case: q=4.0, chi1z=-0.8, chi2z=0.8, initial separation 11.0",
                pub_result,NPN_result,NPN_with_a5_chi1z_sign_error)
print("0.0558369          <- Second iteration value in pub result. Note that NRPyPN value is *closer* to this value.")
```

    ##################################################
      Case: q=4.0, chi1z=-0.8, chi2z=0.8, initial separation 11.0
    ##################################################
    0.0559207          <- Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018)
    0.0558077537453816 <- Result from NRPyPN
    Relative error between NRPyPN & published: 0.201975752482377% <--- NOT GOOD! (see explanation below)

    0.0557629777874552 <- Result from NRPyPN, with chi1z sign error in a_5 expression.
    0.0558369          <- Second iteration value in pub result. Note that NRPyPN value is *closer* to this value.

```python
# 4. Let's consider the case:
# * Mass ratio q=2, chi1= (0,0,0); chi2=(−0.3535, 0.3535, 0.5), radial separation r=10.8
pub_result = 0.7935e-1 # Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018) https://arxiv.org/abs/1810.00036
NPN_with_a5_chi1z_sign_error = 0.0793500403866190 # should be unaffected by sign error, as chi1z=0.
NPN_result = num_eval(p_t,
                      qmassratio =  2.0,  # must be >= 1
                      nr         = 10.8,  # Orbital separation
                      nchi1x     = +0.,
                      nchi1y     = +0.,
                      nchi1z     = +0.,
                      nchi2x     = -0.3535,
                      nchi2y     = +0.3535,
                      nchi2z     = +0.5)
compare_pub_NPN("Case: q=2.0, chi2x=-0.3535, chi2y=+0.3535, chi2z=+0.5, initial separation 10.8",
                pub_result,NPN_result,NPN_with_a5_chi1z_sign_error)
```

    ##################################################
      Case: q=2.0, chi2x=-0.3535, chi2y=+0.3535, chi2z=+0.5, initial separation 10.8
    ##################################################
    0.07935          <- Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018)
    0.0793500403866190 <- Result from NRPyPN
    Relative error between NRPyPN & published: 5.08968103607839e-5% <--- EXCELLENT AGREEMENT!

    0.079350040386619 <- Result from NRPyPN, with chi1z sign error in a_5 expression.

```python
# 5. Let's consider the case:
# * Mass ratio q=8, chi1= (0, 0, 0.5); chi2=(0, 0, 0.5), radial separation r=11
pub_result = 0.345755e-1 # Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018) https://arxiv.org/abs/1810.00036
NPN_with_a5_chi1z_sign_error = 0.0345584951081129 # should be unaffected by sign error, as chi1z=0.
NPN_result = num_eval(p_t,
                      qmassratio =  8.0,  # must be >= 1
                      nr         = 11.0,  # Orbital separation
                      nchi1x     = +0.,
                      nchi1y     = +0.,
                      nchi1z     = +0.5,
                      nchi2x     = +0.,
                      nchi2y     = +0.,
                      nchi2z     = +0.5)
compare_pub_NPN("""
Case: q=8.0, chi1z=chi2z=+0.5, initial separation 11

Note: This one is weird. Clearly the value in the table
      has a typo, such that the p_r and p_t values
      should be interchanged; p_t is about 20% the
      next smallest value in the table, and the
      parameters aren't that different. We therefore
      assume that this is the case, and find agreement
      with the published result to about 0.07%, which
      isn't the best, but given that the table values
      seem to be clearly wrong, it's an encouraging
      sign.
""",pub_result,NPN_result,NPN_with_a5_chi1z_sign_error)
```

    ##################################################

    Case: q=8.0, chi1z=chi2z=+0.5, initial separation 11

    Note: This one is weird. Clearly the value in the table
          has a typo, such that the p_r and p_t values
          should be interchanged; p_t is about 20% the
          next smallest value in the table, and the
          parameters aren't that different. We therefore
          assume that this is the case, and find agreement
          with the published result to about 0.07%, which
          isn't the best, but given that the table values
          seem to be clearly wrong, it's an encouraging
          sign.

    ##################################################
    0.0345755          <- Expected result, from Table V of Ramos-Buades, Husa, and Pratten (2018)
    0.0345503689803291 <- Result from NRPyPN
    Relative error between NRPyPN & published: 0.0726844721578464% <--- EXCELLENT AGREEMENT!

    0.0345584951081129 <- Result from NRPyPN, with chi1z sign error in a_5 expression.

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

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

```python
import os,sys                    # Standard Python modules for multiplatform OS-level functions
import cmdline_helperNRPyPN as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("PN-p_t",location_of_template_file=os.path.join(".."))
```

    Created PN-p_t.tex, and compiled LaTeX file to PDF file PN-p_t.pdf


# このドキュメントの目的

学習パラメータの自動的な探索方法について整理することです。

調整するパラメータは機械学習やディープラーニングのものを対象としています。

構成としては、自動パラメータの調整ライブラリ　hyperopt　の使用法を中心にしてまとめています。

# 参考にした文献

https://qiita.com/kenchin110100/items/ac3edb480d789481f134

https://qiita.com/nazoking@github/items/f67f92dc60001a43b7dc

# hyperopt とは？

Distributed Asynchronous Hyperparameter Optimization in Python

2019/3/30 時点で　970commits, 43contributors, 3200Star, 587Fork のライブラリ

[github] https://github.com/hyperopt/hyperopt

[doccumente] http://hyperopt.github.io/hyperopt/

[公式wiki] https://github.com/hyperopt/hyperopt/wiki/FMin

# インストール

> pip install hyperopt

```python
# 最も単純なhyperoptの使い方

def objective(x):
    return x ** 2

from hyperopt import fmin, tpe, hp
# fmin なので関数を最小化する引数を求める。
# fn=最小化する関数
# space=関数で定義した変数と、それの探索範囲
# algo=探索アルゴリズム
# max_evals=探索の繰り返し回数
best = fmin(
    fn=objective,
    space=hp.uniform('x', -10, 10),
    algo=tpe.suggest,
    max_evals=100
    )

best

```

    100%|██████████| 100/100 [00:00<00:00, 469.86it/s, best loss: 0.003039930524044733]

    {'x': -0.05513556496531738}

```python
# 以下のようにすれば、処理時間も確認することができます。

import pickle
import time
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials

def objective(x):
    return {
        'loss' : x ** 2,
        'status' : STATUS_OK, # STATUS_OK の時だけlossを考慮する？
        # -- store other results like this --
        'eval_time' : time.time(),
        'other_stuff' : {'type' : None, 'value' : [0,1,2]},
        # -- attachments are handled differently
        'attachments' : {'time_module' : pickle.dumps(time.time)}
    }

trials = Trials() #計算中の戻り値を追跡可能にする。
from hyperopt import fmin, tpe, hp
best = fmin(
    fn=objective,
    space=hp.uniform('x', -10, 10),
    algo=tpe.suggest,
    max_evals=3,
    trials=trials
    )

from pprint import pprint as print
print('=== best ===')
print(best)
print('')
print('=== trials.trials ===')
print(trials.trials)
print('')
print('=== trials.losses() ===')
print(trials.losses())
print('')
print('=== trials.statuses() ===')
print(trials.statuses())
print('')
```

    100%|██████████| 3/3 [00:00<00:00, 473.79it/s, best loss: 0.6661991494458527]
    '=== best ==='
    {'x': -0.8162102360579979}
    ''
    '=== trials.trials ==='
    [{'book_time': datetime.datetime(2019, 4, 6, 1, 38, 55, 770000),
      'exp_key': None,
      'misc': {'cmd': ('domain_attachment', 'FMinIter_Domain'),
               'idxs': {'x': [0]},
               'tid': 0,
               'vals': {'x': [-0.8162102360579979]},
               'workdir': None},
      'owner': None,
      'refresh_time': datetime.datetime(2019, 4, 6, 1, 38, 55, 770000),
      'result': {'eval_time': 1554514735.770375,
                 'loss': 0.6661991494458527,
                 'other_stuff': {'type': None, 'value': [0, 1, 2]},
                 'status': 'ok'},
      'spec': None,
      'state': 2,
      'tid': 0,
      'version': 0},
     {'book_time': datetime.datetime(2019, 4, 6, 1, 38, 55, 771000),
      'exp_key': None,
      'misc': {'cmd': ('domain_attachment', 'FMinIter_Domain'),
               'idxs': {'x': [1]},
               'tid': 1,
               'vals': {'x': [1.9971252494478442]},
               'workdir': None},
      'owner': None,
      'refresh_time': datetime.datetime(2019, 4, 6, 1, 38, 55, 772000),
      'result': {'eval_time': 1554514735.77195,
                 'loss': 3.988509261982114,
                 'other_stuff': {'type': None, 'value': [0, 1, 2]},
                 'status': 'ok'},
      'spec': None,
      'state': 2,
      'tid': 1,
      'version': 0},
     {'book_time': datetime.datetime(2019, 4, 6, 1, 38, 55, 773000),
      'exp_key': None,
      'misc': {'cmd': ('domain_attachment', 'FMinIter_Domain'),
               'idxs': {'x': [2]},
               'tid': 2,
               'vals': {'x': [-1.8093289601803857]},
               'workdir': None},
      'owner': None,
      'refresh_time': datetime.datetime(2019, 4, 6, 1, 38, 55, 773000),
      'result': {'eval_time': 1554514735.7733529,
                 'loss': 3.2736712861474357,
                 'other_stuff': {'type': None, 'value': [0, 1, 2]},
                 'status': 'ok'},
      'spec': None,
      'state': 2,
      'tid': 2,
      'version': 0}]
    ''
    '=== trials.losses() ==='
    [0.6661991494458527, 3.988509261982114, 3.2736712861474357]
    ''
    '=== trials.statuses() ==='
    ['ok', 'ok', 'ok']
    ''

```python
# より複雑な探索空間を扱う場合

## define an objective function
def objective(args):
    case, val = args
    if case == 'case 1':
        return val
    else:
        return val ** 2

print( objective(['case 2', 3]) )
```

    9

```python
## 複雑な変数空間を定義する
## define a search space of hyperparameter
from hyperopt import hp
space = hp.choice('a',
                 [
                   ('case 1', 1+hp.lognormal('c1',     0,   1)),
                   ('case 2',          hp.uniform('c2', -10, 10))
                 ])

print('もし上記で定義した変数空間を確認したいなら、以下でサンプリング結果を確認することができます')
import hyperopt.pyll.stochastic
for _ in range(10):
    print(hyperopt.pyll.stochastic.sample(space))

print('--- 次に、探索を行います ---')

# minimize the objective over the space
from hyperopt import fmin, tpe, space_eval
best = fmin(objective, space, algo=tpe.suggest, max_evals=100)

print('=== best ===')
print( best )

print('=== space_eval ===')
print( space_eval(space, best) )

'''注釈：
'a', 'c1', 'c2' は任意に指定できるラベルで、fminが内部的な命名に利用しています。
上記の場合、'a'はケース名として機能し、以下のような意味合いを持ちます。
if a == 0:
    args = ('case 1', 1+hp.lognormal('c1',     0,   1))
elif a == 1:
    args = ('case 2',          hp.uniform('c2', -10, 10))
'''
print('')

```

    'もし上記で定義した変数空間を確認したいなら、以下でサンプリング結果を確認することができます'
    ('case 1', 5.2531236943393465)
    ('case 2', -1.1447309545664304)
    ('case 1', 1.671139091906336)
    ('case 2', -8.76062390252624)
    ('case 1', 1.113744767831003)
    ('case 2', 3.7503944297243876)
    ('case 2', 8.72919330433156)
    ('case 1', 1.7095182325330534)
    ('case 2', 1.3603761669055636)
    ('case 2', -4.894127500903174)
    '--- 次に、探索を行います ---'
    100%|██████████| 100/100 [00:00<00:00, 246.51it/s, best loss: 0.01602711992416122]
    '=== best ==='
    {'a': 1, 'c2': -0.12659826193183388}
    '=== space_eval ==='
    ('case 2', -0.12659826193183388)
    ''

# 2.1パラメータ式

hyperoptの最適化アルゴリズムに用いられる変数スペースの定義は、以下のようなものがあります。

## hp.choice(label, options)

オプションの1つを返します。リストまたはタプルでなければなりません。 optionsの要素は、それ自体が確率的な式で [入れ子] にすることができます。この場合、いくつかのオプションにしか現れない確率的選択が 条件 パラメータになる。

## hp.randint(label, upper)

[0, upper] の範囲の乱数を返します。この分布のセマンティクスは、より遠い整数値と比較して、近くの整数値間の損失関数に相関がないことです。これは、例えばランダムシードを記述するための適切な分布です。損失関数がおそらく近くの整数値に相関する場合は、quniform 、 qloguniform 、 qnormal または qlognormal のいずれかのような「量子化」連続分布の1つを使用するべきです。

## hp.uniform(label, low, high)

low と high の間で均等に値を返します。
最適化するとき、この変数は両側の間隔に制約されます。

## hp.quniform(label, low, high, q)

round(uniform(low, high) / q) * q のような値を返します。
目的が幾分「滑らか」であるが、上と下の両方に拘束されるべき離散値に適している。

## hp.loguniform(label, low, high)

exp(uniform(low, high)) のような対数に一様に分布するように返します。
最適化すると、この変数は区間 [exp(low), exp(high)] に制約されます。

## hp.qloguniform(label, low, high, q)

round(exp(uniform(low, high)) / q) * q のような値を返します。
目的が「滑らか」で、値の大きさがより滑らかになるが、上と下の両方に限定される離散変数に適しています。

## hp.normal(label, mu, sigma)

平均ミューと標準偏差 sigma で正規分布している実際の値を返します。最適化する場合、これは制約のない変数です。

## hp.qnormal(label, mu, sigma, q)

round(normal(mu, sigma) / q) * q のような値を返します。
おそらく mu 付近の値をとるが、根本的に無制限の離散変数に適しています。

## hp.lognormal(label, mu, sigma)

戻り値の対数が正規分布するように exp(normal(mu, sigma)) に従って描画された値を返します。最適化すると、この変数は正の値に制限されます。

## hp.qlognormal(label, mu, sigma, q)

round(exp(normal(mu, sigma)) / q) * q のような値を返します。
目的が滑らかで、一方の側から境界を定められている変数のサイズによってスムーズになる離散変数に適しています。

# 次に sklearn と組み合わせて hyper parameter の調整を行う。

参考にしたURLは、　https://blog.amedama.jp/entry/hyperopt　です。

余談ですが、英語で検索するよりも日本語で検索したほうが、良い記事が多いのはなぜだろうか。

母国語である日本語での検索能力が高いからなのか、日本語の記事が優秀だからだろうか。

```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial

from hyperopt import hp
from hyperopt import fmin
from hyperopt import tpe
from hyperopt import Trials
from hyperopt import space_eval

from sklearn import datasets
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import cross_validate

from sklearn.svm import SVC

def objective(X, y, args):
    """最小化したい目的関数"""
    # モデルのアルゴリズムに SVM を使う
    model = SVC(**args)
    # Stratified 5 Fold Cross Validation
    kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    scores = cross_validate(model, X=X, y=y, cv=kf)
    # 最小化なので符号を反転する
    return -1 * scores['test_score'].mean()

def main():
    # データセットを読み込む
    dataset = datasets.load_iris()
    X, y = dataset.data, dataset.target
    # 目的関数にデータセットを部分適用する
    f = partial(objective, X, y)
    # 変数の値域を定義する
    space = {
        # 底が自然対数なので 2.303 をかけて常用対数に変換する
        'C': hp.loguniform('C', 2.303 * 0, 2.303 * +2),
        'gamma': hp.loguniform('gamma', 2.303 * -2, 2.303 * +1),
        'kernel': hp.choice('kernel', ['linear', 'rbf', 'poly']),
    }
    # 探索過程を記録するオブジェクト
    trials = Trials()
    # 目的関数を最小化するパラメータを探索する
    best = fmin(fn=f, space=space, algo=tpe.suggest, max_evals=100, trials=trials)

    print('--- 結果を出力する ---')
    print(space_eval(space, best))

if __name__ == '__main__':
    main()

```

    100%|██████████| 100/100 [00:03<00:00, 25.86it/s, best loss: -0.9933333333333334]
    '--- 結果を出力する ---'
    {'C': 3.7854270724091728, 'gamma': 0.061837754861085684, 'kernel': 'rbf'}

# 次に最近、論文でも見かけるようになった、Train, Validation, Test の3分割データセットをつかったCrossValidationを実装してみます。

```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial

from hyperopt import hp
from hyperopt import fmin
from hyperopt import tpe
from hyperopt import Trials
from hyperopt import space_eval

from sklearn import datasets
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import cross_validate

from sklearn.svm import SVC

import random
import numpy as np

def split_train_validation_test(X, y, tra_val_tes_rate=[0.6, 0.2, 0.2]):
    # train, validation, test に三分割する
    random_index = list(range(len(y)))
    random.shuffle(random_index)
    tvt_sep = [int(rate*len(y)) for rate in tra_val_tes_rate]
    tvt_sep = [sum(tvt_sep[:i+1]) for i in range(len(tvt_sep))]
    tvt_index = random_index[0:tvt_sep[0]], random_index[tvt_sep[0]:tvt_sep[1]], random_index[tvt_sep[1]:tvt_sep[2]]
    train_X, train_y = X[tvt_index[0]], y[tvt_index[0]]
    validation_X, validation_y = X[tvt_index[1]], y[tvt_index[1]]
    test_X, test_y  = X[tvt_index[2]], y[tvt_index[2]]
    return train_X, train_y, validation_X, validation_y, test_X, test_y

def objective(train_X, train_y, val_X, val_y, args):
    """最小化したい目的関数"""
    # モデルのアルゴリズムに SVM を使う
    model = SVC(**args)
    model.fit(train_X, train_y)
    predict = model.predict(val_X)
    errors = predict - val_y
    # 最小化なので符号を反転する
    return np.abs(errors).mean()

# データセットを読み込む
dataset = datasets.load_iris()
X, y = dataset.data, dataset.target

train_X, train_y, validation_X, validation_y, test_X, test_y = split_train_validation_test(X, y)

# 目的関数にデータセットを適応する
f = partial(objective, train_X, train_y, validation_X, validation_y)
# 変数の値域を定義する
space = {
    # 底が自然対数なので 2.303 をかけて常用対数に変換する
    'C': hp.loguniform('C', 2.303 * 0, 2.303 * +2),
    'gamma': hp.loguniform('gamma', 2.303 * -2, 2.303 * +1),
    'kernel': hp.choice('kernel', ['linear', 'rbf', 'poly']),
}
# 探索過程を記録するオブジェクト
trials = Trials()
# 目的関数を最小化するパラメータを探索する
best = fmin(fn=f, space=space, algo=tpe.suggest, max_evals=100, trials=trials)

print('--- 最適なハイパーパラメータを確認する ---')
print(space_eval(space, best))
print('--- テストスコアを計算する ---')
model = SVC(**space_eval(space, best))
model.fit(train_X, train_y)
predict = model.predict(test_X)
errors = predict - test_y
print(np.abs(errors).mean())
```

# 変数探索に使われるalgoの種類について

公式ドキュメント （https://hyperopt.github.io/hyperopt/）　によれば、現在(2019/3/30)実装されている探索アルゴリズムは下記の２つのみ。

1. Random Search
2. Tree of Parzen Estimators (TPE)

Gaussian processesやregression treeをに基づいた混合ベイズ最適化モデル？（accommodate Bayesian optimization algorithms）というものがある。これに基づいた実装としてhyperopt は開発されたが、まだ実装されてはいない。

Random Search についてはここでは言及しない。なぜなら、簡単だと思うから。

## パラメータの自動調整技術の変遷

[NeuPy] によると、パラメータの自動調整は、SMBO　-> gaussian process -> TPE となっているそうです。

正確には、以下のような変遷のようです。

1. SMBOという計算手続きが提案される。
2. SMBOに、gaussian process を適応する手法が提案される。
3. SMBOに、TPE(Tree-structured Parzen Estimator)を適応する手法が提案される。

SMBO-TPEは2011年にNIPSで発表された論文(https://www.lri.fr/~kegl/research/PDFs/BeBaBeKe11.pdf)です。

上記の論文は、[このスライド]でとても丁寧に解説してくれています。

[NeuPy] http://neupy.com/2016/12/17/hyperparameter_optimization_for_neural_networks.html

[このスライド] https://www.slideshare.net/hskksk/hyperopt

## ベースになっているSMBO（Sequential Model-based Global Optimization）とは？

以下のサイトの解説がわかりやすかったです。

↓↓↓

https://qiita.com/kenchin110100/items/ac3edb480d789481f134

画像引用：

上記の引用にある通り、重要なのは関数Mと関数Sです。

関数Mは、検証済みのパラメータとそのテスト精度を学習して、入力に未検証パラメータをとり、予測テスト精度を出力します。関数Mにgausian processやTPEを適応することになります。

関数Sは、関数Mを用いて次のステップで検証すべきパラメータを選出する手続きを定義します。上述した[このスライド]では Expected improvement としてその手続きについて詳しく説明されています。

## NeuPy の記事を読んでみよう。

NeuPy は Neural Networks のパラメータ最適化モジュールです。自動調整の技術の変遷と概要をまとめた記事があったので、簡単に和訳していきます。

詳細については、上記の[NeuPy]のURLを参照してください。

### 以下、和訳で要約

パラメータの自動調整の種類を、以下に列挙します。

1. Grid Search
2. Random Search
3. Hund-tuning
4. Gaussian Process with Expected Improvement
5. Tree-Structured Parzen Estimators (TPE)

ここでは、4.　と 5. について該当する記事を和訳しておきます。

#### Gaussian Process with Expected Improvement

(記事の場所：http://neupy.com/2016/12/17/hyperparameter_optimization_for_neural_networks.html#bayesian-optimization)

* Bayesian Optimization

Bayesian Optimization　は　派生のある？（derivative-free） の最適化法です。いくつかの最適化タイプがありますが、ここでは　Acquisition Function を使った Gaussian Process に注目します。前述で紹介した　Hand-tuning での説明と似ていると思われるかもしれません。　Gaussian Process は前回のパラメータと精度の結果を使い、次の未観測パラメータを仮説します。Axquisition Function はこれらの情報を使い、次のパラメータセットを提案します。

* Gaussian Process

(内田コメント)　Gaussian Process の一般的理解がないと、以下を読み解くのは難しかった。これ以降でもsklearnのGaussian Processモジュールを応用した実装を行うので、下記をあらかじめ読んでおくべき。

[sklearnの解説記事]

https://scikit-learn.org/stable/modules/gaussian_process.html

[sklearnの実装レファレンス]

https://scikit-learn.org/stable/modules/generated/sklearn.gaussian_process.GaussianProcessRegressor.html#sklearn.gaussian_process.GaussianProcessRegressor

[内田ではよく分からなかったけど、数理的に説明している日本語記事]

https://qiita.com/kilometer/items/6ab1d7f2cffc0c6e4a95

[頭のいい人が「分かりやすい」と言っていたスライド資料。その１]

https://www.ism.ac.jp/~daichi/lectures/H26-GaussianProcess/gp-lecture1-matsui.pdf

[頭のいい人が「分かりやすい」と言っていたスライド資料。その２]　　←　オススメ！

https://www.ism.ac.jp/~daichi/lectures/H26-GaussianProcess/gp-lecture2-daichi.pdf

Gaussian Process の背景には、インプット x は　アウトプット　y　= f(x) を持ち、f は確率関数である、という考えがあります。この確率関数はアウトプットをgaussian分布からサンプリングします。また、インプット　x はgaussian分布に帰属しているといえます。つまり、インプットxを生成されるgaussian　proccessは、いくつかのgaussian分布の平均μと標準偏差σを定義します。

Gaussian Process は　Multivariate Gaussian Distribution(MGD) に基づいた生成です。MGDは平均ベクトルと共分散ベクトルで定義されます。一方で、Gaussian Process は平均関数(mean function)と共分散関数（covariance function）で定義されます。基本的には、関数は無限ベクトルです（取り得る値が無限で、ベクトルで返却する関数ということ？）。また、多変量ガウス分布は離散的な数の可能な入力を持つ関数に対するガウス過程であると言えます。

例えば、二次元のパラメータベクトル　x　を多変量ガウス分布で生成する場合、以下のようなサンプリングとなります。

これを違う可視化にすると、下記のようになります。結ばれた線は同時にサンプリングされたことを示しています。このグラフであれば、パラメータベクトルの次元を増やしても表記することができます。

パラメータベクトルが5次元で、十分な回数サンプリングすれば下記のようになります。

つまり、最適なパラメータベクトルをサンプリングするためには、この多変量ガウス分布の平均ベクトルと共分散行列をなんらかの学習過程で推定し、収束させれば良いのです。

例えば、３層のニューラルネットワークの中間hidden層のニューロン数をパラメータであるとしましょう。
ニューロン数を2回サンプリングして、それぞれの精度を観測します。
すると下図のような、パラメータ（X軸）と精度（Y軸）の予測が更新されます。
後は、iterationを重ねて、サンプリングすべき範囲を狭めていきます。
つまり、下記のようになります。

しかし、前述のGausian Processでは分散を設定しているので、上図のように観測点が一点にはなりません。その精度を決める予測値は決定関数から得られたものであり、その決定関数はほとんどのneural networkにとって正しくないのです(注釈1)。観測されるノイズを決定しているGausian Processの分散パラメータを変更することで、これを修正する。この工夫はそれほど確かではない予測だけでなく、観測データ点を通過しない隠れユニット数の平均を与えます。

[注釈1] この意味が分かる人がいればフォローをお願いします。内田の解釈では、パラメータが決まった状態で学習した結果を、決定関数（a deterministic function）と表現しているのだと思います。決定関数がテストセットの変数から予測値を出力します。そして、予測値とテストセットの正解値から誤差を計算して精度指標（Accuracy）が計算されます。よって、精度指標はパラメータが固定されていても、テストセットと学習結果の重みが確率的に変動すれば分散します。テストセットは一般的に乱数抽出されますし、重みも学習過程に乱数要素を含みます。よって、観測点においても精度は一意に決まらず分散します。

* Acquisition Function

Acquisition Function は次のステップのパラメータセットを決定します。その種類は複数あります。その内の代表的なものが、Expected Improvement です。もし、最小化を目的として次のパラメータセットを探索する場合は、次の数式を用います。

\begin{align}
g_{min}(x)  = max(0, y_{min} - y_{lowest\ expected}) \\
\end{align}

\begin{align}
y_{min} : 観測されたyの最小値
\end{align}
\begin{align}
y_{lowest\ expected} : 各xに関連づけられた信頼区間内の最小値。
\end{align}

今回の隠れ層のユニット数を探索する場合は、Accuracyの最大化問題なので、以下を用います。

\begin{align}
g_{max}(x)  = max(0, y_{highest\ expected} - y_{max}) \\
\end{align}

このExpected Improvement function に従えば、各ステップの次のxは下記のように選出されます。

* Find number of hidden units

それでは、Gaussian Process regression　と　Expected Improvement　function に基づいたハイパーパラメータの探索を実装してみます。ひきつづき、最適な隠れユニット数を探索する問題にとりくみます。おなじみの手書き数値の判別問題です。

まずは、学習、ニューラルネットワーク、予測誤差を関数で定義しましょう。

```python
# numpy への依存部分にエラーがあり、importできなかった。
# とはいえ、コードに目を通すだけでプロセスを理解することはできるので問題ないはず。
from neupy import algorithms, layers

################################
# ニューラルネットワークの定義とその学習。戻り値はテストセットの精度スコアになっている。
def train_network(n_hidden, x_train, x_test, y_train, y_test):
    network = algorithms.Momentum(
        [
            layers.Input(64),
            layers.Relu(n_hidden),
            layers.Softmax(10),
        ],

        # Randomly shuffle dataset before each
        # training epoch.
        shuffle_data=True,

        # Do not show training progress in output
        verbose=False,

        step=0.001,
        batch_size=128,
        loss='categorical_crossentropy',
    )
    network.train(x_train, y_train, epochs=100)

    # Calculates categorical cross-entropy error between
    # predicted value for x_test and y_test value
    return network.score(x_test, y_test)

################################
# 次に手書きデータセットをscikit-learnで読み込む。
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
from neupy import utils

utils.reproducible()

dataset = datasets.load_digits()
n_samples = dataset.target.size
n_classes = 10

# One-hot encoder
target = np.zeros((n_samples, n_classes))
target[np.arange(n_samples), dataset.target] = 1

x_train, x_test, y_train, y_test = train_test_split(
    dataset.data, target, test_size=0.3
)

################################
# パラメータの選択プロセスを定義する。
# はじめに、Gaussian Process regression を実行し特定のベクトル入力に対して予測の平均と標準偏差を返却する関数を定義します。

import numpy as np
from sklearn.gaussian_process import GaussianProcess

def vector_2d(array):
    return np.array(array).reshape((-1, 1))

def gaussian_process(x_train, y_train, x_test):
    '''
    上記の引数名だと、まるで手書きデータセットを受け取ると誤解してしまう。
    実際に引数にとるのは、下記のデータである。

    x_train = 検証済みのパラメータが各ステップごとに格納されている
    y_train = 検証結果の精度スコアが各ステップごとに格納されている
    x_test　= 次のステップで選択されるパラメータの候補が格納されている
    '''
    x_train = vector_2d(x_train)
    y_train = vector_2d(y_train)
    x_test = vector_2d(x_test)

    # Train gaussian process
    gp = GaussianProcess(corr='squared_exponential',
                         theta0=1e-1, thetaL=1e-3, thetaU=1)
    gp.fit(x_train, y_train)

    # Get mean and standard deviation for each possible
    # number of hidden units
    # 取り得る隠れユニット数ごとの平均と標準偏差を取得します。
    y_mean, y_var = gp.predict(x_test, eval_MSE=True)
    y_std = np.sqrt(vector_2d(y_var))

    return y_mean, y_std

####################################
#  予測結果を　Expected Improvement(EI) に適応し、次の最適ステップを探索します。

def next_parameter_by_ei(y_min, y_mean, y_std, x_choices):
    # Calculate expecte improvement from 95% confidence interval
    expected_improvement = y_min - (y_mean - 1.96 * y_std)
    expected_improvement[expected_improvement < 0] = 0

    max_index = expected_improvement.argmax()
    # Select next choice
    next_parameter = x_choices[max_index]

    return next_parameter

####################################
# 最後に、上記で定義したプロシージャを一つの関数にまとめます。

import random

def hyperparam_selection(func, n_hidden_range, func_args=None, n_iter=20):
    ''' 使用例
    best_n_hidden = hyperparam_selection(
        train_network,　# 前述で定義した関数
        n_hidden_range=[50, 1000],
        func_args=[x_train, x_test, y_train, y_test],
        n_iter=6,)
    '''
    if func_args is None:
        func_args = []

    scores = []
    parameters = []

    min_n_hidden, max_n_hidden = n_hidden_range
    n_hidden_choices = np.arange(min_n_hidden, max_n_hidden + 1)

    # To be able to perform gaussian process we need to
    # have at least 2 samples.
    # gaussian processをとりあえず稼働するためのn_hiddenを乱数抽出します。
    n_hidden = random.randint(min_n_hidden, max_n_hidden)
    score = func(n_hidden, *func_args)

    # parameters に検証したn_hiddenを追加。そして、その精度スコアをscoresに追加。
    parameters.append(n_hidden)
    scores.append(score)

    n_hidden = random.randint(min_n_hidden, max_n_hidden)

    for iteration in range(2, n_iter + 1):
        score = func(n_hidden, *func_args)

        parameters.append(n_hidden)
        scores.append(score)

        # 検証済みのparameters と score に、gaussian_processを実行して次のステップで検証すべきn_hiddenを選ぶ。
        y_min = min(scores)
        y_mean, y_std = gaussian_process(parameters, scores,
                                         n_hidden_choices)

        n_hidden = next_parameter_by_ei(y_min, y_mean, y_std,
                                        n_hidden_choices)

        if y_min == 0 or n_hidden in parameters:
            # Lowest expected improvement value have been achieved
            break

    # 結果、精度スコアが最小値となったパラメータを返却する
    min_score_index = np.argmin(scores)
    return parameters[min_score_index]

```

上記のプログラムで定義された手続きのイメージは、以下のようなものである。

# TPE を利用した SMBO について

これを理解するためには、以下のドキュメントを順番に読んで行くことで何とか理解できました。

１、https://www.slideshare.net/hskksk/hyperopt

２、https://qiita.com/nabenabe0928/items/708d221dbccebf31f01c

「これを読んでおけば、わざわざここにまとめる必要なかったんじゃね？」と思ってしまうくらい良くまとめています。


# Tutorial 3: "Why" models
**Week 1, Day 1: Model Types**

**By Neuromatch Academy**

__Content creators:__ Matt Laporte, Byron Galbraith, Konrad Kording

__Content reviewers:__ Dalin Guo, Aishwarya Balwani, Madineh Sarvestani, Maryam Vaziri-Pashkam, Michael Waskom

We would like to acknowledge [Steinmetz _et al._ (2019)](https://www.nature.com/articles/s41586-019-1787-x) for sharing their data, a subset of which is used here.

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

___
# Tutorial Objectives
This is tutorial 3 of a 3-part series on different flavors of models used to understand neural data. In parts 1 and 2 we explored mechanisms that would produce the data. In this tutorial we will explore models and techniques that can potentially explain *why* the spiking data we have observed is produced the way it is.

To understand why different spiking behaviors may be beneficial, we will learn about the concept of entropy. Specifically, we will:

- Write code to compute formula for entropy, a measure of information
- Compute the entropy of a number of toy distributions
- Compute the entropy of spiking activity from the Steinmetz dataset

```python
# @title Video 1: “Why” models
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="OOIDEr1e5Gg", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

# Setup

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

```python
#@title Figure Settings
import ipywidgets as widgets #interactive display

%matplotlib inline
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
#@title Helper Functions

def plot_pmf(pmf,isi_range):
  """Plot the probability mass function."""
  ymax = max(0.2, 1.05 * np.max(pmf))
  pmf_ = np.insert(pmf, 0, pmf[0])
  plt.plot(bins, pmf_, drawstyle="steps")
  plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
  plt.title(f"Neuron {neuron_idx}")
  plt.xlabel("Inter-spike interval (s)")
  plt.ylabel("Probability mass")
  plt.xlim(isi_range);
  plt.ylim([0, ymax])
```

```python
#@title Download Data
import io
import requests
r = requests.get('https://osf.io/sy5xt/download')
if r.status_code != 200:
  print('Could not download data')
else:
  steinmetz_spikes = np.load(io.BytesIO(r.content), allow_pickle=True)['spike_times']
```

# Section 1: Optimization and Information

Neurons can only fire so often in a fixed period of time, as the act of emitting a spike consumes energy that is depleted and must eventually be replenished. To communicate effectively for downstream computation, the neuron would need to make good use of its limited spiking capability. This becomes an optimization problem:

What is the optimal way for a neuron to fire in order to maximize its ability to communicate information?

In order to explore this question, we first need to have a quantifiable measure for information. Shannon introduced the concept of entropy to do just that, and defined it as

\begin{align}
  H_b(X) &= -\sum_{x\in X} p(x) \log_b p(x)
\end{align}

where $H$ is entropy measured in units of base $b$ and $p(x)$ is the probability of observing the event $x$ from the set of all possible events in $X$. See the Appendix for a more detailed look at how this equation was derived.

The most common base of measuring entropy is $b=2$, so we often talk about *bits* of information, though other bases are used as well e.g. when $b=e$ we call the units *nats*.

First, let's explore how entropy changes between some simple discrete probability distributions. In the rest of this tutorial we will refer to these as probability mass functions (PMF), where $p(x_i)$ equals the $i^{th}$ value in an array, and mass refers to how much of the distribution is contained at that value.

For our first PMF, we will choose one where all of the probability mass is located in the middle of the distribution.

```python
n_bins = 50  # number of points supporting the distribution
x_range = (0, 1)  # will be subdivided evenly into bins corresponding to points

bins = np.linspace(*x_range, n_bins + 1)  # bin edges

pmf = np.zeros(n_bins)
pmf[len(pmf) // 2] = 1.0  # middle point has all the mass

# Since we already have a PMF, rather than un-binned samples, `plt.hist` is not
# suitable. Instead, we directly plot the PMF as a step function to visualize
# the histogram:
pmf_ = np.insert(pmf, 0, pmf[0])  # this is necessary to align plot steps with bin edges
plt.plot(bins, pmf_, drawstyle="steps")
# `fill_between` provides area shading
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

If we were to draw a sample from this distribution, we know exactly what we would get every time. Distributions where all the mass is concentrated on a single event are known as *deterministic*.

How much entropy is contained in a deterministic distribution?

## Exercise 1: Computing Entropy

Your first exercise is to implement a method that computes the entropy of a discrete probability distribution, given its mass function. Remember that we are interested in entropy in units of _bits_, so be sure to use the correct log function.

Recall that $\log(0)$ is undefined. When evaluated at $0$, NumPy log functions (such as `np.log2`) return `np.nan` ("Not a Number"). By convention, these undefined terms— which correspond to points in the distribution with zero mass—are excluded from the sum that computes the entropy.

```python
def entropy(pmf):
  """Given a discrete distribution, return the Shannon entropy in bits.

  This is a measure of information in the distribution. For a totally
  deterministic distribution, where samples are always found in the same bin,
  then samples from the distribution give no more information and the entropy
  is 0.

  For now this assumes `pmf` arrives as a well-formed distribution (that is,
  `np.sum(pmf)==1` and `not np.any(pmf < 0)`)

  Args:
    pmf (np.ndarray): The probability mass function for a discrete distribution
      represented as an array of probabilities.
  Returns:
    h (number): The entropy of the distribution in `pmf`.

  """
  ############################################################################
  # Exercise for students: compute the entropy of the provided PMF
  #   1. Exclude the points in the distribution with no mass (where `pmf==0`).
  #      Hint: this is equivalent to including only the points with `pmf>0`.
  #   2. Implement the equation for Shannon entropy (in bits).
  #  When ready to test, comment or remove the next line
  raise NotImplementedError("Excercise: implement the equation for entropy")
  ############################################################################

  # reduce to non-zero entries to avoid an error from log2(0)
  pmf = ...

  # implement the equation for Shannon entropy (in bits)
  h = ...

  # return the absolute value (avoids getting a -0 result)
  return np.abs(h)

# Uncomment to test your entropy function
# print(f"{entropy(pmf):.2f} bits")
```

```python
# to_remove solution
def entropy(pmf):
  """Given a discrete distribution, return the Shannon entropy in bits.

  This is a measure of information in the distribution. For a totally
  deterministic distribution, where samples are always found in the same bin,
  then samples from the distribution give no more information and the entropy
  is 0.

  For now this assumes `pmf` arrives as a well-formed distribution (that is,
  `np.sum(pmf)==1` and `not np.any(pmf < 0)`)

  Args:
    pmf (np.ndarray): The probability mass function for a discrete distribution
      represented as an array of probabilities.
  Returns:
    h (number): The entropy of the distribution in `pmf`.
  """
  # reduce to non-zero entries to avoid an error from log2(0)
  pmf = pmf[pmf > 0]

  # implement the equation for Shannon entropy (in bits)
  h = -np.sum(pmf * np.log2(pmf))

  # return the absolute value (avoids getting a -0 result)
  return np.abs(h)

print(f"{entropy(pmf):.2f} bits")
```

We expect zero surprise from a deterministic distribution. If we had done this calculation by hand, it would simply be

$-1\log_2 1 = -0=0$

Note that changing the location of the peak (i.e. the point and bin on which all the mass rests) doesn't alter the entropy. The entropy is about how predictable a sample is with respect to a distribution. A single peak is deterministic regardless of which point it sits on.

```python
pmf = np.zeros(n_bins)
pmf[2] = 1.0  # arbitrary point has all the mass

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

What about a distribution with mass split equally between two points?

```python
pmf = np.zeros(n_bins)
pmf[len(pmf) // 3] = 0.5
pmf[2 * len(pmf) // 3] = 0.5

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

Here, the entropy calculation is

$-(0.5 \log_2 0.5 + 0.5\log_2 0.5)=1$

There is 1 bit of entropy. This means that before we take a random sample, there is 1 bit of uncertainty about which point in the distribution the sample will fall on: it will either be the first peak or the second one.

Likewise, if we make one of the peaks taller (i.e. its point holds more of the probability mass) and the other one shorter, the entropy will decrease because of the increased certainty that the sample will fall on one point and not the other:

$-(0.2 \log_2 0.2 + 0.8\log_2 0.8)\approx 0.72$

Try changing the definition of the number and weighting of peaks, and see how the entropy varies.

If we split the probability mass among even more points, the entropy continues to increase. Let's derive the general form for $N$ points of equal mass, where $p_i=p=1/N$:

\begin{align}
 -\sum_i p_i \log_b p_i&= -\sum_i^N \frac{1}{N} \log_b \frac{1}{N}\\
                       &= -\log_b \frac{1}{N} \\
                       &= \log_b N
\end{align}
$$$$

If we have $N$ discrete points, the _uniform distribution_ (where all points have equal mass) is the distribution with the highest entropy: $\log_b N$. This upper bound on entropy is useful when considering binning strategies, as any estimate of entropy over $N$ discrete points (or bins) must be in the interval $[0, \log_b N]$.

```python
pmf = np.ones(n_bins) / n_bins  # [1/N] * N

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range);
plt.ylim(0, 1);
```

Here, there are 50 points and the entropy of the uniform distribution is $\log_2 50\approx 5.64$. If we construct _any_ discrete distribution $X$ over 50 points (or bins) and calculate an entropy of $H_2(X)>\log_2 50$, something must be wrong with our implementation of the discrete entropy computation.

# Section 2: Information, neurons, and spikes

```python
# @title Video 2: Entropy of different distributions
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="o6nyrx3KH20", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

Recall the discussion of spike times and inter-spike intervals (ISIs) from Tutorial 1. What does the information content (or distributional entropy) of these measures say about our theory of nervous systems?

We'll consider three hypothetical neurons that all have the same mean ISI, but with different distributions:

1. Deterministic
2. Uniform
3. Exponential

Fixing the mean of the ISI distribution is equivalent to fixing its inverse: the neuron's mean firing rate. If a neuron has a fixed energy budget and each of its spikes has the same energy cost, then by fixing the mean firing rate, we are normalizing for energy expenditure. This provides a basis for comparing the entropy of different ISI distributions. In other words: if our neuron has a fixed budget, what ISI distribution should it express (all else being equal) to maximize the information content of its outputs?

Let's construct our three distributions and see how their entropies differ.

```python
n_bins = 50
mean_isi = 0.025
isi_range = (0, 0.25)

bins = np.linspace(*isi_range, n_bins + 1)
mean_idx = np.searchsorted(bins, mean_isi)

# 1. all mass concentrated on the ISI mean
pmf_single = np.zeros(n_bins)
pmf_single[mean_idx] = 1.0

# 2. mass uniformly distributed about the ISI mean
pmf_uniform = np.zeros(n_bins)
pmf_uniform[0:2*mean_idx] = 1 / (2 * mean_idx)

# 3. mass exponentially distributed about the ISI mean
pmf_exp = stats.expon.pdf(bins[1:], scale=mean_isi)
pmf_exp /= np.sum(pmf_exp)
```

```python
#@title
#@markdown Run this cell to plot the three PMFs
fig, axes = plt.subplots(ncols=3, figsize=(18, 5))

dists =  [# (subplot title, pmf, ylim)
          ("(1) Deterministic", pmf_single, (0, 1.05)),
          ("(1) Uniform", pmf_uniform, (0, 1.05)),
          ("(1) Exponential", pmf_exp, (0, 1.05))]

for ax, (label, pmf_, ylim) in zip(axes, dists):
  pmf_ = np.insert(pmf_, 0, pmf_[0])
  ax.plot(bins, pmf_, drawstyle="steps")
  ax.fill_between(bins, pmf_, step="pre", alpha=0.4)
  ax.set_title(label)
  ax.set_xlabel("Inter-spike interval (s)")
  ax.set_ylabel("Probability mass")
  ax.set_xlim(isi_range);
  ax.set_ylim(ylim);
```

```python
print(
  f"Deterministic: {entropy(pmf_single):.2f} bits",
  f"Uniform: {entropy(pmf_uniform):.2f} bits",
  f"Exponential: {entropy(pmf_exp):.2f} bits",
  sep="\n",
)
```

```python
# @title Video 3: Probabilities from histogram
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="e2U_-07O9jo", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

In the previous example we created the PMFs by hand to illustrate idealized scenarios. How would we compute them from data recorded from actual neurons?

One way is to convert the ISI histograms we've previously computed into discrete probability distributions using the following equation:

\begin{align}
p_i = \frac{n_i}{\sum\nolimits_{i}n_i}
\end{align}

where $p_i$ is the probability of an ISI falling within a particular interval $i$ and $n_i$ is the count of how many ISIs were observed in that interval.

### Exercise 2: Probabilty Mass Function

Your second exercise is to implement a method that will produce a probability mass function from an array of ISI bin counts.

To verify your solution, we will compute the probability distribution of ISIs from real neural data taken from the Steinmetz dataset.

```python
neuron_idx = 283

isi = np.diff(steinmetz_spikes[neuron_idx])
bins = np.linspace(*isi_range, n_bins + 1)
counts, _ = np.histogram(isi, bins)
```

```python
def pmf_from_counts(counts):
  """Given counts, normalize by the total to estimate probabilities."""
  ###########################################################################
  # Exercise: Compute the PMF. Remove the next line to test your function
  raise NotImplementedError("Student excercise: compute the PMF from ISI counts")
  ###########################################################################

  pmf = ...

  return pmf

# Uncomment when ready to test your function
# pmf = pmf_from_counts(counts)
# plot_pmf(pmf,isi_range)
```

```python
# to_remove solution
def pmf_from_counts(counts):
  """Given counts, normalize by the total to estimate probabilities."""
  pmf = counts / np.sum(counts)
  return pmf

pmf = pmf_from_counts(counts)
with plt.xkcd():
  plot_pmf(pmf,isi_range)
```

# Section 3: Calculate entropy from a PMF

```python
# @title Video 4: Calculating entropy from pmf
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="Xjy-jj-6Oz0", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

Now that we have the probability distribution for the actual neuron spiking activity, we can calculate its entropy.

```python
print(f"Entropy for Neuron {neuron_idx}: {entropy(pmf):.2f} bits")
```

## Interactive Demo: Entropy of neurons

We can combine the above distribution plot and entropy calculation with an interactive widget to explore how the different neurons in the dataset vary in spiking activity and relative information. Note that the mean firing rate across neurons is not fixed, so some neurons with a uniform ISI distribution may have higher entropy than neurons with a more exponential distribution.

```python
#@title
#@markdown **Run the cell** to enable the sliders.

def _pmf_from_counts(counts):
  """Given counts, normalize by the total to estimate probabilities."""
  pmf = counts / np.sum(counts)
  return pmf

def _entropy(pmf):
  """Given a discrete distribution, return the Shannon entropy in bits."""
  # remove non-zero entries to avoid an error from log2(0)
  pmf = pmf[pmf > 0]
  h = -np.sum(pmf * np.log2(pmf))
  # absolute value applied to avoid getting a -0 result
  return np.abs(h)

@widgets.interact(neuron=widgets.IntSlider(0, min=0, max=(len(steinmetz_spikes)-1)))
def steinmetz_pmf(neuron):
  """ Given a neuron from the Steinmetz data, compute its PMF and entropy """
  isi = np.diff(steinmetz_spikes[neuron])
  bins = np.linspace(*isi_range, n_bins + 1)
  counts, _ = np.histogram(isi, bins)
  pmf = _pmf_from_counts(counts)

  plot_pmf(pmf,isi_range)
  plt.title(f"Neuron {neuron}: H = {_entropy(pmf):.2f} bits")
```

---
# Summary

```python
# @title Video 5: Summary of model types
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="X4K2RR5qBK8", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

Congratulations! You've finished your first NMA tutorial. In this 3 part tutorial series, we used different types of models to understand the spiking behavior of neurons recorded in the Steinmetz data set.

 - We used "what" models to discover that the ISI distribution of real neurons is closest to an exponential distribution
 - We used "how" models to discover that balanced excitatory and inbhitiory inputs, coupled with a leaky membrane, can give rise to neuronal spiking with exhibiting such an exponential ISI distribution
 - We used "why" models to discover that exponential ISI distributions contain the most information when the mean spiking is constrained

# Bonus

### The foundations for Entropy

In his foundational [1948 paper](https://en.wikipedia.org/wiki/A_Mathematical_Theory_of_Communication) on information theory, Claude Shannon began with three criteria for a function $H$ defining the entropy of a discrete distribution of probability masses $p_i\in p(X)$ over the points $x_i\in X$:
1. $H$ should be continuous in the $p_i$.
  - That is, $H$ should change smoothly in response to smooth changes to the mass $p_i$ on each point $x_i$.
2. If all the points have equal shares of the probability mass, $p_i=1/N$, $H$ should be a non-decreasing function of $N$.
  - That is, if $X_N$ is the support with $N$ discrete points and $p(x\in X_N)$ assigns constant mass to each point, then $H(X_1) < H(X_2) < H(X_3) < \dots$
3. $H$ should be preserved by (invariant to) the equivalent (de)composition of distributions.
  - For example (from Shannon's paper) if we have a discrete distribution over three points with masses $(\frac{1}{2},\frac{1}{3},\frac{1}{6})$, then their entropy can be represented in terms of a direct choice between the three and calculated $H(\frac{1}{2},\frac{1}{3},\frac{1}{6})$. However, it could also be represented in terms of a series of two choices:
    1. either we sample the point with mass $1/2$ or not (_not_ is the other $1/2$, whose subdivisions are not given in the first choice),
    2. if (with probability $1/2$) we _don't_ sample the first point, we sample one of the two remaining points, masses $1/3$ and $1/6$.

    Thus in this case we require that $H(\frac{1}{2},\frac{1}{3},\frac{1}{6})=H(\frac{1}{2},\frac{1}{2}) + \frac{1}{2}H(\frac{1}{3}, \frac{1}{6})$

There is a unique function (up to a linear scaling factor) which satisfies these 3 requirements:

\begin{align}
  H_b(X) &= -\sum_{x\in X} p(x) \log_b p(x)
\end{align}

Where the base of the logarithm $b>1$ controls the units of entropy. The two most common cases are $b=2$ for units of _bits_, and $b=e$ for _nats_.

We can view this function as the expectation of the self-information over a distribution:

$$H_b(X) = \mathbb{E}_{x\in X} \left[I_b(x)\right]$$

$$I_b(x)=-\log_b p(x)$$

Self-information is just the negative logarithm of probability, and is a measure of how surprising an event sampled from the distribution would be. Events with $p(x)=1$ are certain to occur, and their self-information is zero (as is the entropy of the distribution they compose) meaning they are totally unsurprising. The smaller the probability of an event, the higher its self-information, and the more surprising the event would be to observe.


# Neural Model - Gaussian Filtering Step

In this notebook we present a first implementation of a neural model for sound source elevation estimation based on the computational HRTF model. In particular we demonstrate the neural model's ability to resemble the HRTF model's Gaussian filtering step. This is achieved by a local response normalization via feedforward inhibition and by changing the differential equation on the first neuron $r^{in}$ from

\begin{equation}
 \tau \dot r^{in}_\omega = - \alpha \cdot r_\omega + (\beta - r_\omega) \cdot {EX}_{in} - \kappa \cdot r_\omega \cdot {In}_{in}
\end{equation}

to

\begin{equation}
 \tau \dot r^{in}_\omega = - \alpha \cdot r_\omega \cdot {EX}_{in}  + (\beta - r_\omega) \cdot {EX}_{in} - \kappa \cdot r_\omega \cdot {In}_{in}
\end{equation}

to dynamically adapt the decay rate $\alpha$.

Results show the mode output of the $r^{in}$ neuron in comparison with the actual Gaussian filtering step.

```python
# Import necessary modules
from pathlib import Path
from src.data import generateData
from src.features import helpers as hp
from src.visualization import helpers as hpVis
import numpy as np
import matplotlib.pyplot as plt
import os
from IPython.display import clear_output
from scipy.ndimage import gaussian_filter1d,convolve1d

ROOT = Path(os.getcwd()).resolve().parents[0]

SOUND_FILES = ROOT / 'data/raw/sound_samples/'
# create a list of the sound files
SOUND_FILES = list(SOUND_FILES.glob('**/*.wav'))
```

```python
############################################################
#                    Helper functions                      #
############################################################

def process_inputs(psd_all_i, psd_all_c, ear='ipsi', normalization_type='sum_1', sigma_smoothing=0, sigma_gauss_norm=1):
    # filter the data
    psd_mono_c = hp.filter_dataset(psd_all_c, normalization_type=normalization_type,
                                   sigma_smoothing=sigma_smoothing, sigma_gauss_norm=sigma_gauss_norm)
    psd_mono_i = hp.filter_dataset(psd_all_i, normalization_type=normalization_type,
                                   sigma_smoothing=sigma_smoothing, sigma_gauss_norm=sigma_gauss_norm)

    # integrate the signals and filter
    if ear.find('contra') >= 0:
        psd_binaural = hp.filter_dataset(
            psd_mono_c / psd_mono_i, normalization_type=normalization_type, sigma_smoothing=0, sigma_gauss_norm=0)
    else:
        psd_binaural = hp.filter_dataset(
            psd_mono_i / psd_mono_c, normalization_type=normalization_type, sigma_smoothing=0, sigma_gauss_norm=0)

    # calculate different input sounds. should be 4 of them (mono,mono-mean,bin, bin-mean)
    if ear.find('contra') >= 0:
        psd_mono = psd_mono_c
    else:
        psd_mono = psd_mono_i

    psd_mono_mean = psd_mono - \
        np.transpose(np.tile(np.mean(psd_mono, axis=1), [
                     psd_mono.shape[1], 1, 1]), [1, 0, 2])
    psd_binaural = psd_binaural
    psd_binaural_mean = psd_binaural - \
        np.transpose(np.tile(np.mean(psd_binaural, axis=1), [
                     psd_binaural.shape[1], 1, 1]), [1, 0, 2])

    return psd_mono, psd_mono_mean, psd_binaural, psd_binaural_mean

```

```python
############################################################
#                  Simulation functions                    #
############################################################

tau = 0.005

# Defines the output transfer function of q_A_SC
def out_thres(q,threshold = 0.0,slope=1):
    return np.minimum(np.maximum((q-threshold)*slope,0),1)

# Defines the sigmoidal transfer function
def out_sigmoid(x,slope =5,operation_point=0.0):
#     return 1 / (1 + np.exp(-(x-working_point)*slope) )
    tmp = (x-operation_point)*slope
    return (2 / (1 + np.exp(-4*tmp**2)))-1

#define a gauss function
def gauss(x,mean,sigma):
    if sigma == 0.0:
        return np.zeros(x.shape)
    else:
        tmp = np.exp(-(x-mean)**2 /( 2 * sigma**2 ))
        return tmp/np.max(tmp)

# define the ODE for inhibitory input neurons
def ode_p_in(p ,excitatory_in):
 # tau defines how fast the membrane potential builds up
#     tau = 1.0
    # alpha defines the decay rate of the membrane potential but also the value to which it saturates (implicitly)
    alpha=1
    # beta defines the upper limit of the membrane potential
    beta= 1

    # calculate the change of r_Alearn
    d_p  = -alpha *p  + (beta -p )*excitatory_in

    return d_p /tau

# define the ODE for gaussian filter neurons
def ode_r_in(r,excitatory_in,inhibitory_in):
    # tau defines how fast the membrane potential builds up
#     tau = 1.0
    # alpha defines the decay rate of the membrane potential but also the value to which it saturates (implicitly)
    alpha= 1
    # beta defines the upper limit of the membrane potential
    beta= 200
    # gamma defines the subtractive influence of the inhibitory input
    gamma = 0.0
    # kappa defines the divisive influence of the inhibitory input
    kappa = 200

    # calculate the change of r_Alearn
    d_r  = -alpha  * r * excitatory_in  + (beta -r ) * excitatory_in  - (gamma  + kappa  * r ) * inhibitory_in

    return d_r / tau

```

### Parameters

```python
########################################################################
######################## Set parameters ################################
########################################################################
azimuth = 12
snr = 0.0
freq_bands = 128
participant_number = 1

normalize = False
time_window = 0.1  # time window in sec

# filtering parameters
normalization_type = 'sum_1'
sigma_smoothing = 0
sigma_gauss_norm = 1

# use the mean subtracted map as the learned map
mean_subtracted_map = True

ear = 'ipsi'

elevations = np.arange(0, 25, 1)

############################################################
#                  Simulation parameter                    #
############################################################

# Time step
dt = 0.0001 # ->

# Solve differential equation from time 0 to time T
T = 0.1#
# Descretize time into equal steps
ts = np.linspace (0 , T , int ( T / dt )+1)

```

### Inputs

```python

```

### Simulation

```python

#generate inputs
psd_all_c, psd_all_i = generateData.create_data(freq_bands, participant_number, snr, normalize, azimuth, time_window)

# Take only given elevations
input_c = psd_all_c[:, elevations, :]
input_i = psd_all_i[:, elevations, :]

# normalize inputs over frequencies
input_c = input_c / input_c.sum(2)[:,:,np.newaxis]
input_i = input_i / input_i.sum(2)[:,:,np.newaxis]

# Define neurons
sounds_types = np.arange(0,20)
# sounds_types = np.array([0,2])

n_sounds = len(sounds_types)
# elevations_angles = np.array([0])
elevations_angles = np.arange(0,25,1)
n_elevations = len(elevations_angles)

sigma = 3

p_in_c = np.zeros((n_sounds,n_elevations,len(ts),freq_bands))
p_in_i = np.zeros((n_sounds,n_elevations,len(ts),freq_bands))

r_in_c = np.zeros((n_sounds,n_elevations,len(ts),freq_bands))
r_in_i = np.zeros((n_sounds,n_elevations,len(ts),freq_bands))

r_sum = np.zeros((n_sounds,n_elevations,len(ts),freq_bands))

r = np.zeros((n_sounds,n_elevations,len(ts),freq_bands))

q = np.zeros((n_sounds,n_elevations,len(ts),len(elevations)))

x_kernel = np.arange(freq_bands)

gauss_kernel = gauss(np.arange(-4*sigma,4*sigma),0,sigma)

for i_sound,sound in enumerate(sounds_types):
    for i_ele,ele in enumerate(elevations_angles):

        in_i = input_i[sound,ele]
        in_c = input_c[sound,ele]

        # since the input does not change over time. We can do this calculation ouside the loop
        excitatory_in_p_i = convolve1d(out_thres(in_i),weights=gauss_kernel,axis=0,mode='reflect')

        for t in range(0,len(ts)-1):

            # feed inputs ipsi inhibition
            p_in_i[i_sound,i_ele, t+1,:] = p_in_i[i_sound,i_ele, t,:] + dt* ode_p_in(p_in_i[i_sound,i_ele,t,:],excitatory_in_p_i)
            # feed inputs ipsi excitation
            excitatory_in = out_thres(in_i[:])
            inhibitory_in = out_thres(p_in_i[i_sound,i_ele, t,:])
            r_in_i[i_sound,i_ele, t+1,:] = r_in_i[i_sound,i_ele, t,:] + dt* ode_r_in(r_in_i[i_sound,i_ele,t,:],excitatory_in,inhibitory_in)

        if t%1 == 0:
            clear_output(wait=True)
            print('Sound No: '+str(i_sound+1)+' of '+str(n_sounds)+'.\n  -> Elevation : '+str(i_ele+1)+' of '+str(n_elevations))
```

    Sound No: 20 of 20.
      -> Elevation : 25 of 25

```python
fig = plt.figure(figsize=(10,10))

axs = fig.subplots(nrows=3,ncols=2,squeeze=False)

i_sound = 0
filter_sigma = 1

input_ = np.squeeze(input_i[sounds_types[i_sound],:,:])

ax = axs[0,0]
ax.set_title('Ipsi Ear')
tmp1 = input_
a = ax.pcolormesh(tmp1)
ax.set_ylabel('Input Data')
fig.colorbar(a,ax=ax)

ax = axs[0,1]
ax.set_title('Gaussian Model')
tmp1 = input_
a = ax.pcolormesh(tmp1)
ax.set_ylabel('Input Data')
fig.colorbar(a,ax=ax)

ax = axs[1,0]
tmp1 = np.squeeze(p_in_i[i_sound,:,-1:,:])
a = ax.pcolormesh(tmp1)
ax.set_ylabel('P Neuron')
fig.colorbar(a,ax=ax)

ax = axs[1,1]
tmp1 = input_
tmp1 = gaussian_filter1d(tmp1, sigma=filter_sigma, mode='nearest', axis=1)
a = ax.pcolormesh(tmp1)
ax.set_ylabel('Gauss(S)')
fig.colorbar(a,ax=ax)

ax = axs[2,0]
tmp1 = np.squeeze((r_in_i[i_sound,:,-1:,:]))
a = ax.pcolormesh(tmp1)
ax.set_ylabel('R_in Neuron')
fig.colorbar(a,ax=ax)

ax = axs[2,1]
tmp1 = input_
tmp1 = tmp1/gaussian_filter1d(tmp1, sigma=filter_sigma, mode='nearest', axis=1)
a = ax.pcolormesh(tmp1)
ax.set_ylabel('R_in Neuron')
fig.colorbar(a,ax=ax)

plt.show()

```

```python
fig = plt.figure(figsize=(10,10))

axs = fig.subplots(nrows=len(sounds_types),ncols=2,squeeze=False)

for i_sounds,sound in enumerate(sounds_types):

    input_ = np.squeeze(input_i[sound,:,:])

    ax = axs[i_sounds,0]
    tmp1 = np.squeeze(r_in_i[i_sounds,:,-1:,:])
    a = ax.pcolormesh(tmp1)
    if i_sounds == 0:
        ax.set_title('R_in Neuron')
    fig.colorbar(a,ax=ax)

    ax = axs[i_sounds,1]
    if i_sounds == 0:
        ax.set_title('Gaussian Filtering')
    tmp1 = input_
    tmp1 = tmp1/gaussian_filter1d(tmp1, sigma=1, mode='nearest', axis=1)
    a = ax.pcolormesh(tmp1)
#     fig.colorbar(a,ax=ax)

plt.show()
```

```python
plt.plot(r_in_i[0,0,:,19])
plt.plot(p_in_i[0,0,:,13])
plt.show()
```

```python

```

```python

```

notebooks/Neural Model - Gaussian Filtering Step.ipynb