# Solución de ecuaciones

Python en Jupyter Notebook

Creado por Giancarlo Ortiz para el curso de Métodos Numéricos

## Búsqueda de raíces por métodos cerrados
Los métodos iterativos cerrados, por intervalos o de horquillado se caracterizan porque para su ejecución requieren un intervalo que contenga al menos una raíz y en su aplicación se reduce continuamente de tamaño el intervalo manteniendo la raíz en su interior.

Sin importar la rapidez de convergencia, los métodos numéricos cerrados al mantener la raíz en el interior del intervalo garantizan la convergencia del método.

## Agenda
1. Generalidades
1. Método de la bisección
1. Método de la falsa posición

```python
# Importar módulos al cuaderno de jupyter
import math as m
import numpy as np
import pylab as plt

# Definir e incluir nuevas funciones al cuaderno
def _buscar_intervalos(fun, ini, fin):
    """ Método para buscar intervalos en los que ocurra cambio de signo.

        ## Parámetros:
            fun (function): función para analizar.
            ini (int): inicio del análisis.
            fin (int): limite final del análisis.

        ## Devoluciones:
            I (list): Lista de tuplas con los intervalos donde hay un cero.
    """
    i = ini - 1 # Variable para contar iteraciones
    I = []      # Variable para almacenar los intervalos

    while i < fin:
        i += 1
        if fun(i) == 0:
            I.append((i, i))
        elif fun(i) * fun(i+1) < 0:
            I.append((i, i+1))
        else:
            pass
    return I

```

## 1. Métodos Cerrados
---
Basados en el teorema del valor intermedio, los métodos de horquillado determinan intervalos sucesivamente más pequeños que contienen una raíz; por lo tanto, requieren comenzar con un intervalo tal que la función tome signos opuestos en los puntos finales del intervalo.

> **NOTA:** El _teorema del valor intermedio_ establece que para cada $y$ tal que $f(a)<y<f(b)$ existe un $x \in (a, b)$ tal que $f(x) = y$

## 2. Método de la bisección
---
En análisis numérico; el algoritmo de la bisección o de Bolzano nos permite buscar las raíces de una función real y continua partiendo de intervalo inicial $\color{#a78a4d}{(a, b)}$, dividiendo sucesivamente el intervalo a la mitad y seleccionando el sub intervalo que contiene la raíz; hasta hallar una aproximación aceptable o la raíz que satisface la función.

Figura 1. Explicación gráfica bisección

### Formulación:
* Se calcula una semilla $x_o$  como el punto medio del intervalo que contiene la raíz, si el valor no es aceptable se encuentran los limites de un nuevo intervalo y se generaliza para una solución que repita el proceso:

\begin{align}
\tag{1} x_{m_o} &= \frac{b + a}{2} \\
\tag{2} x_o &= x_{m_o} \\
\text{Generalizando (2):$\qquad\qquad$} \\
\tag{3} x_n &= \frac{b_n + a_n}{2} \\
\end{align}

### Error:
*  El error absoluto $\color{#a78a4d}{ea}$ cometido tras realizar $\color{#a78a4d}{n>0}$ iteraciones del método de la bisección tiene una cota de tal forma que:

\begin{align}
\tag{4} ea_n \leq \frac{\left| b - a \right|}{2^n}
\end{align}

* Para lograr un error inferior al valor $\color{#a78a4d}{\varepsilon}$, el número $\color{#a78a4d}{n}$ de iteraciones que se debe realizar es:

\begin{align}
\tag{5} {\displaystyle n>{\frac {\ln(b-a)-\ln(\varepsilon )}{\ln(2)}}-1} \\
\end{align}

### Ventajas:
* Método estable, siempre converge.
* Útil como aproximación inicial de otros métodos como _regula falsi_.

### Desventajas:
* No tiene en cuenta la magnitud de los valores de la función en las aproximaciones calculadas $x_n$, solo tiene en cuenta el signo de $f(x)$.
* Método de convergencia lineal, orden de convergencia, $r = 1$.
* Tasa de convergencia $\mu = 0.5$

```python
# Defino el método iterativo de la Bisección
def _biseccion(Func, Xmin, Xmax, Imax, Tmax):
    """ Método de la Bisección para encontrar raíces en un intervalo.

        ## Parámetros:
            Func (int): función que depende de una variable.
            Xmin (int): limite inferior de intervalo.
            Xmax (int): limite superior de intervalo.
            Imax (int): número máximo de iteraciones.
            Tmax (int): exponente de tolerancia máxima, T = 1e^Tmax.

        ## Devoluciones:
            Km (float): valor de x encontrado.
            No (int)  : iteraciones.
            ea (float): error absoluto final
            Li (list): lista de las soluciones, los errores absolutos y la tabla con la evolución de iteraciones.
    """
    # Inicializar variables
    Xs = Xmax
    Xi = Xmin
    S = []
    E = []
    Tb = f"| I # | {'Xi':>8} | {'Xs':>8} | {'ΔX':>9} | {'Xm':>9} | {'Error Abs':>9} | {'Fm':>9} |\n"
    Tb += "-----------------------------------------------------------------------------\n"
    # Iteraciones
    for i in range(1, Imax + 1):
        ΔX = abs(Xs - Xi)
        Xm = (Xs + Xi) / 2
        ea = ΔX / 2
        S.append(Xm)
        E.append(ea)
        Fm = Func(Xm)
        Tb += f"| {i:3} | {Xi:8.5f} | {Xs:8.5f} | {ΔX:9.5f} | {Xm:9.5f} | {ea:9.5f} | {Fm:9.5f} |\n"
        if ea < (10**Tmax): break
        if Func(Xi) * Func(Xm) < 0:
            Xs = Xm
        else:
            Xi = Xm

    # Salida del método
    return i, Xm, ea, [S, E, Tb]

```

## 3. Método de la falsa posición
---
En análisis numérico; el algoritmo de _regula falsi_ o de la posición falsa nos permite buscar las raíces de una función real y continua partiendo de intervalo inicial $\color{#a78a4d}{(a, b)}$, dividiendo sucesivamente el intervalo con un segmento de línea que conecta las imágenes de los limites del intervalo y seleccionando el sub intervalo que contiene la raíz; hasta hallar una aproximación aceptable o la raíz que satisface la función.

Figura 2. Explicación falsa posición

### Formulación:
* Se calcula una semilla $x_o$ como la raíz de la recta secante que pase por los limites del intervalo, si el valor no es aceptable se encuentran los limites de un nuevo intervalo y se generaliza la solución de manera que se repita el proceso:

\begin{align}
\tag{6} y - f(a) &= \frac{ f(b)-f(a) }{ b-a } \left( x - a \right) \\
\mbox{Despejando $x$ de (6): $\qquad\qquad$} \\
\tag{7} x &= y + a - f(a) \frac{ b-a }{ f(b)-f(a) }\\
\mbox{Evaluando (7) en $y=0$:$\qquad\qquad$} \\
\tag{8} x &= a - f(a) \frac{ b-a }{ f(b)-f(a) }
\end{align}

### Ventajas:
* Método estable, siempre converge.
* Tiene en cuenta la magnitud de los valores de la función en las aproximaciones calculadas $x_n$.
* En la mayoría de los casos converge más rápidamente que el método de la bisección.
* Útil como aproximación inicial de otros métodos como el algoritmo de Illinois.

### Desventajas:
* Hay situaciones que pueden ralentizar su convergencia, a veces hasta un grado prohibitivo.
* Orden de convergencia, $r = 1$.
* Tasa de convergencia $\mu = 2/3$

```python
# Defino el método iterativo del punto falso
def _punto_falso(Func, Xmin, Xmax, Imax, Tmax):
    """ Método de la falsa posición para encontrar raíces en un intervalo.

        ## Parámetros:
            funX (int): función que depende de una variable.
            Xmin (int): limite inferior de intervalo.
            Xmax (int): limite superior de intervalo.
            Imax (int): número máximo de iteraciones.
            Tmax (int): exponente de tolerancia máxima, T = 1e^Tmax.

        ## Devoluciones:
            Km (float): valor de x encontrado.
            No (int)  : iteraciones.
            ea (float): error absoluto final
            Li (list): lista de las soluciones, los errores absolutos y la tabla con la evolución de iteraciones.
    """
    # Inicializar variables
    Xs = Xmax
    Xi = Xmin
    S = []
    E = []
    Tb = f"| I # | {'Xi':>8} | {'Xs':>8} | {'ΔX':>9} | {'Xm':>9} | {'Error Abs':>9} | {'Fm':>9} |\n"
    Tb += "-----------------------------------------------------------------------------\n"
    # Iteraciones
    for i in range(1, Imax + 1):
        ΔX = abs(Xs - Xi)
        Xm = Xi - Func(Xi)*(Xs - Xi) / (Func(Xs) - Func(Xi))
        if i==1:
            ea = abs(Xi - Xm)
        else:
            ea = abs(S[-1] - Xm)
        # Almacenamiento de la soluciones y el error
        S.append(Xm)
        E.append(ea)
        Fm = Func(Xm)
        Tb += f"| {i:3} | {Xi:8.5f} | {Xs:8.5f} | {ΔX:9.5f} | {Xm:9.5f} | {ea:9.5f} | {Fm:9.5f} |\n"
        if ea < (10**Tmax): break
        if Func(Xi) * Func(Xm) < 0:
            Xs = Xm
        else:
            Xi = Xm

    # Salida del método
    return i, Xm, ea, [S, E, Tb]
```

```python
# Ecuación de la altura para el movimiento parabólico
def altura(θ, Vo, ho, t):
    g = 9.8179
    Voy = Vo * np.sin(θ*np.pi/180)
    y = -(1/2) * g * t**2 + Voy * t + ho
    return y

# Renombrar la función
F = lambda t: altura(45, 12, 3, t)

# Buscar intervalos
Lista = _buscar_intervalos(F, 0, 5)

# Establecer limites y tolerancia de la búsqueda de raíces
Kmin = Lista[0][0]
Kmax = Lista[0][1]
Orden_Tolerancia = -4
Max_iteraciones = 50

# Buscar raíces
n1, x1, ea1, paso1 =   _biseccion(F, Kmin, Kmax, Max_iteraciones, Orden_Tolerancia)
n2, x2, ea2, paso2 = _punto_falso(F, Kmin, Kmax, Max_iteraciones, Orden_Tolerancia)
```

```python
print(paso1[2])
```

    | I # |       Xi |       Xs |        ΔX |        Xm | Error Abs |        Fm |
    -----------------------------------------------------------------------------
    |   1 |  2.00000 |  3.00000 |   1.00000 |   2.50000 |   0.50000 |  -6.46773 |
    |   2 |  2.00000 |  2.50000 |   0.50000 |   2.25000 |   0.25000 |  -2.75968 |
    |   3 |  2.00000 |  2.25000 |   0.25000 |   2.12500 |   0.12500 |  -1.13575 |
    |   4 |  2.00000 |  2.12500 |   0.12500 |   2.06250 |   0.06250 |  -0.38132 |
    |   5 |  2.00000 |  2.06250 |   0.06250 |   2.03125 |   0.03125 |  -0.01848 |
    |   6 |  2.00000 |  2.03125 |   0.03125 |   2.01562 |   0.01562 |   0.15934 |
    |   7 |  2.01562 |  2.03125 |   0.01562 |   2.02344 |   0.00781 |   0.07073 |
    |   8 |  2.02344 |  2.03125 |   0.00781 |   2.02734 |   0.00391 |   0.02620 |
    |   9 |  2.02734 |  2.03125 |   0.00391 |   2.02930 |   0.00195 |   0.00387 |
    |  10 |  2.02930 |  2.03125 |   0.00195 |   2.03027 |   0.00098 |  -0.00730 |
    |  11 |  2.02930 |  2.03027 |   0.00098 |   2.02979 |   0.00049 |  -0.00171 |
    |  12 |  2.02930 |  2.02979 |   0.00049 |   2.02954 |   0.00024 |   0.00108 |
    |  13 |  2.02954 |  2.02979 |   0.00024 |   2.02966 |   0.00012 |  -0.00032 |
    |  14 |  2.02954 |  2.02966 |   0.00012 |   2.02960 |   0.00006 |   0.00038 |

```python
print(paso2[2])
```

    | I # |       Xi |       Xs |        ΔX |        Xm | Error Abs |        Fm |
    -----------------------------------------------------------------------------
    |   1 |  2.00000 |  3.00000 |   1.00000 |   2.02085 |   0.02085 |   0.10019 |
    |   2 |  2.02085 |  3.00000 |   0.97915 |   2.02704 |   0.00620 |   0.02961 |
    |   3 |  2.02704 |  3.00000 |   0.97296 |   2.02887 |   0.00183 |   0.00872 |
    |   4 |  2.02887 |  3.00000 |   0.97113 |   2.02941 |   0.00054 |   0.00256 |
    |   5 |  2.02941 |  3.00000 |   0.97059 |   2.02957 |   0.00016 |   0.00075 |
    |   6 |  2.02957 |  3.00000 |   0.97043 |   2.02962 |   0.00005 |   0.00022 |

```python
#
x1 = np.linspace(1, n1, n1, endpoint=True)
x2 = np.linspace(1, n2, n2, endpoint=True)

# Funciones de solución y error
y1 = paso1[0]
e1 = paso1[1]
y2 = paso2[0]
e2 = paso2[1]

# Gráficas
#-------------------
fig, ax = plt.subplots(1, 2)
fig.set_size_inches(16, 4)
fig.suptitle('Raíces de funciones algebraicas y trascendentes'.upper(), fontsize=14, fontweight="bold")

# Gráficas 1
# ax[0].axhline(y = 0, linewidth=1.0, color="#ccc")
ax[0].plot(x1, y1, label="Método bisección $y_1(x)$")
ax[0].plot(x2, y2, label="Método punto falso $y_2(x)$")
ax[0].set_title("Solución", fontsize=8)
ax[0].set_ylabel(" Valor de x")
ax[0].set_xlabel("Iteración [n]")
ax[0].legend()

# Gráficas 2
ax[1].axhline(y = 0, linewidth=1.0, color="#ccc")
ax[1].plot(x1, e1, label="Error bisección P$e_a(n)$")
ax[1].plot(x2, e2, label="Error punto falso $e_a(n)$")
ax[1].set_title("Error ", fontsize=8)
ax[1].set_xlabel("Iteración [n]")
ax[1].legend()

plt.show()
```

---
## Mas Recursos

- [Análisis numérico](https://es.wikipedia.org/wiki/An%C3%A1lisis_num%C3%A9rico) (Wikipedia)
- [Búsqueda de raíces](https://es.wikipedia.org/wiki/Resoluci%C3%B3n_num%C3%A9rica_de_ecuaciones_no_lineales) (Wikipedia)
- [Método de bisección](https://es.wikipedia.org/wiki/M%C3%A9todo_de_bisecci%C3%B3n) (Wikipedia)
- [Estabilidad numérica](https://es.wikipedia.org/wiki/Estabilidad_num%C3%A9rica) (Wikipedia)


```python
# Run boilerplate code to set up environment

%run ../prelude.py

import numpy as np
import scipy
```

    interactive(children=(Dropdown(description='style', options=('tree', 'uncompressed', 'tree+uncompressed'), val…

    Button(description='Run all cells below', style=ButtonStyle())

# Triangular solver

Based on scipy's [scipy.linalg.solve_triangular](https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.solve_triangular.html) and the solver code presented by [Weng et al. at HPCA'20](https://ieeexplore.ieee.org/abstract/document/9065593)

Consider the following system of equations:

$$
\begin{align}
a_{11} x_1&              &= b_1 \\
a_{21} x_1&+ a_{22} x_2 &= b_2
\end{align}
$$

This can be expressed as a matrix equation $\mathbf{A}x=b$ where
$
\mathbf{A} = \left[\begin{array}{c c}
  a_{11} & 0      \\
  a_{21} & a_{22}
  \end{array}
  \right]$
and $
b = \left[\begin{array}{c}
  b_1 \\ b_2 \end{array} \right]
$.

To solve, you would conclude that $x_1 = b_1 / a_{11}$, and plug it into the second equation to solve for $x_2$. However, this would correspond to a column-major traversal of $\mathbf{A}$, so we transpose $\mathbf{A}$ to get a row-major (and hopefully concordant) traversal.

(Evidence of this transposition manifests below as generating `A_data` as an _upper_ triangular matrix, as well as the `trans="T"` keyword argument passed to `solve_triangular()` in the assertion.)

_Note:_ this was developed expecting uncompressed tensors but its use of `getPayload{,Ref}()` should support compressed formats.

```python
M = N = 4

np.random.seed(12345)

# Generate a random upper triangular matrix A
A_data = np.triu(np.random.randint(1, 6, size=(M, N)))

# Generate the desired output vector x (Ax=b)
X_ref_data = np.array(np.random.randint(1, 6, size=(N,)))

# Evaluate the triangular matrix with X_ref to obtain B
#   this corresponds to a column-wise dot product with
B_data = X_ref_data.dot(A_data)

print("A:\n", A_data, "\nx (reference):\n", X_ref_data, "\nb:\n", B_data)

assert(all(X_ref_data == scipy.linalg.solve_triangular(A_data, B_data, trans="T")))
```

```python
# Create uncompressed tensors
A_MN = Tensor.fromUncompressed(["M", "N"], A_data.tolist()).setName("A")
B_N = Tensor.fromUncompressed(["N"], B_data.tolist()).setName("b")
X_ref = Tensor.fromUncompressed(["M"], X_ref_data.tolist()).setName("x_ref")
X = Tensor(rank_ids=["M"]).setName("x")

A = A_MN.getRoot()

inflate = False
if inflate:
    # hack to "inflate" A with coordinates where there are zeros,
    # and allowing us to use square brackets (i.e. __getitem__)
    # because positions == coordinates
    A << Fiber(coords=range(M))
    for j, a_j in A:
        a_j << Fiber(coords=range(N))

B = B_N.getRoot()

displayTensor(A)
displayTensor(B)
```

## Pipelined triangular solver

`P` is a representation of pipelined communication; each row corresponds to the data passed from the previous stage to the next. Each row is N elements wide (same as B).

This mapping corresponds to a pipeline of single-threaded compute engines (CEs) commensurate in size to the input.

_Note:_ I traverse A in the N rank, then the M rank. I think I need a `swapRanks()` somewhere...

```python
# create "pipes" between each stage: N-entry tensors
# (can't simply do a rank-2 tensor because I'd have multiple entries for addActivity for that tensor, and
# current pipelining API doesn't really handle that
#
# for syntactic cleanliness I include B, the input tensor, in this array too
BP = [B_N] + [Tensor(rank_ids=["N"], shape=[N]).setName(f"pipe-{m}") for m in range(M)]

canvas = createCanvas(A_MN, *BP, X, enable_wait=True)

cycle = 0
stage_delay = 1

# "wait" means, for each key-value pair in the supplied dictionary,
# we must wait `value` cycles when the `key`th argument in the canvas has been updated
# in order to add our own activity
wait = None # loop-carried variable

for a_m in range(M):
    # in the first row, read from B, rather than P
    # also, skew these starts by cycle, as these input elements are piped in one after another
    # m_ and n_activities provide pair-wise activity locations for adjacent stages in the pipeline
    # e.g., [(m,), (m,), ()] then [(), (m,), (m,)] then [(), (), (m,)]
    m_activities = [(a_m,) if mm == a_m or mm == a_m+1 else () for mm in range(M+1)]
    # print(m_activities)

    # process element on diagonal
    curStage = BP[a_m]
    nextStage = BP[a_m+1]
    a_n = a_m
    next_m = nextStage.getPayloadRef(a_m)
    next_m <<= curStage.getPayload(a_m) / A.getPayload(a_m, a_n)
    canvas.addActivity((a_m, a_n), *m_activities, worker=str(a_m), skew=(cycle if a_m == 0 else 0), wait=wait)
    cycle += 1

    # process elements off-diagonal on same row
    # change from a_m to a_n (even though M=N) for clarity
    for i_n in range(a_n+1, N):
        n_activities = [(i_n,) if ii == a_n or ii == a_n+1 else () for ii in range(N+1)]
        # print(n_activities)

        cur_n = curStage.getPayload(i_n)
        next_n = nextStage.getPayloadRef(i_n)
        next_n <<= cur_n - next_m * A.getPayload(a_m, i_n)

        canvas.addActivity((a_m, i_n), *n_activities, worker=str(a_m), skew=(cycle if a_m == 0 else 0), wait=wait)
        cycle += 1

    # new values for next value of a_m
    wait = {f"pipe-{a_m}": stage_delay}

for x_m in range(M):
    x_m_ref = X.getPayloadRef(x_m)
    x_m_ref <<= BP[x_m+1].getPayload(x_m) # get m-th value from pipeline stage m
    m_activities = [(x_m,) if mm == x_m else () for mm in range(M)]
    # print(x_m, [], [], m_activities + [(x_m,)])
    canvas.addActivity([], [], *m_activities, (x_m,), wait={f"pipe-{x_m}":stage_delay})

displayCanvas(canvas)

```

```python
# final result
displayTensor(X)
displayTensor(X_ref)
X == X_ref
```

## Check results

Check the result by performing the dot product of `A` and `x` and ensuring that equals the `b` we started with. (The jth entry of `b` corresponds to the dot product of the jth _column_ of `A` and `x`.)

```python
B_check = Fiber(coords=range(N), initial=0)
# displayTensor(B2)

A_NM = A_MN.swapRanks()
A2 = A_NM.getRoot()
displayTensor(A2)

# canvas2 = createCanvas(A, X, B2)
for a2_n in range(N):
    for a2_m in range(M):
        # print(j, i, a_pay[i], x_val, b2_ref)
        b2_ref = B_check.getPayloadRef(a2_n)
        x_val = X.getPayload(a2_m)
        b2_ref += A2.getPayload(a2_n, a2_m) * x_val
        # addActivity(canvas2, (i,j), (i,), (i,))

# displayCanvas(canvas2)

# check results
B == B_check
```

# Tiling schemes

For your convenience, `A_MN` and `B_N` are repeated here:

```python
displayTensor(A_MN)
displayTensor(B_N)

numPEs = 2
numLanes = 2
```

### A_MMNN (due to Joel)

The program above must have `M` PEs for an MxM matrix. This is not tenable for large matrices, and also, such a pipeline would have load imbalance across the PEs: the first PE sees all elements of B, but the last sees just one.

Tiling helps most immediately with the first problem, by splitting the problem into tiles, each of which can be solved by a system with a bounded number of PEs.

Tiling is tantamount to creating an additional rank. Suppose we want to tile `A_MN` so that an `M0`-sized subset of rows can be processed by `M0` PEs at a time. We'll call `splitUniform(M0)` to produce a rank-3 tensor with ranks `M.1`, `M.0`, and `N`, called `A_MMN`. No fiber in rank `M.0` will contain more than `M0` payloads.

If your ranks are all spatial (in other words, not temporal, as pipeline parallelism will do), then splitting one rank won't be enough. This is because `N` could possibly be unbounded, and each PE only has a finite number of elements (i.e., lanes) it can process at a time. This is why we also split the `N` rank with `splitUniform(N0)` so that each PE can process `N0` elements at a time in its `N0` lanes.

This is how we produce a rank-4 tensor A_MMNN:

```python
M0 = numPEs
N0 = numLanes

A_MMNN = A_MN.splitUniform(M0).splitUniform(N0, depth=2)
displayTensor(A_MMNN)
```

### A_MNMN

The convention is to order ranks such that any ranks that are "scale-up" or unbounded appear closer to the root, and any bounded ranks further from them. This is called _normal form_.

The jury is still out as to whether time-based ranks should appear above or below space-based ranks.

To bring it to normal form, the `swapRanks()` at `depth=1` moves the bounded ranks to the bottom (conversely, unbounded ranks to the top).

```python
A_MNMN = A_MMNN.swapRanks(depth=1)
displayTensor(A_MNMN)
```

### A_MMMN (due to Neal)
M0 in the number of lanes, M1 in the number of PEs, and all N elements streamed in time

```python
A_MMMN = A_MN.splitUniform(numPEs).splitUniform(numLanes)
# M.1.1 is "M2", which  and M.1.0 is "M1"
A_MMMN.ranks[0].id = "M.2"
A_MMMN.ranks[1].id = "M.1"
displayTensor(A_MMMN)
```

### A_MNMM
To bring the unbounded rank (N) above bounded ranks (here, M1 for PEs, and M0 for lanes), need to swap N and M0 (depth 2), then N and M1 (depth 1).

Note that this would use B_N, not B_NN (below).

```python
A_MNMM = A_MMMN.swapRanks(depth=2).swapRanks(depth=1)
displayTensor(A_MNMM)
```

### B_NN

```python
B_NN = B_N.splitUniform(N0)
displayTensor(B_NN)
```

### X_MM (output for Joel's version)

```python
X_MM = Tensor(rank_ids=["M1", "M0"])
displayTensor(X_MM)
```

### Fibertree Native Traversal

```python
P = Tensor(rank_ids=["M", "N"], name = "P")
X = Tensor(rank_ids=["M"], name = "X")

a_m = A_MN.getRoot()
b_n = B_N.getRoot()
p_m = P.getRoot()
x_m = X.getRoot()

# Initialize M[0] of P to be B, then don't use B anymore.
p_n0 = P.getRoot().getPayloadRef(0)
for n, (p_ref, b_val) in p_n0 << b_n:
    p_ref <<= b_val

canvas = createCanvas(A_MN, P, X)

for m, (x_ref, (p_n, a_n)) in x_m << (p_m << a_m):
    p_n_prime = p_m.getPayloadRef(m+1)
    is_first = True
    prop_val = 0
    for n, (p_ref, (p_val, a_val)) in p_n_prime << (p_n & a_n):
        if is_first:
            prop_val = p_val / a_val
            x_ref <<= prop_val
            is_first = False
            canvas.addFrame((m, n), (m, n), (m,))
        else:
            p_ref <<= p_val - prop_val * a_val
            canvas.addFrame((m, n), [(m, n), (m+1, n)], ())

#displayTensor(A_MN)
#displayTensor(P)
#displayTensor(X)
displayCanvas(canvas)
```

### MNMN traversal (thanks to Michael Pellauer) (WIP)

```python
P_MNMN = Tensor(rank_ids=["M1", "N1", "M0", "N0"], name = "P")
X_MM = Tensor(rank_ids=["M1", "M0"], name = "X")

a_m1 = A_MNMN.getRoot()
b_n1 = B_NN.getRoot()
p_m1 = P_MNMN.getRoot()
x_m1 = X_MM.getRoot()

# Initialize M1[0] of P to be B, then don't use B anymore.
p_n1_0 = P_MNMN.getRoot().getPayloadRef(0)
for n1, (p_m0, b_n0) in p_n1_0 << b_n1:
    p_n0_0 = p_m0.getPayloadRef(0)
    for n0, (p_ref, b_val) in p_n0_0 << b_n0:
        p_ref <<= b_val

#displayTensor(A_MNMN)
#displayTensor(P_MNMN)

canvas = createCanvas(A_MNMN, P_MNMN, X_MM)

# note leading variable in for loop matches order of traversal
# use << when it's an output
for m1, (x_m0, (p_n1, a_n1)) in x_m1 << (p_m1 << a_m1):
    is_first = [True for m0 in range(M0)]
    prop_val = [0 for m0 in range(M0)]
    for n1, (p_m0, a_m0) in p_n1 & a_n1:
        for m0, (x_ref, (p_n0, a_n0)) in x_m0 << (p_m0 << a_m0):
            m = m0
            next_m = m+1
            next_m1 = (next_m // M0) * M0
            next_m0 = next_m
            rel_m0 = m % M0
            #print(f"{m1}, {m0}")
            p_n0_prime = p_m1.getPayloadRef(next_m1, n1, next_m0)
            for n0, (p_ref, (p_val, a_val)) in p_n0_prime << (p_n0 & a_n0):
                if is_first[rel_m0]:
                    prop_val[rel_m0] = p_val / a_val
                    x_ref <<= prop_val[rel_m0]
                    is_first[rel_m0] = False
                    canvas.addFrame((m1, n1, m0, n0), (m1, n1, m0, n0), (m1, m0))
                else:
                    p_ref <<= p_val - prop_val[rel_m0] * a_val
                    canvas.addFrame((m1, n1, m0, n0), [(m1, n1, m0, n0), (next_m1, n1, next_m0, n0)], ())
#displayTensor(P_MNMN)
displayCanvas(canvas)
```

### X_MMM (output for Neal's version)

```python
X_MMM = Tensor(rank_ids=["M2", "M1", "M0"])
displayTensor(X_MMM)
```

```python

```


局所線形埋め込み法について
=====
この記事は以下の二つの文献を参考に書かれています
カーネル多変量解析，赤穂昭太郎，2008
Saul, L. K., & Roweis, S. T. (2001). An introduction to locally linear embedding. Available from https://cs.nyu.edu/~roweis/lle/papers/lleintro.pdf

今回は局所線形埋め込み法のアルゴリズムの詳細を記述していきます．
主に「カーネル多変量解析」と同じ順番で解説・実装していきます

```python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
```

データを構成します．
今回は「カーネル多変量解析」と同じようなスイスロールのようなデータを構成しましょう

```python
np.random.seed(seed=0)
sampleSize = 5000
R = 16.0
initialTheta = 2.0
thetaRange = 12.0
nLabel = 4
markers = ['o', 'X', 'd', '*', '>', '8', 's', 'p']
colors =['c','m','b','r','y','g','k']
theta = thetaRange * np.random.rand(sampleSize)+initialTheta
y = 50.0 * (np.random.randn(sampleSize) - 0.5)
Labels = []
X = []
for i in range(sampleSize):
    knd = int(float(nLabel)*((theta[i]-initialTheta) / thetaRange))
    Labels += [knd]
    X += [[R*theta[i]  * np.sin(theta[i]), y[i],R*theta[i]* np.cos(theta[i]) ]]
Labels = np.array(Labels)
X = np.array(X)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
for i in range(nLabel):
    ln = int(np.sum(Labels==i))
    ax.scatter(X[Labels == i,0].reshape(ln),
               X[Labels == i,1].reshape(ln),
               X[Labels == i,2].reshape(ln), marker = markers[i], color = colors[i])
ax.set_xlim(-200,200)
ax.set_ylim(-200,200)
ax.set_zlim(-200,200)

plt.show()
```

ここからが局所線形埋め込み法の実装になります．
データ$X= (x_1, x_2, \cdots, x_n)$が与えられたとき，それぞれのデータ$x_i$の近傍の点を求めます

今回は近傍の決定法として，最も近い$K$個の点を近傍とする．
(半径$r$以内の点を近傍とすることもある)

```python
from scipy.spatial.distance import pdist,squareform

K = 50
mat_sq_dists = squareform(pdist(X, 'sqeuclidean'))
sort_dist = np.sort(mat_sq_dists, axis = 1)
boundaryDist = np.sort(mat_sq_dists, axis = 1)[:, K].reshape(sampleSize, 1)
print("近傍に入っている点の個数の平均値:"+str((np.sum(mat_sq_dists<=boundaryDist,axis = 1)-1.0).mean()))
```

    近傍に入っている点の個数の平均値:50.0

例：$x_5$の近傍$N_5$は次のように書くと取り出すことができる

```python
distFilter = (mat_sq_dists <= boundaryDist)&(mat_sq_dists != 0)
print("N_5の近傍："+str(X[distFilter[5]]))

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X[distFilter[5]][:,0], X[distFilter[5]][:,1], X[distFilter[5]][:,2])
ax.set_xlim(-200,200)
ax.set_ylim(-200,200)
ax.set_zlim(-200,200)
```

# Step1
まず，各$x_i$について，次の最適化問題を解くことにより$W_{ij}$を決定していく
$ \min_{W_i} || x_i - \sum_{j \in N_i } W_{ij} x_j ||^2$ subject to $W_i {\bf 1}  = 1, \forall j \not \in N_i\ ( W_{ij} = 0 )$

この式は$C_{jk} = (x_i-x_j)^T (x_i - x_k)$とおくと
$ \min_{W_i} W_i C W_i^T$ subject to $W_i {\bf 1}  = 1$
と書くことができることを利用すると
この最適化問題はラグランジュの未定乗数法により，
$$ w_{ij} = \frac {\sum_{k} C_{jk}^{-1}} {\sum_{lm} C_{lm}^{-1}}$$
となることがわかる

しかし，実用上，$\sum_{k \in N_i} C_{jk} W_{ik} = 1$を満たす$W_{ik}$を求めたのち，$W_i {\bf 1}  = 1$を満たすように正規化するほうが効率的である

上記の方法で求めた$W_i$が最適化問題の解であることは，KKT条件を満たすことより局所的最小，制約条件を満たす$W_i$の定義域が凸集合であり，$W_i C W_i^T$が凸関数であることより大域的最小，といったように確認できる

ただし，Cは半正定値であり，退化してしまっている可能性があるので，計算する前に次のような処理をする
$$C = C + \left ( \frac {\Delta^2} {K} \right ) I$$
$\Delta^2$は$C$の対角成分と比較して十分小さい値をとる．$K$は近傍に入っている点の個数，$I$は単位行列である

```python
W = np.zeros((sampleSize, sampleSize))
delta = 0.00001
for i in range(sampleSize):
    Ni_xi = X[distFilter[i],:] - X[i,:]
    ln = int(np.sum(distFilter[i]))
    C =np.dot(Ni_xi, Ni_xi.T)
    w = np.linalg.solve(C + delta * np.identity(len(C))/float(len(C)), np.ones(len(C)))
    W[i][distFilter[i]] = w/np.sum(w)
```

# Step 2
$W$を求めることができたので，次の段階に移る
各$x_i$が，$L$次元ベクトル$\beta_i$と表せたとしよう．
このとき，$B = (\beta_1,  \beta_2, \cdots, \beta_n)$は次のような関係を持つことが望まれる．
$$
\min_{\beta} \sum_{i=1}^n || \beta_i - \sum_{j \in N_i} W_{ij} \beta_j  ||^2 \ \ \mbox{subject to} \ \ \frac {1} {n} B B^T = I
$$
この式は次のように展開できる
$$
\begin{align}
\sum_{i=1}^n || \beta_i - \sum_{j \in N_i} W_{ij} \beta_j  ||^2 &=& \sum_{i=1}^n || B e_i - B W_i^T   ||^2\\
&=& \sum_{i=1}^n  ( e_i -  W_i^T)^T B^T B  ( e_i -  W_i^T) \\
&=& \sum_{i=1}^n  {\rm tr}\left ( (e_i -  W_i^T)^T B^T B  ( e_i -  W_i^T) \right )\\
&=& \sum_{i=1}^n  {\rm tr}\left (  B  ( e_i -  W_i^T) (e_i -  W_i^T)^T B^T \right )\\
&=&   {\rm tr}\left (  B  \left (\sum_{i=1}^n e_i e_i^T +  W_i^T W_i -  (e_i W_i)^T  -  e_i W_i \right ) B^T \right )\\
&=&   {\rm tr}\left (  B  \left (I +  W^T W -  W^T  -  W \right ) B^T \right )\\
&=&   {\rm tr}\left (  B  \left (I   -  W \right )^T  \left (I   -  W \right )  B^T \right )\\
\end{align}
$$
ラグランジュの未定乗数法により解くことを考えると，ラグランジュ関数$L(B, \lambda )$は
$$
L(B, \lambda ) := {\rm tr}\left (  B  \left (I   -  W \right )^T  \left (I   -  W \right ) B^T - \Lambda  (\frac {1} {n} B B^T - I ) \right )
$$
ただし，$\Lambda$は
$$
\Lambda = \left[\begin{array}{ccc}
            \lambda_1 & \ & \ & O\\
            \ & \lambda_2 & \ &  \\
            \ & \  & \ddots &  \\
            O & \ & \ &\lambda_L
        \end{array}\right] \quad
$$
$B$に関する微分を考えると(注:${\rm tr}(X^TCX)$の$X$についての微分は$(C+C^T)X$)
$$
\nabla_{B} L(B, \lambda ) = {\bf 0} \to   \left (I   -  W \right )^T  \left (I   -  W \right ) B^T = \Lambda  B^T
$$
以上から，$B = (\beta_1,  \beta_2, \cdots, \beta_n)$は$\left (I   -  W \right )^T  \left (I   -  W \right )$の固有値を小さいほうから$L$個とり，それに対応する長さ$n$の$L$個の固有ベクトルを$L$行に並べたものであることがわかる．なぜならば，$\left (I   -  W \right )^T  \left (I   -  W \right )$は実対称行列であることから異なる固有値の固有ベクトルは互いに直交しあうことにより，$\frac {1} {n} B B^T = I$が自動的に導かれる．また，最小化問題であるので，$\left (I   -  W \right )^T  \left (I   -  W \right )$の小さい順にならべたとき前から$L$個の固有値に対応する固有ベクトルがこの最小化問題の解であることが理解できる．

ただし，Step1の時と同様に自明な解${\bf 1}$を除くこと

```python
L = 2
eg, vs = np.linalg.eigh(np.dot(np.eye(sampleSize)-W.T, np.eye(sampleSize)-W))
##
indx = eg==eg
one = np.ones((sampleSize,1))
cnt = 0
for i in range(sampleSize):
    if cnt<L and  np.abs(np.dot(vs[:,i].reshape(1,sampleSize),one))<0.005*sampleSize:
        indx[i] = True
        cnt = cnt + 1
    else:
        indx[i] = False
B = sampleSize * vs[:,indx]

```

$B$が求まったので，さっそくグラフで描画してみよう

```python

for i in range(nLabel):
    plt.scatter(B[Labels==i,0],B[Labels==i,1], color = colors[i], marker = markers[i])
```


```python
%matplotlib inline

# A ton of imports
from IPython.display import display
import ipywidgets as widgets
from ipywidgets import interact, interact_manual, interactive, fixed
from IPython.core.pylabtools import figsize
import numpy as np
import matplotlib.pyplot as plt
import json
import matplotlib
import math

styles = json.load(open("styles/bmh_matplotlibrc.json"))
matplotlib.rcParams.update(styles)

figsize(11, 5)
```

    /usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The examples.directory rcparam was deprecated in Matplotlib 3.0 and will be removed in 3.2. In the future, examples will be found relative to the 'datapath' directory.
      self[key] = other[key]

# Modelo de Hodkin-Huxley

\begin{align}
 C_m \frac{dV}{dt} &= -g_{Na}m^3h(V-E_{Na}) - g_K n^4 (V - E_K) \label{eq:huxley_voltage} \\ \nonumber
                   &- g_L (V - E_L) + I_{ext} \\
 \frac{1}{\gamma(T)} \frac{dn}{dt} &= \alpha_n(V) (1 - n) - \beta_n(V)n = \frac{n^{\infty}(V) - n}{\tau_n(V)} \label{eq:huxley_sodio} \\
 \frac{1}{\gamma(T)} \frac{dm}{dt} &= \alpha_m(V) (1 - m) - \beta_m(V)m = \frac{m^{\infty}(V) - m}{\tau_m(V)} \label{eq:huxley_sodio_compuerta} \\
 \frac{1}{\gamma(T)} \frac{dh}{dt} &= \alpha_h(V) (1 - h) - \beta_h(V)h = \frac{h^{\infty}(V) - h}{\tau_h(V)} \label{eq:huxley_sodio_puerta} \\
 \tau_n(V) &= \frac{1}{\alpha_n(V) + \beta_n(V)} \\
 n_{\infty}(V) &= \frac{\alpha_n(V)}{\alpha_n(V) + \beta_n(V)}
\end{align}

Con [Wells]:
* $C_m = 1.0\ \mu\text{F/cm}^2$ la capacitancia de la membrana por unidad de área
* $V$ la diferencia de potencial en la membrana
* $g_{Na} = 120\ \text{mS/cm}^2$ la conductancia máxima por unidad de área para el sodio
* $g_{K} = 36\ \text{mS/cm}^2$ la conductancia máxima por unidad de área para el potasio
* $g_{L} = 0.3\ \text{mS/cm}^2$ la conductancia adicional, independiente del voltaje
* $E_{Na} = 55\ \text{mV}$ el voltaje de la batería en la compuerta de sodio
* $E_{K} = -72\ \text{mV}$ el voltaje de la batería en la compuerta de potasio
* $E_{L} = -49.387\ \text{mV}$ el voltaje filtrado
* $\gamma(T)$ es una constante de proporcionalidad temporal que depende de la temperatura $T$, a $6.3°$C vale $1$ [Cessac2009].

para el caso del calamar.  Los potenciales están medidos considerando que la membrana se encuentra en su potencial de reposo a los $V_r = -60\ \text{mV}$.  Las corrientes $I$ están medidas en $\mu$A/cm$^2$.

Las funciones de transición $\alpha(V)$ y $\beta(V)$, fueron determinadas experimentalmente.  Su forma general es:
\begin{align}
  \alpha(V) \text{ ó } \beta(V) = \frac{A + BV}{C + H e^{\left(\frac{V+D}{F}\right)}}
\end{align}
donde $V$ está medido con respecto al potencial de reposo $V_r$

Los valores medidos por Hodkin y Huxley para la compuerta de **potasio (K)**, fueron:
\begin{align}
  \alpha_n(V) &= \frac{0.01(10 - V)}{e^{\left(\frac{10-V}{10}\right)} - 1} & \beta_n(V) &= 0.125 e^{-\frac{V}{80}}
\end{align}
Observemos que cuando $V = 10$ la función está indefinida.  Utilizando la regla de L'Hopital, se calcula el límite:
\begin{align}
  \alpha_n(10) &= \frac{-0.01}{e^{\left(\frac{10-V}{10}\right)}(-0.1)}
               = \frac{-0.01}{-0.1} = 0.1
\end{align}

```python
# Programa las función alfa_n como función del voltaje V.
# Necesitarás definir un caso especial cuando V - 10 < epsilon
# Utiliza esta celda para ver como se comporta tu función en esta vecindad
# y elegir un valor de epsilon adecuado
def single_alpha_n(voltage: float) -> float:
    if math.isclose(voltage, 10):
        return 0.1
    else:
        up = 0.01 * (10 - voltage)
        down = math.exp((10 - voltage) / 10) - 1
        return up / down

alpha_n = np.vectorize(single_alpha_n)

@interact(step=widgets.FloatSlider(min=0.0001, max=0.1500, step=0.0010, value=0.1000))
def plotAlphaLimit(step):
    voltages = np.arange(9, 11, step)
    alpha = alpha_n(voltages)
    plt.plot(voltages, alpha)

```

    interactive(children=(FloatSlider(value=0.1, description='step', max=0.15, min=0.0001, step=0.001), Output()),…

```python
# Programa la funciones como función del voltaje V, para la compuerta de K
# Genera luego una gráfica de alfa_n y beta_n
beta_n = np.vectorize(lambda voltage: 0.125 * math.exp(-voltage / 80))

def plotAlpha():
    V = np.linspace(-40, 120)
    alpha = alpha_n(V)
    beta = beta_n(V)
    plt.plot(V, alpha, label="$\\alpha(V)$")
    plt.plot(V, beta, label="$\\beta(V)$")
    plt.title("Tazas de transición")
    plt.xlabel("$V$ con respecto a $V_r$ (mV)")
    plt.ylabel("$taza$ (ms$^{-1}$)")
    plt.legend()

plotAlpha()
```

Las funciones para el **sodio (Na)** son:
\begin{align}
  \alpha_m(V) &= \frac{0.1(25 - V)}{e^{\left(\frac{25-V}{10}\right)} - 1} & \beta_m(V) &= 4 e^{-\frac{V}{18}} \\
  \alpha_h(V) &= 0.07 e^{-\frac{V}{20}} & \beta_h(V) &= \frac{1}{e^{\left(\frac{30-V}{10}\right)} + 1}
\end{align}
Para $\alpha_m$ cuando $V = 25$, utilizamos la regla de L'Hopital para calcular el límite:
\begin{align}
  \alpha_m(25) &= \frac{-0.1}{e^{\left(\frac{25-V}{10}\right)}(-0.1)}
               = \frac{-0.1}{-0.1} = 1
\end{align}

```python
# Agrega las funciones correspondientes para la compuerta de sodio también,
# deduce sus firmas a partir de las llamadas en la función para graficar

def single_alpha_m(voltage: float) -> float:
    if math.isclose(voltage, 25):
        return 1
    else:
        up = 0.1 * (25 - voltage)
        down = math.exp((25 - voltage) / 10) - 1
        return up / down

alpha_m = np.vectorize(single_alpha_m)
alpha_h = np.vectorize(lambda voltage: 0.07 * math.exp(-voltage / 20))

beta_m = np.vectorize(lambda voltage: 4 * math.exp(-voltage / 18))
beta_h = np.vectorize(lambda voltage: 1 / (math.exp((30 - voltage) / 10) + 1))

# Dadas alfa y beta, calcular ahora y graficar n y tau para ambos canales.
tau_infinity = np.vectorize(lambda alpha, beta: 1 / (alpha + beta) )
x_infinity = np.vectorize(lambda alpha, beta: alpha / (alpha + beta) )
```

```python
def plotAlpha():
    V = np.linspace(-40, 120)

    alpha = alpha_n(V)
    beta = beta_n(V)

    n = x_infinity(alpha, beta)
    tau = tau_infinity(alpha, beta)

    plt.plot(V, n, label="$n_\\infty(V)$")
    plt.plot(V, tau, label="$\\tau_\\infty(V)$")

    plt.legend()

plotAlpha()
```

```python
# Graph
def plotParameters():
    fig, axes = plt.subplots(1, 2, figsize=(12, 6))
    V = np.linspace(-100, 100)

    alpha = alpha_n(V)
    beta = beta_n(V)
    axes[0].plot(V, x_infinity(alpha, beta), label="$n_{\infty}(V)$")
    axes[1].plot(V, tau_infinity(alpha, beta), label="$\\tau_{n}(V)$")

    alpha = alpha_m(V)
    beta = beta_m(V)
    axes[0].plot(V, x_infinity(alpha, beta), label="$m_{\infty}(V)$")
    axes[1].plot(V, tau_infinity(alpha, beta), label="$\\tau_{m}(V)$")

    alpha = alpha_h(V)
    beta = beta_h(V)
    axes[0].plot(V, x_infinity(alpha, beta), label="$h_{\infty}(V)$")
    axes[1].plot(V, tau_infinity(alpha, beta), label="$\\tau_{h}(V)$")

    axes[0].set_title("Tazas de transición")
    axes[0].set_xlabel("$V$ realtivo a $V_r$ (mV)")
    axes[0].set_ylabel("adimensional")
    axes[0].legend(loc=2)
    axes[1].set_title("Constantes temporales")
    axes[1].set_xlabel("$V$ realtivo a $V_r$ (mV)")
    axes[1].set_ylabel("$\\tau$ (ms)")
    axes[1].legend(loc=2)

plotParameters()
```

## Simulación con el método numérico de Euler

El método de Euler realiza una aproxima a la función por su tangente.  Dada una ecuación diferencial de la forma:

\begin{align}
  \frac{dy(t)}{dt} = f(y(t))
\end{align}

Partimos de un punto inicial $(t_0, y_0)$ y a partir de él, se calcula el valor de $y$ para el tiempo $t + \Delta t$ iterativamente como:

\begin{align}
  y(t + \Delta t) &= y(t) + \Delta t f(y(t))
\end{align}

El error aproximado por realizar esta aproximación es:
\begin{align}
  E =& \frac{1}{2} \frac{df(t)}{dt}(\Delta t)^2
\end{align}

Aplicando esta fórmula a la ecuación para los canales:

\begin{align}
 \frac{dn}{dt} &= \frac{n^{\infty}(V) - n}{\tau_n(V)}
\end{align}

Obtenemos:

\begin{align}
 n(t + \Delta t) =& n(t) + \Delta t \left[\frac{n^{\infty}(V)}{\tau_n} - \frac{n(t)}{\tau_n(V)} \right] \\
                 =& \left[ 1 - \frac{\Delta t}{\tau_n(V)} \right] n(t) + \frac{\Delta t}{\tau_n(V)}n^{\infty}(V)
\end{align}

Se obtienen fórmulas análogas para $m$ y $h$.  Todas juntas pueden ser escritas en forma matricial:

\begin{align}
  \begin{bmatrix}
  n(t + \Delta t) \\
  m(t + \Delta t) \\
  h(t + \Delta t)
  \end{bmatrix} =&
  \begin{bmatrix}
  (1 - \Delta t/\tau_n(V)) & 0 & 0 \\
  0 & (1 - \Delta t/\tau_m(V)) & 0 \\
  0 & 0 & (1 - \Delta t/\tau_h(V)) \\
  \end{bmatrix}
  \begin{bmatrix}
  n(t) \\
  m(t) \\
  h(t)
  \end{bmatrix} +
  \begin{bmatrix}
  (\Delta t / \tau_n(V)) n^{\infty}(V) \\
  (\Delta t / \tau_m(V)) m^{\infty}(V) \\
  (\Delta t / \tau_h(V)) h^{\infty}(V)
  \end{bmatrix}
\end{align}

Brevemente:

\begin{align}
  \boldsymbol{\Pi}(t + \Delta t) =& \boldsymbol{A}_\pi \boldsymbol{\Pi}(t) + \boldsymbol{B}_\pi
\end{align}

Se debe realizar el mismo procedimiento con la ecuación diferencial para el voltaje.  Para simplificar la notación, introduzcamos:

\begin{align}
  G_{Na} &= g_{Na}m^3h   &   G_{K} &= g_K n^4
\end{align}

Entonces:
\begin{align}
 C_m \frac{dV}{dt} &= -g_{Na}m^3h(V-E_{Na}) - g_K n^4 (V - E_K) - g_L (V - E_L) + I_{ext} \\
 \frac{dV}{dt} &= -\frac{G_{Na}}{C_m}(V-E_{Na}) - \frac{G_K}{C_m} (V - E_K) - \frac{g_L}{C_m} (V - E_L) + \frac{1}{C_m}I_{ext}
\end{align}

Utilizando el método de Euler:

\begin{align}
 V(t + \Delta t) &= V(t) - \frac{\Delta t}{C_m} \left[ G_{Na}(V-E_{Na}) + G_K (V - E_K) + g_L (V - E_L) + I_{ext}(t) \right] \\
 V(t + \Delta t) &= V(t) - \Delta t \begin{bmatrix} \frac{G_{Na}}{C_m} & \frac{G_K}{C_m} & \frac{g_L}{C_m} \end{bmatrix}  \begin{bmatrix}
                 V(t)-E_{Na} \\
                 V(t)- E_K \\
                 V(t) - E_L
                \end{bmatrix} + \frac{\Delta t}{C_m}I_{ext}(t)
\end{align}

Para los valores por defecto propuestos en la celda siguiente, programar una simulación utilizando el método de Euler produce la figura siguiente:

```python
def plotSimulation(resultado):
    fig, axes = plt.subplots(2, 2, figsize=(12,10))
    tiempo = resultado['T']
    axes[0][0].plot(tiempo, resultado['V'])

    axes[0][0].set_title("Voltaje en la membrana")
    axes[0][0].set_xlabel("t (ms)")
    axes[0][0].set_ylabel("Voltaje relativo en la membrana (mV)")

    axes[0][1].plot(tiempo, resultado['GK'], label="$G_{K}$")
    axes[0][1].plot(tiempo, resultado['GNa'], label="$G_{Na}$")

    axes[0][1].set_title("Conductancia de los canales")
    axes[0][1].set_ylabel("Conductancia (mS/cm$^2$)")
    axes[0][1].set_xlabel("t (ms)")
    axes[0][1].legend()

    axes[1][0].plot(tiempo, resultado['n'], label="$n$")
    axes[1][0].plot(tiempo, resultado['m'], label="$m$")
    axes[1][0].plot(tiempo, resultado['h'], label="$h$")
    axes[1][0].set_title("Tazas de transición")
    axes[1][0].set_xlabel("t (ms)")
    axes[1][0].set_ylabel("adimensional")
    axes[1][0].legend()

    axes[1][1].plot(tiempo, resultado['tauN'], label="$\\tau_n$")
    axes[1][1].plot(tiempo, resultado['tauM'], label="$\\tau_m$")
    axes[1][1].plot(tiempo, resultado['tauH'], label="$\\tau_h$")
    axes[1][1].set_title("Constantes temporales")
    axes[1][1].set_xlabel("t (ms)")
    axes[1][1].set_ylabel("$\\tau$ (ms)")
    axes[1][1].legend()
```

```python
parametros = {
    'DeltaT' : 0.01,   # ms
    'maxT' : 50.0,     # ms
    'V0' : 0.0,        # mV
    'gNa' : 120.0,     # mS/cm2
    'gK' : 36.0,
    'gL' : 0.3,
    'Cm' : 1.0,        # micro F/cm2
    'ENa' : 115.0,     # Utilizando la convensión original de Hodkin-Huxley
    'EK' : -12.0,
    'EL' : 10.613
    }

def makeIext(t0 = 10.0, tfin = 16.0, current = 6.5):
    def Iext(t):
        """ Devuelve el valor de la corriente aplicada a la membrana, al tiempo t, en microampers.
        """
        if t > t0 and t < tfin:
            return current    # micro A/cm2
        else:
            return 0
    return Iext
```

```python
# Implementa aquí el algoritmo de intregración numérica de Euler para calcular V(t)
##
# Asegúrate de que, al ejecutar la simulación con los parámetros por defecto,
# se reproduce la imagen de arriba.
##
# Después prueba con pulsos de t en 10->30, con corrientes de 10 y -10 microamperes
# ¿Qué observas?
##
# Inserta al final una celda con tus comentarios y resultados

def EulerHodkinHuxley(**parameters):
    time = np.arange(0, parameters["maxT"], parameters["DeltaT"])
    voltages = np.zeros(shape=time.shape)
    Gk = np.zeros(shape=time.shape)
    GNa = np.zeros(shape=time.shape)

    functions = np.zeros(shape=(time.shape[0], 3))
    taus = np.zeros(shape=(functions.shape))

    v0 = parameters["V0"]
    dt = parameters["DeltaT"]

    gNa = parameters["gNa"]
    gK = parameters["gK"]
    gL = parameters["gL"]
    Cm = parameters["Cm"]
    es = np.array([parameters["ENa"], parameters["EK"], parameters["EL"]])

    functions[0] = [
        x_infinity(alpha_n(v0),  beta_n(v0)),
        x_infinity(alpha_m(v0), beta_m(v0)),
        x_infinity(alpha_h(v0), beta_h(v0))
    ]

    for i, t in enumerate(time):
        # Try n, m, h
        v = voltages[i]

        an = alpha_n(v)
        am = alpha_m(v)
        ah = alpha_h(v)

        bn = beta_n(v)
        bm = beta_m(v)
        bh = beta_h(v)

        tn = tau_infinity(an, bn)
        tm = tau_infinity(am, bm)
        th = tau_infinity(ah, bh)

        inverse_taus = np.array([1 / tn, 1 / tm, 1 / th])

        infis = np.array(
            [x_infinity(an, bn), x_infinity(am, bm), x_infinity(ah, bh)])

        try:
            # check this part
            A = (1 - dt * inverse_taus)
            B = dt * inverse_taus * infis
            functions[i + 1] = (A * functions[i]) + B
            taus[i] = np.array([tn, tm, th])
        except IndexError:
            pass

        # Try v(t)
        n = functions[i][0]
        m = functions[i][1]
        h = functions[i][2]

        GNa[i] = gNa * m ** 3 * h
        Gk[i] = gK * n ** 4
        try:
            horizontal = (1 / Cm) * np.matrix([GNa[i], Gk[i], gL])
            vertical = v - es
            A = (horizontal).dot(vertical)
            B = (dt / Cm) * parameters["Iext"](t)
            voltages[i + 1] = v - (dt * A) + B
        except IndexError:
            pass

        #print(f"t = {t}")
        #print(f"v = {v}")
        #print(f"Gna = {GNa[i]}, Gk = {Gk[i]}")
        #print(f"n = {n}, m = {m}, h = {h}")
        #print(f"tn = {taus[i, 0]}, tm = {taus[i, 1]}, th = {taus[i, 2]}")
        #print()

    return {
        "T": time,
        "V": voltages,
        "GK": Gk,
        "GNa": GNa,
        "n": functions[:, 0],
        "m": functions[:, 1],
        "h": functions[:, 2],
        "tauN": taus[:, 0],
        "tauM": taus[:, 1],
        "tauH": taus[:, 2]
    }
```

```python
@interact_manual(
    V0 = widgets.FloatSlider(min=-90, max=120, step=1, value=parametros['V0']),
    current = widgets.FloatSlider(min=-10, max=10, step=0.1, value=2.4),
    t0 = 10.0,
    tfin = 16.0
)
def simulaHodkinHuxley(V0, t0, tfin, current):
    parametros['Iext'] = makeIext(t0, tfin, current)
    parametros['V0'] = V0
    resultado = EulerHodkinHuxley(**parametros)
    plotSimulation(resultado)
```

    interactive(children=(FloatSlider(value=0.0, description='V0', max=120.0, min=-90.0, step=1.0), FloatSlider(va…

```python
from IPython.core.display import HTML
def css_styling():
    styles = open("styles/custom.css", "r").read() #or edit path to custom.css
    return HTML(styles)
css_styling()
```

<style>
    img {
        display: block;
		margin-left: auto;
		margin-right: auto;
	}
</style>


# Scientific computing

- [Scientific computing](day2/scicomp.ipynb):
    - [Numpy](#Numpy): advanced array operations
    - Scipy introduction: from linear algebra to image analisys
    - Simpy: symbolic math
    - Networks with networkx: centrality computation
    - Fitting a curve, cleaning a signal.

https://docs.scipy.org/doc/numpy/user/basics.html

Numpy: advanced array operations

Scipy introduction: from linear algebra to image analisys

Simpy: symbolic math

Further reading:

- [https://scipy-lectures.github.io/index.html](https://scipy-lectures.github.io/index.html) - served as my main inspiration source for this chapter.
- [http://docs.scipy.org/doc/scipy-0.15.1/reference/](http://docs.scipy.org/doc/scipy-0.15.1/reference/) - The Scipy reference guide, containing a very good tutorial for each of the libraries.
- [http://www.scipy.org/topical-software.html](http://www.scipy.org/topical-software.html) This page is containing links to some of the most common Python modules. However it is far from complete, for example PIL, a library commonly used in image processing in Python, is not listed.

## Numpy

Most of the time when doing scientific computing speed is critical. There are several reasons Numpy is much faster than standard Python. The most important is that Numpy enforces strong typing, while Python is a dynamic typed language. That translates in Numpy using less heap space for representing data. Because array operations are the core of scientific computing, we will look at this library in greater depth.

```python
import numpy as np
L = range(1000)
%timeit [i**2 for i in L]
a = np.arange(1000)
%timeit a**2
print(type(L[1]))
print(a.dtype)
```

    292 µs ± 2.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    2.55 µs ± 27.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    <class 'int'>
    int64

```python
import numpy as np
##Get help!
#np.lookfor('create array')
#np.array?
#np.arr*?
```

```python
a = np.array([0, 1, 2, 3])
b = np.array([[0, 1, 2], [3, 4, 5]])
c = np.array([[[1], [2]], [[3], [4]]])
print(a)
print(b)
print(b.shape)
```

    [0 1 2 3]
    [[0 1 2]
     [3 4 5]]
    (2, 3)

#### Data types

There are 5 basic numerical types representing booleans (bool), integers (int), unsigned integers (uint) floating point (float) and complex. Those with numbers in their name indicate the bitsize of the type (i.e. how many bits are needed to represent a single value in memory).

```python
import numpy as np
x = np.float32(1.0)
print(x)

y = np.int_([1,2,4])
print(y)

z = np.arange(3, dtype=np.uint8)
print(z)

print(np.array([1, 2, 3], dtype='f'))
print(z.astype(float))

print(z.dtype)

d = np.dtype(int)
# d = 1 # question: why does it fail?
d = np.uint8(1)
print(d)

print(np.issubdtype(d, np.integer))

```

    1.0
    [1 2 4]
    [0 1 2]
    [1. 2. 3.]
    [0. 1. 2.]
    uint8
    1
    True

#### Array creation

```python
# extrinsic
x = np.array([2,3,1,0])
print(x)
x = np.array([[ 1., 2.], [ 0., 0.], [ 1., 3.]])
print(x)

#intrinsic
b = np.arange(1, 9, 2)
print(b)
c = np.linspace(0, 1, 6)
print(c)

a = np.ones((3, 3))
b = np.zeros((2, 2))
c = np.eye(3)
print("ones, zeros, eyes:")
print(a,"\n", b,"\n",c)
```

    [2 3 1 0]
    [[1. 2.]
     [0. 0.]
     [1. 3.]]
    [1 3 5 7]
    [0.  0.2 0.4 0.6 0.8 1. ]
    ones, zeros, eyes:
    [[1. 1. 1.]
     [1. 1. 1.]
     [1. 1. 1.]]
     [[0. 0.]
     [0. 0.]]
     [[1. 0. 0.]
     [0. 1. 0.]
     [0. 0. 1.]]

```python
%pylab inline
import matplotlib.pyplot as plt

x = np.diag(np.array([1, 2, 3, 4]))
print(x)
x = np.random.rand(4)       # uniform in [0, 1]
print(x)
x = np.random.rand(4)
print(x)
x = np.random.randn(4,3)      # Gaussian
print(x)
#np.random.seed(1234)
image = np.random.rand(30, 30)
plt.imshow(image, cmap=plt.cm.hot)
plt.colorbar()
```

```python
# more generators
print(np.linspace(1., 4., 6))
print(np.indices((3,2)))
print(np.arange(35).reshape(5,7))
print(np.triu(np.ones((3, 3)), 1))
print(np.triu(np.ones((3, 3)), 2))
print(np.tile(np.arange(0, 40, 10), (3, 1)))
```

    [1.  1.6 2.2 2.8 3.4 4. ]
    [[[0 0]
      [1 1]
      [2 2]]

     [[0 1]
      [0 1]
      [0 1]]]
    [[ 0  1  2  3  4  5  6]
     [ 7  8  9 10 11 12 13]
     [14 15 16 17 18 19 20]
     [21 22 23 24 25 26 27]
     [28 29 30 31 32 33 34]]
    [[0. 1. 1.]
     [0. 0. 1.]
     [0. 0. 0.]]
    [[0. 0. 1.]
     [0. 0. 0.]
     [0. 0. 0.]]
    [[ 0 10 20 30]
     [ 0 10 20 30]
     [ 0 10 20 30]]

#### Indexing, slicing and selection

```python
a = np.arange(10)
print(a[0], a[2], a[-1], a[-3])
print(a[2:5], a[2:], a[:-2], a[::2], a[2::2])
```

    0 2 9 7
    [2 3 4] [2 3 4 5 6 7 8 9] [0 1 2 3 4 5 6 7] [0 2 4 6 8] [2 4 6 8]

```python
a = np.diag(np.arange(3))
print(a)
print(a[1, 1])

a[2, 1] = 10 # !third line, !second column
print("\na = \n",a)
print("\na[1] = \n",a[1])
print("\na[:,1], a[1,:] = \n",a[:,1], a[1,:])
print("\na[1:,2:] = \n",a[1:,1:])
```

    [[0 0 0]
     [0 1 0]
     [0 0 2]]
    1

    a =
     [[ 0  0  0]
     [ 0  1  0]
     [ 0 10  2]]

    a[1] =
     [0 1 0]

    a[:,1], a[1,:] =
     [ 0  1 10] [0 1 0]

    a[1:,2:] =
     [[ 1  0]
     [10  2]]

```python
# array indexes
x = np.arange(10,1,-1)
print(x)
print(x[np.array([3, 3, 1, 8])])
print(x[np.array([3,3,-3,8])])
print(x[np.array([[1,1],[2,3]])])
```

    [10  9  8  7  6  5  4  3  2]
    [7 7 9 2]
    [7 7 4 2]
    [[9 9]
     [8 7]]

```python
a = np.random.randint(0, 20 + 1, 15)
print(a)
print(a%3==0)
print(a[a%3==0])
a[a % 3 == 0] = -1
print(a)
```

    [10  4 10  3  0 14 18 15  7 14 17 19 18 17 15]
    [False False False  True  True False  True  True False False False False
      True False  True]
    [ 3  0 18 15 18 15]
    [10  4 10 -1 -1 14 -1 -1  7 14 17 19 -1 17 -1]

```python
a = np.arange(0, 100, 10)
print(a)
print(a[[2, 3, 2, 4, 2]])
a[[9, 7]] = -100
print(a)

a = np.arange(0,100,10)
idx = np.array([[3,4],[9,7]])
print(a[idx])
```

    [ 0 10 20 30 40 50 60 70 80 90]
    [20 30 20 40 20]
    [   0   10   20   30   40   50   60 -100   80 -100]
    [[30 40]
     [90 70]]

```python
y = np.arange(35).reshape(5,7)
print(y)
print(y[np.array([0,2,4]), np.array([0,1,2])])
# multidimensional arrays have a less intuitive form of indexing
# first value of the resultant array is y[0,0]. The next value is y[2,1], and the last is y[4,2].
```

    [[ 0  1  2  3  4  5  6]
     [ 7  8  9 10 11 12 13]
     [14 15 16 17 18 19 20]
     [21 22 23 24 25 26 27]
     [28 29 30 31 32 33 34]]
    [ 0 15 30]

```python
print(y[np.array([0,2,4]), 1])
print(y[np.array([0,2,4])])
```

    [ 1 15 29]
    [[ 0  1  2  3  4  5  6]
     [14 15 16 17 18 19 20]
     [28 29 30 31 32 33 34]]

Task:
- What does this do:

```python
# What does it do?
print(np.arange(0, 51, 10)[:, np.newaxis])
print(np.arange(6))
print(np.arange(6) + np.arange(0, 51, 10)[:, np.newaxis])
```

    [[ 0]
     [10]
     [20]
     [30]
     [40]
     [50]]
    [0 1 2 3 4 5]
    [[ 0  1  2  3  4  5]
     [10 11 12 13 14 15]
     [20 21 22 23 24 25]
     [30 31 32 33 34 35]
     [40 41 42 43 44 45]
     [50 51 52 53 54 55]]

```python
# How does it work?
# Print the primes!
def get_primes():
    primes = np.ones((100,), dtype=bool)
    primes[:2] = 0
    N_max = int(np.sqrt(len(primes)))
    for j in range(2, N_max):
        primes[2*j::j] = 0
    return primes
print(get_primes())

```

    [False False  True  True False  True False  True False False False  True
     False  True False False False  True False  True False False False  True
     False False False False False  True False  True False False False False
     False  True False False False  True False  True False False False  True
     False False False False False  True False False False False False  True
     False  True False False False False False  True False False False  True
     False  True False False False False False  True False False False  True
     False False False False False  True False False False False False False
     False  True False False]

#### Broadcasting, assignment, structured arrays

```python
a = np.arange(10)
b = a[::2]
b[0] = 12
print(a, b)
print(np.may_share_memory(a, b))

a = np.arange(10)
c = a[::2].copy()  # force a copy
c[0] = 12
print(a, c)
print(np.may_share_memory(a, c))
```

    [12  1  2  3  4  5  6  7  8  9] [12  2  4  6  8]
    True
    [0 1 2 3 4 5 6 7 8 9] [12  2  4  6  8]
    False

```python
#Array operations
a = np.array([1, 2, 3, 4])
print("a: ", a)
print("a + 1, 2**a: ", a + 1, 2**a)

b = np.ones(4) + 1
print("a: ",a)
print("b: ",b)
print("a - b, a * b: ", a - b, a * b)

j = np.arange(5)
print ("2**(j + 1) - j: ", 2**(j + 1) - j)
```

    a:  [1 2 3 4]
    a + 1, 2**a:  [2 3 4 5] [ 2  4  8 16]
    a:  [1 2 3 4]
    b:  [2. 2. 2. 2.]
    a - b, a * b:  [-1.  0.  1.  2.] [2. 4. 6. 8.]
    2**(j + 1) - j:  [ 2  3  6 13 28]

```python
c = np.ones((3, 3))
print("\n2*c + 1: \n", 2*c + 1)
print("\nc * c: \n", c * c)
print("\nc + c: \n", c + c)
print("\nc.dot(c): \n", c.dot(c)) #matrix multiplication!
```

    2*c + 1:
     [[3. 3. 3.]
     [3. 3. 3.]
     [3. 3. 3.]]

    c * c:
     [[1. 1. 1.]
     [1. 1. 1.]
     [1. 1. 1.]]

    c + c:
     [[2. 2. 2.]
     [2. 2. 2.]
     [2. 2. 2.]]

    c.dot(c):
     [[3. 3. 3.]
     [3. 3. 3.]
     [3. 3. 3.]]

```python
a = np.arange(5)
print(np.sin(a), np.log(a), np.exp(a))
```

    [ 0.          0.84147098  0.90929743  0.14112001 -0.7568025 ] [      -inf 0.         0.69314718 1.09861229 1.38629436] [ 1.          2.71828183  7.3890561  20.08553692 54.59815003]

    /media/sergiu/lappie/programs/miniconda3/envs/pycourse/lib/python3.7/site-packages/ipykernel_launcher.py:3: RuntimeWarning: divide by zero encountered in log
      This is separate from the ipykernel package so we can avoid doing imports until

```python
# shape manipulation

x = np.array([1, 2, 3])
print(x), print()
print(x[:, np.newaxis]), print()
print(x[np.newaxis, :]), print()
```

    [1 2 3]

    [[1]
     [2]
     [3]]

    [[1 2 3]]

    (None, None)

```python
# flatten, reshape
a = np.array([[1, 2, 3], [4, 5, 6]])
print(a)
print(a.ravel()), print()

b = a.ravel()
b = b.reshape((2, 3)) #!reshape may return a copy!
print(b)
```

    [[1 2 3]
     [4 5 6]]
    [1 2 3 4 5 6]

    [[1 2 3]
     [4 5 6]]

```python
# open (i.e. not fleshed out) mesh-grid when indexed
x, y = np.ogrid[0:5, 0:5]
print(x)
print(y), print()

# mesh grid (closed)
x, y = np.mgrid[0:5, 0:5]
print(x)
print(y)
```

    [[0]
     [1]
     [2]
     [3]
     [4]]
    [[0 1 2 3 4]]

    [[0 0 0 0 0]
     [1 1 1 1 1]
     [2 2 2 2 2]
     [3 3 3 3 3]
     [4 4 4 4 4]]
    [[0 1 2 3 4]
     [0 1 2 3 4]
     [0 1 2 3 4]
     [0 1 2 3 4]
     [0 1 2 3 4]]

```python
# Multidimensional transpose matrices

a = np.arange(4*3*2).reshape(4, 3, 2)
print(a), print()

print(a.shape, a[0, 2, 1])
b = a.transpose(1, 2, 0)
print(b.shape, b[2, 1, 0])
```

    [[[ 0  1]
      [ 2  3]
      [ 4  5]]

     [[ 6  7]
      [ 8  9]
      [10 11]]

     [[12 13]
      [14 15]
      [16 17]]

     [[18 19]
      [20 21]
      [22 23]]]

    (4, 3, 2) 5
    (3, 2, 4) 5

```python
# resizing behaves differently if the array is row-major vs column major
x = np.array([[0, 1], [2, 3]], order='C')
#x = np.array([[0, 1], [2, 3]]
#x = np.array([[0, 1], [2, 3]], order='F')
print(x)
x.resize((2, 1))
print(x)
```

    [[0 1]
     [2 3]]
    [[0]
     [1]]

```python
# sorting matrices, "finally"!

a = np.array([[4, 3, 5], [1, 2, 1]])
b = np.sort(a, axis=1) #sorting per row
print(a)
print(b)
a.sort(axis=1)
print(a), print()

a = np.array([4, 3, 1, 2])
j = np.argsort(a)
print(j, a[j]), print()

# sorting structured arrays
# oh, about structured arrays ...
# https://docs.scipy.org/doc/numpy/user/basics.rec.html
a = np.array([('a', 2), ('c', 1)], dtype=[('x', 'S1'), ('y', int)])
print(a)
a.sort(order='y')
print(a)
```

    [[4 3 5]
     [1 2 1]]
    [[3 4 5]
     [1 1 2]]
    [[3 4 5]
     [1 1 2]]

    [2 3 1 0] [1 2 3 4]

    [(b'a', 2) (b'c', 1)]
    [(b'c', 1) (b'a', 2)]

#### Reductions

```python
x = np.array([1, 2, 3, 4])
print("unidimensional:")
print(x)
print(np.sum(x), x.sum(), x.sum(axis=0))

x = np.array([[1, 1], [2, 2]])
print("multidimensional:")
print(x)
print(x.sum(axis=0))   # columns (first dimension)
print(x[:, 0], x[:, 0].sum()) # column sum, selected column only
print(x.sum(axis=1))  # rows (second dimension)
print(x[0, :], x[0, :].sum()) # row sum, selected column only
```

    unidimensional:
    [1 2 3 4]
    10 10 10
    multidimensional:
    [[1 1]
     [2 2]]
    [3 3]
    [1 2] 3
    [2 4]
    [1 1] 2

Task: What does it do?

```python
x = np.random.rand(2,2,2)
print(x)
print("\nx.sum(axis=2)[0,1] =", x.sum(axis=2)[0,1])
print("\nx[0,1,:].sum() =", x[0,1,:].sum())
```

    [[[0.20420415 0.28556622]
      [0.32451766 0.95174246]]

     [[0.14594388 0.23950013]
      [0.08366297 0.87672861]]]

    x.sum(axis=2)[0,1] = 1.2762601136179088

    x[0,1,:].sum() = 1.2762601136179088

```python
x = np.array([1, 3, 2])
print(x)
print(x.min(), x.max(), x.argmin(), x.argmax())

print(np.all([True, True, False]), np.any([True, True, False]))

x = np.array([1, 2, 3, 1])
y = np.array([[1, 2, 3], [5, 6, 1]])
print(x.mean(), x.std(), np.median(x), np.median(y, axis=-1))
```

    [1 3 2]
    1 3 0 1
    False True
    1.75 0.82915619758885 1.5 [2. 5.]

```python
a = np.zeros((100, 100))
print(np.any(a != 0), np.all(a == a))

a = np.array([1, 2, 3, 2])
b = np.array([2, 2, 3, 2])
c = np.array([6, 4, 4, 5])
print(((a <= b) & (b <= c)).all())
```

    False True
    True

Task:
- Replace all values greater than 25 with 9 and all values smaller than 10 with 29.
- Use numpy preferentially!

```python
np.random.seed(100)
x = np.random.uniform(1,50, 20)
x
```

    array([27.62684215, 14.64009987, 21.80136195, 42.39403048,  1.23122395,
            6.95688692, 33.86670515, 41.466785  ,  7.69862289, 29.17957314,
           44.67477576, 11.25090398, 10.08108276,  6.31046763, 11.76517714,
           48.95256545, 40.77247431,  9.42510962, 40.99501269, 14.42961361])

Task: Create a rank array having the same shape as x.

```python
np.random.seed(100)
a = np.random.randint(50, size=[2,5])
a
```

    array([[ 8, 24,  3, 39, 23],
           [15, 48, 10, 30, 34]])

### Scipy

Scipy is Python's favorite library for scientific computing. Some of its functionality overlaps with Numpy and Scikit-learn, but in general Scipy modules are viewed as the equivalent of Matlab's standard toolboxes. One is supposed to use it in conjunction with the so call "Scipy-stack" so for example it is best to deal array functionality with Numpy and machine learning with Scikit-learn. We will quickly go through several more common uses of Scipy, while it is good to keep in mind that we are barely scratching the skin of actual scientific computing.
http://www.c3se.chalmers.se/common/python_course_2012/Lecture5_SciPy_2012.pdf

#### [scipy.linalg](http://docs.scipy.org/doc/scipy/reference/linalg.html#scipy.linalg) - Singuar Value Decomposition

There are three main uses of this module: solving linear equations, solving eigenvalues problems and matrix factorizations.

One of the most common techniques for data summarization is Singuar Value Decomposition (SVD) and Partial Component Analysis (PCA). We will solve one SVD task here, and a PCA task later in the machine learning section. Keep in mind that most machine learning algorithms in scikit-learn are wrappers for the basic functions in this scipy module. From the linear algebra perspective PCA is a matrix decomposition method. Several other such methods are available in the linalg module.

If X is a matrix with each variable in a column and each observation in a row then the SVD is
$$X = U S V$$
where the columns of U are orthogonal (left singular vectors), the columns of V are orthogonal (right singluar vectors) and S is a diagonal matrix of zeroes with main diagonal s (singular values).

In the example below, most of the variation in the dataset is explained by the first two singular values, corresponding to the first two features.

Obs:

scipy.linalg.orth(A) - uses SVD to find an orthonormal basis for A.

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from scipy import linalg
from sklearn.datasets import load_iris

iris = load_iris()
# print(iris.feature_names, iris.target_names)
#print(iris.DESCR)
#print(iris.data)

# perform SVD
A = iris.data
U, s, V = linalg.svd(A)
print("U.shape, V.shape, s.shape: ", U.shape, V.shape, s.shape)
print("Singular values:", s)
#a = U.dot(eye())

# transform back to A
S = linalg.diagsvd(s, 150, 4)
At = np.dot(U, np.dot(S, V))
print("Closeness test: ", np.allclose(A, At))
# np.allclose?

fig, axes = plt.subplots(1, 2, figsize=(15,5))

# fig = plt.figure()
ax1 = axes[0]
colors = np.array(['blue','red','black'])
labels = np.array(['setosa','versicolour','verginica'])
ax1.scatter(U[:,0], U[:,1], color = colors[iris.target])
ax1.set_xlabel("First singular vector")
ax1.set_ylabel("Second singular vector")
ax1.legend()

ax2 = axes[1]
colors = np.array(['blue','red','black'])
labels = np.array(['setosa','versicolour','verginica'])
ax2.scatter(A[:,0], A[:,1], color = colors[iris.target])
ax2.set_xlabel("First feature vector")
ax2.set_ylabel("Second feature vector")
ax2.legend()
```

#### scipy.signal and scipy.fftpack: Signal theory

Signal processing is useful in order to interpret the data of many measuring instruments, especially if there is a time delayed response. We are performing a simple example, but for those that want to learn more applications of Python for signal processing I reccomend a number of online IPython courses.

- [http://nbviewer.ipython.org/github/calebmadrigal/FourierTalkOSCON/tree/master/](http://nbviewer.ipython.org/github/calebmadrigal/FourierTalkOSCON/tree/master/): Basic entry level course focused only on FFT

- [http://nbviewer.ipython.org/github/unpingco/Python-for-Signal-Processing/tree/master/](http://nbviewer.ipython.org/github/unpingco/Python-for-Signal-Processing/tree/master/): A more complex and complete course of signal processing with Python.

A small example would be a noisy signal whose frequency is unknown to the observer, who only knows the sampling time step. The signal is supposed to come from a real function so the Fourier transform will be symmetric. The scipy.fftpack.fftfreq() function will generate the sampling frequencies and scipy.fftpack.fft() will compute the fast Fourier transform:

```python
%matplotlib inline
import numpy as np
from scipy import fftpack
import matplotlib.pyplot as plt

time_step = 0.22
period = 5.
time_vec = np.arange(0, 20, time_step)
sig = np.sin(2 * np.pi / period * time_vec) + 0.5 * np.random.randn(time_vec.size)

from scipy import fftpack

#print(sig.size)
sample_freq = fftpack.fftfreq(sig.size, d=time_step)
sig_fft = fftpack.fft(sig)
pidxs = np.where(sample_freq > 0)
freqs, power = sample_freq[pidxs], np.abs(sig_fft)[pidxs]
freq = freqs[power.argmax()]
print("Determined frequency:",freq)

sig_fft[np.abs(sample_freq) > freq] = 0
main_sig = fftpack.ifft(sig_fft)#Discrete inverse Fourier transform

fig = plt.figure()

ax1 = fig.add_subplot(311)
ax1.plot(time_vec,sig)
ax1.set_title('Signal')

ax2 = fig.add_subplot(312)
ax2.plot(freqs, power)
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('power')
ax2.set_title('Peak frequency')

ax3 = fig.add_subplot(313)
ax3.plot(time_vec,main_sig)
ax1.set_title('Cleaned signal')
```

#### scipy.optimize: Local and global optimization, fitting and root finding

In the statistics chapter we use this package for line fitting. We also have a self standing optimization chapter where we will get back to this module.

We estimated the parameters of a function by performing an error minimization. An optimization problem complexity is dependent on several factors:
- Do you intend a local or a global optimization?
- Is the function linear or nonlinear?
- Is the function convex or not?
- Can a gradient be computed?
- Can the Hessian matrix be computed?
- Do we perform optimization under constraints?
Scipy does not cover all solvers efficiently but there are several Python packages specialized for certain classes of optimization problems. In general though heavy optimization is solved with dedicated programs, many of whom have language bindings for Python.

To exemplify, we use Newton's optimization to find the minima of a nonlinear function. (also covered in the optimization chapter)

```python
import numpy as np
import scipy.optimize as optimize

def f(x):   # The rosenbrock function
    return .5*(1 - x[0])**2 + (x[1] - x[0]**2)**2
def fprime(x):
    return np.array((-2*.5*(1 - x[0]) - 4*x[0]*(x[1] - x[0]**2), 2*(x[1] - x[0]**2)))
print optimize.fmin_ncg(f, [2, 2], fprime=fprime)

def hessian(x): # Computed with sympy
    return np.array(((1 - 4*x[1] + 12*x[0]**2, -4*x[0]), (-4*x[0], 2)))
print optimize.fmin_ncg(f, [2, 2], fprime=fprime, fhess=hessian)

%matplotlib inline
from matplotlib import cm
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection='3d')
X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)
Z = .5*(1 - X)**2 + (Y - X**2)**2
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.coolwarm,
        linewidth=0, antialiased=False)
ax.set_zlim(-1000.01, 1000.01)
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()
```

#### scipy.interpolate: Cubic interpolation

Interpolation is useful when we have sampled a function but want to approximate its values on different points. A well known class of interpolation functions are the splines, most commonly three spline curves are combined in order to interpolate a smooth curved line between two datapoints.

```python
%matplotlib inline
import numpy as np
from scipy.interpolate import interp1d
import pylab as pl

measured_time = np.linspace(0, 1, 10)
noise = 0.1 * np.random.randn(10)
measures = np.sin(2 * np.pi * measured_time) + noise

linear_interp = interp1d(measured_time, measures)
computed_time = np.linspace(0, 1, 50)
linear_results = linear_interp(computed_time)
cubic_interp = interp1d(measured_time, measures, kind='cubic')
cubic_results = cubic_interp(computed_time)

pl.plot(measured_time, measures, 'o', ms=6, label='measures')
pl.plot(computed_time, linear_results, label='linear interp')
pl.plot(computed_time, cubic_results, label='cubic interp')
pl.legend()
```

#### scipy.integrate: Integration and ODE solvers

This submodule is useful for summing up function values over intervals (integration) and solving ordinary differential equations. Partial differential equations are not covered and require other Python packages. As a quick example, we solve a case of Michaelis-Menten enzime kinetics.

```python
%matplotlib inline
#from scipy import *
import scipy.integrate as integrate

'''
Slightly modified from a sample code generated by this program,
that formulates a solver for different cases of enzime reactions:
http://code.google.com/p/kinpy/

## Reaction ##

#Michaelis-Menten enzyme kinetics.

E + S <-> ES
ES <-> E + P

## Mapping ##

E       0        -1*v_0(y[1], y[0], y[2]) +1*v_1(y[3], y[0], y[2])
S       1        -1*v_0(y[1], y[0], y[2])
ES      2        +1*v_0(y[1], y[0], y[2]) -1*v_1(y[3], y[0], y[2])
P       3        +1*v_1(y[3], y[0], y[2])
'''

dy = lambda y, t: array([\
 -1*v_0(y[1], y[0], y[2]) +1*v_1(y[3], y[0], y[2]),\
 -1*v_0(y[1], y[0], y[2]),\
 +1*v_0(y[1], y[0], y[2]) -1*v_1(y[3], y[0], y[2]),\
 +1*v_1(y[3], y[0], y[2])\
])

#Initial concentrations:
y0 = array([\
#E
0.6,\
#S
1.2,\
#ES
3.0,\
#P
0.2,\
])

#E + S <-> ES
v_0 = lambda S, E, ES : k0 * E**1 * S**1 - k0r * ES**1
k0 = 1.2
k0r = 1.5

#ES <-> E + P
v_1 = lambda P, E, ES : k1 * ES**1 - k1r * E**1 * P**1
k1 = 0.9
k1r = 1.9

t = arange(0, 10, 0.01)
Y = integrate.odeint(dy, y0, t)

import pylab as pl

pl.plot(t, Y, label='y')
```

#### [scipy.ndimage](https://docs.scipy.org/doc/scipy/reference/ndimage.html) - Image processing

This module is useful for containing functions for multidimensional image manipulation. It mainly contains filters, interpolation and morphology functions.

- https://scipy-lectures.org/advanced/image_processing/index.html
- https://scipy-lectures.org/packages/scikit-image/index.html

```python
%matplotlib inline
import numpy as np
from scipy import ndimage
from scipy import misc
import matplotlib.pyplot as plt
import pylab as pl

koon = misc.face(gray=True)
#from scipy import misc
#face = misc.face(gray=True)
plt.imshow(koon)
plt.show()

blurred_koon = ndimage.gaussian_filter(koon, sigma=5)

plt.imshow(blurred_koon)
plt.show()

noisy_koon = np.copy(koon).astype(np.float)
noisy_koon += koon.std()*np.random.standard_normal(koon.shape)

plt.imshow(noisy_koon)
plt.show()

from scipy import signal
wiener_koon = signal.wiener(blurred_koon, (5,5))

plt.imshow(wiener_koon)
plt.show()
```

### Sympy

Symbolic math is sometimes important, especially if we are weak at calculus or if we need to perform automated calculus on long formulas. We are briefly going through a few test cases, to get the feel of it. Symbolic math is especially developed for [Mathematica](http://www.wolfram.com/mathematica/?source=nav), or [Sage](http://www.sagemath.org/) which is an open-source equivalent.

```python
import sympy
print sympy.sqrt(8)
import math
print math.sqrt(8)
```

    2*sqrt(2)
    2.82842712475

```python
from sympy import symbols
x, y, z, t = symbols('x y z t')
expr = x + 2*y
print expr
print x * expr
from sympy import expand, factor, simplify
expanded_expr = expand(x*expr)
print expanded_expr
print factor(expanded_expr)
exp = expanded_expr.subs(x, z**t)
print exp
print simplify(exp)
```

    x + 2*y
    x*(x + 2*y)
    x**2 + 2*x*y
    x*(x + 2*y)
    2*y*z**t + z**(2*t)
    z**t*(2*y + z**t)

In the scipy.optimize paragraph we needed the Hessian matrix for a function f. Here is how you can obtain it in sympy:

```python
import sympy
x, y = sympy.symbols('x y')
f = .5*(1 - x)**2 + (y - x**2)**2
h = sympy.hessian(f, [x,y])
print(h)
from IPython.display import Latex
Latex(sympy.latex(h))

```

    Matrix([[12*x**2 - 4*y + 1.0, -4*x], [-4*x, 2]])

\left[\begin{matrix}12 x^{2} - 4 y + 1.0 & - 4 x\\- 4 x & 2\end{matrix}\right]

```python
from IPython.display import HTML
HTML('')
```

```python

```


```python
# https://www.youtube.com/watch?v=1VcX-HZiDgQ&t=266s
```

```python
import matplotlib.pyplot as plt
%matplotlib widget
import scipy as  sci
import sympy as sym
x,y = sym.symbols('x y')
f = sym.Function('f')(x)

sym.Eq(f.diff(x)-0.3*y,0)
sym.dsolve(f.diff(x)-0.3*y,f)

sym.dsolve(f.diff(x,x)-5*f,f)
```

$\displaystyle f{\left(x \right)} = C_{1} e^{- \sqrt{5} x} + C_{2} e^{\sqrt{5} x}$

```python
def ode(t,state):
    x,y,z = state
    return 10*(y-x), x*(28-z)-y, x*y-8/3*z

def sol():
    tspan = [0,30]
    state0 = [-.1,.5,1]
    state = sci.integrate.solve_ivp(ode, tspan, state0)
    return state.t, state.y[0,:], state.y[1,:], state.y[2,:]

def plot(t,x,y,z):
    _, axs = plt.subplots(2,2)
    axs[0,0].plot(t,x)
    axs[0,1].plot(t,y)
    axs[1,0].plot(t,z)
    axs[1,1].plot(x,z)
t,x,y,z = sol()
```

```python
import numpy as np
def dydt(y,t):
    k = 0.3
    return -k * y
y0 = 5
t = [0,20]
y = sci.integrate.solve_ivp(dydt,y0,t)
fig = plt.figure()
ax = fig.add_subplot()
ax.plot(t,y)
plt.show()
```

```python

```


# One Component Plasma (OCP)

Oh the good ol' OCP.
The YAML input file can be found at [input_file](https://raw.githubusercontent.com/murillo-group/sarkas/master/docs/examples/OCP/input_files/coulomb_cgs.yaml) and this notebook at [notebook](https://raw.githubusercontent.com/murillo-group/sarkas/master/docs/examples/OCP/OCP.ipynb).

```python
# Import the usual libraries
%pylab
%matplotlib inline

import os
plt.style.use('MSUstyle')

import pandas as pd
pd.options.display.max_columns = 15

# Import sarkas
from sarkas.processes import Simulation, PostProcess, PreProcess

# Create the file path to the YAML input file
input_file_name = os.path.join('input_files', 'coulomb_cgs.yaml')
```

    Using matplotlib backend: Qt5Agg
    Populating the interactive namespace from numpy and matplotlib

## PreProcessing

The following code is commented out since the simulation has been run before. We leave it here so that it is easy to copy and paste in your notebook.

```python
# preproc = PreProcess(input_file_name)
# preproc.setup(read_yaml=True)
# preproc.run(pppm_estimate=True)
```

## Simulation

The following code is commented out since the simulation has been run before.
We leave it here so that it is easy to copy and paste in your notebook.

```python
# sim = Simulation(input_file_name)
# sim.setup(read_yaml=True)
# sim.run()
```

## PostProcessing

```python
postproc = PostProcess(input_file_name)
postproc.setup(read_yaml=True)

postproc.therm.setup(postproc.parameters)
postproc.therm.temp_energy_plot(postproc)
```

## Radial Distribution Function

```python
postproc.rdf.setup(postproc.parameters)
postproc.rdf.compute()
```

      0%|          | 0/1 [00:00<?, ?it/s]

    Radial Distribution Function Calculation Time: 0 sec 54 msec 472 usec 443 nsec

```python
postproc.rdf.plot(scaling = postproc.rdf.a_ws,
                  y = ("H-H RDF", "Mean"),
                  xlabel = r'$r/a_{\rm ws}$')
```

Calculate the Pressure from the RDF

```python
n_beta =  postproc.therm.beta / ( postproc.parameters.total_num_ptcls)
nkT, u_h, u_c, p_h, p_c = postproc.therm.compute_from_rdf(postproc.rdf, postproc.potential)
print('The excess pressure is = {:.4e} / (n k_B T)'.format(u_c[0] * n_beta ))
```

    The excess pressure is = -8.2438e+01 / (n k_B T)

## Dynamic Structure Factor

```python
from sarkas.tools.observables import DynamicStructureFactor

dsf = DynamicStructureFactor()
dsf.max_ka_value = 10
dsf.angle_averaging = 'principal_axis'
dsf.no_slices = 4
dsf.setup(postproc.parameters)
dsf.pretty_print()
```

    ====================== Dynamic Structure Factor ======================

    k wavevector information saved in:
     Simulations/OCP_cgs/PostProcessing/k_space_data/k_arrays.npz
    n(k,t) data saved in:
     Simulations/OCP_cgs/PostProcessing/k_space_data/nkt.h5
    Data saved in:
     Simulations/OCP_cgs/PostProcessing/DynamicStructureFactor/Production/DynamicStructureFactor_OCP_cgs.h5
    Data accessible at: self.k_list, self.k_counts, self.ka_values, self.frequencies, self.dataframe

    Frequency Space Parameters:
    	No. of slices = 1
    	No. dumps per slice = 50001
    	Frequency step dw = 2 pi (no_slices * prod_dump_step)/(production_steps * dt)
    	dw = 0.0031 w_p = 5.2642e+12 [rad/s]
    	Maximum Frequency w_max = 2 pi /(prod_dump_step * dt)
    	w_max = 78.5487 w_p = 1.3161e+17 [rad/s]

    Wavevector parameters:
    Smallest wavevector k_min = 2 pi / L = 3.9 / N^(1/3)
    k_min = 0.1809 / a_ws = 3.4252e+07 [1/cm]

    Angle averaging choice: principal_axis
    	Maximum k harmonics = n_x, n_y, n_z = 55, 55, 55
    	Largest wavector k_max = k_min * n_x
    	k_max = 9.9505 / a_ws = 1.8839e+09 [1/cm]

    Total number of k values to calculate = 165
    No. of unique ka values to calculate = 55

```python
dsf.parse()
```

```python
dsf.dataframe
```

      H-H

      Mean
      ...
      Std

      Frequencies
      ka1 = 0.1809
      ka2 = 0.3618
      ka3 = 0.5428
      ka4 = 0.7237
      ka5 = 0.9046
      ka6 = 1.0855
      ...
      ka49 = 8.8650
      ka50 = 9.0459
      ka51 = 9.2269
      ka52 = 9.4078
      ka53 = 9.5887
      ka54 = 9.7696
      ka55 = 9.9505

      0
      -1.316086e+17
      4.879578e-25
      1.472862e-25
      2.390647e-25
      2.450585e-25
      7.464846e-25
      1.143595e-24
      ...
      3.108407e-22
      2.788587e-22
      1.524755e-22
      2.245008e-22
      5.311622e-23
      2.032679e-22
      1.299485e-22

      1
      -1.315876e+17
      4.879578e-25
      1.472866e-25
      2.390647e-25
      2.450578e-25
      7.464843e-25
      1.143596e-24
      ...
      3.108408e-22
      2.788584e-22
      1.524759e-22
      2.245011e-22
      5.311582e-23
      2.032684e-22
      1.299482e-22

      2
      -1.315665e+17
      4.879578e-25
      1.472870e-25
      2.390648e-25
      2.450572e-25
      7.464841e-25
      1.143597e-24
      ...
      3.108409e-22
      2.788580e-22
      1.524764e-22
      2.245014e-22
      5.311542e-23
      2.032690e-22
      1.299479e-22

      3
      -1.315455e+17
      4.879579e-25
      1.472874e-25
      2.390650e-25
      2.450566e-25
      7.464840e-25
      1.143599e-24
      ...
      3.108411e-22
      2.788577e-22
      1.524769e-22
      2.245017e-22
      5.311503e-23
      2.032697e-22
      1.299476e-22

      4
      -1.315244e+17
      4.879581e-25
      1.472878e-25
      2.390651e-25
      2.450560e-25
      7.464840e-25
      1.143600e-24
      ...
      3.108413e-22
      2.788575e-22
      1.524774e-22
      2.245021e-22
      5.311465e-23
      2.032703e-22
      1.299474e-22

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      12495
      1.315033e+17
      4.879589e-25
      1.472846e-25
      2.390648e-25
      2.450623e-25
      7.464874e-25
      1.143593e-24
      ...
      3.108409e-22
      2.788611e-22
      1.524735e-22
      2.245000e-22
      5.311834e-23
      2.032654e-22
      1.299503e-22

      12496
      1.315244e+17
      4.879585e-25
      1.472849e-25
      2.390647e-25
      2.450615e-25
      7.464866e-25
      1.143593e-24
      ...
      3.108408e-22
      2.788606e-22
      1.524738e-22
      2.245001e-22
      5.311790e-23
      2.032658e-22
      1.299499e-22

      12497
      1.315455e+17
      4.879583e-25
      1.472852e-25
      2.390646e-25
      2.450607e-25
      7.464860e-25
      1.143593e-24
      ...
      3.108407e-22
      2.788601e-22
      1.524742e-22
      2.245002e-22
      5.311747e-23
      2.032663e-22
      1.299495e-22

      12498
      1.315665e+17
      4.879581e-25
      1.472855e-25
      2.390646e-25
      2.450599e-25
      7.464854e-25
      1.143594e-24
      ...
      3.108406e-22
      2.788596e-22
      1.524746e-22
      2.245004e-22
      5.311705e-23
      2.032668e-22
      1.299492e-22

      12499
      1.315876e+17
      4.879579e-25
      1.472859e-25
      2.390646e-25
      2.450592e-25
      7.464850e-25
      1.143595e-24
      ...
      3.108407e-22
      2.788591e-22
      1.524750e-22
      2.245006e-22
      5.311663e-23
      2.032673e-22
      1.299488e-22

12500 rows × 111 columns

```python
ax = dsf.plot(scaling = dsf.total_plasma_frequency,
         y = [("H-H","Mean", "ka1 = 0.1809"), ("H-H","Mean", "ka3 = 0.5428"), ("H-H","Mean", "ka5 = 0.9046")],
         xlabel = r'$\omega/\omega_p$',
         xlim = (-2.5, 2.5),
        logy = True)
ax.legend()
```

### Collective Mode Dispersion

```python
w = dsf.dataframe[(" "," ","Frequencies")]/ dsf.total_plasma_frequency

ka_mesh, w_mesh = np.meshgrid(dsf.ka_values, w)

Skw = dsf.dataframe[("H-H", "Mean")]
fig, ax = plt.subplots(1,1)
pc = ax.pcolormesh(ka_mesh, w_mesh, Skw,
                   shading = 'auto',
                   norm = mpl.colors.LogNorm(vmin = 1e-22, vmax = 1e-15),
                   cmap='nipy_spectral')
ax.set(ylim = (0,3),
       xlim = (0,8),
       xlabel = r'$ka$',
       ylabel = r'$\omega/\omega_p$')
plt.colorbar(pc)
```

```python
from sarkas.tools.observables import CurrentCorrelationFunction

ccf = CurrentCorrelationFunction()
ccf.max_ka_value = 10
ccf.angle_averaging = 'principal_axis'
ccf.no_slices = 4
ccf.setup(postproc.parameters)
ccf.pretty_print()
```

    ==================== Current Correlation Function ====================

    k wavevector information saved in:
     Simulations/OCP_cgs/PostProcessing/k_space_data/k_arrays.npz
    v(k,t) data saved in:
     Simulations/OCP_cgs/PostProcessing/k_space_data/vkt.h5
    Data saved in:
    Simulations/OCP_cgs/PostProcessing/CurrentCorrelationFunction/Production/CurrentCorrelationFunction_OCP_cgs.h5
    Data accessible at: self.k_list, self.k_counts, self.ka_values, self.frequencies,
    	 self.dataframe

    Frequency Space Parameters:
    	No. of slices = 4
    	No. dumps per slice = 12500
    	Frequency step dw = 2 pi (no_slices * prod_dump_step)/(production_steps * dt)
    	dw = 0.0126 w_p = 2.1057e+13 [rad/s]
    	Maximum Frequency w_max = 2 pi /(prod_dump_step * dt)
    	w_max = 78.5487 w_p = 1.3161e+17 [rad/s]

    Wavevector parameters:
    Smallest wavevector k_min = 2 pi / L = 3.9 / N^(1/3)
    k_min = 0.1809 / a_ws = 3.4252e+07 [1/cm]

    Angle averaging choice: principal_axis
    	Maximum k harmonics = n_x, n_y, n_z = 55, 55, 55
    	Largest wavector k_max = k_min * n_x
    	k_max = 9.9505 / a_ws = 1.8839e+09 [1/cm]

    Total number of k values to calculate = 165
    No. of unique ka values to calculate = 55

```python
# ccf.compute()
ccf.parse()
```

```python
ccf.dataframe
```

      Longitudinal
      ...
      Transverse

      H-H
      ...
      H-H

      Mean
      ...
      Std

      ka1 = 0.1809
      ka2 = 0.3618
      ka3 = 0.5428
      ka4 = 0.7237
      ka5 = 0.9046
      ka6 = 1.0855
      ...
      ka49 = 8.8650
      ka50 = 9.0459
      ka51 = 9.2269
      ka52 = 9.4078
      ka53 = 9.5887
      ka54 = 9.7696
      ka55 = 9.9505

      Frequencies
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      ...
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

      0
      -1.316086e+17
      297360.958948
      550841.305756
      741502.511222
      1.016348e+06
      1.514701e+06
      2.271082e+06
      ...
      7.750616e+07
      5.889132e+07
      9.418742e+07
      5.108627e+07
      7.961882e+07
      1.341673e+08
      2.069157e+08

      1
      -1.315876e+17
      297360.776416
      550842.124002
      741502.375627
      1.016346e+06
      1.514700e+06
      2.271084e+06
      ...
      7.750625e+07
      5.889105e+07
      9.418693e+07
      5.108631e+07
      7.961854e+07
      1.341673e+08
      2.069154e+08

      2
      -1.315665e+17
      297360.647363
      550843.139040
      741502.337668
      1.016345e+06
      1.514699e+06
      2.271087e+06
      ...
      7.750636e+07
      5.889082e+07
      9.418642e+07
      5.108637e+07
      7.961826e+07
      1.341674e+08
      2.069150e+08

      3
      -1.315455e+17
      297360.545013
      550844.190843
      741502.511066
      1.016343e+06
      1.514697e+06
      2.271090e+06
      ...
      7.750648e+07
      5.889055e+07
      9.418595e+07
      5.108642e+07
      7.961800e+07
      1.341674e+08
      2.069148e+08

      4
      -1.315244e+17
      297360.479300
      550845.235350
      741502.714944
      1.016342e+06
      1.514696e+06
      2.271093e+06
      ...
      7.750661e+07
      5.889032e+07
      9.418548e+07
      5.108648e+07
      7.961773e+07
      1.341676e+08
      2.069145e+08

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      12495
      1.315033e+17
      297362.379926
      550837.777453
      741504.525321
      1.016359e+06
      1.514712e+06
      2.271074e+06
      ...
      7.750583e+07
      5.889277e+07
      9.419012e+07
      5.108613e+07
      7.962035e+07
      1.341672e+08
      2.069178e+08

      12496
      1.315244e+17
      297362.038693
      550838.295542
      741504.077521
      1.016357e+06
      1.514710e+06
      2.271075e+06
      ...
      7.750587e+07
      5.889246e+07
      9.418956e+07
      5.108616e+07
      7.962006e+07
      1.341671e+08
      2.069173e+08

      12497
      1.315455e+17
      297361.694330
      550838.950322
      741503.502435
      1.016354e+06
      1.514707e+06
      2.271076e+06
      ...
      7.750592e+07
      5.889217e+07
      9.418901e+07
      5.108617e+07
      7.961974e+07
      1.341672e+08
      2.069169e+08

      12498
      1.315665e+17
      297361.427877
      550839.707170
      741503.103073
      1.016352e+06
      1.514705e+06
      2.271078e+06
      ...
      7.750600e+07
      5.889188e+07
      9.418847e+07
      5.108619e+07
      7.961942e+07
      1.341672e+08
      2.069165e+08

      12499
      1.315876e+17
      297361.173344
      550840.413166
      741502.795738
      1.016350e+06
      1.514703e+06
      2.271080e+06
      ...
      7.750607e+07
      5.889160e+07
      9.418794e+07
      5.108622e+07
      7.961910e+07
      1.341672e+08
      2.069160e+08

12500 rows × 221 columns

```python
w = dsf.dataframe[(" "," ","Frequencies")]/ ccf.total_plasma_frequency

ka_mesh, w_mesh = np.meshgrid(dsf.ka_values, w)

Lkw = ccf.dataframe[("Longitudinal", "H-H", "Mean")]
fig, ax = plt.subplots(1,1)
pc = ax.pcolormesh(ka_mesh, w_mesh, Lkw,
                   shading = 'auto',
                   norm = mpl.colors.LogNorm(vmin = 0.5e10, vmax = 0.5e15),
                   cmap='nipy_spectral')
ax.set(ylim = (0,3),
       xlim = (0,8),
       xlabel = r'$ka$',
       ylabel = r'$\omega/\omega_p$')
plt.colorbar(pc)
```

```python
w = dsf.dataframe[(" "," ","Frequencies")]/ ccf.total_plasma_frequency

ka_mesh, w_mesh = np.meshgrid(dsf.ka_values, w)

Lkw = ccf.dataframe[("Transverse", "H-H", "Mean")]
fig, ax = plt.subplots(1,1)
pc = ax.pcolormesh(ka_mesh, w_mesh, Lkw,
                   shading = 'auto',
                   norm = mpl.colors.LogNorm(vmin = 3e9, vmax = 3e14),
                   cmap='nipy_spectral')
ax.set(ylim = (0,3),
       xlim = (0,8),
       xlabel = r'$ka$',
       ylabel = r'$\omega/\omega_p$')
plt.colorbar(pc)
```

## Diffusion

Diffusion is calculated from the integral of the velocity autocorrelation function, $\left \langle \bar{\mathbf v} (t) \cdot \bar{\mathbf v}(0) \right \rangle$,

\begin{equation}
D = \frac{1}{3} \int_0^{\tau} dt \, \left \langle \bar{\mathbf v} (t) \cdot \bar{\mathbf v}(0) \right \rangle
\end{equation}

where $\bar{\mathbf v}$ is the particle-averaged velocity.

```python
from sarkas.tools.observables import VelocityAutoCorrelationFunction
from sarkas.tools.transport import TransportCoefficients
```

```python
vacf = VelocityAutoCorrelationFunction()
vacf.setup(postproc.parameters, no_slices=4)
```

```python
vacf.parse()
```

```python
tc = TransportCoefficients(postproc.parameters, no_slices = 4)
tc.diffusion(vacf, plot=True)
```

```python
rescaling = 1.0/vacf.total_plasma_frequency/vacf.a_ws**2
fig, ax = plt.subplots(1,1, figsize=(10,7))
ax.plot(tc.diffusion_df["Time"].iloc[:,0]*vacf.total_plasma_frequency,
        tc.diffusion_df[("H Diffusion","Mean")] * rescaling,
       label = r'Diffusion')

ax.plot(tc.diffusion_df["Time"].iloc[:,0]*vacf.total_plasma_frequency,
        tc.diffusion_df[("H Diffusion","Mean")].expanding().mean() * rescaling,
        ls = '--', label = r'Mov Avg')

ax.legend(ncol = 2)
ax.set(xlabel = r"$\tau \omega_p$",ylabel = r" $D/a^2 \omega_p$")
```

```python

```


# Calculation of $G_\mathrm{ep}$ and heat capacities from DFT results (spin resolved)

This notebook calculates the electron-phonon coupling parameter $G_\mathrm{ep}$ and the electron and phonon heat capacity from DFT results (electronic density of states, phonon density of states, Fermi level, Eliashberg function).

Note that this is the version for a spin-resolved DFT calculation (e.g. for ferromagnetic materials), use the notebook main_not_spin_resolved for a non-spin-resolved DFT calculation.

## Load required modules, settings from config file, DFT results and other material parameters

In order to perform the calculation for another material, create a .py file named material+'_spin_resolved' in the folder 'load_inputs' that loads all necessary material-specific data (see examples). Furthermore, change the material entry in the file confic.cfg.

```python
import numpy as np
%matplotlib notebook
import matplotlib.pyplot as plt

# import functions saved separately
from functions.chemical_potential import calculate_chemical_potential
from functions.electron_phonon_coupling import calculate_electron_phonon_coupling
from functions.electron_heat_capacity import calculate_electron_heat_capacity
from functions.phonon_heat_capacity import calculate_phonon_heat_capacity

# import settings from config file
import configparser
config = configparser.ConfigParser()
config.read('config.cfg')
# material (string), to load the corresponding material-specific data and to save the results in the right text file:
material=config['GENERAL']['Material']
# lattice temperature in K, for the calculation of the electron-phonon coupling parameter G:
lattice_temperature=float(config['GENERAL']['Lattice_temperature'])
# desired temperature range in which all quantities should be calculated, in K:
temperatures = np.linspace(int(config['TEMPERATURE']['Temperature_min']),\
                          int(config['TEMPERATURE']['Temperature_max']),\
                           int(config['TEMPERATURE']['Temperature_points']))

# run the script that imports all required material-specific data (DFT calculation results and unit cell volume)
input_script_name='load_inputs/'+material+'_spin_resolved'
%run $input_script_name
```

    Material-specific data for iron has been loaded.

## Chemical potential
The chemical potential is required for calculating the electron-phonon coupling and the electronic heat capacity.
 The chemical potential ($\mu$) is temperature-dependent because the number of electrons per unit cell ($N_\mathrm{e}$) is constant:

$
N_\mathrm{e}=\int_{-\infty}^{\infty} g_\mathrm{e}(\epsilon) \frac{1}{\mathrm{exp}\left( \frac{\epsilon-\mu}{k_\mathrm{B}T}\right)+1}d\epsilon=\int_{-\infty}^{\epsilon_\mathrm{F}} g_\mathrm{e}(\epsilon) d\epsilon.
$

Here $g_\mathrm{e}$ is the electronic density of states, $T$ is the temperature, $\epsilon_\mathrm{F}$ is the Fermi energy and $\epsilon$ denotes the energy.

Using this equation, the chemical potential as a function of temperature is calculated numerically.
Here, separate chemical potentials are assumed for majority and minority carriers. Alternatively, also a common chemical potential can be assumed (for this, majority and minority electronic densities of states need to be summed).

```python
mu_majo = calculate_chemical_potential(temperatures,e_dos_majo,fermi_energy)
mu_mino = calculate_chemical_potential(temperatures,e_dos_mino,fermi_energy)

# plot the result (optional)
plt.figure()
plt.plot(temperatures,mu_majo), plt.plot(temperatures,mu_mino)
plt.xlabel('Temperature [K]'), plt.ylabel('Chemical potential [eV]'), plt.legend(['majority','minority']);
```

    <IPython.core.display.Javascript object>

## Electron-phonon coupling parameter ($G_\mathrm{ep}$)

The electron-phonon coupling parameter is calculated as in Waldecker et al., Phys. Rev. X 6, 021003 (https://doi.org/10.1103/PhysRevX.6.021003), Equation 9, with the (small) difference that changes of the chemical potential with electron temperature are considered here. The corresponding equations are:

$Z(T_\mathrm{e},T_\mathrm{l})=-\frac{2\pi}{g_\mathrm{e}(\mu)}\int_0^\infty\left(\hbar \omega\right)^2 \alpha^2F(\omega)\hspace{2pt} \left[ n_\mathrm{BE}(\omega,T_\mathrm{e})-n_\mathrm{BE}(\omega,T_\mathrm{l})\right]d\omega \hspace{2pt} \int_{-\infty}^\infty g_\mathrm{e}^2(\epsilon)\frac{\partial n_\mathrm{FD}(\epsilon,T_\mathrm{e},\mu)}{\partial \epsilon}d\epsilon.$

$G_\mathrm{ep}(T_\mathrm{e},T_\mathrm{l})=\frac{Z(T_\mathrm{e},T_\mathrm{l})}{T_\mathrm{e}-T_\mathrm{l}}.$

Here, $T_\mathrm{e}$ and $T_\mathrm{l}$ are the temperatures of the electrons and the lattice, respectively. $n_\mathrm{BE}$ is the Bose-Einstein distribution function (with $\mu=0$) and $n_\mathrm{FD}$ is the Fermi-Dirac distribution function. $\alpha^2F(\omega)$ denotes the Eliashberg function, $g_\mathrm{e}$ is the electronic density of states, and $\mu$ is the chemical potential (see above).

$G_\mathrm{ep}$ is calculated for majority and minority carriers separately and the results are summed.

```python
g_majo = calculate_electron_phonon_coupling(temperatures,lattice_temperature,mu_majo,e_dos_majo,fermi_energy,eliashberg_majo)
g_mino = calculate_electron_phonon_coupling(temperatures,lattice_temperature,mu_mino,e_dos_mino,fermi_energy,eliashberg_mino)

# convert from W/(unit cell K) to W/(m^3 K)
g_majo = g_majo/unit_cell_volume
g_mino = g_mino/unit_cell_volume

# add contributions
g_sum = g_majo+g_mino

# plot the result (optional)
plt.figure()
plt.plot(temperatures,g_majo*1e-18), plt.plot(temperatures,g_mino*1e-18), plt.plot(temperatures,g_sum*1e-18)
plt.xlabel('Temperature [K]'), plt.ylabel('Electron-phonon coupling parameter G$_\mathrm{ep}$ [10$^{18}$ W/(m$^3$K)]')
plt.legend(['majority','minority','sum'])

# save the result as text file
np.savetxt('results/'+material+'_spinResolved_electronPhononCoupling.txt',np.transpose([temperatures,g_sum]),fmt='%07.2f %e',\
    header='material: '+material+' \n'
           'electron-phonon coupling parameter G_ep, sum of majority and minority contributions '
           '(DFT calculation spin resolved) \n'
           'temperature [K], g_ep [W/(m^3K)] ')

```

    <IPython.core.display.Javascript object>

## Electron heat capacity

The electron heat capacity, $c_\mathrm{e}$, is the derivative of the electron energy ($E_\mathrm{e}$) with respect to the temperature:

\begin{equation}
    c_\mathrm{e}(T)=\left(\frac{\partial E_\mathrm{e}}{\partial T}\right)_V=\frac{d}{dT}\left[\int_{-\infty}^{\infty} g_\mathrm{e}(\epsilon) \frac{\epsilon}{\mathrm{exp}\left( \frac{\epsilon-\mu(T)}{k_\mathrm{B}T}\right)+1}d\epsilon\right].
    \label{eq:c_e}
\end{equation}

$g_\mathrm{e}$ is the electronic density of states and $\mu$ is the chemical potential.

The equation corresponds to the electron heat capacity at constant volume.

Like for $G_\mathrm{ep}$, contributions of majority and minority carriers are calculated separately and the results are summed.

```python
electron_heat_capacity_majo = calculate_electron_heat_capacity(temperatures,mu_majo,e_dos_majo,fermi_energy)
electron_heat_capacity_mino = calculate_electron_heat_capacity(temperatures,mu_mino,e_dos_mino,fermi_energy)
# Due to the numerical differentiation, the temperature sampling changes. Therefore, the output has two colums
# with the first column corresponding to the new temperature points.

# convert from J/(unit cell K) to J/(m^3 K)
electron_heat_capacity_majo[:,1] = electron_heat_capacity_majo[:,1]/unit_cell_volume
electron_heat_capacity_mino[:,1] = electron_heat_capacity_mino[:,1]/unit_cell_volume

# add contributions
electron_heat_capacity_sum = electron_heat_capacity_majo;
electron_heat_capacity_sum[:,1] = electron_heat_capacity_majo[:,1]+electron_heat_capacity_mino[:,1];

# plot the result (optional)
plt.figure()
plt.plot(electron_heat_capacity_majo[:,0],electron_heat_capacity_majo[:,1]*1e-6)
plt.plot(electron_heat_capacity_mino[:,0],electron_heat_capacity_mino[:,1]*1e-6)
plt.plot(electron_heat_capacity_majo[:,0],(electron_heat_capacity_majo[:,1]+electron_heat_capacity_mino[:,1])*1e-6)
plt.xlabel('Temperature [K]'), plt.ylabel('Electron heat capacity [10$^6$ J/(m$^3$K)]')
plt.legend(['majority','minority','sum'])

# save the result as text file
np.savetxt('results/'+material+'_spinResolved_electronHeatCapacity.txt',electron_heat_capacity_sum,fmt='%07.2f %e',\
    header='material: '+material+' \n'
           'electron heat capacity , sum of majority and minority contributions (DFT calculation spin resolved) \n'
           'temperature [K], heat capacity [J/(m^3K)] ')

```

    <IPython.core.display.Javascript object>

## Phonon heat capacity

The phonon heat capacity is calculated analogously to the electron heat capacity, with the difference that the Bose-Einstein distribution instead of the Fermi-Dirac distribution is required here:

\begin{equation}
    c_\mathrm{l}(T)=\left(\frac{\partial E_\mathrm{l}}{\partial T}\right)_V=\frac{d}{dT}\left[\int_{-\infty}^{\infty} g_\mathrm{l}(\epsilon) \frac{\epsilon}{\mathrm{exp}\left( \frac{\epsilon}{k_\mathrm{B}T}\right)-1}d\epsilon\right].
    \label{eq:c_l}
\end{equation}

$g_\mathrm{l}$ is the phonon density of states and $E_\mathrm{l}$ is the total energy in the phonon system.

Also here, the equation corresponds to the heat capacity at constant volume.

```python
phonon_heat_capacity = calculate_phonon_heat_capacity(temperatures,v_dos)
# Due to the numerical differentiation, the temperature sampling changes. Therefore, the output has two colums
# with the first column corresponding to the new temperature points.

# convert from J/(unit cell K) to J/(m^3 K)
phonon_heat_capacity[:,1] = phonon_heat_capacity[:,1]/unit_cell_volume

# plot the result (optional)
plt.figure()
plt.plot(phonon_heat_capacity[:,0],phonon_heat_capacity[:,1]*1e-6)
plt.xlabel('Temperature [K]'), plt.ylabel('Phonon heat capacity [10$^6$ J/(m$^3$K)]')

# save the result as text file
np.savetxt('results/'+material+'_spinResolved_phononHeatCapacity.txt',phonon_heat_capacity,fmt='%07.2f %e',\
    header='material: '+material+' \n'
           'phonon heat capacity (DFT calculation spin resolved) \n'
           'temperature [K], heat capacity [J/(m^3K)] ')

```

    <IPython.core.display.Javascript object>