```python
%matplotlib widget
```

```python
import numpy as np
import sympy as sym
import matplotlib.pyplot as plt
import scipy.signal as sctrl
import pydae.ssa as ssa
from sympy.physics.quantum import TensorProduct
```

```python

```

# Control in real domain

### Plant model

```python
Δt = 50e-6
R_t =  0.039269908169872414
L_t =  0.00125
C_m =  4e-06
G_d =  1.0
R_s =  0.039269908169872414
L_s =  0.00125
omega = 2*np.pi*50

R = R_t + R_s
L = L_t + L_s
A =  np.array([
    [  -R/L, omega],
    [-omega,  -R/L],
    ])

B = np.array([
    [ 1/L,0],
    [ 0,1/L],
    ])

B_g = np.array([
    [ -1/L,0],
    [ 0,-1/L],
    ])

C_c = np.array([
    [ 1, 0],
    [ 0, 1],
    ])

D_c = np.array([
    [ 0, 0],
    [ 0, 0],
    ])

C_o = np.array([
    [ 1],
    ])

D_o = np.array([
    [ 0, 0],
    [ 0, 0],
    ])

# plant discretization
A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A,B,C_c,D_c),Δt,method='zoh')
A_,B_gd,C_,D_,Dt   = sctrl.cont2discrete((A,B_g,C_c,D_c),Δt,method='zoh')
A_,B_,C_o,D_o,Dt   = sctrl.cont2discrete((A,B,C_o,D_o),Δt,method='zoh')

```

## Controller

```python
def acker(A,B,poles):
    '''
    This function is a copy from the original in: https://github.com/python-control/python-control
    but it allows to work with complex A and B matrices. It is experimental and the original should be
    considered

    ----------
    A : numpy array_like (complex can be used)
        Dynamics amatrix of the system
    B : numpy array_like (complex can be used)
        Input matrix of the system
    poles : numpy array_like
        Desired eigenvalue locations.

    Returns
    -------
    K : numpy array_like
        Gain such that A - B K has eigenvalues given in p.

    '''

    N_x = np.shape(A)[0]

    ctrb = np.hstack([B] + [np.dot(np.linalg.matrix_power(A, i), B)
                                             for i in range(1, N_x)])

    # Compute the desired characteristic polynomial
    p = np.real(np.poly(poles))

    n = np.size(p)
    pmat = p[n-1] * np.linalg.matrix_power(A, 0)
    for i in np.arange(1,n):
        pmat = pmat + np.dot(p[n-i-1], np.linalg.matrix_power(A, i))
    K = np.linalg.solve(ctrb, pmat)

    K = K[-1][:]                # Extract the last row            # Extract the last row

    return K
```

```python
# Controller ##################################################################################
N_x_c,N_u_d = B_d.shape
N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_c))
O_xu = np.zeros((N_x_c,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref

A_e = np.block([
                [    A_d,    B_d, O_xu],    # Δx_d
                [   O_ux,   O_uu, O_uu],    # Δx_r
                [ Δt*C_d,   O_uu, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   O_xu],
                [   I_uu],
                [   O_uu],
               ])

# weighting matrices
Q_c = np.eye(A_e.shape[0])
Q_c[-1,-1] = 1e7
Q_c[-2,-2] = 1e7

R_c = np.eye(B_e.shape[1])*10

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)

E_cont = np.log(E_c)/Δt

gamma = np.exp(-1000*50.0e-6)
poles = np.array([gamma,gamma]*3);

K_c = acker(A_e,B_e,poles)

```

```python
B_e
```

    array([[0., 0.],
           [0., 0.],
           [1., 0.],
           [0., 1.],
           [0., 0.],
           [0., 0.]])

```python
N_x = 2
N_r = 2
N_i = 2

Dx_d = sym.Matrix([sym.Symbol(f'Dx_d[{it}]', real=True) for it in range(N_x)])
Dx_r = sym.Matrix([sym.Symbol(f'Dx_r[{it}]', real=True) for it in range(N_r)])
Dx_i = sym.Matrix([sym.Symbol(f'Dx_i[{it}]', real=True) for it in range(N_i)])

Dx_e = sym.Matrix([[Dx_d],[Dx_r],[Dx_i]])

Du_r = -K_c @ Dx_e

Du_r = sym.N(sym.simplify(u_r))

for it in range(2):
    print(f'Du_r[{it}] = {str(sym.N(Du_r[it],8))};')

print('\nWarning: Control output is v_t_dq!!')
```

    Du_r[0] = -2.1004933*Dx_d[0] - 0.015941738*Dx_d[1] - 876.73711*Dx_i[0] + 322.0766*Dx_i[1] - 0.041168797*Dx_r[0] - 0.00031799792*Dx_r[1];
    Du_r[1] = 0.015941738*Dx_d[0] - 2.1004933*Dx_d[1] - 322.0766*Dx_i[0] - 876.73711*Dx_i[1] + 0.00031799792*Dx_r[0] - 0.041168797*Dx_r[1];

    Warning: Control output is v_t_dq!!

```python

```

## Controller in complex domain without delay

### Plant model

```python
Δt = 50e-6
R_t =  0.039269908169872414
L_t =  0.00125
C_m =  4e-06
G_d =  1.0
R_s =  0.039269908169872414
L_s =  0.00125
omega = 2*np.pi*50

R = R_t + R_s
L = L_t + L_s
A =  np.array([
    [-R/L-1j*omega],
    ])

B = np.array([
    [ 1/L],
    ])

B_g = np.array([
    [ -1/L],
    ])

C_c = np.array([
    [ 1],
    ])

D_c = np.array([
    [ 0],
    ])

C_o = np.array([
    [ 1],
    ])

D_o = np.array([
    [ 0],
    ])

# plant discretization
A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A,B,C_c,D_c),Δt,method='zoh')
A_,B_gd,C_,D_,Dt   = sctrl.cont2discrete((A,B_g,C_c,D_c),Δt,method='zoh')
A_,B_,C_o,D_o,Dt   = sctrl.cont2discrete((A,B,C_o,D_o),Δt,method='zoh')

```

```python

```

```python
# Controller ##################################################################################
N_x_c,N_u_d = B_d.shape
N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_c))
O_xu = np.zeros((N_x_c,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref

A_e = np.block([
                [    A_d,   O_xu],    # Δx_d
                [ Δt*C_d,   I_uu],    # Δx_i
               ])

B_e = np.block([
                [   B_d],
                [   O_uu],
               ])

# weighting matrices
Q_c = np.eye(A_e.shape[0])
Q_c[-1,-1] = 1e7

R_c = np.eye(B_e.shape[1])*10

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)

E_cont = np.log(E_c)/Δt

gamma = np.exp(-1000*50.0e-6)
poles = np.array([gamma,gamma]);

K_c = ssa.acker(A_e,B_e,poles).reshape(1,2)
```

```python
Dx_e
```

$\displaystyle \left[\begin{matrix}Dx_{d[0]}\\Dx_{d[1]}\\Dx_{r[0]}\\Dx_{r[1]}\\Dx_{i[0]}\\Dx_{i[1]}\end{matrix}\right]$

```python
N_x_cplx = 1
N_i_cplx = 1

Dx_d = sym.Matrix([sym.Symbol(f'Dx_d[{it}]', real=True) + 1j*sym.Symbol(f'Dx_d[{it+1}]', real=True)  for it in range(0,N_x_cplx,2)])
Dx_i = sym.Matrix([sym.Symbol(f'Dx_i[{it}]', real=True) + 1j*sym.Symbol(f'Dx_i[{it+1}]', real=True)  for it in range(0,N_i_cplx,2)])

Dx_e = sym.Matrix([[Dx_d],[Dx_i]])

u_r = -K_c @ Dx_e

Du_r_1 = str(sym.N(sym.simplify(sym.re(u_r[0])),8))
Du_r_2 = str(sym.N(sym.simplify(sym.im(u_r[0])),8))

print(f'Du_r[0] = {Du_r_1};')
print(f'Du_r[1] = {Du_r_2};')
print('\nWarning: Control output is v_t_dq!!')
```

    Du_r[0] = -4.8022489*Dx_d[0] - 0.74707379*Dx_d[1] - 2380.3887*Dx_i[0] + 18.691019*Dx_i[1];
    Du_r[1] = 0.74707379*Dx_d[0] - 4.8022489*Dx_d[1] - 18.691019*Dx_i[0] - 2380.3887*Dx_i[1];

    Warning: Control output is v_t_dq!!

```python
K_c
```

    array([   4.80224889 -0.74707379j, 2380.3887398 +18.69101907j])

## Controller

```python
# Controller ##################################################################################
N_x_c,N_u_d = B_d.shape
N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_c))
O_xu = np.zeros((N_x_c,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref

A_e = np.block([
                [    A_d,    B_d, O_xu],    # Δx_d
                [   O_ux,   O_uu, O_uu],    # Δx_r
                [ Δt*C_d,   O_uu, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   O_xu],
                [   I_uu],
                [   O_uu],
               ])

A_e = np.block([
                [    A_d,   O_xu],    # Δx_d
                [ Δt*C_d,   I_uu],    # Δx_i
               ])

B_e = np.block([
                [   B_d],
                [   O_uu],
               ])

# weighting matrices
Q_c = np.eye(A_e.shape[0])
Q_c[-1,-1] = 1e7

R_c = np.eye(B_e.shape[1])*10

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)

E_cont = np.log(E_c)/Δt

```

```python

```

```python
K
```

    array([ 5.00382521e+01+3.92904700e-01j, -9.03149268e+05-7.09160647e+03j])

```python
A_e
```

    array([[9.98307263e-01-0.01568266j, 0.00000000e+00+0.j        ],
           [5.00000000e-05+0.j        , 1.00000000e+00+0.j        ]])

```python
# Control without observer Du_r = -K_c*Dx_e
Dx_d_1,Dx_d_2 = sym.symbols('Dx_d_1,Dx_d_2', real=True)
Dx_r_1,Dx_r_2 = sym.symbols('Dx_r_1,Dx_r_2', real=True)
Dx_i_1,Dx_i_2 = sym.symbols('Dx_i_1,Dx_i_2', real=True)

Dx_d_12 = Dx_d_1 + 1j*Dx_d_2
Dx_r_12 = Dx_r_1 + 1j*Dx_r_2
Dx_i_12 = Dx_i_1 + 1j*Dx_i_2

K_c = np.array([4.8037 - 0.7471j, 2.3818e+03 + 1.8702e+01j ]).reshape(1,2)

x_e = sym.Matrix([Dx_d_12,Dx_r_12,Dx_i_12])
x_e = sym.Matrix([Dx_d_12,Dx_i_12])
u_r = -K_c @ x_e

Du_r_1 = str(sym.N(sym.simplify(sym.re(u_r[0])),8))
Du_r_2 = str(sym.N(sym.simplify(sym.im(u_r[0])),8))

print(f'Du_r_1 = {Du_r_1};')
print(f'Du_r_2 = {Du_r_2};')
print('\nWarning: Control output is v_t_dq!!')
```

    Du_r_1 = -4.8037*Dx_d_1 - 0.7471*Dx_d_2 - 2381.8*Dx_i_1 + 18.702*Dx_i_2;
    Du_r_2 = 0.7471*Dx_d_1 - 4.8037*Dx_d_2 - 18.702*Dx_i_1 - 2381.8*Dx_i_2;

    Warning: Control output is v_t_dq!!

```python
omega*L
```

    0.7853981633974483

```python
E_cont
```

    array([-7.13979286e+05-58453.58811349j, -4.30854602e+02  +604.05182493j,
           -4.24472904e+02  -289.89255957j])

## Observer

```python
N_z_o = C_o.shape[0]

Q_o = np.eye(A_d.shape[0])

R_o = np.diag([1]*N_z_o)
K_o_T,S_o,E_o = ssa.dlqr(A_d.T,C_o.T,Q_o,R_o)
K_o = K_o_T.T

print('damp_ctrl',-E_c.real/np.abs(E_c))
print('damp_obs',-E_o.real/np.abs(E_o))
```

    damp_ctrl [-0.99954394 -0.99954394 -0.99989495 -0.99989495 -1.          1.        ]
    damp_obs [-0.99987663]

```python

```

```python

```

```python

```

```python

```

## Simulink

```python
# Control without observer Du_r = -K_c*Dx_e
Dx_d_1 = sym.symbols('Dx_d_1')
Dx_r_1 = sym.symbols('Dx_r_1')
Dx_i_1 = sym.symbols('Dx_i_1')

x_e = sym.Matrix([Dx_d_1,Dx_r_1,Dx_i_1 ])
u_r = -K_c * x_e

u_r_d = str(sym.N(u_r[0],8))

print(f'Du_r_1 = {u_r_d};')

print('\nWarning: Control output is v_t_dq!!')
```

    Du_r_1 = Dx_d_1*(-2.1004933 - 0.015941738*I) + Dx_i_1*(-876.73711 + 322.0766*I) + Dx_r_1*(-0.041168797 - 0.00031799792*I);

    Warning: Control output is v_t_dq!!

```python

```

```python
from sympy.utilities.codegen import codegen
from sympy.abc import f, g

from sympy import Eq

[(c_name, c_code), (h_name, c_header)] = codegen(

     [("myfcn", u_r_d)],

     "C99", header=False, empty=False)

print(c_code)

```

    #include "myfcn.h"
    #include <math.h>
    double myfcn(double Dx_d_1, double Dx_i_1, double Dx_r_1) {
       double myfcn_result;
       myfcn_result = Dx_d_1*(-0.0032323944000000001 + 2.3373778999999999e-5*I) + Dx_i_1*(-0.10354921 + 0.99459178000000004*I) + Dx_r_1*(-6.4596544999999994e-5 - 2.0057920000000001e-8*I);
       return myfcn_result;
    }

```python

```

```python
it_ini = 4
Δx_o = sym.Matrix([sym.Symbol(f'xD[{it+it_ini}]') for it in range(6)])
Δz_o = sym.Matrix([sym.Symbol(item) - sym.Symbol(item+'_0') for item in ['v_md', 'v_mq', 'i_sd', 'i_sq']])
Δx_r = sym.Matrix([sym.Symbol(f'Dx_r_{it+1}') for it in range(2)])
Δu_pert = sym.Matrix([sym.Symbol(f'Du_pert_{it+1}') for it in range(2)])

Δx_o_kp1 = A_d @ Δx_o + B_b@(Δx_r) +  K_o @ (Δz_o - C_o @ Δx_o) # + B_pert@Δu_pert  +  K_o @ (Δz_o - C_o @ Δx_o - D_o @ (Δx_r))

for it in range(6):
    print(f'xD[{it+it_ini}] = {Δx_o_kp1[it]};')
```

    xD[4] = 0.0392952872921423*Dx_r_1 + 0.000617299701089028*Dx_r_2 + 0.167670572903802*i_sd - 0.167670572903802*i_sd_0 + 0.427599429013122*v_md - 0.427599429013122*v_md_0 + 0.962811636697318*xD[4] - 0.430586049216921*xD[6] - 0.132051772873075*xD[8];
    xD[5] = -0.000617299701089028*Dx_r_1 + 0.0392952872921423*Dx_r_2 + 0.167670572903802*i_sq - 0.167670572903802*i_sq_0 + 0.427599429013122*v_mq - 0.427599429013122*v_mq_0 + 0.962811636697318*xD[5] - 0.430586049216921*xD[7] - 0.132051772873075*xD[9];
    xD[6] = 0.0356406531431186*Dx_r_1 + 0.000559888120127431*Dx_r_2 - 0.425448653071038*i_sd + 0.425448653071038*i_sd_0 + 0.443158057967852*v_md - 0.443158057967852*v_md_0 + 0.933318813687106*xD[4] - 0.449166750687225*xD[6] - 0.507870160616068*xD[8];
    xD[7] = -0.000559888120127431*Dx_r_1 + 0.0356406531431186*Dx_r_2 - 0.425448653071038*i_sq + 0.425448653071038*i_sq_0 + 0.443158057967853*v_mq - 0.443158057967853*v_mq_0 + 0.933318813687106*xD[5] - 0.449166750687225*xD[7] - 0.507870160616068*xD[9];
    xD[8] = 0.000668382397147673*Dx_r_1 + 1.0499789730636e-5*Dx_r_2 + 0.590326602628365*i_sd - 0.590326602628365*i_sd_0 - 0.0126468749218836*v_md + 0.0126468749218836*v_md_0 + 0.0356188000307263*xD[4] + 0.0156334951256824*xD[6] + 0.372485034068953*xD[8];
    xD[9] = -1.0499789730636e-5*Dx_r_1 + 0.000668382397147673*Dx_r_2 + 0.590326602628365*i_sq - 0.590326602628365*i_sq_0 - 0.0126468749218836*v_mq + 0.0126468749218836*v_mq_0 + 0.0356188000307263*xD[5] + 0.0156334951256824*xD[7] + 0.372485034068953*xD[9];

```python
#eta_dq[0] = Du_r_1*2/v_dc[0] + (v_sd)*2/v_dc[0] ;
#eta_dq[1] = Du_r_2*2/v_dc[0] + (v_sq)*2/v_dc[0] ;
#Du_r_1 = eta_dq[0]*v_dc[0]/2
#Du_r_2 = eta_dq[1]*v_dc[0]/2

Du_r_1,Du_r_2 = sym.symbols('Du_r_1,Du_r_2')
Du_r = sym.Matrix([Du_r_1,Du_r_2 ])

Dx_r = W@Du_r

Dx_r_1 = str(sym.N(Dx_r[0],8))
Dx_r_1 = str(sym.N(Dx_r[1],8))

print(f'Dx_r_1 = {Dx_r_1};')
print(f'Dx_r_2 = {Dx_r_1};')
```

    Dx_r_1 = -0.015707317*Du_r_1 + 0.99987663*Du_r_2;
    Dx_r_2 = -0.015707317*Du_r_1 + 0.99987663*Du_r_2;

```python
# Control with observer Du_r = -K_c*Dx_o
x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6 = sym.symbols('xD[4],xD[5],xD[6],xD[7],xD[8],xD[9]')
x_r_1,x_r_2 = sym.symbols('Dx_r_1,Dx_r_2')
x_i_1,x_i_2 = sym.symbols('Dx_i_1,Dx_i_2')

x_e = sym.Matrix([x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6,x_r_1,x_r_2,x_i_1,x_i_2])
u_r = -K_c * x_e

u_r_d = str(sym.N(u_r[0],8))
u_r_q = str(sym.N(u_r[1],8))

print(f'Du_r_1 = {u_r_d};')
print(f'Du_r_2 = {u_r_q};')

print('\nWarning: Control output is eta_dq!!')
```

    Du_r_1 = -2715.7977*Dx_i_1 + 1040.9568*Dx_i_2 - 0.071871552*Dx_r_1 + 0.00052148736*Dx_r_2 - 1.8262175*xD[4] + 0.019754652*xD[5] - 0.0048070784*xD[6] + 0.0010494948*xD[7] - 3.4543535*xD[8] + 0.39761572*xD[9];
    Du_r_2 = -1040.9568*Dx_i_1 - 2715.7977*Dx_i_2 - 0.00052148736*Dx_r_1 - 0.071871552*Dx_r_2 - 0.019754652*xD[4] - 1.8262175*xD[5] - 0.0010494948*xD[6] - 0.0048070784*xD[7] - 0.39761572*xD[8] - 3.4543535*xD[9];

    Warning: Control output is eta_dq!!

## Symbolic obtention of the plant model
# as in pydae example (synchronous dq):
j*di_tD = j/L_t*(v_tD - R_t*i_tD + omega*L_t*i_tQ - v_mD)
  di_tQ = 1/L_t*(v_tQ - R_t*i_tQ - omega*L_t*i_tD - v_mQ)
dv_mD = 1/C_m*(i_tD + C_m*omega*v_mQ - G_d*v_mD - i_sD)
dv_mQ = 1/C_m*(i_tQ - C_m*omega*v_mD - G_d*v_mQ - i_sQ)
di_sD = 1/L_s*(v_mD - R_s*i_sD + omega*L_s*i_sQ - v_sD)
di_sQ = 1/L_s*(v_mQ - R_s*i_sQ - omega*L_s*i_sD - v_sQ)

# complex equivalent:
di_tD = 1/L_t*(v_tD - R_t*i_tD - v_mD)
di_tQ = 1/L_t*(v_tQ - R_t*i_tQ - v_mQ)
dv_mD = 1/C_m*(i_tD - G_d*v_mD - i_sD)
dv_mQ = 1/C_m*(i_tQ - G_d*v_mQ - i_sQ)
di_sD = 1/L_s*(v_mD - R_s*i_sD - v_sD)
di_sQ = 1/L_s*(v_mQ - R_s*i_sQ - v_sQ)

j*di_tD + di_tQ =  1/L_t*(j*v_tD + v_tQ - R_t*(j*i_tD + i_tQ)  + omega*L_t * j/j*(j*i_tQ - i_tD) - (j*v_mD + v_mQ))
di_t = 1/L_t*(v_t - R_t*i_t + omega*L_t*j*i_t - v_m)
di_t = -R_t/L_t+j*omega

```python
R_t,L_t,C_m,G_d,R_s,L_s = sym.symbols('R_t,L_t,C_m,G_d,R_s,L_s', real=True)
i_tD,i_tQ,v_mD,v_mQ,i_sD,i_sQ = sym.symbols('i_tD,i_tQ,v_mD,v_mQ,i_sD,i_sQ', real=True)
v_tD,v_tQ = sym.symbols('v_tD,v_tQ', real=True)
v_sD,v_sQ = sym.symbols('v_sD,v_sQ', real=True)

```

```python

```

```python
R_t,L_t,C_m,G_d,R_s,L_s = sym.symbols('R_t,L_t,C_m,G_d,R_s,L_s', real=True)
i_tD,i_tQ,v_mD,v_mQ,i_sD,i_sQ = sym.symbols('i_tD,i_tQ,v_mD,v_mQ,i_sD,i_sQ', real=True)
v_tD,v_tQ = sym.symbols('v_tD,v_tQ', real=True)
v_sD,v_sQ = sym.symbols('v_sD,v_sQ', real=True)

A = sym.Matrix([
    [-R_t/L_t,       0,   -1/L_t,        0,        0,        0],
    [       0,-R_t/L_t,        0,   -1/L_t,        0,        0],
    [   1/C_m,       0, -G_d/C_m,        0,   -1/C_m,        0],
    [       0,   1/C_m,        0, -G_d/C_m,        0,   -1/C_m],
    [       0,       0,    1/L_s,        0, -R_s/L_s,        0],
    [       0,       0,        0,    1/L_s,        0, -R_s/L_s],
    ])

B = sym.Matrix([
    [ 1/L_t,        0],
    [       0,  1/L_t],
    [       0,      0],
    [       0,      0],
    [       0,      0],
    [       0,      0],
    ])

B_g = sym.Matrix([
    [       0,      0],
    [       0,      0],
    [       0,      0],
    [       0,      0],
    [-1/L_s,        0],
    [       0, -1/L_s],
    ])

x = sym.Matrix([i_tD,i_tQ,v_mD,v_mQ,i_sD,i_sQ])
u = sym.Matrix([v_tD,v_tQ])
u_g = sym.Matrix([v_sD,v_sQ])

dx_new = A@x + B@u + B_g@u_g

di_tD = 1/L_t*(v_tD - R_t*i_tD - v_mD)
di_tQ = 1/L_t*(v_tQ - R_t*i_tQ - v_mQ)
dv_mD = 1/C_m*(i_tD - G_d*v_mD - i_sD)
dv_mQ = 1/C_m*(i_tQ - G_d*v_mQ - i_sQ)
di_sD = 1/L_s*(v_mD - R_s*i_sD - v_sD)
di_sQ = 1/L_s*(v_mQ - R_s*i_sQ - v_sQ)
dx = sym.Matrix([di_tD,di_tQ,dv_mD,dv_mQ,di_sD,di_sQ])

dx = sym.Matrix([di_tD,di_tQ,dv_mD,dv_mQ,di_sD,di_sQ])
sym.simplify(dx_new - dx)  # just to check the model is ok
```

$\displaystyle \left[\begin{matrix}0\\0\\0\\0\\0\\0\end{matrix}\right]$

```python
import os

```

```python
os.name
```

    'nt'

```python

```

    FloatProgress(value=0.0, max=1.0)

    VBox(children=(FloatSlider(value=1.0, max=2.0, min=0.1), Button(description='Reiniciar', style=ButtonStyle(), …

```python
time.sleep?
```

    Docstring:
    sleep(seconds)

    Delay execution for a given number of seconds.  The argument may be
    a floating point number for subsecond precision.
    Type:      builtin_function_or_method

```python

```


# Modeling spiking neural networks with Brian

## Homework

Some general setup first...

```python
# If Brian2 is not yet installed, the following will install it
# (otherwise it will print a number of "Requirement already satisfied" lines)
%pip install brian2
```

    Requirement already satisfied: brian2 in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (2.2.2.1)
    Requirement already satisfied: cython>=0.29 in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (from brian2) (0.29.13)
    Requirement already satisfied: sympy!=1.1.0,>=0.7.6 in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (from brian2) (1.4)
    Requirement already satisfied: numpy>=1.10 in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (from brian2) (1.16.2)
    Requirement already satisfied: pyparsing in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (from brian2) (2.3.1)
    Requirement already satisfied: jinja2>=2.7 in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (from brian2) (2.10.1)
    Requirement already satisfied: setuptools>=21 in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (from brian2) (41.0.0)
    Requirement already satisfied: MarkupSafe>=0.23 in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (from jinja2>=2.7->brian2) (1.1.1)
    Requirement already satisfied: mpmath>=0.19 in /home/mgraupe/.virtualenvs/locorungs/lib/python3.6/site-packages (from sympy!=1.1.0,>=0.7.6->brian2) (1.1.0)
    WARNING: You are using pip version 21.0.1; however, version 21.3.1 is available.
    You should consider upgrading via the '/home/mgraupe/.virtualenvs/locorungs/bin/python3.6 -m pip install --upgrade pip' command.
    Note: you may need to restart the kernel to use updated packages.

```python
from brian2 import *
prefs.codegen.target = 'numpy'
```

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

%matplotlib inline
```

A plotting function that we will use later

```python
def plot_activity(exc_spike_mon, exc_pop_mon, inh_spike_mon):
    """Plots raster plots, population activity and a histogram of the coefficient of variation (CV) of the
       interspike intervals (ISI) – see Lecture 5 for more details.

       exc_spike_mon : a SpikeMonitor recording the excitatory population
       exc_pop_mon : a PopulationRateMonitor recording the excitatory population
       inh_spike_mon : a SpikeMonitor recording the inhibitory population
       """
    fig, axs = plt.subplots(3, 2, sharex='col', gridspec_kw={'height_ratios': [3, 2, 1],
                                                             'width_ratios': [3, 1]},
                           figsize=(12, 8))
    axs[0, 0].plot(exc_spike_mon.t/ms, exc_spike_mon.i, '|')
    axs[0, 0].set(ylabel='neuron index', title='excitatory neurons')
    # Get the individual spike trains for each neuron
    spike_trains = exc_spike_mon.spike_trains()
    # Calculate the CV of the ISI distribution for each neuron
    CVs = []
    for train in spike_trains.values():
        # We ignore spikes in the first 50ms to focus on the steady-state behaviour
        spikes = train[train>50*ms]
        if len(spikes) < 2:
            CVs.append(np.nan)
        else:
            ISIs = np.diff(spikes/ms)
            CVs.append(np.std(ISIs)/np.mean(ISIs))
    axs[0, 1].hist(CVs, bins=20, range=(0., 1.))
    axs[0, 1].xaxis.set_tick_params(labelbottom=True)
    axs[0, 1].set(xlabel='CV of ISI', yticks=[])
    axs[1, 0].plot(exc_pop_mon.t/ms, exc_pop_mon.smooth_rate(width=2*ms))
    axs[1, 0].hlines(np.mean(exc_pop_mon.rate/Hz), exc_pop_mon.t[0]/ms, exc_pop_mon.t[-1]/ms,
                     color='darkgray')
    axs[1, 0].set_ylabel('pop. firing rate (Hz)')
    axs[1, 0].set_ylim(0, axs[1, 0].get_ylim()[1])
    axs[2, 0].plot(inh_spike_mon.t/ms, inh_spike_mon.i, '|', color='C1')
    axs[2, 0].set(ylabel='neuron index', title='inhibitory neurons')
    axs[2, 0].set_xlabel('time (ms)')
    axs[1, 1].axis('off')
    axs[2, 1].axis('off')
    fig.tight_layout()
```

## Network

The following network is almost identical to the one we used in the tutorial. There are only three differences:
* The initial value of the membrane potential and the external input current are random and not fixed, they are given in Brian syntax referring to a uniformly distributed random number between 0 and 1 as `rand()`. This means that the membrane potential $V$ is initialized randomly between $E_L$ and the threshold, and the external current is randomly chosen between 20mV and 30mV.
* The neuron model has an absolute refractory period of 5ms (a period after each spike where the membrane potential
  is clamped to its value so the neuron cannot spike again)
* We have split up the excitatory and inhibitory synapses a bit more to chose their strengths independently: there are now separate excitatory → excitatory, excitatory → inhibitory, and inhibitory → excitatory synapses (inhibitory → inhibitory synapses have been removed).

```python
start_scope()
N_E = 4000  # excitatory neurons
N_I = 1000  # inhibitory neurons
E_L = -50*mV
V_threshold = -30*mV
V_reset = -55*mV
C = 70*pF
g_L = 10*nS
tau = C/g_L
tau_syn = 2*ms
w_exc_to_exc = 0*mV
w_exc_to_inh = 0*mV
w_inh_to_exc = 0*mV
duration = 1000*ms

neurons = NeuronGroup(N_E + N_I,
                      '''dV/dt = (E_L - V + I_syn + I_ext)/tau : volt (unless refractory)
                         I_ext : volt (constant)
                         dI_syn/dt = -I_syn/tau_syn : volt # decay between spikes''',
                      threshold='V>V_threshold', reset='V=V_reset', method='euler', refractory=5*ms)
# Initialize values
neurons.V = 'E_L + rand()*(V_threshold - E_L)'
exc_neurons = neurons[:N_E]  # Uses "slicing" to get subpopulations of neurons
inh_neurons = neurons[N_E:]

exc_neurons.I_ext = '20*mV + rand()*10*mV'

# Connect neurons to each other
exc_to_exc = Synapses(exc_neurons, exc_neurons, on_pre='I_syn_post += w_exc_to_exc')
exc_to_exc.connect(p=0.02)
exc_to_inh = Synapses(exc_neurons, inh_neurons, on_pre='I_syn_post += w_exc_to_inh')
exc_to_inh.connect(p=0.02)
inh_to_exc = Synapses(inh_neurons, exc_neurons, on_pre='I_syn_post -= w_inh_to_exc')
inh_to_exc.connect(p=0.02)

# Records from 100 excitatory neurons and 25 inhibitory neurons only, to get simpler plots
spike_mon_exc = SpikeMonitor(exc_neurons[:100])
spike_mon_inh = SpikeMonitor(inh_neurons[:25])
pop_mon_exc = PopulationRateMonitor(exc_neurons)  # Records the population rate over time

# run simulation
run(duration, report='text')  # for simulations that take longer to run
```

    Starting simulation at t=0. s for a duration of 1. s
    1.0 (100%) simulated in 5s

```python
# Plot the activity using the provided function
plot_activity(spike_mon_exc, pop_mon_exc, spike_mon_inh)
```

## Exercise 1

Run the network once with the parameters above (**"configuration A"**: `w_exc_to_exc = 0*mV`, `w_exc_to_inh = 0*mV`, `w_inh_to_exc = 0*mV`) and once with the parameters `w_exc_to_exc = 0.5*mV`, `w_exc_to_inh = 2*mV`, `w_inh_to_exc = 2*mV` (**"configuration B"**).

For each model, state whether you'd say that individual neurons show *regular* or *irregular activity*, and whether the network is *globally asynchronous* or *globally synchronous*.

## Exercise 2

For both parametrizations, plot the spike count of the excitatory neurons as a function of the external input current that they receive. Which of the models has a very clear dependency?

**Hint**: The `SpikeMonitor` `spike_mon_exc` only records from 100 neurons. You can restrict your analysis to these 100 neurons, or change the definition of the `SpikeMonitor` so that it records from all neurons.

## Exercise 3

Change the definition of the `exc_to_exc` (excitatory → excitatory) synapses so that each synapse has a **random delay** (the time it takes to propagate a spike to its target) **between 1ms and 3ms**.

You can set delays in the same way as e.g. the input current `I_ext`, by assigning to the `exc_to_exc.delay` attribute, i.e. by using `exc_to_exc.delay = ...`. (Note: You need to add this line *after* `exc_to_exc.connect(...)`).

When you run the new network with random delays with the parameters from *configuration B*: what changes in the network activity compared to the network without delays?


# 7. Alfven operator

```python
from numpy import linspace, meshgrid, pi, zeros, asarray
from scipy.linalg import eig

import matplotlib.pyplot as plt
%matplotlib inline

from IPython.display import Math

from sympy.core.containers import Tuple
from sympy import symbols
from sympy import Symbol
from sympy import Lambda
from sympy import IndexedBase

from gelato.glt import glt_symbol
from gelato.glt import glt_lambdify
from gelato.glt import glt_approximate_eigenvalues
from gelato.printing.latex import glt_latex
from gelato.calculus   import (Dot, Cross, Grad, Curl, Rot, Div)
from gelato.calculus   import Constant
from gelato.fem.assembly import assemble_matrix
from gelato.fem.utils    import compile_kernel
from gelato.fem.utils    import compile_symbol

from spl.fem.splines import SplineSpace
from spl.fem.tensor  import TensorFemSpace
from spl.fem.vector  import VectorFemSpace
```

```python
x,y,z = symbols('x y z')

u = IndexedBase('u')
v = IndexedBase('v')

bx = Constant('bx')
by = Constant('by')
bz = Constant('bz')
b = Tuple(bx, by, bz)

c0,c1,c2 = symbols('c0 c1 c2')

a = Lambda((x,y,z,v,u), (  c0 * Dot(u, v)
                         + c1 * Div(u) * Div(v)
                         + c2 * Dot(Curl(Cross(b,u)), Curl(Cross(b,v)))))

```

```python
# ...  create a finite element space
p1  = 2 ; p2  = 2 ; p3  = 2
ne1 = 8 ; ne2 = 8 ; ne3 = 8

grid_1 = linspace(0., 1., ne1+1)
grid_2 = linspace(0., 1., ne2+1)
grid_3 = linspace(0., 1., ne3+1)

V1 = SplineSpace(p1, grid=grid_1)
V2 = SplineSpace(p2, grid=grid_2)
V3 = SplineSpace(p3, grid=grid_3)

W = TensorFemSpace(V1, V2, V3)
# ...

V = VectorFemSpace(W, W, W)
```

```python
from IPython.display import Math
from gelato.printing.latex import glt_latex
from sympy import latex

symbol = glt_symbol(a, space=V, evaluate=False)

Math(latex(symbol))
#Math(glt_latex(symbol))
```

$$\left[\begin{matrix}by^{2} c_{2} m_{1} m_{3} s_{2} + by^{2} c_{2} m_{2} m_{3} s_{1} + 2 by bz a_{2} a_{3} c_{2} m_{1} + bz^{2} c_{2} m_{1} m_{2} s_{3} + bz^{2} c_{2} m_{2} m_{3} s_{1} + c_{0} m_{1} m_{2} m_{3} + c_{1} m_{2} m_{3} s_{1} & - bx by c_{2} m_{1} m_{3} s_{2} - bx by c_{2} m_{2} m_{3} s_{1} - bx bz a_{2} a_{3} c_{2} m_{1} - by bz a_{1} a_{3} c_{2} m_{2} + bz^{2} a_{1} a_{2} c_{2} m_{3} + a_{1} a_{2} c_{1} m_{3} & - bx by a_{2} a_{3} c_{2} m_{1} - bx bz c_{2} m_{1} m_{2} s_{3} - bx bz c_{2} m_{2} m_{3} s_{1} + by^{2} a_{1} a_{3} c_{2} m_{2} - by bz a_{1} a_{2} c_{2} m_{3} + a_{1} a_{3} c_{1} m_{2}\\- bx by c_{2} m_{1} m_{3} s_{2} - bx by c_{2} m_{2} m_{3} s_{1} - bx bz a_{2} a_{3} c_{2} m_{1} - by bz a_{1} a_{3} c_{2} m_{2} + bz^{2} a_{1} a_{2} c_{2} m_{3} + a_{1} a_{2} c_{1} m_{3} & bx^{2} c_{2} m_{1} m_{3} s_{2} + bx^{2} c_{2} m_{2} m_{3} s_{1} + 2 bx bz a_{1} a_{3} c_{2} m_{2} + bz^{2} c_{2} m_{1} m_{2} s_{3} + bz^{2} c_{2} m_{1} m_{3} s_{2} + c_{0} m_{1} m_{2} m_{3} + c_{1} m_{1} m_{3} s_{2} & bx^{2} a_{2} a_{3} c_{2} m_{1} - bx by a_{1} a_{3} c_{2} m_{2} - bx bz a_{1} a_{2} c_{2} m_{3} - by bz c_{2} m_{1} m_{2} s_{3} - by bz c_{2} m_{1} m_{3} s_{2} + a_{2} a_{3} c_{1} m_{1}\\- bx by a_{2} a_{3} c_{2} m_{1} - bx bz c_{2} m_{1} m_{2} s_{3} - bx bz c_{2} m_{2} m_{3} s_{1} + by^{2} a_{1} a_{3} c_{2} m_{2} - by bz a_{1} a_{2} c_{2} m_{3} + a_{1} a_{3} c_{1} m_{2} & bx^{2} a_{2} a_{3} c_{2} m_{1} - bx by a_{1} a_{3} c_{2} m_{2} - bx bz a_{1} a_{2} c_{2} m_{3} - by bz c_{2} m_{1} m_{2} s_{3} - by bz c_{2} m_{1} m_{3} s_{2} + a_{2} a_{3} c_{1} m_{1} & bx^{2} c_{2} m_{1} m_{2} s_{3} + bx^{2} c_{2} m_{2} m_{3} s_{1} + 2 bx by a_{1} a_{2} c_{2} m_{3} + by^{2} c_{2} m_{1} m_{2} s_{3} + by^{2} c_{2} m_{1} m_{3} s_{2} + c_{0} m_{1} m_{2} m_{3} + c_{1} m_{1} m_{2} s_{3}\end{matrix}\right]$$

```python
d_types = {'c0': 'double', 'c1': 'double', 'c2': 'double',
           'bx': 'double', 'by': 'double', 'bz': 'double'}
```

```python
kernel = compile_kernel('kernel_alfven', a, V,
                        d_args=d_types,
                        backend='fortran')
```

```python
symbol_f90 = compile_symbol('symbol_alfven', a, V,
                            d_args=d_types,
                            backend='fortran')
```

```python
d_args = {'c0': 1.e-12, 'c1': 1.e-4, 'c2': 1.e-2,
          'bx': 0., 'by': 0., 'bz': 1.}
```

```python
M = assemble_matrix(V, kernel, args=d_args)
```

```python
from utils import coo_from_blocks

n_block_rows = 3 ; n_block_cols = 3
matrices = []
for ir in range(0, n_block_rows):
    line = []
    for ic in range(0, n_block_cols):
        coo = M[ir][ic].tocoo()
        coo.eliminate_zeros()
        line.append(coo)
    matrices.append(line)
_M = M
M = coo_from_blocks(matrices,
                    n_block_rows=n_block_rows,
                    n_block_cols=n_block_cols)
```

```python
# import eigenvalue solver
from scipy.linalg import eig

M = M.todense()
w, v = eig(M)
wr = w.real
wr.sort()
```

```python
# ... example of symbol evaluation
n1 = V1.nbasis
n2 = V2.nbasis
n3 = V3.nbasis
t1 = linspace(-pi,pi, n1)
t2 = linspace(-pi,pi, n2)
t3 = linspace(-pi,pi, n3)
x1 = linspace(0.,1., n1)
x2 = linspace(0.,1., n2)
x3 = linspace(0.,1., n3)
e = zeros((3, 3, n1, n2, n3), order='F')
symbol_f90(x1,x2,x3,t1,t2,t3, e,
           d_args['bx'], d_args['by'], d_args['bz'],
           d_args['c0'], d_args['c1'], d_args['c2'])
# ...
```

```python
W = []
for i1 in range(0, n1):
    for i2 in range(0, n2):
        for i3 in range(0, n3):
            mat = e[:,:,i1,i2,i3]
            w,v = eig(mat)
            W += list(w.real)
t = asarray(W)
t.sort()
```

```python
plt.plot(t, "+b", label="glt symbol")
plt.plot(wr, "xr", label="eigenvalues")
plt.legend(loc=2);
```

```python

```

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

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>
<style>
    @font-face {
        font-family: "Computer Modern";
        src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
    }
    div.cell{
        width:600px;
        margin-left:16% !important;
        margin-right:auto;
    }
    h1 {
        font-family: 'Alegreya Sans', sans-serif;
    }
    h2 {
        font-family: 'Fenix', serif;
    }
    h3{
		font-family: 'Fenix', serif;
        margin-top:12px;
        margin-bottom: 3px;
       }
	h4{
		font-family: 'Fenix', serif;
       }
    h5 {
        font-family: 'Alegreya Sans', sans-serif;
    }
    div.text_cell_render{
        font-family: 'Alegreya Sans',Computer Modern, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
        line-height: 135%;
        font-size: 120%;
        width:600px;
        margin-left:auto;
        margin-right:auto;
    }
    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/
    .text_cell_render h1 {
        font-weight: 200;
        font-size: 50pt;
		line-height: 100%;
        color:#054BCD;
        margin-bottom: 0.5em;
        margin-top: 0.5em;
        display: block;
    }
    .text_cell_render h5 {
        font-weight: 300;
        font-size: 16pt;
        color: #054BCD;
        font-style: italic;
        margin-bottom: .5em;
        margin-top: 0.5em;
        display: block;
    }

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

```python

```


```python
from __future__ import print_function
import sisl
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```

In this analysis example we will show how to plot the wavefunction for a periodic system (the same scheme may be used to plot molecular orbitals).

The basic principle of plotting the real-space wave functions may be written as this (for a given $\mathbf k$-point):
\begin{equation}
   \psi_i(\mathbf k, \mathbf r) = \sum_\nu e^{i\mathbf k \cdot \mathbf r_\nu}c_{i\nu}\phi_\nu(\mathbf r - \mathbf r_\nu),
\end{equation}
where $c_{i\nu}=\langle\tilde\phi_\nu|\psi_i\rangle$ is the coefficient for the $i$th eigenstate and $\nu$ basis orbital and $\phi_\nu(\mathbf r - \mathbf r_\nu)$ is the basis orbital $\nu$ centered at $\mathbf r_\nu$.

`sisl` will in most cases, automatically read the necessary information from a Siesta run to be able to construct the $\phi_\nu$ basis functions in real-space. If the basis-information is not available `sisl` will inform you when trying to calculate real-space quantities.

In this example we will calculate the eigenstates for a given $\mathbf k$-point for graphene and plot a cut through the real-space wavefunction at a fixed distance above the graphene plane.

## Exercises

1. Run Siesta.
2. Read in the Hamiltonian using the `RUN.fdf` file, see e.g. [S 1](../S_01/run.ipynb).
3. Calculate the eigenstate for the $\Gamma$-point (see. `Hamiltonian.eigenstate`)
4. Read the entry about the `EigenstateElectron` in the [sisl](http://zerothi.github.io/sisl/docs/latest/api-generated/sisl.physics.html#electrons-electron).
   Figure out which method you should use in order to calculate the real-space wavefunction.
   Use the below method (`plot_grid`) to plot a cut through the wavefunction grid.
   *HINT*: this may be a useful grid `grid = Grid(0.05, sc=H.geometry.sc)`
5. If you **don't** see a warning like this:

        info:0: SislInfo: wavefunction: summing 18 different state coefficients, will continue silently!

    then you have done it correctly! Look in the documentation and figure out how to take a *sub*set of the eigenstates and only plot a single one of them on the grid. This is important since otherwise you are plotting the super-position of all eigenstates at the $\mathbf k$-point.
   *HINT*: If you have VMD/XCrySDen on your laptop it may be *fun* to plot the real-space quantities using cube files, if you want, figure out how to save the `Grid` into a cube/xsf file.
6. Try and *play* with the supercell you pass to the `Grid` initialization and plot the wavefunction for different sizes of the grid.
    What do you see for increasing size of grids? Are there any periodicites, if so, what is the periodicity and why?
7. Calculate the eigenstates such that the wavefunctions has a periodicity of $3$ along the first lattice vector and $2$ along the second lattice vector (only plot one of the eigenstates)
   *HINT*: $e^{i\mathbf k \cdot \mathbf R}$

```python
def plot_grid(grid, plane_dist=1):
    """ Plot the grid in either 1 (real-only) or 2 (complex wavefunction) graphs

    A cut through the grid will be plotted corresponding to `plane_dist` above the z-coordinate
    """
    z_index = grid.index(plane_dist, 2)
    x, y = np.mgrid[:grid.shape[0], :grid.shape[1]]
    dcell = grid.dcell
    x, y = x * dcell[0, 0] + y * dcell[1, 0], x * dcell[0, 1] + y * dcell[1, 1]
    if grid.dtype in [np.complex64, np.complex128]:
        fig, axs = plt.subplots(1, 2, figsize=(15, 5))
        axs[0].contourf(x, y, grid.grid[:, :, z_index].real)
        im = axs[1].contourf(x, y, grid.grid[:, :, z_index].imag)
        for ax in axs:
            ax.set_xlabel(r'$x$ [Ang]'); ax.set_ylabel(r'$y$ [Ang]')
        axs[0].set_title('Real part')
        axs[1].set_title('Imaginary part')
    else:
        fig, ax = plt.subplots(1, 1) ; axs = [ax]
        axs = [ax]
        im = ax.contourf(x, y, grid.grid[:, :, z_index])
        ax.set_xlabel(r'$x$ [Ang]'); ax.set_ylabel(r'$y$ [Ang]')
    # Also plot the atomic coordinates
    try:
        xyz = grid.geometry.xyz
        for ax in axs:
            ax.scatter(xyz[:, 0], xyz[:, 1], 50, 'k', alpha=.6)
    except: pass
    fig.colorbar(im);
```

```python
# Add code here to 1) read in Hamiltonian with basis orbitals, 2) calculate the eigenstate for a given k-point,
# 3) create a grid to plot the wavefunction on and 4) plot the grid using the above method

```

```python

```


# Neuromatch Academy: Week 3, Day4, Tutorial 1
# Deep Learning: Decoding Neural Responses
By Jorge A. Menendez; reviewed by Carsen Stringer, Roozbeh Farhoodi, and Madineh Sarvestani

#Tutorial Objectives
In this tutorial, we'll use deep learning to decode stimulus information from the responses of sensory neurons. Specifically, we'll look at the activity of ~20,000 neurons in mouse primary visual cortex responding to oriented gratings recorded in [this study](https://www.biorxiv.org/content/10.1101/679324v2.abstract). Our task will be to decode the orientation of the presented stimulus from the responses of the whole population of neurons. We could do this in a number of ways, but here we'll use deep learning. Deep learning is particularly well-suited to this problem for a number of reasons:
* The data are very high-dimensional: the neural response to a stimulus is a ~20,000 dimensional vector. Many machine learning techniques fail in such high dimensions, but deep learning actually thrives in this regime as long as you have enough data (which we do here!).
* As you'll be able to see below, different neurons can respond quite differently to stimuli. This complex pattern of responses will, therefore, require non-linear methods to be decoded, which we can easily do with non-linear activation functions in deep networks.
* Deep learning architectures are highly flexible, meaning we can easily adapt the architecture of our decoding model to optimize decoding. Here, we'll focus on a single architecture, but you'll see that it can easily be modified with few changes to the code.

In this tutorial, we will learn how to:
* Build a deep feed-forward network using PyTorch
* Evaluate the network's outputs using PyTorch built-in loss functions
* Compute gradients of the loss with respect to each parameter of the network using automatic differentiation
* Implement stochastic gradient descent to optimize the network parameters

```python
#@title Video: decoding from neural data & feed-forward network architectures and computation
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="BXvfDj3AP_A", width=854, height=480, fs=1)
print("Video available at https://youtu.be/" + video.id)
video
```

    Video available at https://youtu.be/BXvfDj3AP_A

---
## Setup

**Don't forget to execute the hidden cells below!**

```python
import os
import numpy as np
import torch
from torch import nn
from torch import optim
import matplotlib as mpl
from matplotlib import pyplot as plt
```

```python
# @title Download data

# Download data from OSF
data_filename = 'mouseV1.npy'
if data_filename in os.listdir():
  print('data already downloaded!')
else:
  print('downloading data...')
  !wget -O mouseV1.npy https://osf.io/6g4nz/download
```

    downloading data...
    --2020-07-01 23:04:54--  https://osf.io/6g4nz/download
    Resolving osf.io (osf.io)... 35.190.84.173
    Connecting to osf.io (osf.io)|35.190.84.173|:443... connected.
    HTTP request sent, awaiting response... 302 FOUND
    Location: https://files.ca-1.osf.io/v1/resources/hygbm/providers/osfstorage/5edacf4208aad3008943c865?action=download&direct&version=1 [following]
    --2020-07-01 23:04:55--  https://files.ca-1.osf.io/v1/resources/hygbm/providers/osfstorage/5edacf4208aad3008943c865?action=download&direct&version=1
    Resolving files.ca-1.osf.io (files.ca-1.osf.io)... 35.241.38.243
    Connecting to files.ca-1.osf.io (files.ca-1.osf.io)|35.241.38.243|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 867771933 (828M) [application/octet-stream]
    Saving to: ‘mouseV1.npy’

    mouseV1.npy         100%[===================>] 827.57M   107MB/s    in 6.6s

    2020-07-01 23:05:02 (125 MB/s) - ‘mouseV1.npy’ saved [867771933/867771933]

```python
#@title Figure Settings

%matplotlib inline
%config InlineBackend.figure_format='retina'

mpl.rcParams['font.family'] = 'sans-serif'
mpl.rcParams['axes.spines.right'] = False
mpl.rcParams['axes.spines.top'] = False
mpl.rcParams['lines.linewidth'] = 3
mpl.rcParams['legend.frameon'] = False
mpl.rcParams['axes.labelsize'] = 16
mpl.rcParams['figure.figsize'] = (8, 6)

```

```python
#@title Helper Functions

def load_data():
  """Load mouse V1 data from Stringer et al. (2019)

  Data from study reported in this preprint:
  https://www.biorxiv.org/content/10.1101/679324v2.abstract

  These data comprise time-averaged responses of ~20,000 neurons
  to ~4,000 stimulus gratings of different orientations, recorded
  through Calcium imaginge. The responses have been normalized by
  spontanous levels of activity and then z-scored over stimuli, so
  expect negative numbers.

  This function returns the relevant data (neural responses and
  stimulus orientations) in a torch.Tensor of data type torch.float32
  in order to match the default data type for nn.Parameters in
  Google Colab.

  Returns:
    resp (torch.Tensor): n_stimuli x n_neurons matrix of neural responses,
        each row contains the responses of each neuron to a given stimulus
    stimuli: (torch.Tensor): n_stimuli x 1 column vector with orientation
        of each stimulus, in radians

  """
  data = np.load(data_filename, allow_pickle=True).item()

  # Neural responses
  resp = torch.tensor(data['resp'], dtype=torch.float32)

  # Stimuli
  stimuli = torch.tensor(data['stimuli'], dtype=torch.float32).unsqueeze(1)  # add singleton dimension to make a column vector

  return resp, stimuli

def plot_data_matrix(X):
  """Visualize data matrix of neural responses using a heatmap

  Args:
    X (torch.Tensor or np.ndarray): matrix of neural responses to visualize
        with a heatmap

  """
  fig, ax = plt.subplots()

  cax = ax.imshow(X, cmap=mpl.cm.pink, vmin=np.percentile(X, 1), vmax=np.percentile(X, 99))
  cbar = plt.colorbar(cax, ax=ax, label='normalized neural response')

  ax.set_aspect('auto')
  ax.set_xticks([])
  ax.set_yticks([])

class progress_bar():
  """Progress bar for displaying progress over neural network training

  Args:
    n_epochs (int): number of epochs that network will be trained for

  Attributes:
    bar (list of str): the current state of the progress bar
    counter (int): counter tracking the current number of epochs run
    n_epochs (int): total number of epochs to be run

  """

  def __init__(self, n_epochs):
    self.bar = ['|']
    for i in range(n_epochs):
        self.bar.append(' ')
    self.bar.append('|')
    self.counter = 0
    self.n_epochs = n_epochs

  def update(self):
    """Update the progress bar and print it"""
    self.counter += 1
    self.bar[self.counter] = '='
    print('%s     (epoch %i/%i complete)' % (''.join(self.bar), self.counter, self.n_epochs))
```

---
## Load and visualize data

In the next cell, we have provided code to load the data and plot the matrix `resp` of neural responses.

```python
# Load data
resp, stimuli = load_data()
n_stimuli, n_neurons = resp.shape
print(f'resp contains responses of {n_neurons} neurons to {n_stimuli} stimuli')

# Visualize data matrix
plot_data_matrix(resp[:300, :300].T)  # plot responses of first 300 neurons to first 300 stimuli
plt.xlabel('stimulus')
plt.ylabel('neuron')
plt.show()
```

### Exercise 1

Plot the tuning curve of a single neuron. The tuning curve should show the responses of that neuron to each stimulus as a function of the stimulus orientation.

You'll need the following variables:
* `resp` contains the responses of every neuron to every stimulus. The $i$th column contains the responses of the $i$th neuron to each stimulus.
* `stimuli` contains the orientations of each stimulus

**Suggestions**
* convert the stimuli from radians to degrees to get a more interpretable $x$-axis

```python
def plot_tuning():
  """Plot the tuning curve of a random neuron"""

  ################################################################################
  ## TO DO for students: sample a random neuron and plot its tuning curve
  # neuron_indx = ...  # pick random neuron
  # plt.plot(..., ..., '.')  # plot its responses as a function of stimulus orientation
  raise NotImplementedError("Student exercise: write code for plotting tuning curve")
  ################################################################################

  plt.title('neuron %i' % neuron_indx)
  plt.xlabel('stimulus orientation ($^o$)')
  plt.ylabel('neural response')
  plt.xticks(np.linspace(0, 360, 5))
  plt.show()

# plot_tuning()  # UNCOMMENT
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D4_DeepLearning1/solutions/W3D4_Tutorial1_Solution_2a11ec87.py)

*Example output:*

---
## Building deep feed-forward networks with *PyTorch*

We'll now build a simple deep neural network that takes as input a vector of neural responses and outputs a single number representing the decoded stimulus orientation. Specifically, we'll build a deep network with one hidden layer.

Let $\mathbf{r}^{(n)} = \begin{bmatrix} r_1^{(n)} & r_2^{(n)} & \ldots & r_N^{(n)} \end{bmatrix}^T$ denote the vector of neural responses (of neurons $1, \ldots, N$) to the $n$th stimulus. The network we will use is described by the following set of equations:
\begin{align}
    \mathbf{h}^{(n)} &= \mathbf{W}^{in} \mathbf{r}^{(n)} + \mathbf{b}^{in}, && [\mathbf{W}^{in}: M \times N], \\
    y^{(n)} &= \mathbf{W}^{out} \mathbf{h}^{(n)} + \mathbf{b}^{out},  && [\mathbf{W}^{out}: 1 \times M],
\end{align}
where $y^{(n)}$ denotes the scalar output of the network: the decoded orientation of the $n$th stimulus.

The $M$-dimensional vector $\mathbf{h}^{(n)}$ denotes the activations of the **hidden layer** of the network. The reason it is called hidden can be glimpsed from the schematic below, showing how the hidden layer is, in a sense, "hidden" from the input and output of the network. Deeper networks may have multiple hidden layers, but here we'll just focus on a single hidden layer.

We have provided code below for building such a network in PyTorch, using the PyTorch `nn.Module` object class. It contains three key ingredients:

* `__init__()` method to initialize its parameters, like in any other Python class. In this case, it takes a single argument which specifies the number of units in the hidden layer.

* `nn.Linear()` modules, which are built-in PyTorch objects containing all the weights and biases for a given network layer. See [here](https://pytorch.org/docs/master/generated/torch.nn.Linear.html) for documentation. It takes two arguments:
   * \# of inputs to that layer, and
   * \# of outputs from that layer

  For the input layer, for example, we have:
   * \# of inputs = \# of neurons whose responses are to be decoded ($N$)
   * \# of outputs = \# of hidden layer units ($M$)

  PyTorch will initialize all weights and biases randomly.

* `forward()` method, which takes as argument an input to the network and returns the network output. In our case, this comprises computing the output $y$ from a given input $\mathbf{r}$ using the above two equations. We provide the code for doing this using the built-in PyTorch `nn.Linear()` objects.

```python
#@title DeepNet
class DeepNet(nn.Module):
  """Deep Network with one hidden layer

  Args:
    n_hidden (int): number of units in hidden layer

  Attributes:
    in_layer (nn.Linear): weights and biases of input layer
    out_layer (nn.Linear): weights and biases of output layer

  """

  def __init__(self, n_hidden):
    super().__init__()  # needed to invoke the properties of the parent class nn.Module
    self.in_layer = nn.Linear(n_neurons, n_hidden) # neural activity --> hidden units
    self.out_layer = nn.Linear(n_hidden, 1) # hidden units --> output

  def forward(self, r):
    """Decode stimulus orientation from neural responses

    Args:
      r (torch.Tensor): vector of neural responses to decode, must be of
        length n_neurons. Can also be a tensor of shape p x n_neurons,
        containing p vectors of neural responses

    Returns:
      y (torch.Tensor): network outputs for each input provided in r. If
        r is a vector, then y is a 1D tensor of length 1. If r is a 2D
        tensor then y is a 2D tensor of shape p x 1.

    """
    h = self.in_layer(r)  # hidden representation
    y = self.out_layer(h)
    return y
```

We next provide code for running the deep network, i.e. for using the deep network to decode stimulus orientation from a vector of neural responses.

An important thing to note in this code snippet is the `.detach()` method. The PyTorch `nn.Module` class is special in that, behind the scenes, each of the variables linked to it are linked to each other in a computational graph, for the purposes of automatic differentiation (which will be discussed below). As a result, if you want to do anything that is not a `torch` operation to the parameters or outputs of an `nn.Module` class, you'll need to first "detach" it from its computational graph. This is what the `.detach()` method does. You'll see in this code snippet we need to call it on the outputs of the network so that we can pass it through the `np.rad2deg()` function.

```python
# Initialize a deep network with M=200 hidden units
net = DeepNet(200)

# Decode stimulus orientation from neural responses to the first stimulus
# in the data set. Note that the output of the network will be nonsense
# as its weights have been initialized randomly.
istim = 0  # index of first stimulus
r = resp[istim]  # neural responses to this stimulus
out = net(r)  # compute output from network using forward() method
ori_decode = np.rad2deg(out.detach())  # decoded orientation, in degrees -- need to use .detach() so that the torch.Tensor can be treated as a numpy array
ori_true = np.rad2deg(stimuli[istim])  # true stimulus orientation, in degrees

print('decoded orientation: %.2f degrees' % ori_decode)
print('true orientation: %.2f degrees' % ori_true)
```

    decoded orientation: -32.62 degrees
    true orientation: 150.13 degrees

Note that the deep network we constructed above comprises solely **linear** operations on each layer: each layer is just a weighted sum of the elements in the previous layer. We'll next incorporate a certain class of **non-linear** operations. Using non-linear layer-to-layer transformations, in fact, allows deep networks to perform much more complex computations, which is why non-linearities are always used in practice.

### Exercise 2

Create a new class `DeepNetReLU` by modifying our above deep network model to implement a non-linear transformation from the input layer to the hidden layer. In other words, modify it so that the hidden layer activations are given by
\begin{equation}
    \mathbf{h}^{(n)} = \phi(\mathbf{W}^{in} \mathbf{r}^{(n)} + \mathbf{b}^{in})
\end{equation}
where $\phi$ is some non-linear function, referred to as the **activation function**. We'll use linear rectification:
\begin{equation}
  \phi(x) =
  \begin{cases}
    x & \text{if } x > 0 \\
    0 & \text{else}
  \end{cases}
\end{equation}
which can be implemented in PyTorch using `torch.relu()`. Hidden layers with this activation function are typically referred to as "**Re**ctified **L**inear **U**nits", or **ReLU**'s.

**Hint**: you only need to modify the `forward()` method of the above `DeepNet()` class.

```python
class DeepNetReLU(nn.Module):

  def __init__(self, n_hidden):
    super().__init__()  # needed to invoke the properties of the parent class nn.Module
    ##############################################################
    ## TO DO for students: initialize network weights using nn.Linear() objects
    raise NotImplementedError("Student exercise: write code for initializing deep network")
    ##############################################################

  def forward(self, r):
    ##############################################################
    ## TO DO for students: write code for computing network output, using a
    ## rectified linear activation function for the hidden units
    raise NotImplementedError("Student exercise: write code for computing network output")
    ##############################################################
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D4_DeepLearning1/solutions/W3D4_Tutorial1_Solution_48c0b82d.py)

```python
#@title Video: loss functions & SGD
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="rx8d2EkV1Mg", width=854, height=480, fs=1)
print("Video available at https://youtu.be/" + video.id)
video
```

    Video available at https://youtu.be/rx8d2EkV1Mg

---
## Evaluating loss functions

Because the weights of the network are currently randomly chosen, the outputs of the network are nonsense: the decoded stimulus orientation is nowhere close to the true stimulus orientation. We'll shortly write some code to change these weights so that the network does a better job of decoding.

But to do so, we first need to define what we mean by "better". One simple way of defining this is to use the **squared error**
\begin{equation}
    L = (y - \tilde{y})^2
\end{equation}
where $y$ is the network output and $\tilde{y}$ is the true stimulus orientation. When the decoded stimulus orientation is far from the true stimulus orientation, $L$ will be large. We thus refer to $L$ as the **loss function**, as it quantifies how *bad* the network is at decoding stimulus orientation.

PyTorch actually carries with it a number of built-in loss functions. The one corresponding to squared error is called `nn.MSELoss()`. This will take as arguments a number of network outputs $y_1, y_2, \ldots, y_P$ and corresponding target outputs $\tilde{y}_1, \tilde{y}_2, \ldots, \tilde{y}_P$, and compute the **mean squared error (MSE)**
\begin{equation}
    L = \frac{1}{P}\sum_{n=1}^P \left(y^{(n)} - \tilde{y}^{(n)}\right)^2
\end{equation}

We provide code below evaluating the mean squared error of our linear deep network for the neural responses to the first 100 stimuli.

```python
# PyTorch mean squared error loss function
loss_fn = nn.MSELoss()

# Evaluate mean squared error on neural responses
# to first 100 stimuli
istim = np.arange(100)  # indeces of first 100 stimuli
out = net(resp[istim])  # decoded stimulus orientations
ori = stimuli[istim]  # true stimulus orientations
print('mean squared error: %.2f' % loss_fn(out, ori))
```

    mean squared error: 12.17

### Exercise 3

Write a function that will evaluate the mean squared error for a given set of neural responses. Use the  initialized `loss_fn` above to compute the mean squared error.

Then, initialize a deep network with $M=20$ rectified linear units in the hidden layer, and use your function to evaluate its mean squared error on population responses to 100 random stimuli.

```python
######################################################################
## TO DO for students:
##   - initialize a deep network with one hidden layer of 20 rectified
##     linear units, using the above implemented DeepNetReLU() class
##   - pick 100 random stimuli
##   - use the network to decode stimulus orientation from responses to
##     these stimuli
##   - evaluate the mean squared error using loss_fn()
# net = ...  # initialize deep network, using DeepNetReLU()
# istim = ...  # indices of 100 random stimuli
# out = ...  # use network to decode orientation from neural responses
# loss = ...  # evaluate mean squared error using loss_fn()
# print('mean squared error: %.2f' % loss)
######################################################################
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D4_DeepLearning1/solutions/W3D4_Tutorial1_Solution_70ebabdf.py)

---
## Optimization with stochastic gradient descent

Our goal is to modify the weights to make the mean squared error loss $L$ as small as possible over the whole data set. To do this, we'll use the **stochastic gradient descent (SGD)** algorithm, which consists of iterating four simple steps:
1. **Randomly sample a *mini-batch* of training data**. This is only strictly necessary when the full data set is too big to pass through the network all at once. That said, using mini-batches turns out to also help avoid overfitting.
2. **Evaluate the loss** at this mini-batch
3. **Compute the gradient of the loss** with respect to each of the network weights. In PyTorch, we can do this with one line of code: if the loss is stored in a variable `loss`, all you need to do is run
```
loss.backward()
```
PyTorch will then compute the gradients of this quantity with respect to each network parameter using [automatic differentiation](https://en.wikipedia.org/wiki/Automatic_differentiation), and store them behind the scenes.
4. **Update the network weights** by descending the gradient. In Pytorch, we can do this with one line of code by using built-in optimizers. The SGD optimizer is called `optim.SGD` (documentation [here](https://pytorch.org/docs/stable/optim.html#torch.optim.SGD)), and takes as an argument
  * the parameters to be updated
  * the learning rate to use

  For example, to optimize *all* the parameters of a network `net` using a learning rate of .001, one would use
  ```
  optimizer = optim.SGD(net.parameters(), lr=.001)
  ```
  where `.parameters()` is a method of the `nn.Module` class that returns a [Python generator object](https://wiki.python.org/moin/Generators) over all the parameters of that `nn.Module` class (in our case, $\mathbf{W}^{in}, \mathbf{b}^{in}, \mathbf{W}^{out}, \mathbf{b}^{out}$).

  To update each of these parameters, we then need only call the `.step()` method of this optimizer:
  ```
  optimizer.step()
  ```
  This single line of code will execute the SGD updates for each parameter given to the optimizer when it was initialized. Note that this is true no matter how big/small the network is, allowing us to use the same two lines of code for any deep network model built using PyTorch.

Finally, an important detail to remember is that the gradients of each parameter need to be cleared before calling `.backward()`, or else PyTorch will try to accumulate gradients across iterations. This can again be done using built-in optimizers via the method `zero_grad()`, as follows:
```
optimizer.zero_grad()
```

For the mathematical details of the SGD algorithm, see the appendix.

### Exercise 4

1. Complete the function `train()` that optimizes the weights of a given network with stochastic gradient descent on some given training data. We've provided most of the code for iterating over steps 1-4 listed above. The only piece left for you to complete is step 2: evaluating the loss $L$ at the current mini-batch.

  **Hint:** you already wrote code for computing the MSE term $L_{MSE}$ in the previous exercise, and you can use the `L2penalty()` function to evaluate the regularization term $\mathcal{R}_{L2}$. Don't forget to mutiply the regularization term by the regularization weight `gamma` before summing it with the MSE!

  **Note:** this function has two nested loops. The outer loop is over epochs. The inner loop iterates the SGD steps 1-4 outlined above over random mini-batches within each epoch. (cf. appendix)

2. Initialize a deep network with one hidden layer of 20 rectified linear units and use this function to train it on some training data (with the given default SGD parameters).

  **Hint:** use your `DeepNetReLU()` class to initialize the deep network.

Note that SGD is essentially an algorithm for fitting the network's parameters to the given training data. Selecting this training data is thus crucial for ensuring that the optimized parameters **generalize** to unseen data they weren't trained on. In our case, for example, we want to make sure that our trained network is good at decoding stimulus orientations from neural responses to any orientation, not just those in our data set.

To ensure this, we will split up the full data set into a **training set** and a **test set**. We'll use the training set for optimizing the parameters with SGD, and then evaluate how good the optimized parameters are by using the trained network to decode stimulus orientations from neural responses in the test set. Good decoding performance on this test set should then be indicative of good decoding performance on the neurons' responses to any other stimulus orientation. This procedure is commonly used in machine learning (not just in deep learning)and is typically referred to as **cross-validation**.

When using this for SGD, it is also worth noting that the random subsampling of our data used to build the training set induces a certain amount of stochasticity into our gradient descent algorithm, which will help avoid potential local minima.

```python
def train(net, train_data, train_labels):
  """Run stochastic gradient descent for a given network

  Args:
    net (nn.Module): deep network whose parameters to optimize with SGD
    train_data (torch.Tensor): n_train x n_neurons tensor with neural
      responses to train on
    train_labels (torch.Tensor): n_train x 1 tensor with orientations of the
      stimuli corresponding to each row of train_data

  """

  # Set SGD hyperparameters
  learning_rate = 1e-3  # learning rate for SGD
  n_epochs = 10  # number of epochs to run SGD
  batch_size = 250  # number of data points in each mini-batch

  # Initialize PyTorch SGD optimizer
  optimizer = optim.SGD(net.parameters(), lr=learning_rate)

  # Placeholder to save MSE at each iteration
  train_mse = []

  # Progress bar to track progress
  pbar = progress_bar(n_epochs)

  # Loop over epochs
  for i in range(n_epochs):

    # Split up training data into random non-overlapping mini-batches
    ishuffle = torch.randperm(train_data.shape[0])  # random ordering of training data
    minibatch_data = torch.split(train_data[ishuffle], batch_size)  # split train_data into minibatches
    minibatch_labels = torch.split(train_labels[ishuffle], batch_size)  # split train_labels into minibatches

    # Loop over mini-batches
    for r, ori in zip(minibatch_data, minibatch_labels):

      # Evaluate mean squared error loss
      ######################################################################
      ## TO DO for students: evaluate loss at current mini-batch
      # out = ...  # use network to decode orientation from neural responses in this minibatch
      # loss = ...  # evaluate mean squared error for this minibatch
      raise NotImplementedError("Student exercise: write code for evaluating loss at current mini-batch")
      ######################################################################

      # Store current mean squared error
      train_mse.append(loss.item())  # .item() transforms the tensor to a scalar and does .detach() for us

      # Compute gradients
      optimizer.zero_grad()  # clear gradients
      loss.backward()

      # Update weights
      optimizer.step()

    # Track progress
    pbar.update()

  # Plot the loss
  plt.plot(train_mse)
  plt.xlim([0, None])
  plt.ylim([0, None])
  plt.xlabel('iterations of stochastic gradient descent')
  plt.ylabel('mean squared error\non training data')
  plt.show()

# Split data into training set and testing set
n_train = int(0.75 * n_stimuli)  # putting 75% of data into training set
ishuffle = torch.randperm(n_stimuli)
itrain = ishuffle[:n_train]  # indices of data samples to include in training set
itest = ishuffle[n_train:]  # indices of data samples to include in testing set

# Initialize network and train it on training set
######################################################################
## TO DO for students: initialize deep network and train it on training set
# net = ...  # use M=20 hidden units
# train(net, ..., ...)
######################################################################
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D4_DeepLearning1/solutions/W3D4_Tutorial1_Solution_3455cc67.py)

*Example output:*

### Exercise 5

1. Complete the function `test()` that decodes stimulus orientations from a given set of test data and plots these against the true stimulus orientations, in degrees.
2. Use this function to evaluate the network trained in the previous exercise on data from the test set, indexed by `itest`.

**Hint:** don't forget to use `.detach()` to do any plotting or `numpy` operations on the network output

```python
def test(net, test_data, test_labels):
  """Decode stimulus orientation from neural responses in test data and plot
  against the true stimulus orientations, in degrees

  Args:
    net (nn.Module): deep network to use to decode stimulus orientation
    test_data (torch.Tensor): n_test x n_neurons tensor with neural
      responses to decode
    test_labels (torch.Tensor): n_test x 1 tensor with orientations of the
      stimuli corresponding to each row of test_data, in radians

  """

  ########################################################################
  ## TO DO for students: decode stimulus orientation from neural responses in test_data
  ## and plot against true stimulus orientations in test_labels
  # out = ...  # decode stimulus orientation for each population response in test set
  # ori_decode = ...  # transform from radians to degrees
  # ori_true = ...  # true stimulus orientations, in degrees
  raise NotImplementedError("Student exercise: write code for decoding stimulus orientation from neural responses in test set")
  ########################################################################

  # Plot
  plt.plot(ori_true, ori_decode, '.')  # plot true orientation vs decoded orientation
  plt.xlabel('true stimulus orientation ($^o$)')
  plt.ylabel('decoded stimulus orientation ($^o$)')
  axticks = np.linspace(0, 360, 5)
  plt.xticks(axticks)
  plt.yticks(axticks)
  plt.show()

###############################################################################
## TO DO for students: use test() function to evaluate network trained in previous exercise
## on the test set
# test(net, ..., ...)
###############################################################################
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D4_DeepLearning1/solutions/W3D4_Tutorial1_Solution_a7fbc2de.py)

*Example output:*

---
## Model criticism

Let's now take a step back and think about how our model is succeeding/failing and how to improve it.

### Exercise 6

1. Complete the function `decoding_error()` that computes the decoding error (decoded stimulus orientation minus true stimulus orientation, in degrees) on a given set of test data and plots it as a function of the true stimulus orientation.

2. Use this function to evaluate and plot the decoding error of the network trained in the previous exercise on data from the test set, indexed by `itest`.

3. Interpret what you see. Some questions to think about:
  * Are some stimulus orientations harder to decode than others?
  * If so, in what sense? Are the decoded orientations for these stimuli more variable and/or are they biased?
  * Can you explain this variability/bias? What makes these stimulus orientations different from the others?
  * Can you think of a way to modify the deep network in order to avoid this?

```python
def decoding_error(net, test_data, test_labels):
  """Plot decoding error as a function of true stimulus orientation, in degrees

  Args:
    net (nn.Module): deep network to use to decode stimulus orientation
    test_data (torch.Tensor): n_test x n_neurons tensor with neural
      responses to decode
    test_labels (torch.Tensor): n_test x 1 tensor with orientations of the
      stimuli corresponding to each row of test_data, in radians

  """

  ########################################################################
  ## TO DO for students: compute and plot the decoding error
  # out = ...  # decode stimulus orientation for each population response in test set
  # ori_decode = ...  # transform from radians to degrees
  # ori_true = ...  # true stimulus orientations, in degrees
  # error = ...  # decoding error, in degrees
  # plt.plot(..., ..., '.')   # plot decoding error as a function of true orientation
  raise NotImplementedError("Student exercise: write code for computing and plotting the decoding error")
  ########################################################################

  plt.xlabel('true stimulus orientation ($^o$)')
  plt.ylabel('decoding error ($^o$)')
  plt.xticks(np.linspace(0, 360, 5))
  plt.yticks(np.linspace(-360, 360, 9))
  plt.show()

###############################################################################
## TO DO for students: use decoding_error() function to plot decoding error on the test set
## for the network trained above
# decoding_error(net, ..., ...)
###############################################################################
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D4_DeepLearning1/solutions/W3D4_Tutorial1_Solution_8af20f38.py)

*Example output:*

---
## Summary

We have now covered a number of common and powerful techniques for applying deep learning to decoding from neural data, some of which are common to almost any machine learning problem:
* Building and training neural network models with **PyTorch and automatic differentiation**
* Choosing and evaluating **loss functions**
* Implementing **stochastic gradient descent** by iterating over random **mini-batches** of training data
* Testing a trained model on unseen data by splitting the data into a **training set and test set**

---
# Appendix

## Stochastic gradient descent

Here we provide a more detailed explanation of the **stochastic gradient descent (SGD) algorithm** as applied to our decoding problem, and its implementation in PyTorch.

This algorithm consists of iterating four simple steps:
1. **Randomly sample a *mini-batch* of training data.** In our case, that means randomly sampling a set of neural response vectors $\mathbf{r}^{(1)}, \mathbf{r}^{(2)}, \ldots, \mathbf{r}^{(P)}$ and corresponding stimulus orientations $\tilde{y}^{(1)}, \tilde{y}^{(2)}, \ldots, \tilde{y}^{(P)}$.
2. **Evaluate the loss** at this mini-batch. For a mean squared error loss, this is given by
\begin{equation}
    L = \frac{1}{P}\sum_{n=1}^P (y^{(n)} - \tilde{y}^{(n)})^2
\end{equation}
where recall that $y^{(n)}$ denotes the stimulus orientation decoded from the population response $\mathbf{r}^{(n)}$ to the $n$th stimulus, and $\tilde{y}^{(n)}$ is the true orientation of this stimulus.
3. **Compute the gradient of the loss** with respect to each of the network weights. In our case, that entails computing the quantities
\begin{equation}
    \frac{\partial L}{\partial \mathbf{W}^{in}}, \frac{\partial L}{\partial \mathbf{b}^{in}}, \frac{\partial L}{\partial \mathbf{W}^{out}}, \frac{\partial L}{\partial \mathbf{b}^{out}}
\end{equation}
Usually, this would require lots of math in order to derive these gradients, and lots of code to compute them. But this is where PyTorch comes to the rescue! Using a cool technique called [automatic differentiation](https://en.wikipedia.org/wiki/Automatic_differentiation), PyTorch will calculate these gradients automatically for you. If the loss is stored in a variable `loss`, all you need to do is run
```
loss.backward()
```
PyTorch will then compute the gradient of this quantity with respect to each network parameter. These are computed and stored behind the scenes, and can be accessed through the `.grad` attribute of each of the network's parameters. But, as you'll see next, we won't even need to.

4. **Update the network weights** by descending the gradient:
\begin{align}
    \mathbf{W}^{in} &\leftarrow \mathbf{W}^{in} - \alpha \frac{\partial L}{\partial \mathbf{W}^{in}} \\
    \mathbf{b}^{in} &\leftarrow \mathbf{b}^{in} - \alpha \frac{\partial L}{\partial \mathbf{b}^{in}} \\
    \mathbf{W}^{out} &\leftarrow \mathbf{W}^{out} - \alpha \frac{\partial L}{\partial \mathbf{W}^{out}} \\
    \mathbf{b}^{out} &\leftarrow \mathbf{b}^{out} - \alpha \frac{\partial L}{\partial \mathbf{b}^{out}}
\end{align}
where $\alpha$ is called the **learning rate**. This **hyperparameter** of the SGD algorithm controls how far we descend the gradient on each iteration. It should be as large as possible so that fewer iterations are needed, but not too large so that the parameter updates can't skip over minima in the loss landscape.

  In Pytorch we can implement all of these updates with just a single line of code. We first construct an SGD optimizer using the `optim.SGD` class (documentation [here](https://pytorch.org/docs/stable/optim.html#torch.optim.SGD)), and tell it which parameters to update and what learning rate to use. For example, to optimize the parameters of a network `net` using a learning rate of .001, we construct an SGD optimizer as follows
  ```
  optimizer = optim.SGD(net.parameters(), lr=.001)
  ```
  where the method `net.parameters()` returns all of the parameters (i.e. weights and biases) in `net`. To update each of these parameters, we then need only call the `.step()` method of this optimizer:
  ```
  optimizer.step()
  ```
  This single line of code will execute each of the updates written above. Moreover, for more complicated networks with more layers and more parameters, this single line of code will suffice to update *all* of its parameters according to the analogous gradient descent equations.

## Epochs and mini-batches

Note that our above implementation of SGD contains two nested loops. The inner loop is over mini-batches, implementing steps 1-4 above. But take a close look at exactly how these mini-batches are being randomly sampled. Rather than sampling a totally random mini-batch of training data each iteration, we split up the full training data into non-overlapping mini-batches to ensure that these mini-batches will cover the whole training data set. Each time we loop over all the mini-batches covering the training data set is called an **epoch**. The outer loop is over **epochs**: we loop over the entire training data set `n_epoch` times.


```python
# Erasmus+ ICCT project (2018-1-SI01-KA203-047081)

# Toggle cell visibility

from IPython.display import HTML
tag = HTML('''
Toggle cell visibility <a href="javascript:code_toggle()">here</a>.''')
display(tag)
```

Toggle cell visibility here.

```python
%matplotlib notebook
import numpy as np
import control as control
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from ipywidgets import widgets
from ipywidgets import interact
import scipy.signal as signal
import sympy as sym

```

## Mechanical systems

#### General mass-spring-damper model
> The mass-spring-damper model consists of discrete mass nodes distributed throughout an object and interconnected via a network of springs and dampers. This model is well-suited for modelling object with complex material properties such as nonlinearity and viscoelasticity. (source: [Wikipedia](https://en.wikipedia.org/wiki/Mass-spring-damper_model "Mass-spring-model"))
#### 1/4 car model
> 1/4 car model is used to analyze the ride quality of automotive suspension systems. Mass $m_1$ is the "sprung mass", which is one-quarter of the vehicle mass that is supported by the suspension system. Mass $m_2$ is the "unsprung mass", which is lumped mass composed of one wheel and half-axle assembly, plus the shock absorber and suspensison springs. The stiffness and damping of the suspension system are modeled by the ideal spring constant $k_1$ and friction coefficient $B$, respecitvely. Tire stifness is modeled by spring constant $k_2$. (source: [Chegg Study](https://www.chegg.com/homework-help/questions-and-answers/figure-p230-shows-1-4-car-model-used-analyze-ride-quality-automotive-suspension-systems-ma-q26244005 "1/4 car model"))

---

### How to use this notebook?
1. Toggle between *mass-spring-damper* and *1/4 car model* system by clicking on a corresponding button.
2. Toggle betweeen *step function*, *impulse function*, *ramp function*, and *sine function* to select the function of the force $F$.
3. Move the sliders to change the values of the mass ($m$; $m_1$ and $m_2$), spring coefficients ($k$; $k_1$ and $k_2$), damping constant ($B$), input signal amplification and initial conditions ($x_0$, $\dot{x}_0$, $y_0$, $\dot{y}_0$).

        Mass-spring-damper
        1/4 car model

```python
# create figure
fig = plt.figure(figsize=(9.8, 4),num='Mechanical systems')

# add sublot
ax = fig.add_subplot(111)
ax.set_title('Time Response')
ax.set_ylabel('input, output')
ax.set_xlabel('$t$ [s]')

ax.grid(which='both', axis='both', color='lightgray')

inputf, = ax.plot([], [])
responsef, = ax.plot([], [])
responsef2, = ax.plot([], [])
arrowf, = ax.plot([],[])

style = {'description_width': 'initial'}

selectSystem=widgets.ToggleButtons(
    options=[('mass-spring-damper',0),('1/4 car model',1)],
    description='Select system: ', style=style) # define toggle buttons
selectForce = widgets.ToggleButtons(
    options=[('step function', 0), ('impulse function', 1), ('ramp function', 2), ('sine function', 3)],
    description='Select $F$ function: ', style=style)
display(selectSystem)
display(selectForce)

def build_model(M,K,B,M1,M2,B1,K1,K2,amp,x0,xpika0,y0,ypika0,select_System,index):

    num_of_samples = 1000
    total_time = 25
    t = np.linspace(0, total_time, num_of_samples) # time for which response is calculated (start, stop, step)

    global inputf, responsef, responsef2, arrowf

    if select_System==0:

        system0 = control.TransferFunction([1], [M, B, K])

        if index==0:
            inputfunc = np.ones(len(t))*amp
            inputfunc[0]=0
            time, response, xx = control.forced_response(system0, t, inputfunc, X0=[xpika0,x0])

        elif index==1:
            inputfunc=signal.unit_impulse(1000, 0)*amp
            time, response, xx = control.forced_response(system0, t, inputfunc, X0=[xpika0,x0])

        elif index==2:
            inputfunc=t;
            time, response, xx = control.forced_response(system0, t, inputfunc, X0=[xpika0,x0])

        elif index==3:
            inputfunc=np.sin(t)*amp
            time, response, xx = control.forced_response(system0, t, inputfunc, X0=[xpika0,x0])

    elif select_System==1:

        system1=control.TransferFunction([M2, B1, K1+K2], [M1*M2, M1*B1+M2*B1, M2*K1+M1*(K1+K2), K2*B1, K1*K2])
        system2=control.TransferFunction([B1*K1*M2**2, B1**2*K1*M2, B1*K1**2*M2 + 2*B1*K1*K2*M2,
                                          B1**2*K1*K2, B1*K1**2*K2 + B1*K1*K2**2],
                                         [M1**2*M2**2, B1*M1**2*M2 + 2*B1*M1*M2**2,
                                          B1**2*M1*M2 + B1**2*M2**2 + K1*M1**2*M2 + 2*K1*M1*M2**2 + 2*K2*M1**2*M2 + K2*M1*M2**2,
                                          2*B1*K1*M1*M2 + 2*B1*K1*M2**2 + B1*K2*M1**2 + 5*B1*K2*M1*M2 + B1*K2*M2**2,
                                          B1**2*K2*M1 + 2*B1**2*K2*M2 + K1**2*M1*M2 + K1**2*M2**2 + K1*K2*M1**2 + 5*K1*K2*M1*M2 + K1*K2*M2**2 + K2**2*M1**2 + 2*K2**2*M1*M2,
                                          2*B1*K1*K2*M1 + 4*B1*K1*K2*M2 + 3*B1*K2**2*M1 + 2*B1*K2**2*M2,
                                          B1**2*K2**2 + K1**2*K2*M1 + 2*K1**2*K2*M2 + 3*K1*K2**2*M1 + 2*K1*K2**2*M2 + K2**3*M1,
                                          2*B1*K1*K2**2 + B1*K2**3,
                                          K1**2*K2**2 + K1*K2**3])
        if index==0:
            inputfunc = np.ones(len(t))*amp
            inputfunc[0]=0
            time, response, xx = control.forced_response(system1, t, inputfunc, X0=[0,0,xpika0,x0])
            time2, response2, xx2 = control.forced_response(system2, t, inputfunc, X0=[0,0,0,0,0,0,ypika0,y0])

        elif index==1:
            inputfunc=signal.unit_impulse(1000, 0)*amp
            time, response, xx = control.forced_response(system1, t, inputfunc, X0=[0,0,xpika0,x0])
            time2, response2, xx2 = control.forced_response(system2, t, inputfunc, X0=[0,0,0,0,0,0,ypika0,y0])

        elif index==2:
            inputfunc=t;
            time, response, xx = control.forced_response(system1, t, inputfunc, X0=[0,0,xpika0,x0])
            time2, response2, xx2 = control.forced_response(system2, t, inputfunc, X0=[0,0,0,0,0,0,ypika0,y0])

        elif index==3:
            inputfunc=np.sin(t)*amp
            time, response, xx = control.forced_response(system1, t, inputfunc, X0=[0,0,xpika0,x0])
            time2, response2, xx2 = control.forced_response(system2, t, inputfunc, X0=[0,0,0,0,0,0,ypika0,y0])

    ax.lines.remove(responsef)
    ax.lines.remove(inputf)
    ax.lines.remove(responsef2)
    ax.lines.remove(arrowf)

    inputf, = ax.plot(t,inputfunc,label='$F$',color='C0')
    responsef, = ax.plot(time, response,label='$x$',color='C3')

    if select_System==1:
        responsef2, = ax.plot(time, response2,label='$y$',color='C2')
    elif select_System==0:
        responsef2, = ax.plot([],[])

    if index==1:
        if amp>0:
            arrowf, = ax.plot([-0.1,0,0.1],[amp-((amp*0.05)/2),amp,amp-((amp*0.05)/2)],color='C0',linewidth=4)
        elif amp==0:
            arrowf, = ax.plot([],[])
        elif amp<0:
            arrowf, = ax.plot([-0.1,0,0.1],[amp-((amp*0.05)/2),amp,amp-(amp*(0.05)/2)],color='C0',linewidth=4)
    else:
        arrowf, = ax.plot([],[])

    ax.relim()
    ax.autoscale_view()

    ax.legend()

def update_sliders(index):
    global m1_slider, b1_slider, k1_slider, m21_slider, m22_slider, b2_slider, k21_slider, k22_slider
    global x0_slider, xpika0_slider, y0_slider, ypika0_slider

    m1val = [0.1,0.1,0.1,0.1]
    k1val = [1,1,1,1]
    b1val = [0.1,0.1,0.1,0.1]
    m21val = [0.1,0.1,0.1,0.1]
    m22val = [0.1,0.1,0.1,0.1]
    b2val = [0.1,0.1,0.1,0.1]
    k21val = [1,1,1,1]
    k22val = [1,1,1,1]
    x0val = [0,0,0,0]
    xpika0val = [0,0,0,0]
    y0val = [0,0,0,0]
    ypika0val = [0,0,0,0]

    m1_slider.value = m1val[index]
    k1_slider.value = k1val[index]
    b1_slider.value = b1val[index]
    m21_slider.value = m21val[index]
    m22_slider.value = m22val[index]
    b2_slider.value = b2val[index]
    k21_slider.value = k21val[index]
    k22_slider.value = k22val[index]
    x0_slider.value = x0val[index]
    xpika0_slider.value = xpika0val[index]
    y0_slider.value = y0val[index]
    ypika0_slider.value = ypika0val[index]

def draw_controllers(type_select,index):

    global m1_slider, b1_slider, k1_slider, m21_slider, m22_slider, b2_slider, k21_slider, k22_slider
    global x0_slider, xpika0_slider, y0_slider, ypika0_slider

    x0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='$x_0$ [dm]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
    xpika0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='${\dot{x}}_0$ [dm/s]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)

    if type_select==0:

        amp_slider = widgets.FloatSlider(value=1.,min=-2.,max=2.,step=0.1,
            description='Input signal amplification:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',style=style)

        m1_slider = widgets.FloatSlider(value=.1, min=.01, max=1., step=.01,
                                        description='$m$ [kg]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
        k1_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
                                       description='$k$ [N/m]:',disabled=False,continuous_update=False,
                                       orientation='horizontal',readout=True,readout_format='.1f',)
        b1_slider = widgets.FloatSlider(value=.1,min=0.0,max=0.5,step=.01,
                                        description='$B$ [Ns/m]:',disabled=False,continuous_update=False,
                                        rientation='horizontal',readout=True,readout_format='.2f',)
        m21_slider = widgets.FloatSlider(value=.1,min=.01,max=1.,step=.01,
            description='$m_1$ [kg]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        m22_slider = widgets.FloatSlider(value=.1,min=.0,max=1.,step=.01,
            description='$m_2$ [kg]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        b2_slider = widgets.FloatSlider(value=.1,min=0.0,max=2,step=.01,
            description='$B$ [Ns/m]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        k21_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
            description='$k_1$ [N/m]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',
        )
        k22_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
            description='$k_2$ [N/m]:',disabled=True,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',
        )

        y0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='$y_0$ [dm]:',disabled=True,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
        ypika0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='${\dot{y}}_0$ [dm/s]:',disabled=True,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
    elif type_select==1:

        amp_slider = widgets.FloatSlider(value=1.,min=-2.,max=2.,step=0.1,
            description='Input signal amplification:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',style=style)

        m1_slider = widgets.FloatSlider(value=.1, min=.01, max=1., step=.01,
                                        description='$m$ [kg]:',disabled=True,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
        k1_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
                                       description='$k$ [N/m]:',disabled=True,continuous_update=False,
                                       orientation='horizontal',readout=True,readout_format='.1f',)
        b1_slider = widgets.FloatSlider(value=.1,min=0.0,max=0.5,step=.01,
                                        description='$B$ [Ns/m]:',disabled=True,continuous_update=False,
                                        rientation='horizontal',readout=True,readout_format='.2f',)
        m21_slider = widgets.FloatSlider(value=.1,min=.01,max=1.,step=.01,
            description='$m_1$ [kg]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        m22_slider = widgets.FloatSlider(value=.1,min=.0,max=1.,step=.01,
            description='$m_2$ [kg]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        b2_slider = widgets.FloatSlider(value=.1,min=0.0,max=2,step=.01,
            description='$B$ [Ns/m]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.2f',
        )
        k21_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
            description='$k_1$ [N/m]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',
        )
        k22_slider = widgets.FloatSlider(value=1.,min=0.,max=20.,step=.1,
            description='$k_2$ [N/m]:',disabled=False,continuous_update=False,orientation='horizontal',readout=True,readout_format='.1f',
        )

        y0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='$y_0$ [dm]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
        ypika0_slider=widgets.FloatSlider(value=0, min=-1, max=1., step=.1,
                                        description='${\dot{y}}_0$ [dm/s]:',disabled=False,continuous_update=False,
                                        orientation='horizontal',readout=True,readout_format='.2f',)
    input_data = widgets.interactive_output(build_model, {'M':m1_slider, 'K':k1_slider, 'B':b1_slider, 'M1':m21_slider,
                                                    'M2':m22_slider, 'B1':b2_slider, 'K1':k21_slider, 'K2':k22_slider, 'amp':amp_slider,
                                                    'x0':x0_slider,'xpika0':xpika0_slider,'y0':y0_slider,'ypika0':ypika0_slider,
                                                    'select_System':selectSystem,'index':selectForce})

    input_data2 = widgets.interactive_output(update_sliders, {'index':selectForce})

    box_layout = widgets.Layout(border='1px solid black',
                    width='auto',
                    height='',
                    flex_flow='row',
                    display='flex')

    buttons1=widgets.HBox([widgets.VBox([amp_slider],layout=widgets.Layout(width='auto')),
                         widgets.VBox([x0_slider,xpika0_slider]),
                         widgets.VBox([y0_slider,ypika0_slider])],layout=box_layout)
    display(widgets.VBox([widgets.Label('Select the values of the input signal amplification and intial conditions:'), buttons1]))
    display(widgets.HBox([widgets.VBox([m1_slider,k1_slider,b1_slider], layout=widgets.Layout(width='45%')),
                          widgets.VBox([m21_slider,m22_slider,k21_slider,k22_slider,b2_slider], layout=widgets.Layout(width='45%'))]), input_data)

widgets.interactive_output(draw_controllers, {'type_select':selectSystem,'index':selectForce})
```

    <IPython.core.display.Javascript object>

    ToggleButtons(description='Select system: ', options=(('mass-spring-damper', 0), ('1/4 car model', 1)), style=…

    ToggleButtons(description='Select $F$ function: ', options=(('step function', 0), ('impulse function', 1), ('r…

    Output()

```python

```


```python
try:
    import openmdao.api as om
    import dymos as dm
except ImportError:
    !python -m pip install openmdao[notebooks]
    !python -m pip install dymos[docs]
    import openmdao.api as om
    import dymos as dm
```

# Hyper-Sensitive Problem

This example is based on the Hyper-Sensitive problem given in
Patterson {cite}`patterson2015ph`. In this problem we seek to minimize both
the distance traveled when moving between fixed boundary conditions and
also to minimize the control $u$ used. The cost function to be minimized is:

\begin{align}
    J &= \frac{1}{2} \int_{0}^{t_f} (x^2 + u^2) dt
\end{align}

The system is subject to the dynamic constraints:

\begin{align}
    \frac{dx}{dt} &= -x + u
\end{align}

The boundary conditions are:

\begin{align}
    x(t_0) &= 1.5 \\
    x(t_f) &= 1
\end{align}

The control $u$ is unconstrained while the final time $t_f$ is fixed.

Due to the nature of dynamics, for sufficiently large values of $t_f$,
the problem exhibits a _dive_, _cruise_, and _resurface_ type
structure, where the all interesting behavior occurs at the beginning and
end while remaining relatively constant in the middle.

This problem has a known analytic optimal solution:

\begin{align}
    x^*(t) &= c_1 e^{\sqrt{2} t} + c_2 e^{-\sqrt{2} t} \\
      u^*(t) &= \dot{x}^*(t) + x^*(t)
\end{align}

where:

\begin{align}
    c_1 &= \frac{1.5 e^{-\sqrt{2} t_f} - 1}{e^{-\sqrt{2} t_f} - e^{\sqrt{2} t_f}} \\
    c_2 &= \frac{1 - 1.5 e^{\sqrt{2} t_f}}{e^{-\sqrt{2} t_f} - e^{\sqrt{2} t_f}}
\end{align}

## The ODE System: hyper\_sensitive\_ode.py

```python
import numpy as np
import openmdao.api as om

class HyperSensitiveODE(om.ExplicitComponent):
    states = {'x': {'rate_source': 'x_dot'},
              'xL': {'rate_source': 'L'}}

    parameters = {'u': {'targets': 'u'}}

    def initialize(self):
        self.options.declare('num_nodes', types=int)

    def setup(self):
        nn = self.options['num_nodes']

        # inputs
        self.add_input('x', val=np.zeros(nn), desc='state')
        self.add_input('xL', val=np.zeros(nn), desc='cost_state')

        self.add_input('u', val=np.zeros(nn), desc='control')

        self.add_output('x_dot', val=np.zeros(nn), desc='state rate', units='1/s')
        self.add_output('L', val=np.zeros(nn), desc='Lagrangian', units='1/s')

        # Setup partials
        self.declare_partials(of='x_dot', wrt='x', rows=np.arange(nn), cols=np.arange(nn), val=-1)
        self.declare_partials(of='x_dot', wrt='u', rows=np.arange(nn), cols=np.arange(nn), val=1)

        self.declare_partials(of='L', wrt='x', rows=np.arange(nn), cols=np.arange(nn))
        self.declare_partials(of='L', wrt='u', rows=np.arange(nn), cols=np.arange(nn))

    def compute(self, inputs, outputs):
        x = inputs['x']
        u = inputs['u']

        outputs['x_dot'] = -x + u
        outputs['L'] = (x ** 2 + u ** 2) / 2

    def compute_partials(self, inputs, jacobian):
        x = inputs['x']
        u = inputs['u']

        jacobian['L', 'x'] = x
        jacobian['L', 'u'] = u
```

## Building and running the problem

The following code shows the procedure for solving the problem

```python
import numpy as np
tf = np.float128(10)

def solution():
    sqrt_two = np.sqrt(2)
    val = sqrt_two * tf
    c1 = (1.5 * np.exp(-val) - 1) / (np.exp(-val) - np.exp(val))
    c2 = (1 - 1.5 * np.exp(val)) / (np.exp(-val) - np.exp(val))

    ui = c1 * (1 + sqrt_two) + c2 * (1 - sqrt_two)
    uf = c1 * (1 + sqrt_two) * np.exp(val) + c2 * (1 - sqrt_two) * np.exp(-val)
    J = 0.5 * (c1 ** 2 * (1 + sqrt_two) * np.exp(2 * val) + c2 ** 2 * (1 - sqrt_two) * np.exp(-2 * val) -
               (1 + sqrt_two) * c1 ** 2 - (1 - sqrt_two) * c2 ** 2)
    return ui, uf, J
```

```python
import matplotlib.pyplot as plt
import openmdao.api as om
import dymos as dm
from dymos.examples.plotting import plot_results

# Initialize the problem and assign the driver
p = om.Problem(model=om.Group())
p.driver = om.pyOptSparseDriver()
p.driver.options['optimizer'] = 'SLSQP'
p.driver.declare_coloring()

# Setup the trajectory and its phase
traj = p.model.add_subsystem('traj', dm.Trajectory())

transcription = dm.Radau(num_segments=30, order=3, compressed=False)

phase = traj.add_phase('phase0',
                       dm.Phase(ode_class=HyperSensitiveODE, transcription=transcription))

phase.set_time_options(fix_initial=True, fix_duration=True)
phase.add_state('x', fix_initial=True, fix_final=False, rate_source='x_dot', targets=['x'])
phase.add_state('xL', fix_initial=True, fix_final=False, rate_source='L', targets=['xL'])
phase.add_control('u', opt=True, targets=['u'])

phase.add_boundary_constraint('x', loc='final', equals=1)

phase.add_objective('xL', loc='final')

p.setup(check=True)

p.set_val('traj.phase0.states:x', phase.interp('x', [1.5, 1]))
p.set_val('traj.phase0.states:xL', phase.interp('xL', [0, 1]))
p.set_val('traj.phase0.t_initial', 0)
p.set_val('traj.phase0.t_duration', tf)
p.set_val('traj.phase0.controls:u', phase.interp('u', [-0.6, 2.4]))

#
# Solve the problem.
#
dm.run_problem(p)

#
# Verify that the results are correct.
#
ui, uf, J = solution()

#
# Simulate the explicit solution and plot the results.
#
exp_out = traj.simulate()

plot_results([('traj.phase0.timeseries.time', 'traj.phase0.timeseries.states:x',
               'time (s)', 'x $(m)$'),
              ('traj.phase0.timeseries.time', 'traj.phase0.timeseries.controls:u',
               'time (s)', 'u $(m/s^2)$')],
             title='Hyper Sensitive Problem Solution\nRadau Pseudospectral Method',
             p_sol=p, p_sim=exp_out)

plt.show()
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p.get_val('traj.phase0.timeseries.controls:u')[0],
                  ui,
                  tolerance=1.5e-2)

assert_near_equal(p.get_val('traj.phase0.timeseries.controls:u')[-1],
                  uf,
                  tolerance=1.5e-2)

assert_near_equal(p.get_val('traj.phase0.timeseries.states:xL')[-1],
                  J,
                  tolerance=1e-2)
```

## References

```{bibliography}
:filter: docname in docnames
```