# Tutorial 1: Part 2

Objectives:
- Learn how to define a simple lattice and compute the TWISS functions using MAD-X.
- Thick vs thin lens approximation TWISS comparison for a lattice with only quadrupoles.
- Tune and $\beta$-function dependence on K1.

**My first accelerator: a FODO cell**

1. Make a simple lattice FODO cell with:
    - $L_{cell}$= 100 m.
    - Focusing and defocusing quadrupoles of 5 m long ($L_{quad}$).
    - Put the start of the first qudrupole at the start of the sequence.
    - Each quadrupole has a focal length f = 200 m (HINT: k1 x $L_{quad}$ = 1/f).

2. Define a proton beam at $E_{tot}$= 2 GeV. Activate the sequence and try to find the periodic solution and plot the $\beta$-functions. If you found $\beta_{max}$= 460 m you succeded.

3. Using the plot you obtained can you estimate the phase advance of the cell. Compare with the tunes obtained with the TWISS.

**Matching the FODO cell using a parametric plot**

4. Try to twiss it powering the quadrupoles to obtain a $\Delta \mu \approx 90^\circ$ in the cell using the thin lens approximation (HINT: using the figures from Tutorial 1: Part 1). What is the actual phase advance computed by MAD-X?

**BONUS:**

5. What is the $\beta_{max}$? Compare with the thin lens approximation (using the figures from Tutorial 1: Part 1).

6. Halve the focusing strength of the quadrupoles, what is the effect of it on the $\beta_{max}$ and $\beta_{min}$ and on the $\Delta \mu$? Compare with the parametric plots.

7. Compute the maximum beam size $\sigma$ assuming a normalized emittance of 3 mrad mm and $E_{tot}= 7 TeV$.

8. Try with $E_{tot}$ = 0.7 GeV: what is the MAD-X error message?

9. Try with f= 20 m: what is the MAD-x error message?

```python
#Import the needed libraries
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
from cpymad.madx import Madx
```

# Launching MAD-X

```python
myMad = Madx(stdout=True)
```

1. Make a simple lattice FODO cell with:
    - $L_{cell}$= 100 m.
    - Focusing and defocusing qudrupoles of 5 m long ($L_{quad}$).
    - Put the start of the first qudrupole at the start of the sequence.
    - Each quadrupole has a focal length f = 200 m (HINT: k1 x $L_{quad}$ = 1/f).

2. Define a proton beam at $E_{tot}$= 2 GeV. Activate the sequence and try to find the periodic solution and plot the $\beta$-functions. If you found $\beta_{max}$= 460 m you succeded.

```python
#myMad = Madx(stdout=True)
myMad = Madx()
myString='''
! *********************************************************************
! Definition of parameters
! *********************************************************************

l_cell=100;
quadrupoleLenght=5;
f=200;
myK:=1/f/quadrupoleLenght;// m^-2

! *********************************************************************
! Definition of magnets
! *********************************************************************
QF: quadrupole, L=quadrupoleLenght, K1:=myK;
QD: quadrupole, L=quadrupoleLenght, K1:=-myK;

! *********************************************************************
! Definition of sequence
! *********************************************************************
myCell:sequence, refer=entry, L=L_CELL;
quadrupole1: QF, at=0;
marker1: marker, at=25;
quadrupole2: QD, at=50;
marker2: marker, at=75;
endsequence;

! *********************************************************************
! Definition of beam
! *********************************************************************
beam, particle=proton, energy=2;

! *********************************************************************
! Use of the sequence
! *********************************************************************
use, sequence=myCell;

! *********************************************************************
! TWISS
! *********************************************************************
title, 'My first twiss';
twiss, file=MyfirstFODO.madx;
plot, haxis=s, vaxis=betx,bety,dx,colour=100, title="test",file=MyfirstFODO;

value myK;
'''
myMad.input(myString);
```

      ++++++++++++++++++++++++++++++++++++++++++++
      +     MAD-X 5.07.00  (64 bit, Darwin)      +
      + Support: mad@cern.ch, http://cern.ch/mad +
      + Release   date: 2021.05.03               +
      + Execution date: 2022.01.13 17:18:14      +
      ++++++++++++++++++++++++++++++++++++++++++++
    enter Twiss module

    iteration:   1 error:   0.000000E+00 deltap:   0.000000E+00
    orbit:   0.000000E+00  0.000000E+00  0.000000E+00  0.000000E+00  0.000000E+00  0.000000E+00

    ++++++ table: summ

                length             orbit5               alfa            gammatr
                   100                 -0                  0                  0

                    q1                dq1            betxmax              dxmax
         0.03853349451     -0.04384718845        463.6232883                  0

                 dxrms             xcomax             xcorms                 q2
                     0                  0                  0      0.03853349451

                   dq2            betymax              dymax              dyrms
        -0.04384718845        463.6232883                  0                  0

                ycomax             ycorms             deltap            synch_1
                     0                  0                  0                  0

               synch_2            synch_3            synch_4            synch_5
                     0                  0                  0                  0

               synch_6            synch_8             nflips
                     0                  0                  0
    Plot - default table plotted: twiss

     GXPLOT-X11  1.50 initialized

     plot number =            1
    myk                =              0.001 ;

```python
########################
# Using MAD-X commands #
########################
myString='''
value, table(SUMM,Q1);
value, table(SUMM,betymax);
'''
myMad.input(myString);
```

    table( summ q1 )   =      0.03853349451 ;
    table( summ betymax ) =        463.6232883 ;

```python
#########################
# Using python commands #
#########################
# SUMM table
myDF=myMad.table.summ.dframe()
myDF
myDF["q1"]
```

    #e    0.038533
    Name: q1, dtype: float64

```python
#########################
# Using python commands #
#########################
# TWISS table
myDF=myMad.table.twiss.dframe()
myDF[['name','s','betx','bety','alfx','alfy','mux','muy']]
```

      name
      s
      betx
      bety
      alfx
      alfy
      mux
      muy

      #s
      mycell$start:1
      0.0
      463.623288
      369.779162
      -1.156109
      0.929316
      0.000000
      0.000000

      quadrupole1
      quadrupole1:1
      5.0
      463.623288
      369.779162
      1.156109
      -0.929316
      0.001709
      0.002161

      drift_0[0]
      drift_0:0
      25.0
      419.394867
      408.967742
      1.055312
      -1.030113
      0.008930
      0.010350

      marker1
      marker1:1
      25.0
      419.394867
      408.967742
      1.055312
      -1.030113
      0.008930
      0.010350

      drift_1[0]
      drift_1:0
      50.0
      369.779162
      463.623288
      0.929316
      -1.156109
      0.019041
      0.019493

      quadrupole2
      quadrupole2:1
      55.0
      369.779162
      463.623288
      -0.929316
      1.156109
      0.021202
      0.021202

      drift_2[0]
      drift_2:0
      75.0
      408.967742
      419.394867
      -1.030113
      1.055312
      0.029391
      0.028423

      marker2
      marker2:1
      75.0
      408.967742
      419.394867
      -1.030113
      1.055312
      0.029391
      0.028423

      drift_3[0]
      drift_3:0
      100.0
      463.623288
      369.779162
      -1.156109
      0.929316
      0.038533
      0.038533

      #e
      mycell$end:1
      100.0
      463.623288
      369.779162
      -1.156109
      0.929316
      0.038533
      0.038533

```python
%matplotlib notebook
plt.rcParams['savefig.dpi'] = 80
plt.rcParams['figure.dpi'] = 80
# Plot
plt.plot(myDF['s'],myDF['betx'],'.-b',label='$\\beta_x$')
plt.plot(myDF['s'],myDF['bety'],'.-r',label='$\\beta_y$')
plt.legend()
plt.grid()
plt.xlabel('s [m]')
plt.ylabel('[m]')
plt.title('My first FODO cell')
```

    <IPython.core.display.Javascript object>

    Text(0.5, 1.0, 'My first FODO cell')

**If you found $\beta_{max}$= 463.6 m you succeded!**

# Tune from the plot
3. Using the plot you obtained can you estimate the phase advance of the cell. Compare with the tunes obtained with the TWISS.

For the phase advance one can consider the definition

\begin{equation}
\mu=\int\frac{1}{\beta(s)}ds.
\end{equation}

Remember that the unit of phase in MAD-X is [2$\pi$].

```python
# A very basic approximation considering a constant beta
# The mean beta value is
1/417.*100/2/np.pi
```

    0.03816665301963917

```python
# Computing the integral
np.trapz(1/myDF['betx'],myDF['s'])/2/np.pi
```

    0.038571104937361426

```python
# Correct values from MAD-X TWISS commands
myDF.iloc[-1]['mux']
```

    0.03853349450910022

```python
# Phase Advance in units of degrees
myDF.iloc[-1]['mux']*2*180
```

    13.87205802327608

# **Matching the FODO cell using a parametric plot**
4. Try to twiss it powering the quadrupoles to obtain a $\Delta \mu \approx 90^\circ$ in the cell using the thin lens approximation (using the figures from Tutorial 1: Part 1). What is the actual phase advance computed by MAD-X?

```python
myMad = Madx(stdout=True)
```

```python
# From the plot from Tutoria 1 - part1, for 90 degrees phase advance k*Lcell*lq=2.8
quadrupoleLenght=5
cellLength=100
myK=2.8/cellLength/quadrupoleLenght
print(myK)
```

    0.005599999999999999

In comparison to the previous case as we want to increase the phase advance we need to increase the k if we keep constant the length of the cell and of the quadrupole. We move towards the right on the parmetric plot of Tutorial 1 - Part 1.

```python
myString='''
! *********************************************************************
! Definition of parameters
! *********************************************************************

l_cell=100;
quadrupoleLenght=5;
myK:=2.8/l_cell/quadrupoleLenght;// m^-2

! *********************************************************************
! Definition of magnets
! *********************************************************************
QF: quadrupole, L=quadrupoleLenght, K1:=myK;
QD: quadrupole, L=quadrupoleLenght, K1:=-myK;

! *********************************************************************
! Definition of sequence
! *********************************************************************
myCell:sequence, refer=entry, L=L_CELL;
quadrupole1: QF, at=0;
marker1: marker, at=25;
quadrupole2: QD, at=50;
marker2: marker, at=75;
endsequence;

! *********************************************************************
! Definition of beam
! *********************************************************************
beam, particle=proton, energy=2;

! *********************************************************************
! Use of the sequence
! *********************************************************************
use, sequence=myCell;

! *********************************************************************
! TWISS
! *********************************************************************
title, 'My first twiss';
twiss, file=MyfirstFODO.madx;
plot, haxis=s, vaxis=betx,bety,dx,colour=100, title="test",file=MyfirstFODO;

value myK;
'''
myMad.input(myString);
```

```python
myDFTable=myMad.table.twiss.dframe()
myDFTable[["name", "keyword","betx","bety","alfx","alfy", "mux", "muy" ]]
```

      name
      keyword
      betx
      bety
      alfx
      alfy
      mux
      muy

      #s
      mycell$start:1
      marker
      160.603655
      34.217492
      -2.259847
      0.548735
      0.000000
      0.000000

      quadrupole1
      quadrupole1:1
      quadrupole
      160.603655
      34.217492
      2.259847
      -0.548735
      0.004841
      0.023892

      drift_0[0]
      drift_0:0
      drift
      85.419675
      71.376768
      1.499352
      -1.309229
      0.032151
      0.090204

      marker1
      marker1:1
      marker
      85.419675
      71.376768
      1.499352
      -1.309229
      0.032151
      0.090204

      drift_1[0]
      drift_1:0
      drift
      34.217492
      160.603655
      0.548735
      -2.259847
      0.108661
      0.127712

      quadrupole2
      quadrupole2:1
      quadrupole
      34.217492
      160.603655
      -0.548735
      2.259847
      0.132553
      0.132553

      drift_2[0]
      drift_2:0
      drift
      71.376768
      85.419675
      -1.309229
      1.499352
      0.198864
      0.159863

      marker2
      marker2:1
      marker
      71.376768
      85.419675
      -1.309229
      1.499352
      0.198864
      0.159863

      drift_3[0]
      drift_3:0
      drift
      160.603655
      34.217492
      -2.259847
      0.548735
      0.236373
      0.236373

      #e
      mycell$end:1
      marker
      160.603655
      34.217492
      -2.259847
      0.548735
      0.236373
      0.236373

```python
myString='''
value, table(SUMM,Q1);
value, table(SUMM,Q2);
'''
myMad.input(myString);
```

```python
#Phase advance computed by MADX in rad
0.236*2*np.pi
```

    1.4828317324943823

```python
#Phase advance computed by MADX in degrees
1.4828317324943823*180/np.pi
```

    84.96

**BONUS:**

5. What is the $\beta_{max}$? Compare with the thin lens approximation (using the figures from Tutorial 1: Part 1).

```python
# From the MAD-X calculation
myDFTable['betx'].max()
```

    160.60365457633435

```python
myDFTable['bety'].max()
```

    160.60365457633424

```python
#From the parametric plot Figure 1
#K1*Lcell*Lq=
0.0056*100*5
```

    2.8

```python
#From the parametric plot Figure 2 to be compared with 160.6036545763343 m (with MAD-X)
1.697*100
```

    169.70000000000002

6. Halve the focusing strength of the quadrupoles, what is the effect of it on the $\beta_{max}$ and $\beta_{min}$ and on the $\Delta \mu$? Compare with the parametric plots.

```python
myString='''

cellLength=100;
quadrupoleLenght=5;
myK=1.4/cellLength/quadrupoleLenght;// m^-2
twiss, file=firstTwiss.txt;
'''

myMad.input(myString);
```

```python
myString='''
value, table(SUMM,Q1);
value, table(SUMM,Q2);
value, table(SUMM,betxmax);
value, table(SUMM,betymax);
'''
myMad.input(myString);
```

```python
myDFTable=myMad.table.twiss.dframe()
myDFTable
```

      name
      keyword
      s
      betx
      alfx
      mux
      bety
      alfy
      muy
      x
      ...
      sig54
      sig55
      sig56
      sig61
      sig62
      sig63
      sig64
      sig65
      sig66
      n1

      #s
      mycell$start:1
      marker
      0.0
      205.460401
      -1.434644
      0.000000
      106.483860
      0.764835
      0.000000
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      quadrupole1
      quadrupole1:1
      quadrupole
      5.0
      205.460401
      1.434644
      0.003828
      106.483860
      -0.764835
      0.007564
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_0[0]
      drift_0:0
      drift
      25.0
      154.028513
      1.136951
      0.021759
      143.031119
      -1.062528
      0.033470
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      marker1
      marker1:1
      marker
      25.0
      154.028513
      1.136951
      0.021759
      143.031119
      -1.062528
      0.033470
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_1[0]
      drift_1:0
      drift
      50.0
      106.483860
      0.764835
      0.053029
      205.460401
      -1.434644
      0.056764
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      quadrupole2
      quadrupole2:1
      quadrupole
      55.0
      106.483860
      -0.764835
      0.060592
      205.460401
      1.434644
      0.060592
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_2[0]
      drift_2:0
      drift
      75.0
      143.031119
      -1.062528
      0.086499
      154.028513
      1.136951
      0.078523
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      marker2
      marker2:1
      marker
      75.0
      143.031119
      -1.062528
      0.086499
      154.028513
      1.136951
      0.078523
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_3[0]
      drift_3:0
      drift
      100.0
      205.460401
      -1.434644
      0.109792
      106.483860
      0.764835
      0.109792
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      #e
      mycell$end:1
      marker
      100.0
      205.460401
      -1.434644
      0.109792
      106.483860
      0.764835
      0.109792
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

10 rows × 256 columns

If we reduce the k the bmax increases and therfore the beam size.

```python
# If compared with the thin lens approximtion
# From the plot
# K1*Lcell*Lq
0.0028*100*5
```

    1.4

```python
#Max. betax
2.042*100
```

    204.2

```python
# Value from MADX
bmax=np.max(myDFTable["betx"])
bmax
```

    205.46040125139584

Better agreement is observed as we move to the left on the parametric plot (smaller K or smaller Lq for a fixed value of the cell length) as the thin lens approximation condition is better satisfied.

7. Compute the maximum beam size $\sigma$ assuming a normalized emittance of 3 mrad mm and $E_{tot}= 7 TeV$.

One has to remember
\begin{equation} \sigma=\sqrt{\frac{\beta \epsilon_n}{ \gamma_r}} \end{equation}

```python
import numpy as np
emittance_n=3e-6 #m*rad
beta_gamma=7000/.938 # this is an approximation
np.sqrt(myDFTable['betx'].max()*emittance_n/beta_gamma)
```

    0.00028739359996886

# Varying the energy
8. Try with $E_{tot}$ = 0.7 GeV: what is the MAD-X error message?

```python
# with this simple wrapper in case of error the code enter in an infinite loop you have to stop manually.
myString='''
beam, particle=proton, energy=0.7;
title, 'My third twiss';
twiss;
'''
myMad.input(myString);
```

There is an error due to the fact that the total energy is lower than the rest proton mass.

# With f=20 m
9. Try with f= 20 m: what is the MAD-x error message?

```python
myMad = Madx(stdout=True)
```

```python
myString='''
! *********************************************************************
! Definition of parameters
! *********************************************************************

l_cell=100;
quadrupoleLenght=5;
f=20;
myK:=1/f/quadrupoleLenght;// m^-2

value myK;

! *********************************************************************
! Definition of magnet
! *********************************************************************
QF: quadrupole, L=quadrupoleLenght, K1:=myK;
QD: quadrupole, L=quadrupoleLenght, K1:=-myK;

! *********************************************************************
! Definition of sequence
! *********************************************************************
myCell:sequence, refer=entry, L=L_CELL;
quadrupole1: QF, at=0;
marker1: marker, at=25;
quadrupole2: QD, at=50;
endsequence;

! *********************************************************************
! Definition of beam
! *********************************************************************
beam, particle=proton, energy=2;

! *********************************************************************
! Use of the sequence
! *********************************************************************
use, sequence=myCell;

! *********************************************************************
! TWISS
! *********************************************************************
select, flag=twiss, clear;
select, flag=twiss, column=name, keyword, s, betx, alfx, mux, bety, alfy, muy, x, px, y, py, dx, dy, dx, dpx, dy, dpy;
twiss, file=f20.txt;
'''
myMad.input(myString);
```

**INTERPRETATION**: The cell is unstable due to the fact that the focal length is too short. Please note the value of the cosmux and cosmuy. **REMEMBER** |Trace(M)|< 2; -1 <= cos $\mu$ <= 1

# EXTRA

# Adding markers

This is an example to add markers in the sequence using a macros.

```python
myMad=Madx(stdout=True)
myString='''

! *********************************************************************
! Definition of parameters
! *********************************************************************

option, echo=false, info=false, warn=false;
l_cell=100;
quadrupoleLenght=5;
f=200;
myK:=1/f/quadrupoleLenght;// m^-2

! *********************************************************************
! Definition of magnet
! *********************************************************************
QF: quadrupole, L=quadrupoleLenght, K1:=myK;
QD: quadrupole, L=quadrupoleLenght, K1:=-myK;

installMarkers(nn): macro ={
markernn: marker, at=nn;
!value,f;
};
N=6;

! *********************************************************************
! Definition of sequence
! *********************************************************************
myCell:sequence, REFER=centre, L=L_CELL;
quadrupole1: QF, at=2.5;
while (N<50) {
exec, installMarkers($N);
N=N+1;
}
quadrupole2: QD, at=52.5;
N=56;
while (N<100) {
exec, installMarkers($N);
N=N+1;
}
endsequence;

! *********************************************************************
! Definition of beam
! *********************************************************************
beam, particle=proton, energy=2;

! *********************************************************************
! Use of the sequence
! *********************************************************************
use, sequence=myCell;

! *********************************************************************
! TWISS
! *********************************************************************
select, flag=twiss, clear;
select, flag=twiss, column=name, keyword, L, s, betx, alfx, mux, bety, alfy, muy, x, px, y, py, dx, dy, dx, dpx, dy, dpy;

title, 'My fourth twiss';
twiss, file=WithMarkers.txt;
'''
myMad.input(myString);
```

```python
myDF=myMad.table.twiss.dframe()
myDF.head()
```

      name
      keyword
      s
      betx
      alfx
      mux
      bety
      alfy
      muy
      x
      ...
      sig54
      sig55
      sig56
      sig61
      sig62
      sig63
      sig64
      sig65
      sig66
      n1

      #s
      mycell$start:1
      marker
      0.0
      463.623288
      -1.156109
      0.000000
      369.779162
      0.929316
      0.000000
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      quadrupole1
      quadrupole1:1
      quadrupole
      5.0
      463.623288
      1.156109
      0.001709
      369.779162
      -0.929316
      0.002161
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_0[0]
      drift_0:0
      drift
      6.0
      461.316110
      1.151069
      0.002053
      371.642834
      -0.934356
      0.002590
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      marker6
      marker6:1
      marker
      6.0
      461.316110
      1.151069
      0.002053
      371.642834
      -0.934356
      0.002590
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_1[0]
      drift_1:0
      drift
      7.0
      459.019012
      1.146029
      0.002399
      373.516585
      -0.939396
      0.003018
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

5 rows × 256 columns

```python
%matplotlib notebook
plt.plot(myDF['s'],myDF['betx'],'.-b',label='$\\beta_x$')
plt.plot(myDF['s'],myDF['bety'],'.-r',label='$\\beta_y$')
plt.xlabel('s [m]')
plt.ylabel('[m]')
plt.legend(loc='best')
plt.grid()
```

    <IPython.core.display.Javascript object>

```python
np.trapz(1/myDF['betx'],myDF['s'])/2/np.pi
```

    0.03853163530259353

```python
# Value from MADX
bmax=np.max(myDF["betx"])
bmax
```

    463.6232883078409

# Thin lens approximation

```python
myString='''
select, flag=makethin, class=quadrupole, slice=5;
makethin,sequence=myCell;
use,sequence=myCell;
twiss,sequence=myCell,file=thin.txt;
'''
myMad.input(myString);
```

```python
myDF_thin=myMad.table.twiss.dframe()
myDF_thin
```

      name
      keyword
      s
      betx
      alfx
      mux
      bety
      alfy
      muy
      x
      ...
      sig54
      sig55
      sig56
      sig61
      sig62
      sig63
      sig64
      sig65
      sig66
      n1

      #s
      mycell$start:1
      marker
      0.000000
      463.623069
      -1.156108
      0.000000
      369.779039
      0.929315
      0.000000
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_0[0]
      drift_0:0
      drift
      0.416667
      464.587367
      -1.158208
      0.000143
      369.005484
      0.927215
      0.000180
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      quadrupole1..1
      quadrupole1..1:1
      multipole
      0.416667
      464.587367
      -0.693620
      0.000143
      369.005484
      0.558210
      0.000180
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_1[0]
      drift_1:0
      drift
      1.458333
      466.035869
      -0.696941
      0.000499
      367.846404
      0.554507
      0.000630
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      quadrupole1..2
      quadrupole1..2:1
      multipole
      1.458333
      466.035869
      -0.230905
      0.000499
      367.846404
      0.186661
      0.000630
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

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

      marker98
      marker98:1
      marker
      98.000000
      459.018797
      -1.146028
      0.037843
      373.516458
      0.939395
      0.037677
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_97[0]
      drift_97:0
      drift
      99.000000
      461.315893
      -1.151068
      0.038189
      371.642709
      0.934355
      0.038104
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      marker99
      marker99:1
      marker
      99.000000
      461.315893
      -1.151068
      0.038189
      371.642709
      0.934355
      0.038104
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      drift_98[0]
      drift_98:0
      drift
      100.000000
      463.623069
      -1.156108
      0.038533
      369.779039
      0.929315
      0.038533
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      #e
      mycell$end:1
      marker
      100.000000
      463.623069
      -1.156108
      0.038533
      369.779039
      0.929315
      0.038533
      0.0
      ...
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

201 rows × 256 columns

```python
%matplotlib notebook
plt.plot(myDF_thin['s'],myDF_thin['betx'],'.-b',label='$\\beta_x$')
plt.plot(myDF_thin['s'],myDF_thin['bety'],'.-r',label='$\\beta_y$')
plt.xlabel('s [m]')
plt.ylabel('[m]')
plt.legend(loc='best')
plt.grid()
```

    <IPython.core.display.Javascript object>

```python
np.trapz(1/myDF_thin['betx'],myDF_thin['s'])/2/np.pi
```

    0.038533549044816415

```python
mux=np.max(myDF_thin["mux"])
mux
```

    0.03853347061040913

```python
# Value from MADX
bmax=np.max(myDF_thin["betx"])
bmax
```

    466.5193742711588


# Example Data Project: the Train Illusion

Marius 't Hart, Megan Peters, Paul Schrater, Jean Laurens, Gunnar Blohm

**Disclaimer**:  this is a "toy" data neuroscience project used to demonstrate the [10 step procedure of how-to-model](https://doi.org/10.1523/ENEURO.0352-19.2019). It is not meant to be state of the art research.

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

```python
# @title Setup

# for matrices and plotting:
import numpy as np
import matplotlib.pyplot as plt

# for random distributions:
from scipy.stats import norm, poisson

# for logistic regression:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
!pip install tqdm --quiet
```

```python
# @title Generate Data

def generateSpikeTrains():

  gain        = 2
  neurons     = 50
  movements   = [0,1,2]
  repetitions = 800

  np.random.seed(37)

  # set up the basic parameters:
  dt = 1/100
  start, stop = -1.5, 1.5
  t = np.arange(start, stop+dt, dt)                                            # a time interval
  Velocity_sigma = 0.5                                                         # std dev of the velocity profile
  Velocity_Profile = norm.pdf(t,0,Velocity_sigma)/norm.pdf(0,0,Velocity_sigma) # The Gaussian velocity profile, normalized to a peak of 1

  # set up the neuron properties:
  Gains = np.random.rand(neurons) * gain          # random sensitivity between 0 and `gain`
  FRs   = (np.random.rand(neurons) * 60 ) - 10    # random base firing rate between -10 and 50

  # output matrix will have this shape:
  target_shape = [len(movements), repetitions, neurons, len(Velocity_Profile)]

  # build matrix for spikes, first, they depend on the velocity profile:
  Spikes = np.repeat(Velocity_Profile.reshape([1,1,1,len(Velocity_Profile)]),len(movements)*repetitions*neurons,axis=2).reshape(target_shape)

  # multiplied by gains:
  S_gains = np.repeat(np.repeat(Gains.reshape([1,1,neurons]), len(movements)*repetitions, axis=1).reshape(target_shape[:3]), len(Velocity_Profile)).reshape(target_shape)
  Spikes = Spikes * S_gains

  # and multiplied by the movement:
  S_moves = np.repeat( np.array(movements).reshape([len(movements),1,1,1]), repetitions*neurons*len(Velocity_Profile), axis=3 ).reshape(target_shape)
  Spikes = Spikes * S_moves

  # on top of a baseline firing rate:
  S_FR = np.repeat(np.repeat(FRs.reshape([1,1,neurons]), len(movements)*repetitions, axis=1).reshape(target_shape[:3]), len(Velocity_Profile)).reshape(target_shape)
  Spikes = Spikes + S_FR

  # can not run the poisson random number generator on input lower than 0:
  Spikes = np.where(Spikes < 0, 0, Spikes)

  # so far, these were expected firing rates per second, correct for dt:
  Spikes = poisson.rvs(Spikes * dt)

  return(Spikes)

def subsetPerception(spikes):

  movements = [0,1,2]
  split     = 400
  subset    = 40
  hwin      = 3

  [num_movements, repetitions, neurons, timepoints] = np.shape(spikes)

  decision = np.zeros([num_movements, repetitions])

  # ground truth for logistic regression:
  y_train = np.repeat([0,1,1],split)
  y_test  = np.repeat([0,1,1],repetitions-split)

  m_train = np.repeat(movements, split)
  m_test  = np.repeat(movements, split)

  # reproduce the time points:
  dt = 1/100
  start, stop = -1.5, 1.5
  t = np.arange(start, stop+dt, dt)

  w_idx = list( (abs(t) < (hwin*dt)).nonzero()[0] )
  w_0 = min(w_idx)
  w_1 = max(w_idx)+1  # python...

  # get the total spike counts from stationary and movement trials:
  spikes_stat = np.sum( spikes[0,:,:,:], axis=2)
  spikes_move = np.sum( spikes[1:,:,:,:], axis=3)

  train_spikes_stat = spikes_stat[:split,:]
  train_spikes_move = spikes_move[:,:split,:].reshape([-1,neurons])

  test_spikes_stat = spikes_stat[split:,:]
  test_spikes_move = spikes_move[:,split:,:].reshape([-1,neurons])

  # data to use to predict y:
  x_train = np.concatenate((train_spikes_stat, train_spikes_move))
  x_test  = np.concatenate(( test_spikes_stat,  test_spikes_move))

  # this line creates a logistics regression model object, and immediately fits it:
  population_model = LogisticRegression(solver='liblinear', random_state=0).fit(x_train, y_train)

  # solver, one of: 'liblinear', 'newton-cg', 'lbfgs', 'sag', and 'saga'
  # some of those require certain other options
  #print(population_model.coef_)       # slope
  #print(population_model.intercept_)  # intercept

  ground_truth = np.array(population_model.predict(x_test))
  ground_truth = ground_truth.reshape([3,-1])

  output = {}
  output['perception'] = ground_truth
  output['spikes']     = spikes[:,split:,:subset,:]

  return(output)

def getData():

  spikes = generateSpikeTrains()

  dataset = subsetPerception(spikes=spikes)

  return(dataset)

dataset = getData()
perception = dataset['perception']
spikes = dataset['spikes']
```

```python
# @title Plot Functions

def rasterplot(spikes,movement,trial):

  [movements, trials, neurons, timepoints] = np.shape(spikes)

  trial_spikes = spikes[movement,trial,:,:]

  trial_events = [((trial_spikes[x,:] > 0).nonzero()[0]-150)/100 for x in range(neurons)]

  plt.figure()
  dt=1/100
  plt.eventplot(trial_events, linewidths=1);
  plt.title('movement: %d - trial: %d'%(movement, trial))
  plt.ylabel('neuron')
  plt.xlabel('time [s]')

def plotCrossValAccuracies(accuracies):
  f, ax = plt.subplots(figsize=(8, 3))
  ax.boxplot(accuracies, vert=False, widths=.7)
  ax.scatter(accuracies, np.ones(8))
  ax.set(
    xlabel="Accuracy",
    yticks=[],
    title=f"Average test accuracy: {accuracies.mean():.2%}"
  )
  ax.spines["left"].set_visible(False)
```

----
# Phenomenon
*Part of Steps 1-2*

The train illusion occurs when sitting on a train and viewing another train outside the window. Suddenly, the other train *seems* to move, i.e. you experience visual motion of the other train relative to your train. But which train is actually moving?

Often people mix this up. In particular, they think their own train might be moving when it's the other train that moves; or vice versa. The illusion is usually resolved once you gain vision of the surroundings that lets you disambiguate the relative motion; or if you experience strong vibrations indicating that it is indeed your own train that is in motion.

----
# Question

*Part of Step 1*

We assume that we have build the train illusion model (see the other example project colab). That model predicts that accumulated sensory evidence from vestibular signals determines the decision of whether self-motion is experienced or not. We now have vestibular neuron data (simulated in our case, but let's pretend) and would like to see if that prediction holds true.

The data contains $N$ neurons and $M$ trials for each of 3 motion conditions: no self-motion, slowly accelerating self-motion and faster accelerating self-motion.

\begin{align}
N &= 40\\
M &= 400\\
\end{align}

**So we can ask the following question**: "Does accumulated vestibular neuron activity correlate with self-motion judgements?"

# Background

_Part of step 2_

While it seems a well-known fact that vestibular signals are noisy, we should check if we can also find this in the literature.

Let's also see what's in our data, there should be a 4d array called `spikes` that has spike counts (positive integers), a 2d array called `perception` with self-motion judgements (0=no motion or 1=motion). Let's see what this data looks like:

```python
print(np.shape(spikes))
print(np.shape(perception))
print(np.mean(perception, axis=1))
```

    (3, 400, 40, 301)
    (3, 400)
    [0.355  0.7575 0.975 ]

In the `spikes` array, we see our 3 acceleration conditions (first dimension), with 400 trials each (second dimensions) and simultaneous recordings from 40 neurons (third dimension), across 3 seconds in 10 ms bins (fourth dimension). The first two dimensions are also there in the `perception` array.

Perfect perception would have looked like [0, 1, 1]. The average judgements are far from correct (lots of self-motion illusions) but they do make some sense: it's closer to 0 in the no-motion condition and closer to 1 in both of the real-motion conditions.

The idea of our project is that the vestibular signals are noisy so that they might be mis-interpreted by the brain. Let's see if we can reproduce the stimuli from the data:

```python
for move_no in range(3):
  plt.plot(np.arange(-1.5,1.5+(1/100),(1/100)),np.mean(np.mean(spikes[move_no,:,:,:], axis=0), axis=0), label=['no motion', '$1 m/s^2$', '$2 m/s^2$'][move_no])
plt.xlabel('time [s]');
plt.ylabel('averaged spike counts');
plt.legend()
```

Blue is the no-motion condition, and produces flat average spike counts across the 3 s time interval. The orange and green line do show a bell-shaped curve that corresponds to the acceleration profile. But there also seems to be considerable noise: exactly what we need. Let's see what the spike trains for a single trial look like:

```python
for move in range(3):
  rasterplot(spikes = spikes, movement = move, trial = 0)
```

You can change the trial number in the bit of code above to compare what the rasterplots look like in different trials. You'll notice that they all look kind of the same: the 3 conditions are very hard (impossible?) to distinguish by eye-balling.

Now that we have seen the data, let's see if we can extract self-motion judgements from the spike counts.

----
# Ingredients

*Part of step 3*

In order to address our question we need to design an appropriate computational data analysis pipeline. We did some brainstorming and think that we need to somehow extract the self-motion judgements from the spike counts of our neurons. Based on that, our algorithm needs to make a decision: was there self motion or not? This is a classical 2-choice classification problem. We will have to transform the raw spike data into the right input for the algorithm (spike pre-processing).

So we determined that we probably needed the following ingredients:

* spike trains $S$ of 3-second trials (10ms spike bins)
* ground truth movement $m_r$ (real) and perceived movement $m_p$
* some form of classifier $C$ giving us a classification $c$
* spike pre-processing

----
# Hypotheses

*Part of step 4*

We think that noise in the signal drives whether or not people perceive self motion. Maybe the brain uses the strongest signal at peak acceleration to decide on self motion, but we actually think it is better to accumulate evidence over some period of time. We want to test this. The noise idea also means that when the signal-to-noise ratio is higher, the brain does better, and this would be in the faster acceleration condition. We want to test this too.

We came up with the following hypotheses focussing on specific details of our overall research question:

* Hyp 1: Accumulated vestibular spike rates explain self-motion judgements better than average spike rates around peak acceleration.
* Hyp 2: Classification performance should be better for faster vs slower self-motion.

> There are many other hypotheses you could come up with, but for simplicity, let's go with those.

Mathematically, we can write our hypotheses as follows (using our above ingredients):
* Hyp 1: $\mathbf{E}(c_{accum})>\mathbf{E}(c_{win})$
* Hyp 2: $\mathbf{E}(c_{fast})>\mathbf{E}(c_{slow})$

Where $\mathbf{E}$ denotes taking the expected value (in this case the mean) of its argument: classification outcome in a given trial type.

----
# Selected toolkit

*Part of step 5*

We want to build some form of classification pipeline. There are many options here, but a simple decoder would be a good start. Since we will learn all about Generalized Linear Models soon, we decided to use that! From W1D4: Generalized Linear Models there were two models that looked relevant:

* Linear-Nonlinear-Poisson GLM: predict spike counts from stimulus
* Logistic Regression: predict stimulus from spike counts

Here we want to predict self-motion from spike counts, so we pick Logistic Regression.

----
# Model draft

*Part of step 6*

Brainstorming on the whiteboard, we came up with this set of steps:

1. convert spike count data into design matrix: trials x neurons (with accumulated or windowed data)
2. get the classes each trial belongs to: stimulus (stationary / moving) or self-motion judgements (moving or not?) as a vector array
3. split data into train and test sets (200 trials each for the 3 conditions?) with an equal number of each class
4. fit logistic regression model on training set, and get predictions for test set

----
# Model implementation

*Part of step 7*

Below is a function that gets us the design matrix of observations X features (in this case: trials X neurons). It sums all spike counts, either across the whole time interval, or a window, specified in seconds before and after the peak acceleration at time=0 (it is the half-width of the window: `halfwin`).

```python
m_r = np.repeat([0,1,1], 400)
m_p = perception.reshape([-1])
a_r = np.repeat([0,1,2], 400) # for grouping in cross validation? maybe not

def getDesignMatrix(spikes, motions, classes, halfwin=None):
  """
    Get the design matrix and vector of classes for a logistic regression.

    Args:
        spikes (numpy.ndarray): a 4d matrix with spike counts for 1) three motion
        conditions, 2) 400 trials, 3) 40 neurons, 4) 301 time bins
        motions (list): motion conditions to include, any subset of [0,1,2]
        classes (list): list of length 3 * 400 with classes to use for the
        logistic regression
        halfwin (None or float): None to use the whole 3 s time interval, or a
        value between 0 and 1.5 seconds as the extent of the window from 0 (when
        acceleration peaks) in both directions.

    Returns:
        (list of numpy.ndarray): first element of the list is the design matrix
        of shape [trials] X [neurons], second elements is a 1d matrix to use as
        a vector of classes to predict from the design matrix

    This function can be used to get the right design matrix and set of class
    labels for a variety of logistic regressions we might want to do.
  """

  # select motion conditions:
  spikes = spikes[motions,:,:,:]
  y = (np.array(classes).reshape([3,-1])[motions,:]).reshape([-1])

  [movstims, trials, neurons, timepoints] = np.shape(spikes)

  # first we get the window (if any):
  if halfwin is None:
    return( [ np.sum(spikes, axis=3).reshape([movstims*trials,neurons]), y ] )
  else:
    dt = 1/100
    t = np.arange(-1.5,1.5+dt,dt)
    win_idx = (abs(t) < halfwin).nonzero()[0]
    w_0, w_1 = min(win_idx), max(win_idx)+1
    return( [ np.sum(spikes[:,:,:,w_0:w_1], axis=3).reshape([movstims*trials,neurons]), y ] )

# test the function:
[desmat, y] = getDesignMatrix(spikes, motions=[0,1], classes=m_r)
# let's check the shape:
print(np.shape(desmat))
# and let's check if the mean sum of spikes checks out:
print(np.mean(np.sum(spikes,axis=3),axis=(1,2)))
for mov in range(2):
  print(np.mean(desmat[(mov*400):((mov+1)*400),:]))
```

    (800, 40)
    [76.33475 77.53275 78.61975]
    76.33475
    77.53275

First, the design matrix: `desmat` has the right shape: 800 trials (for two conditions), by 40 neurons.

We also get the same average spike counts from our function (76.3 and 77.5) as when calculating it 'by hand' ([76.33475 77.53275 78.61975]).

This means our function `getDesignMatrix()` works correctly (unit test).

It also gets us the classes for each trial. We can use both judgements, $m_p$, and stimuli, $m_r$, as one vector, and `getDesignMatrix()` returns the correct subset (depending on which motion conditions are used) as the second variable.

We wanted to split the data into a training and test set, but we can have scikit-learn do this for us with the `cross_val_score()` function, as we saw in the GLM day:

```python
accuracies = cross_val_score(LogisticRegression(solver='liblinear'), X=desmat, y=y, cv=8)
plotCrossValAccuracies(accuracies) # this plotting function is copied from W1D4: Generalized Linear Models
```

We asked for 8 cross validations, which show up as the blue dots in the graph (two have the same accuracy). Prediction accuracy ranges from 56% to 72%, with the average at 65%, and the orange line is the median. Given the noisy data, that is not too bad actually.

But maybe it's better to split it according to the conditions where there is motion but of different magnitude. It should work better to classify higher acceleration motion from no motion as compared to classifying the lower acceleration motion.

Maybe it also works better to ignore some of the noise at the beginning and end of each trial by focussing on the spikes around the maximum acceleration, using our window option. The average spike count plot above, seems to best discriminate between the three motion conditions around peak acceleration (at 0 s).

We also want to test if it's possible to predict the self-motion judgements rather than the actual motion.

So we write a function that brings it all together:

```python
m_r = np.repeat([0,1,1], 400)
m_p = perception.reshape([-1])

def classifyMotionFromSpikes(spikes, classes, halfwin=None, motions=[0,1,2], cv=8):
  """
    Runs one logistic regression using the specified parameters.

    Args:
        spikes (numpy.ndarray): a 4d matrix with spike counts for 1) three motion
        conditions, 2) 400 trials, 3) 40 neurons, 4) 301 time bins
        classes (list): list of length 3 * 400 with classes to use for the
        logistic regression
        halfwin: None to use the whole 3 s time interval, or a value between 0
        and 0.750 seconds as the extent of the window from 0 (when acceleration
        peaks)
        motions (list): motion conditions to include, any subset of [0,1,2]
        cv (int): number of cross validations to do in logistic regression

    Returns:
        (numpy.ndarray): 1d array of shape (cv,) with prediction accuracies for
        each cross validation

    This function uses our getDesignMatrix() function according to specification
    and then uses cross-validated logistic regression and returns the accuracy
    for each run of the model.
  """

  # get the right design matrix:
  X, y = getDesignMatrix(spikes, motions, classes=classes, halfwin=halfwin)

  # right now, we are not using regularization:
  return( cross_val_score(LogisticRegression(solver='liblinear'), X=X, y=y, cv=cv) )

accuracies = classifyMotionFromSpikes(spikes, m_r, motions=[0,1])
plotCrossValAccuracies(accuracies)
```

This is the exact same figure as before, so our function `classifyMotionFromSpikes()` also works as intended.

----
# Model completion

*Part of step 8*

**Can we answer our question?** Question: "Does accumulated vestibular neuron activity correlate with self-motion judgements?" Yes, we can get an answer to the question by applying our analysis to predict self-motion judgements.

**Can we speak to our hypothesis?** We had two hypotheses: 1) prediction is better with total accumulated spike counts than from a window around peak acceleration, and 2) prediction is better with higher acceleration as the signal-to-noise ratio increases.

**Does the model reach our goals?** We want to understand if motion judgement and not true motion correlates better with vestibular neural activity.

Note: We have somewhat refined our goal here by explicitly contrasting true motion with motion judgements.

----
# Model evaluation & testing

*Part of step 9*

We can now evaluate the performance of our analysis pipeline.

To do so, we will run the logistic regression quite a few times: on all data or split by the velocities, and then we use the data to predict the presence of real motion, or the judgements of self-motion. We repeat this for total accumulated spikes and for a window around the peak acceleration. And then we'll plot the average classification performance in those 12 cases.

```python
def runAnalysis(spikes):
  """
    Runs one logistic regression using the specified parameters.

    Args:
        spikes (numpy.ndarray): a 4d matrix with spike counts for 1) three motion
        conditions, 2) 400 trials, 3) 40 neurons, 4) 301 time bins

    Returns:
        (numpy.ndarray): array of shape (2, 2, 3, 12), where the first dimension
        denotes full interval or 100 ms window, the second dimension denotes
        real motion or self-motion judgements, the third dimension denotes slow,
        fast or all data, and for each there 12 prediction accuracies for each
        of the 12 cross validations

    This function uses our classifyMotionFromSpikes()) function according to
    specification for all the different ways we want to classify the data and
    gives us the classification performance.
  """

  # variables we use for testing our analysis pipeline:
  m_r = np.repeat([0,1,1], 400)
  m_p = perception.reshape([-1])
  halfwins    = [None, 0.050]
  motions     = [[0,1],[0,2],[0,1,2]]
  class_sets  = [m_r, m_p]
  cv = 12

  # empty array to collect classification performance:
  accuracies = np.zeros([len(halfwins),len(class_sets),len(motions),cv])

  for halfwin_no in range(len(halfwins)):

    halfwin = halfwins[halfwin_no]
    lty = ['-','--'][halfwin_no]
    leg_hw = ['accumulated', '100 ms window'][halfwin_no]

    for classes_no in range(len(class_sets)):

      classes = class_sets[classes_no]
      leg_class = ['real', 'judgements'][classes_no]
      color = ['orange','purple'][classes_no]

      for motions_no in range(len(motions)):

        motion = motions[motions_no]
        cond_acc = classifyMotionFromSpikes(spikes  = spikes,
                                            classes = classes,
                                            halfwin = halfwin,
                                            motions = motion,
                                            cv=12)
        accuracies[halfwin_no,classes_no,motions_no,:] = cond_acc

      m_acc = np.mean(accuracies[halfwin_no,classes_no,:,:], axis=1)

  return(accuracies)

# here we run the above function and store the output:
accuracies = runAnalysis(spikes)
```

We will need to plot this output, and to simplify the code, we do that in a separate function, which we write here:

```python
def plotAccuracies(accuracies):
  """
    Plot the accuracies from our main fitting function.

    Args:
        accuarcies (numpy.ndarray): a 4d matrix with performance accuracy for
        1) full interval or 100 ms window, 2) real motion or self-motion
        judgements, 3) slow, fast or all conditions, and 4) the 12 cross
        validations

    Returns:

    This function plots the output of our data analysis pipeline and does not
    return anything.
  """
  ax = plt.figure(figsize=(6,4))
  plt.title('classification accuracy')
  plt.xlim([0.5,3.5])
  plt.ylim([0.4,1.0])
  plt.ylabel('proportion correct')
  plt.xlabel('velocity condition vs. stationary')

  for halfwin_no in range(2):

    lty = ['-','--'][halfwin_no]
    leg_hw = ['accumulated', '100 ms window'][halfwin_no]

    for classes_no in range(2):

      leg_class = ['real', 'judgements'][classes_no]
      color = ['orange','purple'][classes_no]

      m_acc = np.mean(accuracies[halfwin_no,classes_no,:,:], axis=1)
      plt.plot([1,2,3], m_acc, lty, color=color, label=leg_class+' '+leg_hw)

  plt.xticks(ticks=[1,2,3], labels=['slow ($1 m/s^2$)','fast ($2 m/s^2$)','both'])
  plt.legend(loc='lower right')
  plt.show()

plotAccuracies(accuracies)
```

Well, that's interesting! The logistic regression doesn't do a perfect job, but there is information in these results.

1. The dashed lines reflect predictions based on a small window of data, and they do worse than the full data set: this could mean that the brain also integrates signals across longer time frames for perception.

2. In the predictions based on accumulated spike counts, the slow movements are harder to separate from no movements than the faster movements. This is clearer when predicting real motion than when predicting self-motion judgements.

Those were our two hypotheses, but we notice something else:

3. Self-motion judgments display higher decoding accuracy than the actual motion.

Actually, if self-motion judgements and our logistic regression use input from the same noisy sensors, it kind of makes sense that they would both give similar output. This is in line the notion that self-motion judgements can be wrong because the underlying sensory signals are noisy. Of course, this only works if we record activity from neuronal populations that contribute to self-motion judgements. On the other hand, we would also see this result if the sensory signal was not noisy and we recorded from one of several populations that contribute to self-motion judgements in a noisy way. So we need to do more research here.

Either way, we learned something today!

----
# Summary
*Part of Step 10*

Let's write a simple abstract following the guidelines.

**A. What is the phenomena?** Here summarize the part of the phenomena which your model addresses.

_When sitting in a stationary train and seeing the train next to them move, people may experience the "train illusion": they feel like they are moving when they are not, or vice versa. Vestibular information can disambiguate self motion from motion of the adjacent train._

**B. What is the key scientific question?** Clearly articulate the question which your model tries to answer.

_However, it is unknown if accumulated, noisy vestibular neuron activity correlates with self-motion judgement._

**C. What was our hypothesis?** Explain the key relationships which we relied on to simulate the phenomena.

_Based on previous modeling efforts, we hypothesized that cumulative spike counts (not instantaneous) recorded from vestibular neurons can better discriminate experienced self motion judgements as opposed to true self motion._

**D. How did your model work?** Give an overview of the model, it's main components, and how the model works. "Here we ..."

_Here we use logistic regression to classify both true self motion and experienced self motion judgements from vestibular neuron activity. This was done both with the vestibular signal accumulated across the full trial and with a 100 ms window ('instantaneous') around peak acceleration, and with 2 different speeds compared to a no-motion condition._

**E. What did we find?** Did the model work? Explain the key outcomes of your model evaluation.

_Decoding performance for accumulated vestibular signals was higher than for instantaneous signals. Further, experienced self motion judgements could be classified better than true self motion._

**F. What can we conclude?** Conclude as much as you can with reference to the hypothesis, within the limits of the model.

_We conclude that accumulated, noisy vestibular signals during slowly-accelerating self motion may drive the train illusion._

**G. What are the limitations and future directions?** What is left to be learned? Briefly argue the plausibility of the approach and/or what you think is essential that may have been left out.

_Future research should examine how visual signals may combine with vestibular signals in real-world scenarios to causally link these sensory signals to self-motion experience and illusions._

If we put this all in one paragraph, we have our final complete abstract. But, first, do not include the letters in _your_ abstract, and second, we did paraphrase the answers a little so they fit together.

# Abstract

(A) When sitting in a stationary train and seeing the train next to them move, people may experience the "train illusion": they feel like they are moving when they are not, or vice versa. Vestibular information can disambiguate self motion from motion of the adjacent train. (B) However, it is unknown if accumulated, noisy vestibular neuron activity correlates with self-motion judgement. (C) Based on previous modeling efforts, we hypothesized that cumulative spike counts (not instantaneous) recorded from vestibular neurons can better discriminate experienced self motion judgements as opposed to true self motion. (D) Here we use logistic regression to classify both true self motion and experienced self motion judgements from vestibular neuron activity. This was done both with the vestibular signal accumulated across the full trial and with a 100 ms window ('instantaneous') around peak acceleration, and with 2 different speeds compared to a no-motion condition. (E) Decoding performance for accumulated vestibular signals was higher than for instantaneous signals. Further, experienced self motion judgements could be classified better than true self motion. (F) We conclude that accumulated, noisy vestibular signals during slowly-accelerating self motion may drive the train illusion. (G) Future research should model possible mechanisms by which visual signals may combine with vestibular signals to causally link these sensory signals to self-motion experience and illusions.

----
# Final thoughts

Note that the analysis pipeline we built here was extremely simple and we used artificial data on purpose. It allowed us to go through all the steps of building a data neuroscience project, and hopefully you noticed that it is not always a linear process, you will go back to different steps if you hit a roadblock somewhere.

There are many issues that we did not address, e.g.:
* we could have tried different decoders
* or find the optimal window
* what if we had more data...
* how do different neurons contribute to the decision? And why?
  * this is something that you want to explore as a neuroscientist
* we could have run a GLM on the neurons*time array and then analyze the weightings to see if all acceleration steps are weighted equally (perfect integration)... or not?
* what is the role of visual motion in this phenomenon?

However, this project is not meant to be complete, and yours doesn't have to be either. The goal of the projects is to go through _the process_ of a modeling or data science project and put into practice one or more of the toolkits you learn at NMA with your group.


# Circuitos RL sem fonte

Jupyter Notebook desenvolvido por [Gustavo S.S.](https://github.com/GSimas)

Considere a conexão em série de um resistor e um indutor, conforme mostra a
Figura 7.11. Em t = 0, supomos que o indutor tenha uma
corrente inicial Io.

\begin{align}
I(0) = I_0
\end{align}

Assim, a energia correspondente armazenada no indutor como segue:

\begin{align}
w(0) = \frac{1}{2} LI_0²
\end{align}

Exponenciando em e, obtemos:

\begin{align}
i(t) = I_0 e^{-t \frac{R}{L}}
\end{align}

Isso demonstra que a resposta natural de um circuito RL é uma queda exponencial
da corrente inicial. A resposta em corrente é mostrada na Figura 7.12. Fica
evidente, da Equação, que a constante de tempo para o circuito RL é:

\begin{align}
τ = \frac{L}{R}
\end{align}

A tensão no resistor como segue:

\begin{align}
v_R(t) = I_0 R e^{-t/τ}
\end{align}

A potência dissipada no resistor é:

\begin{align}
p = v_R i = I_0^2 R e^{-2t/τ}
\end{align}

A energia absorvida pelo resistor é:

\begin{align}
w_R(t) = \int_{0}^{t} p(t)dt = \frac{1}{2} L I_0^2 (1 - e^{-2t/τ})
\end{align}

**Enquanto t → ∞, wr(∞) → 1/2 L I0², que é o mesmo que wl(0), a energia armazenada inicialmente no indutor**

Assim, os procedimentos são:

1. Determinar corrente inicial i(0) = I0 por meio do indutor.
2. Determinar a constante de tempo τ = L/R

**Exemplo 7.3**

Supondo que i(0) = 10 A, calcule i(t) e ix(t) no circuito da Figura 7.13.

```python
print("Exemplo 7.3")

import numpy as np
from sympy import *

I0 = 10
L = 0.5
R1 = 2
R2 = 4
t = symbols('t')

#Determinar Req = Rth
    #Io hipotético = 1 A
    #Analise de Malhas
    #4i2 + 2(i2 - i0) = -3i0
    #6i2 = 5
    #i2 = 5/6
    #ix' = i2 - i1 = 5/6 - 1 = -1/6
    #Vr1 = ix' * R1 = -1/6 * 2 = -1/3
    #Rth = Vr1/i0 = (-1/3)/(-1) = 1/3
Rth = 1/3
tau = L/Rth
i = I0*exp(-t/tau)

print("Corrente i(t):",i,"A")

vl = L*diff(i,t)
ix = vl/R1

print("Corrente ix(t):",ix,"A")
```

    Exemplo 7.3
    Corrente i(t): 10*exp(-0.666666666666667*t) A
    Corrente ix(t): -1.66666666666667*exp(-0.666666666666667*t) A

**Problema Prático 7.3**

Determine i e vx no circuito da Figura 7.15. Façamos i(0) = 12 A.

```python
print("Problema Prático 7.3")

L = 2
I0 = 12
R1 = 1

#Determinar Req = Rth

    #i0 hipotetico = 1 A
    #vx = 4 V
    #vx + 2(i0 - i1) + 2vx - v0 = 0
    #-2i1 - v0 = -14

    #-2vx + 2(i1 - i0) + 6i1 = 0
        #8i1 = 10
        #i1 = 10/8 = 5/4

    #v0 = vx + 2(i0 - i1) + 2vx
        #v0 = 4 + 2 - 5/2 + 8 = 11.5

    #Rth = v0/i0 = 11.5/1 = 11.5
Rth = 11.5
tau = L/Rth
i = I0*exp(-t/tau)

print("Corrente i(t):",i,"A")

vx = -R1*i

print("Tensão vx(t):",vx,"V")
```

    Problema Prático 7.3
    Corrente i(t): 12*exp(-5.75*t) A
    Tensão vx(t): -12*exp(-5.75*t) V

**Exemplo 7.4**

A chave do circuito da Figura 7.16 foi fechada por um longo período. Em t = 0, a chave
é aberta. Calcule i(t) para t > 0.

```python
print("Exemplo 7.4")

Vs = 40
L = 2

def Req(x,y): #funcao para calculo de resistencia equivalente em paralelo
    res = (x*y)/(x + y)
    return res

Req1 = Req(4,12)
V1 = Vs*Req1/(Req1 + 2)
I0 = V1/4

Req2 = 12 + 4
Rth = Req(Req2, 16)

tau = L/Rth

i = I0*exp(-t/tau)

print("Corrente i(t):",i,"A")
```

    Exemplo 7.4
    Corrente i(t): 6.0*exp(-4.0*t) A

**Problema Prático 7.4**

Para o circuito da Figura 7.18, determine i(t) para t > 0.

```python
print("Problema Prático 7.4")

L = 2
Cs = 15
R1 = 24

Req1 = Req(12,8)
i1 = Cs*R1/(R1 + Req1)
I0 = i1*8/(8 + 12)

Rth = Req(12+8,5)
tau = L/Rth

i = I0*exp(-t/tau)

print("Corrente i(t):",i,"A")
```

    Problema Prático 7.4
    Corrente i(t): 5.0*exp(-2.0*t) A

**Exemplo 7.5**

No circuito indicado na Figura 7.19, encontre io, vo e i durante todo o tempo, supondo
que a chave fora aberta por um longo período.

```python
print("Exemplo 7.5")

Vs = 10
L = 2

print("Para t < 0, i0:",0,"A")

I0 = Vs/(2 + 3)
v0 = 3*I0

print("Para t < 0, i:",I0,"A")
print("Para t < 0, v0:",v0,"V")

Rth = Req(3,6)
tau = L/Rth
i = I0*exp(-t/tau)
v0 = -L*diff(i,t)
i0 = -i*3/(3 + 6)

print("Para t > 0, i0:",i0,"A")
print("Para t > 0, v0:",v0,"V")
print("Para t > 0 i:",i,"A")
```

    Exemplo 7.5
    Para t < 0, i0: 0 A
    Para t < 0, i: 2.0 A
    Para t < 0, v0: 6.0 V
    Para t > 0, i0: -0.666666666666667*exp(-1.0*t) A
    Para t > 0, v0: 4.0*exp(-1.0*t) V
    Para t > 0 i: 2.0*exp(-1.0*t) A

**Problema Prático 7.5**

Determine i, io e vo para todo t no circuito mostrado na Figura 7.22.

```python
print("Problema Prático 7.5")

Cs = 24
L = 1

#Para t < 0
i = Cs*4/(4 + 2)
i0 = Cs*2/(2 + 4)
v0 = 2*i
print("Para t < 0, i =",i,"A")
print("Para t < 0, i0 =",i0,"A")
print("Para t < 0, v0 =",v0,"V")

#Para t > 0
R = Req(4 + 2,3)
tau = L/R
I0 = i
i = I0*exp(-t/tau)
i0 = -i*3/(3 + 4 + 2)
v0 = -i0*2
print("Para t < 0, i =",i,"A")
print("Para t < 0, i0 =",i0,"A")
print("Para t < 0, v0 =",v0,"V")
```

    Problema Prático 7.5
    Para t < 0, i = 16.0 A
    Para t < 0, i0 = 8.0 A
    Para t < 0, v0 = 32.0 V
    Para t < 0, i = 16.0*exp(-2.0*t) A
    Para t < 0, i0 = -5.33333333333333*exp(-2.0*t) A
    Para t < 0, v0 = 10.6666666666667*exp(-2.0*t) V


# Overview

We kick things off with a few more administrative things.

It might seem boring to hear about admin, **but this is the stuff you need to know to do well in the class**. So I recommend watching and paying attention!

Then, we kick into gear and get started on the actual work. This week is about getting familiar with networks, and we'll focus on four main elements

* A short talk on the basic mathematical description of networks
* The `NetworkX` library
* Matplotlib, binning, and plotting degree distributions
* Random networks

(The work-load is still a little bit light, since we want to allow those who are not super-strong at programming a bit of time to catch up. If you're bored and in need of extra work, let me know and I'll point you in the right direction.)

# Part 0: The Admin Stuff
### (GitHub Page Overview Edition)

Last week, I gave you a lot of practical information about how to follow the class and discussed the principles of how I teach. How each lecture is structured to maximize your learning according to state-of-the-art principles.

This week, the admin video talks about the structure of the Course Page, which actually contains a lot of useful information.

You'll notice that I've made it for the class that runs in the spring (Social Data Analysis and Visualization), but the web-page structure is similar, so everyhting should still work.

```python
from IPython.display import YouTubeVideo
YouTubeVideo("42KjmxgYYNI",width=800, height=450)
```

Next week, I'll talk about the final elements of admin we have to cover for now

* Forming groups
* Handing in assignments
* Peer Evaluations
* The structure of the class

# Part 1: Basic mathematical description of networks

This week, let's start with some lecturing. I love networks, so I'll take some time today to tell you about them.

> **_Video Lecture_**. Then check out a few comments on "Network Notation" (don't worry it's short).

```python
YouTubeVideo("MMziC5xktHs",width=800, height=450)
```

Now for some reading

> _Reading_. We'll be reading the textbook _Network Science_ (NS) by Laszlo Barabasi. You can read the whole
> thing for free [**here**](http://barabasi.com/networksciencebook/).
>
> * Read chapter 2\.
>

And the associated exercises

> *Exercises*: Chapter 2 in *Network Science*.
>
> * Section 2.5 states that real networks are sparse. Can you think of a real network where each node has _many_ connections? Is that network still sparse? If yes, can you explain why?
>
> There are more questions on Chapter 2 below.
>

Network of train connections between the European cities has many connections but still it is not sparse as there are multiplecities without rails.

# Part 2: Exercises using the `NetworkX` library

Let's try to solve a few of the exercises from the book using `NetworkX`

> *Exercises*:
>
> * Go to NS Section 2.12: Homework, then
>   * Write the solution exercise **2.1** (the 'Königsberg Problem') from NS in your `notebook`.
>   * Solve exercise **2.3** ('Graph representation') from NS using `NetworkX` in your `notebook`. (You don't have to solve the last sub-question about *cycles of length 4* ... but I'll be impressed if you do it).
>   * Solve exercise **2.5** ('Bipartite Networks') from NS using `NetworkX` in your `notebook`. *Important note*: There is a a mistake in the book. When it says "Block diagonal", they mean "Anti-diagonal" (all elements are away from the diagonal blocks).
>     - *Note*: For those without the physical book (and therefore no exercise numbers), the part "Bipartite Networks - General Considerations" does not need to be solved (the two last questions do not make much sense to me).
>

**2.1**

Based on the Euler Theorem those would be icons a, c and d

**2.3**

```python
import networkx as nx
G = nx.Graph()
GD = nx.DiGraph()

G.add_node(5)

G.add_edges_from([
    (1, 2), (1, 3), (1, 4), (1, 6),
    (2, 3), (2, 4),
    (3, 6)
                 ])

GD.add_node(5)

GD.add_edges_from([
    (1, 2), (3, 1), (4, 1), (6, 1),
    (2, 3), (2, 4),
    (3, 2), (6, 3)
                 ])

```

```python
# a
nx.to_numpy_matrix(G)
```

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

```python
nx.to_numpy_matrix(GD)
```

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

```python
# b
list(G.edges)
```

    [(1, 2), (1, 3), (1, 4), (1, 6), (2, 3), (2, 4), (3, 6)]

```python
list(GD.edges)
```

    [(1, 2), (2, 3), (2, 4), (3, 1), (3, 2), (4, 1), (6, 1), (6, 3)]

```python
# c
nx.average_clustering(G)
```

    0.6388888888888888

```python
# d
print(nx.to_numpy_matrix(G))
G.add_edges_from([(5, 1), (5, 3)])
G.remove_edges_from([(6, 1), (6, 3)])
print(nx.to_numpy_matrix(G))
```

    [[0. 0. 0. 0. 0. 0.]
     [0. 0. 1. 1. 1. 1.]
     [0. 1. 0. 1. 1. 0.]
     [0. 1. 1. 0. 0. 1.]
     [0. 1. 1. 0. 0. 0.]
     [0. 1. 0. 1. 0. 0.]]
    [[0. 1. 0. 1. 0. 0.]
     [1. 0. 1. 1. 1. 0.]
     [0. 1. 0. 1. 1. 0.]
     [1. 1. 1. 0. 0. 0.]
     [0. 1. 1. 0. 0. 0.]
     [0. 0. 0. 0. 0. 0.]]

```python
# e
```

Link list does not have any information about the nodes that are not connected to any other nodes

```python
# f
AM = nx.to_numpy_matrix(G)
print(G.nodes)
AM**3
# the answer is 7
```

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

    matrix([[2., 6., 3., 5., 3., 0.],
            [6., 6., 7., 7., 6., 0.],
            [3., 7., 4., 7., 5., 0.],
            [5., 7., 7., 4., 3., 0.],
            [3., 6., 5., 3., 2., 0.],
            [0., 0., 0., 0., 0., 0.]])

**2.5**

```python
list(range(1,12))
```

    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

```python
G = nx.Graph()

G.add_nodes_from(range(1,12))

G.add_edges_from([(1, 7), (2, 9), (3, 7), (3, 8), (3, 9), (4, 9), (4, 10), (5, 9), (5, 11), (6, 11)])
```

```python
G.nodes
```

    NodeView((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))

```python
G.edges
```

    EdgeView([(1, 7), (2, 9), (3, 7), (3, 8), (3, 9), (4, 9), (4, 10), (5, 9), (5, 11), (6, 11)])

```python
# a
nx.to_numpy_matrix(G)
# its block-diagonal because we have a bipartite graph
```

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

```python
# b
from networkx.algorithms import bipartite
GP = bipartite.projected_graph(G, range(1,7))
```

```python
GP.nodes
```

    NodeView((1, 2, 3, 4, 5, 6))

```python
GP.edges
```

    EdgeView([(1, 3), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5), (5, 6)])

```python
GG = bipartite.projected_graph(G, range(7,12))
GG.edges
```

    EdgeView([(7, 8), (7, 9), (8, 9), (9, 10), (9, 11)])

```python
# c
```

```python
import numpy as np
np.mean([val for (node, val) in G.degree(range(1,7))])
```

    1.6666666666666667

```python
np.mean([val for (node, val) in G.degree(range(7,12))])
```

    2.0

```python
np.mean([val for (node, val) in GP.degree()])
```

    2.6666666666666665

```python
np.mean([val for (node, val) in GG.degree()])
```

    2.0

```python
GG.edges
```

    EdgeView([(7, 8), (7, 9), (8, 9), (9, 10), (9, 11)])

```python
GP.edges
```

    EdgeView([(1, 3), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5), (5, 6)])

> **_Video Lecture_**: Once again, it's time to stop working for a couple of minutes to hear me talk about plotting with `NetworkX`.
>

```python
YouTubeVideo("iDlb9On_TDQ",width=800, height=450)
```

# Part 3: Plotting degree distributions

As always we'll learn about degree-distribution plotting by creating a `notebook` and trying it out.

As a little public service announcement about this exercise: I know that you've probably tried binning data before. And that there are many functions designed to make binning easy, so there is a good chance that you think that binning data into bins is something that you don't have to worry about.

**BUT** ... with this exercise, I aim to convince you that binning isn't always trivial. And that there are key things to think about every time you need to put data into buckets. So please read the exercise carefully. And follow my instructions carefully.

>
> *Exercises*:
>
> Begin by importing the right packages. Start by importing `matplotlib.pyplot` (for plotting), `numpy` (for binning and other stuff),
> `random` (for generating random numbers), and `networkx` (for generating networks.)
>
> * Binning real numbers
>   * Let's do a gentle start and use the `random` library generate 5000 data points from a Gaussian distribution with $\mu = 2$ and $\sigma = 0.125$.
>   * Now, let's use `numpy.histogram` to bin those number into 10 bins. What does the `numpy.histogram` function return? Do the two arrays have the same length?
>   * Then we use `matplotlib.pyplot.plot` to plot the binned data. You will have to deal with the fact that the counts- and bin-arrays have different lengths. Explain how you deal with this problem and why.
> * Binning integers
>   * But binning real numbers into a fixed number of bins is easy when `numpy.histogram` does all the work and finds the right bin boundaries for you.
> Now we'll generate a bunch of integers and set the bin boundaries manually. This time, let's grab data from a Poisson distribution. As it turns out
> `numpy` also has some convenient random number generators. Use `numpy.random.poisson` to generate 5000 numbers drawn from a Poisson distribution
> characterized by $\lambda = 10$. Find the maximum and minimum value of your 5000 random numbers.
>   * Instead of simply specifying the number of bins for `numpy.histogram`, let's specify the bins we want using a vector.
> Create a vector $v$
> that results in a binning that puts each integer value in its own bin and where the first bin contains the minimum number you found above,
> and the last bin contains the maximum number.
> Use the vector by setting `numpy.histogram`'s `bin` parameter as `bin = ` $v$.
> What is the sum over bin counts? Explain how the binning-vectors first and last element
> relates to the min and max from the Poisson distribution.
>   * Now, use a bar chart (`matplotlib.pyplot.bar`) to plot the distribution
> * Binning and plotting degree distributions.
>   * Let's generate the Erdös-Renyi (ER) network which has a degree distribution that matches the Poisson distribution above.
> First we have to figure out which values the ER parameters (_N_ and _p_) should assume. It's easy to see that $N = 5000$, but how do you find $p$?
> **Hint**: The parameter $\lambda$ in the Poisson distribution corresponds to the average degree, so you have to find a $p$ that results in an average
> degree, $k = 10$. And you know that $\langle k \rangle = p (N-1)$, which will give you $p$.
>   * Now, use `networkx` to create the graph and extract the degree distribution.
>   * Finally, create a nice bar plot of the degree distribution, including axes labels and a plot title. Make sure that it looks like the Poisson distribution you plotted above.
>

```python
import random
import matplotlib.pyplot as plt
```

```python
x = np.random.normal(2,0.125, 5000)
```

```python
counts, edges = np.histogram(x, bins=10)
```

```python
counts
```

    array([  10,   72,  309,  740, 1298, 1309,  854,  308,   90,   10],
          dtype=int64)

```python
edges
```

    array([1.54791573, 1.6372078 , 1.72649987, 1.81579194, 1.905084  ,
           1.99437607, 2.08366814, 2.17296021, 2.26225227, 2.35154434,
           2.44083641])

```python
# I calculate the widths of the bins and remove the last starting point of the bin
```

```python
edges[:-1]
```

    array([1.54791573, 1.6372078 , 1.72649987, 1.81579194, 1.905084  ,
           1.99437607, 2.08366814, 2.17296021, 2.26225227, 2.35154434])

```python
np.diff(edges)
```

    array([0.08929207, 0.08929207, 0.08929207, 0.08929207, 0.08929207,
           0.08929207, 0.08929207, 0.08929207, 0.08929207, 0.08929207])

```python
plt.bar(edges[:-1], counts, width=np.diff(edges))
plt.show()
```

```python
x = np.random.poisson(10, 5000)
```

```python
v = np.linspace(min(x), max(x), 11)
```

```python
counts, edges = np.histogram(x,v)
```

```python
edges
```

    array([ 1. ,  3.3,  5.6,  7.9, 10.2, 12.5, 14.8, 17.1, 19.4, 21.7, 24. ])

```python
counts
```

    array([  58,  288,  763, 1802, 1063,  625,  326,   51,   20,    4],
          dtype=int64)

```python
np.diff(edges)
```

    array([2.3, 2.3, 2.3, 2.3, 2.3, 2.3, 2.3, 2.3, 2.3, 2.3])

```python
plt.bar(edges[:-1], counts, width=np.diff(edges))
plt.show()
```

```python
p = 10/(5000-1)
```

```python
GER = nx.erdos_renyi_graph(5000, p)
```

```python
np.mean([val for (node, val) in GER.degree()])
```

    9.9436

```python
x = [val for (node, val) in GER.degree()]
counts, edges = np.histogram(x, bins=11)
plt.bar(edges[:-1], counts, width=np.diff(edges))
plt.title("Degree distribution of Erdos Renyi graph")
plt.xlabel('Number of nodes')
plt.ylabel('Degree of the node')
plt.show()
```

# Part 4: Random networks

> **_Video Lecture_**. Now it's time to relax and watch a few minutes of info on _Random Networks_.

```python
YouTubeVideo("c_SbQCzgqb0",width=800, height=450)
```

> _Reading_. Read section 3.1-3.7 (the most important part is 3.1-3.4) of Chapter 3 of _Network Science_. You can find the entire book [**here**](http://barabasi.com/networksciencebook/).

> _Exercises_ (should be completed in a `notebook`):
>
> * Work through NS exercise 3.1 ('Erdős-Rényi Networks'). The exercise can be found in Section 3.11: Homework.
> * Paths. Plot a random network with 200 nodes and an average degree of 1.5\. (I suggest using `networkx.draw` and reading [the documentation](https://networkx.github.io/documentation/stable/reference/drawing.html) carefully to get an overview of all the options and what they look like. For example, you may want to shrink the node size).
>   * Extract the Giant Connected Component, GCC. (Hint: You can use `networkx.connected_component_subgraphs` <- it works in ealier versions of NetworkX (<2.5) || Otherwise, you can use `nx.connected_components(G)` and `G.subgraph(c).copy()`)
>   * Choose a node at random from the GCC. (Hint: You may want to try `random.choice`.)
>   * Find all nodes that are precisely 2 steps away from that node. (Hint. I suggest `networkx.single_source_shortest_path_length`)
>   * Plot the GCC with the following choice of colors. Starting node _black_ (`"#000000"`). The nodes 2 steps away _red_ (`"#ff0000"`). All other nodes _blue_ (`"#A0CBE2"`). Again, I suggest using `networkx.draw()` and reading [the documentation](https://networkx.github.io/documentation/stable/reference/drawing.html) carefully find out how to color individual nodes.
>

By the way, the default plot coming out of `networkx` doesn't look like the one below. But by tweaking parameters, you should be able to create something that looks like it.

```python
N = 3000
p = 1e-3
```

```python
G = nx.erdos_renyi_graph(N, p)
```

```python
# expected number of links
p/2*N*(N-1)
```

    4498.5

```python
# average degree
p*(N-1)
```

    2.999

```python
k_cr = np.log(N)
# supercritical regime
```

```python
k_cr
```

    8.006367567650246

```python
# probability for critical point
k_cr/(N-1)
```

    0.002669679082244163

```python
# from sympy import *
# p, N = symbols('p N')

# p=log(N)/(N-1)

# # set the expression, y, equal to 0 and solve
# result = solve(Eq(p, 0.001))

# print(result)
```

```python
N = 9000
G = nx.erdos_renyi_graph(N, p)
k_cr = np.log(N)
k_cr
```

    9.104979856318357

```python
np.mean([val for (node, val) in G.degree()])
```

    9.060222222222222

```python
# k avg
k_avg = p*(N-1)
print(k_avg)
# average distance
d_avg = np.log(N)/np.log(k_avg)
print(d_avg)
```

    8.999
    4.144064483635564

```python
k = 5
np.exp(-k_avg)*k_avg**k/np.math.factorial(k)
```

    0.06075387319345324

```python
x = [val for (node, val) in G.degree()]
counts, edges = np.histogram(x, bins=11)
plt.bar(edges[:-1], counts, width=np.diff(edges))
plt.title("Degree distribution of Erdos Renyi graph")
plt.xlabel('Number of nodes')
plt.ylabel('Degree of the node')
plt.show()
```

```python
## Task 2
k_avg = 1.5
N= 200
p = k_avg/(N-1)
G = nx.erdos_renyi_graph(N, p)
```

```python
nx.draw(
    G,
    with_labels = False,
    node_size = 15
)
```

```python
largest_cc = max(nx.connected_components(G), key=len)
GCC = G.subgraph(largest_cc).copy()
```

```python
nx.draw(
    GCC,
    with_labels = False,
    node_size = 15
)
```

```python
nodeid = np.random.choice(GCC.nodes)
nodeid
```

    32

```python
nx.single_source_shortest_path(G,nodeid,2)
```

    {32: [32],
     4: [32, 4],
     94: [32, 94],
     125: [32, 125],
     181: [32, 181],
     194: [32, 194],
     36: [32, 94, 36],
     44: [32, 94, 44],
     70: [32, 125, 70],
     148: [32, 125, 148],
     188: [32, 181, 188]}

```python
val_map = {32: "black",
           36: "red",
          44: "red",
          70: "red",
          148: "red",
          188: "red"}

values = [val_map.get(node, "green") for node in GCC.nodes()]
nx.draw(
    GCC,
    with_labels = False,
    node_size = 40,
    node_color=values
)
```

```python

```