# Neuromatch Academy: Week 3, Day 1, Tutorial 1
# Real Neurons: The Leaky Integrate-and-Fire (LIF) Neuron Model
__Content creators:__ Qinglong Gu, Songtin Li, John Murray, Richard Naud, Arvind Kumar

__Content reviewers:__  Maryam Vaziri-Pashkam, Ella Batty, Lorenzo Fontolan, Richard Gao, Matthew Krause, Spiros Chavlis, Michael Waskom

---
# Tutorial Objectives
This is Tutorial 1 of a series on implementing realistic neuron models. In this tutorial, we will build up a leaky integrate-and-fire (LIF) neuron model and study its dynamics in response to various types of inputs. In particular, we are going to write a few lines of code to:

- simulate the LIF neuron model

- drive the LIF neuron with external inputs, such as direct currents, Gaussian white noise, and Poisson spike trains, etc.

- study how different inputs affect the  LIF neuron's output (firing rate and spike time irregularity)

Here, we will especially emphasize identifying conditions (input statistics) under which a neuron can spike at low firing rates and in an irregular manner. The reason for focusing on this is that in most cases, neocortical neurons spike in an irregular manner.

---
# Setup

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

```python
# @title Figure Settings
import ipywidgets as widgets  # interactive display
%config InlineBackend.figure_format = 'retina'
# use NMA plot style
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
my_layout = widgets.Layout()
```

```python
# @title Helper functions

def plot_volt_trace(pars, v, sp):
  """
  Plot trajetory of membrane potential for a single neuron

  Expects:
  pars   : parameter dictionary
  v      : volt trajetory
  sp     : spike train

  Returns:
  figure of the membrane potential trajetory for a single neuron
  """

  V_th = pars['V_th']
  dt, range_t = pars['dt'], pars['range_t']
  if sp.size:
    sp_num = (sp / dt).astype(int) - 1
    v[sp_num] += 20  # draw nicer spikes

  plt.plot(pars['range_t'], v, 'b')
  plt.axhline(V_th, 0, 1, color='k', ls='--')
  plt.xlabel('Time (ms)')
  plt.ylabel('V (mV)')
  plt.legend(['Membrane\npotential', r'Threshold V$_{\mathrm{th}}$'],
             loc=[1.05, 0.75])
  plt.ylim([-80, -40])

def plot_GWN(pars, I_GWN):
  """
  Args:
    pars  : parameter dictionary
    I_GWN : Gaussian white noise input

  Returns:
    figure of the gaussian white noise input
  """

  plt.figure(figsize=(12, 4))
  plt.subplot(121)
  plt.plot(pars['range_t'][::3], I_GWN[::3], 'b')
  plt.xlabel('Time (ms)')
  plt.ylabel(r'$I_{GWN}$ (pA)')
  plt.subplot(122)
  plot_volt_trace(pars, v, sp)
  plt.tight_layout()

def my_hists(isi1, isi2, cv1, cv2, sigma1, sigma2):
  """
  Args:
    isi1 : vector with inter-spike intervals
    isi2 : vector with inter-spike intervals
    cv1  : coefficient of variation for isi1
    cv2  : coefficient of variation for isi2

  Returns:
    figure with two histograms, isi1, isi2

  """
  plt.figure(figsize=(11, 4))
  my_bins = np.linspace(10, 30, 20)
  plt.subplot(121)
  plt.hist(isi1, bins=my_bins, color='b', alpha=0.5)
  plt.xlabel('ISI (ms)')
  plt.ylabel('count')
  plt.title(r'$\sigma_{GWN}=$%.1f, CV$_{\mathrm{isi}}$=%.3f' % (sigma1, cv1))

  plt.subplot(122)
  plt.hist(isi2, bins=my_bins, color='b', alpha=0.5)
  plt.xlabel('ISI (ms)')
  plt.ylabel('count')
  plt.title(r'$\sigma_{GWN}=$%.1f, CV$_{\mathrm{isi}}$=%.3f' % (sigma2, cv2))
  plt.tight_layout()
  plt.show()

# the function plot the raster of the Poisson spike train
def my_raster_Poisson(range_t, spike_train, n):
  """
  Generates poisson trains

  Args:
    range_t     : time sequence
    spike_train : binary spike trains, with shape (N, Lt)
    n           : number of Poisson trains plot

  Returns:
    Raster plot of the spike train
  """

  # find the number of all the spike trains
  N = spike_train.shape[0]

  # n should smaller than N:
  if n > N:
    print('The number n exceeds the size of spike trains')
    print('The number n is set to be the size of spike trains')
    n = N

  # plot rater
  i = 0
  while i < n:
    if spike_train[i, :].sum() > 0.:
      t_sp = range_t[spike_train[i, :] > 0.5]  # spike times
      plt.plot(t_sp, i * np.ones(len(t_sp)), 'k|', ms=10, markeredgewidth=2)
    i += 1
  plt.xlim([range_t[0], range_t[-1]])
  plt.ylim([-0.5, n + 0.5])
  plt.xlabel('Time (ms)', fontsize=12)
  plt.ylabel('Neuron ID', fontsize=12)
```

---
# Section 1: The Leaky Integrate-and-Fire (LIF) model

```python
#@title Video 1: Reduced Neuron Models
from IPython.display import YouTubeVideo
video = YouTubeVideo(id='rSExvwCVRYg', width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=rSExvwCVRYg

## Implementation of an LIF neuron model

Now, it's your turn to implement one of the simplest mathematical model of a neuron: the leaky integrate-and-fire (LIF) model. The basic idea of LIF neuron was proposed in 1907 by Louis Édouard Lapicque, long before we understood the electrophysiology of a neuron (see a translation of [Lapicque's paper](https://pubmed.ncbi.nlm.nih.gov/17968583/) ). More details of the model can be found in the book [**Theoretical neuroscience**](http://www.gatsby.ucl.ac.uk/~dayan/book/) by Peter Dayan and Laurence F. Abbott.

The subthreshold membrane potential dynamics of a LIF neuron is described by

\begin{eqnarray}
C_m\frac{dV}{dt} = -g_L(V-E_L) + I,\quad (1)
\end{eqnarray}

where $C_m$ is the membrane capacitance, $V$ is the membrane potential, $g_L$ is the leak conductance ($g_L = 1/R$, the inverse of the leak resistance $R$ mentioned in previous tutorials), $E_L$ is the resting potential, and $I$ is the external input current.

Dividing both sides of the above equation by $g_L$ gives

\begin{align}
\tau_m\frac{dV}{dt} = -(V-E_L) + \frac{I}{g_L}\,,\quad (2)
\end{align}

where the $\tau_m$ is membrane time constant and is defined as $\tau_m=C_m/g_L$.

You might wonder why dividing capacitance by conductance gives units of time! Find out yourself why this is true.

Below, we will use Eqn.(2) to simulate LIF neuron dynamics.

If $I$ is sufficiently strong such that $V$ reaches a certain threshold value $V_{\rm th}$, $V$ is reset to a reset potential $V_{\rm reset}< V_{\rm th}$, and voltage is clamped to $V_{\rm reset}$ for $\tau_{\rm ref}$ ms, mimicking the refractoriness of the neuron during an action potential:

\begin{eqnarray}
\mathrm{if}\quad V(t_{\text{sp}})\geq V_{\rm th}&:& V(t)=V_{\rm reset} \text{  for } t\in(t_{\text{sp}}, t_{\text{sp}} + \tau_{\text{ref}}]
\end{eqnarray}
where $t_{\rm sp}$ is the spike time when $V(t)$ just exceeded $V_{\rm th}$.

(__Note__: in the lecture slides, $\theta$ corresponds to the threshold voltage $V_{th}$, and $\Delta$ corresponds to the refractory time $\tau_{\rm ref}$.)

Thus, the LIF model captures the facts that a neuron:
- performs spatial and temporal integration of synaptic inputs
- generates a spike when the voltage reaches a certain threshold
- goes refractory during the action potential
- has a leaky membrane

The LIF model assumes that the spatial and temporal integration of inputs is linear. Also, membrane potential dynamics close to the spike threshold are much slower in LIF neurons than in real neurons.

## Exercise 1: Python code to simulate the LIF neuron

We now write Python code to calculate Eqn. (2) and simulate the LIF neuron dynamics. We will use the Euler method, which you saw in the linear systems case last week,  to numerically integrate Eq 2.

The cell below initializes a dictionary that stores parameters of the LIF neuron model and the simulation scheme. You can use `pars=default_pars(T=simulation_time, dt=time_step)` to get the parameters (you can try to print the dictionary `pars`). Note that, `simulation_time` and `time_step` have the unit `ms`. In addition, you can add the value to a new parameter by `pars['New_param'] = value`.

```python
# @title

# @markdown Execute this code to initialize the default parameters

def default_pars(**kwargs):
  pars = {}

  # typical neuron parameters#
  pars['V_th'] = -55.     # spike threshold [mV]
  pars['V_reset'] = -75.  # reset potential [mV]
  pars['tau_m'] = 10.     # membrane time constant [ms]
  pars['g_L'] = 10.       # leak conductance [nS]
  pars['V_init'] = -75.   # initial potential [mV]
  pars['E_L'] = -75.      # leak reversal potential [mV]
  pars['tref'] = 2.       # refractory time (ms)

  # simulation parameters #
  pars['T'] = 400.  # Total duration of simulation [ms]
  pars['dt'] = .1   # Simulation time step [ms]

  # external parameters if any #
  for k in kwargs:
    pars[k] = kwargs[k]

  pars['range_t'] = np.arange(0, pars['T'], pars['dt'])  # Vector of discretized time points [ms]

  return pars

pars = default_pars()
```

The cell below defines the function to simulate the LIF neuron when receiving external current inputs. You can use `v, sp = run_LIF(pars, Iinj)` to get the membrane potential (`v`) and spike train (`sp`) given the dictionary `pars` and input current `Iinj`.

```python
def run_LIF(pars, Iinj, stop=False):
  """
  Simulate the LIF dynamics with external input current

  Args:
    pars       : parameter dictionary
    Iinj       : input current [pA]. The injected current here can be a value
                 or an array
    stop       : boolean. If True, use a current pulse

  Returns:
    rec_v      : membrane potential
    rec_sp     : spike times
  """

  # Set parameters
  V_th, V_reset = pars['V_th'], pars['V_reset']
  tau_m, g_L = pars['tau_m'], pars['g_L']
  V_init, E_L = pars['V_init'], pars['E_L']
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size
  tref = pars['tref']

  # Initialize voltage and current
  v = np.zeros(Lt)
  v[0] = V_init
  Iinj = Iinj * np.ones(Lt)
  if stop:  # set end of current to 0 if current pulse
    Iinj[:int(len(Iinj) / 2) - 1000] = 0
    Iinj[int(len(Iinj) / 2) + 1000:] = 0
  tr = 0.  # the count for refractory duration

  # Simulate the LIF dynamics
  rec_spikes = []  # record spike times
  for it in range(Lt - 1):
    if tr > 0:  # check for refractoriness
      v[it] = V_reset
      tr = tr - 1
    elif v[it] >= V_th:  # reset voltage and record spike event
      rec_spikes.append(it)
      v[it] = V_reset
      tr = tref / dt

    ########################################################################
    ## TODO for students: compute the membrane potential v, spike train sp #
    # Fill out function and remove
    raise NotImplementedError('Student Exercise: calculate the dv/dt and the update step!')
    ########################################################################
    # calculate the increment of the membrane potential
    dv = ...

    # update the membrane potential
    v[it+1] = ...

  rec_spikes = np.array(rec_spikes) * dt

  return v, rec_spikes

pars = default_pars(T=500)
# Uncomment below to test your function
# v, sp = run_LIF(pars, Iinj=100, stop=True)
# plot_volt_trace(pars, v, sp)
# plt.ylim([-80, -60])
# plt.show()
```

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

*Example output:*

---
# Section 2: Response of an LIF model to different types of input currents
In the following section, we will learn how to inject direct current and white noise to study the response of an LIF neuron.

```python
#@title Video 2: Response of the LIF neuron to different inputs
from IPython.display import YouTubeVideo
video = YouTubeVideo(id='preNGdab7Kk', width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=preNGdab7Kk

## Section 2.1: Direct current (DC)

### LIF neuron driven by constant current
Investigate the voltage response of the LIF neuron when receiving a DC input of 300 pA by `run_LIF` function.

```python
pars = default_pars(T=100)  # get the parameters

# Run the model to obtain v and sp
v, sp = run_LIF(pars, Iinj=300)

plot_volt_trace(pars, v, sp)
plt.show()
```

In the plot above, you see the membrane potential of an LIF neuron. You may notice that the neuron generates a spike. But this is just a cosmetic spike only for illustration purposes. In an LIF neuron, we only need to keep track of times when the neuron hit the threshold so the postsynaptic neurons can be informed of the spike.

### Interactive Demo: Parameter exploration of DC input amplitude
Here's an interactive demo that shows how the LIF neuron behavior changes for DC input with different amplitudes.

How much DC is needed to reach the threshold (rheobase current)? How does the membrane time constant affect the frequency of the neuron?

```python
# @title
# @markdown Make sure you execute this cell to enable the widget!
my_layout.width = '450px'
@widgets.interact(
    I_dc=widgets.FloatSlider(50., min=0., max=300., step=10.,
                               layout=my_layout),
    tau_m=widgets.FloatSlider(10., min=2., max=20., step=2.,
                                layout=my_layout)
)

def diff_DC(I_dc=200., tau_m=10.):
  pars = default_pars(T=100.)
  pars['tau_m'] = tau_m
  v, sp = run_LIF(pars, Iinj=I_dc)
  plot_volt_trace(pars, v, sp)
  plt.show()
```

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

## Section 2.2: Gaussian white noise (GWN) current

Given the noisy nature of neuronal activity _in vivo_, neurons usually receive complex, time-varying inputs.

To mimic this, we will now investigate the neuronal response when the LIF neuron receives Gaussian white noise $\xi(t)$ with mean

\begin{eqnarray}
E[\xi(t)]=\mu=0,
\end{eqnarray}

and autocovariance
\begin{eqnarray}
E[\xi(t)\xi(t+\tau)]=\sigma_\xi^2 \delta(\tau)
\end{eqnarray}

Note that the GWN has zero mean, that is, it describes only the fluctuations of the input received by a neuron. We can thus modify our definition of GWN to have a nonzero mean value $\mu$ that equals the DC input, since this is the average input into the cell. The cell below defines the modified gaussian white noise currents with nonzero mean $\mu$.

```python
#@title
#@markdown Execute this cell to get function to generate GWN

def my_GWN(pars, mu, sig, myseed=False):
  """
  Function that generates Gaussian white noise input

  Args:
    pars       : parameter dictionary
    mu         : noise baseline (mean)
    sig        : noise amplitute (standard deviation)
    myseed     : random seed. int or boolean
                 the same seed will give the same
                 random number sequence

  Returns:
    I          : Gaussian white noise input
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # set random seed
  # you can fix the seed of the random number generator so that the results
  # are reliable however, when you want to generate multiple realization
  # make sure that you change the seed for each new realization.
  if myseed:
      np.random.seed(seed=myseed)
  else:
      np.random.seed()

  # generate GWN
  # we divide here by 1000 to convert units to sec.
  I_gwn = mu + sig * np.random.randn(Lt) / np.sqrt(dt / 1000.)

  return I_gwn
```

### Exercise 2: LIF neuron driven by GWN

You can generate a noisy input with `my_GWN(pars, mu, sig, myseed=False)`. Here, $\mu=250$ and $\sigma=5$. Note that fixing the value of the random seed (e.g., `myseed=2020`) will allow you to obtain the same result every time you run this.

```python
pars = default_pars(T=100.)
sig_gwn = 5.
mu_gwn = 250.
#######################################################
## TODO for students: compute the noisy input I_GWN   #
# and then compute v, sp with inject I_GWN into LIF   #
#######################################################

# Calculate the GWN current
# I_GWN = my_GWN(pars, ..., ..., myseed=2020)

# Run the model and calculate the v and the sp
# v, sp = run_LIF(pars, ...)

# Uncomment these lines after obtaining v, sp
# plot_GWN(pars, I_GWN)
# plt.show()
```

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

*Example output:*

### Interactive Demo: LIF neuron Explorer for noisy input

The mean of the GWN is the amplitude of DC. Indeed, when $\sigma = 0$, GWN is just a DC.

So the question arises how does $\sigma$ of the GWN affect the spiking behavior of the neuron. For instance we may want to know
- how does the minimum input (i.e. $\mu$) needed to make a neuron spike change with increase in $\sigma$
- how does the spike regularity change with increase in $\sigma$

To get an intuition about these questions you can use the following interactive demo that shows how the LIF neuron behavior changes for noisy input with different amplitudes (the mean $\mu$) and fluctuation sizes ($\sigma$).

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

my_layout.width = '450px'
@widgets.interact(
    mu_gwn=widgets.FloatSlider(200., min=100., max=300., step=5.,
                               layout=my_layout),
    sig_gwn=widgets.FloatSlider(2.5, min=0., max=5., step=.5,
                                layout=my_layout)
)

def diff_GWN_to_LIF(mu_gwn, sig_gwn):
  pars = default_pars(T=100.)
  I_GWN = my_GWN(pars, mu=mu_gwn, sig=sig_gwn)
  v, sp = run_LIF(pars, Iinj=I_GWN)
  plt.figure(figsize=(12, 4))
  plt.subplot(121)
  plt.plot(pars['range_t'][::3], I_GWN[::3], 'b')
  plt.xlabel('Time (ms)')
  plt.ylabel(r'$I_{GWN}$ (pA)')
  plt.subplot(122)
  plot_volt_trace(pars, v, sp)
  plt.tight_layout()
  plt.show()
```

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

## Think!
- As we increase the input average ($\mu$) or the input fluctuation ($\sigma$), the spike count changes. How much can we increase the spike count, and what might be the relationship between GWN mean/std or DC value and spike count?

- We have seen above that when we inject DC, the neuron spikes in a regular manner (clock like), and this regularity is reduced when GWN is injected. The question is, how irregular can we make the neurons spiking by changing the parameters of the GWN?

---
# Section 3: Firing rate and spike time irregularity
When we plot the output firing rate as a function of GWN mean or DC value, it is called the input-output transfer function of the neuron (so simply F-I curve).

Spike regularity can be quantified as the **coefficient of variance (CV) of the inter-spike-interval (ISI)**:
\begin{align}
\text{CV}_{\text{ISI}} = \frac{std(\text{ISI})}{mean(\text{ISI})}
\end{align}

A Poisson train is an example of high irregularity, in which $\textbf{CV}_{\textbf{ISI}} \textbf{= 1}$. And for a clocklike (regular) process we have $\textbf{CV}_{\textbf{ISI}} \textbf{= 0}$ because of **std(ISI)=0**.

## Interactive Demo: F-I Explorer for different `sig_gwn`

How does the F-I curve of the LIF neuron change as we increase the $\sigma$ of the GWN? We can already expect that the F-I curve will be stochastic and the results will vary from one trial to another. But will there be any other change compared to the F-I curved measured using DC?

Here's an interactive demo that shows how the F-I curve of a LIF neuron changes for different levels of fluctuation $\sigma$.

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

my_layout.width = '450px'
@widgets.interact(
    sig_gwn=widgets.FloatSlider(3.0, min=0., max=6., step=0.5,
                                layout=my_layout)
)

def diff_std_affect_fI(sig_gwn):
  pars = default_pars(T=1000.)
  I_mean = np.arange(100., 400., 10.)
  spk_count = np.zeros(len(I_mean))
  spk_count_dc = np.zeros(len(I_mean))

  for idx in range(len(I_mean)):
      I_GWN = my_GWN(pars, mu=I_mean[idx], sig=sig_gwn, myseed=2020)
      v, rec_spikes = run_LIF(pars, Iinj=I_GWN)
      v_dc, rec_sp_dc = run_LIF(pars, Iinj=I_mean[idx])
      spk_count[idx] = len(rec_spikes)
      spk_count_dc[idx] = len(rec_sp_dc)

  # Plot the F-I curve i.e. Output firing rate as a function of input mean.
  plt.figure()
  plt.plot(I_mean, spk_count, 'k',
           label=r'$\sigma_{\mathrm{GWN}}=%.2f$' % sig_gwn)
  plt.plot(I_mean, spk_count_dc, 'k--', alpha=0.5, lw=4, dashes=(2, 2),
           label='DC input')
  plt.ylabel('Spike count')
  plt.xlabel('Average injected current (pA)')
  plt.legend(loc='best')
  plt.show()
```

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

### Exercise 3: Compute $CV_{ISI}$ values

As shown above, the F-I curve becomes smoother while increasing the amplitude of the fluctuation ($\sigma$). In addition, the fluctuation can also change the irregularity of the spikes. Let's investigate the effect of $\mu=250$ with $\sigma=0.5$ vs $\sigma=3$.

Fill in the code below to compute ISI, then plot the histogram of the ISI and compute the $CV_{ISI}$. Note that, you can use `np.diff` to calculate ISI.

```python
def isi_cv_LIF(spike_times):
  """
  Calculates the inter-spike intervals (isi) and
  the coefficient of variation (cv) for a given spike_train

  Args:
    spike_times : (n, ) vector with the spike times (ndarray)

  Returns:
    isi         : (n-1,) vector with the inter-spike intervals (ms)
    cv          : coefficient of variation of isi (float)

  """
  ########################################################################
  ## TODO for students: compute the membrane potential v, spike train sp #
  # Fill out function and remove
  raise NotImplementedError('Student Exercise: calculate the isi and the cv!')
  ########################################################################
  if len(spike_times) >= 2:
    # Compute isi
    isi = ...
    # Compute cv
    cv = ...
  else:
    isi = np.nan
    cv = np.nan

  return isi, cv

pars = default_pars(T=1000.)
mu_gwn = 250
sig_gwn1 = 0.5
sig_gwn2 = 3.0

I_GWN1 = my_GWN(pars, mu=mu_gwn, sig=sig_gwn1, myseed=2020)
_, sp1 = run_LIF(pars, Iinj=I_GWN1)

I_GWN2 = my_GWN(pars, mu=mu_gwn, sig=sig_gwn2, myseed=2020)
_, sp2 = run_LIF(pars, Iinj=I_GWN2)

# Uncomment to check your function
# isi1, cv1 = isi_cv_LIF(sp1)
# isi2, cv2 = isi_cv_LIF(sp2)

# my_hists(isi1, isi2, cv1, cv2, sig_gwn1, sig_gwn2)
```

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

*Example output:*

## Interactive Demo: Spike irregularity explorer for different `sig_gwn`

In the above illustration, we see that the CV of inter-spike-interval (ISI) distribution depends on $\sigma$ of GWN. What about the mean of GWN, should that also affect the CV$_{\rm ISI}$? If yes, how? Does the efficacy of $\sigma$ in increasing the CV$_{\rm ISI}$ depend on $\mu$?

In the following interactive demo, you will examine how different levels of fluctuation $\sigma$ affect the CVs for different average injected currents ($\mu$).

```python
#@title

#@markdown Make sure you execute this cell to enable the widget!

my_layout.width = '450px'
@widgets.interact(
    sig_gwn=widgets.FloatSlider(0.0, min=0., max=10.,
                                step=0.5, layout=my_layout)
)

def diff_std_affect_fI(sig_gwn):
  pars = default_pars(T=1000.)
  I_mean = np.arange(100., 400., 20)
  spk_count = np.zeros(len(I_mean))
  cv_isi = np.empty(len(I_mean))

  for idx in range(len(I_mean)):
    I_GWN = my_GWN(pars, mu=I_mean[idx], sig=sig_gwn)
    v, rec_spikes = run_LIF(pars, Iinj=I_GWN)
    spk_count[idx] = len(rec_spikes)
    if len(rec_spikes) > 3:
      isi = np.diff(rec_spikes)
      cv_isi[idx] = np.std(isi) / np.mean(isi)

  # Plot the F-I curve i.e. Output firing rate as a function of input mean.
  plt.figure()
  plt.plot(I_mean[spk_count > 5], cv_isi[spk_count > 5], 'bo', alpha=0.5)
  plt.xlabel('Average injected current (pA)')
  plt.ylabel(r'Spike irregularity ($\mathrm{CV}_\mathrm{ISI}$)')
  plt.ylim(-0.1, 1.5)
  plt.grid(True)
  plt.show()
```

### Try to answer the following:
- Does the standard deviation of the injected current affect the F-I curve in any qualitative manner?
- Why does increasing the mean of GWN reduce the $CV_{ISI}$?
- If you plot spike count (or rate) vs. $CV_{ISI}$, should there be a relationship between the two? Try out yourself.

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

---
# Section 4: Generation of Poisson type spike trains

*In the next tutorials, we will often use Poisson type spike train to explore properties of neurons and synapses. Therefore, it is good to know how to generate Poisson type spike trains.*

Mathematically, a spike train is a Point Process. One of the simplest models of a sequence of presynaptic pulse inputs is the Poisson process. We know that given temporal integration and refractoriness, neurons cannot behave as a Poisson Process, and Gamma Process gives a better approximation (*find out what might be the difference in the two processes*).
Here, however, we will assume that the incoming spikes are following Poisson statistics. A question arises about how to simulate a Poisson process. The generation of the Poisson process can be realized by at least two following ways:

- By definition, for a Poisson process with rate $\lambda$, the probability of finding one event in the time window with a sufficiently small length $\Delta t$ is $P(N = 1) = \lambda \Delta t$. Therefore, in each time window, we generate a uniformly distributed random variable $r \in [0,1]$ and generate a Poisson event when $r <\lambda \Delta t$. This method allows us to generate Poisson distributed spikes in an online manner.

- The interval $t_{k+1}-t_{k}$ between two Poisson events with rate $\lambda$ follows the exponential distribution, i.e., $P(t_{k+1}-t_{k}<t) = 1 - e^{\lambda t}$. Therefore, we only need to generate a set of exponentially distributed variables $\{s_k\}$ to obtain the timing of Poisson events $t_{k+1}=t_{k}+s_{k}$. In this method, we need to generate all future spikes at once.

Below, we use the first method in a function `Poisson_generator`, which takes arguments `(pars, rate, n, myseed)`.

```python
# @title
# @markdown Execute this cell to get a Poisson_generator function

def Poisson_generator(pars, rate, n, myseed=False):
  """
  Generates poisson trains

  Args:
    pars            : parameter dictionary
    rate            : noise amplitute [Hz]
    n               : number of Poisson trains
    myseed          : random seed. int or boolean

  Returns:
    pre_spike_train : spike train matrix, ith row represents whether
                      there is a spike in ith spike train over time
                      (1 if spike, 0 otherwise)
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # set random seed
  if myseed:
      np.random.seed(seed=myseed)
  else:
      np.random.seed()

  # generate uniformly distributed random variables
  u_rand = np.random.rand(n, Lt)

  # generate Poisson train
  poisson_train = 1. * (u_rand < rate * (dt / 1000.))

  return poisson_train

```

```python
# we can use Poisson_generator to mimic presynaptic spike trains
pars = default_pars()
pre_spike_train = Poisson_generator(pars, rate=10, n=100, myseed=2020)
my_raster_Poisson(pars['range_t'], pre_spike_train, 100)

```

How do we make sure that the above spike trains are following Poisson statistics?
A Poisson process must have the following properties:
- The ratio of the mean and variance of spike count is 1
- Inter-spike-intervals are exponentially distributed
- Spike times are irregular i.e. $CV_{\rm ISI} = 1$
- Adjacent spike intervals are independent of each other.

---
# Summary

Congratulations! You've just built a leaky integrate-and-fire (LIF) neuron model from scratch, and studied its dynamics in response to various types of inputs, having:

- simulated the LIF neuron model

- driven the LIF neuron with external inputs, such as direct current, Gaussian white noise, and Poisson spike trains, etc.

- studied how different inputs affect the  LIF neuron's output (firing rate and spike time irregularity),

with a special focus on low rate and irregular firing regime to mimc real cortical neurons. The next tutorial will look at how spiking statistics may be influenced by a neuron's input statistics.

However, if you have extra time, follow the section below to explore a different type of noise input.

---
# Bonus 1: Orenstein-Uhlenbeck Process

When a neuron receives spiking input, the synaptic current is Shot Noise -- which is a kind of colored noise and the spectrum of the noise determined by the synaptic kernel time constant. That is, a neuron is driven by **colored noise** and not GWN.

We can model colored noise using the Ohrenstein-Uhlenbeck process - filtered white noise.

## Ornstein-Uhlenbeck (OU) current

We next study if the input current is temporally correlated and is modeled as an Ornstein-Uhlenbeck process $\eta(t)$, i.e., low-pass filtered GWN with a time constant $\tau_{\eta}$:

$$\tau_\eta \frac{d}{dt}\eta(t) = \mu-\eta(t) + \sigma_\eta\sqrt{2\tau_\eta}\xi(t).$$

**Hint:** An OU process as defined above has

$$E[\eta(t)]=\mu$$

and autocovariance

$$[\eta(t)\eta(t+\tau)]=\sigma_\eta^2e^{-|t-\tau|/\tau_\eta},$$

which can be used to check your code.

```python
# @title `my_OU(pars, mu, sig, myseed)`
# @markdown Ececute this cell to enable the OU process

def my_OU(pars, mu, sig, myseed=False):
  """
  Function that produces Ornstein-Uhlenbeck input

  Args:
    pars       : parameter dictionary
    sig        : noise amplitute
    myseed     : random seed. int or boolean

  Returns:
    I_ou       : Ornstein-Uhlenbeck input current
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size
  tau_ou = pars['tau_ou']  # [ms]

  # set random seed
  if myseed:
    np.random.seed(seed=myseed)
  else:
    np.random.seed()

  # Initialize
  noise = np.random.randn(Lt)
  I_ou = np.zeros(Lt)
  I_ou[0] = noise[0] * sig

  # generate OU
  for it in range(Lt-1):
    I_ou[it+1] = I_ou[it] + (dt / tau_ou) * (mu - I_ou[it]) + np.sqrt(2 * dt / tau_ou) * sig * noise[it + 1]

  return I_ou
```

### Interactive Demo: LIF Explorer with OU input
In the following, we will check how a neuron responds to a noisy current that follows the statistics of an OU process.

```python
# @title

# @markdown Remember to enable the widget by running the cell!

my_layout.width = '450px'
@widgets.interact(
  tau_ou=widgets.FloatSlider(10.0, min=5., max=20.,
                             step=2.5, layout=my_layout),
  sig_ou=widgets.FloatSlider(10.0, min=5., max=40.,
                             step=2.5, layout=my_layout),
  mu_ou=widgets.FloatSlider(190.0, min=180., max=220.,
                            step=2.5, layout=my_layout)
)

def LIF_with_OU(tau_ou=10., sig_ou=40., mu_ou=200.):
  pars = default_pars(T=1000.)
  pars['tau_ou'] = tau_ou  # [ms]

  I_ou = my_OU(pars, mu_ou, sig_ou)

  v, sp = run_LIF(pars, Iinj=I_ou)

  plt.figure(figsize=(12, 4))
  plt.subplot(121)
  plt.plot(pars['range_t'], I_ou, 'b', lw=1.0)
  plt.xlabel('Time (ms)')
  plt.ylabel(r'$I_{\mathrm{OU}}$ (pA)')
  plt.subplot(122)
  plot_volt_trace(pars, v, sp)
  plt.tight_layout()
  plt.show()
```

## Think!
- How does the OU type input change neuron responsiveness?
- What do you think will happen to the spike pattern and rate if you increased or decreased the time constant of the OU process?

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

---
# Bonus 2: Generalized Integrate-and-Fire models

LIF model is not the only abstraction of real neurons. If you want to learn about more realistic types of neuronal models, watch the Bonus Video!

```python
#@title Video 3 (Bonus): Extensions to Integrate-and-Fire models
from IPython.display import YouTubeVideo
video = YouTubeVideo(id='G0b6wLhuQxE', width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=G0b6wLhuQxE


###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under BSD 3-Clause license. (c)2014 Lorena A. Barba, Olivier Mesnard. Thanks: NSF for support via CAREER award #1149784.

# Vortex

Let's recap. In the first three lessons of _AeroPython_, you computed:

1. [a source-sink pair](01_Lesson01_sourceSink.ipynb)—you learned how to make a `streamplot()` and use `scatter()` to mark the location of the singular points. If you did your challenge task, you saw that equipotential lines are perpendicular to streamlines.

2. [a source-sink pair in a free stream](02_Lesson02_sourceSinkFreestream.ipynb), creating a _Rankine oval_. For the first time, we see that we can model the flow around an object using potential flow solutions. You also learned to define custom functions, to make Python work for you and look even more like plain English.

3. [a doublet](03_Lesson03_doublet.ipynb)—this one is wonderful, because the stream-line pattern gives the flow around a circular cylinder (in two dimensions). You encountered the _D'Alembert paradox_ and hopefully that got you thinking. (If not, go back to that lesson and _think_!)

Did you also do the [assignment](03_Lesson03_Assignment.ipynb)? It shows you how a *distribution of sources* can be used to model potential flow around an  airfoil. This starts looking like aerodynamics!

But what is the most important thing we want from applied aerodynamics? We want to make things fly, of course! And to fly, there must be a force of *aerodynamic lift* to counteract the weight of the object.

In this section of the course, we learn about lift. First, we will compute the flow of a potential vortex. It turns out, vortex circulation and lift are intimately related.

## What's a vortex?

This question is deeper than you might think! The simple answer is that a vortex is motion in circular streamlines. Imagine streamlines that are concentric circles about a given point—what's confusing is that this _does not mean_ that fluid elements are themselves rotating!

In an irrotational vortex, the tangential velocity is constant along a (circular) streamline and inversely proportional to the radius, while the radial velocity is zero. In polar coordinates:

\begin{equation}
u_\theta\left(r,\theta\right) = \frac{\text{constant}}{r}  \quad \text{,} \quad  u_r\left(r,\theta\right) = 0
\end{equation}

The vorticity is zero everywhere, except at the location of the point vortex, where the derivative of $u_\theta$ is infinite.

We introduced the concept of circulation in the first lesson ([Source & Sink](01_Lesson01_sourceSink.ipynb)). Let's use that. Around any circular streamline enclosing the vortex, and using the sign convention that a negative vortex circulates anti-clockwise, we have:

\begin{equation}\Gamma = -\oint \mathbf{v}\cdot d\vec{l} = -u_\theta 2 \pi r\end{equation}

Thus, the constant in the expression for $u_\theta$ in $(1)$ is equal to $\Gamma/2\pi$, and we now write:

\begin{equation}u_\theta\left(r,\theta\right) = \frac{\Gamma}{2\pi r}\end{equation}

We can get the stream function by integrating the velocity components:

\begin{equation}\psi\left(r,\theta\right) = \frac{\Gamma}{2\pi}\ln r\end{equation}

In Cartesian coordinates, the stream function is

\begin{equation}\psi\left(x,y\right) = \frac{\Gamma}{4\pi}\ln\left(x^2+y^2\right)\end{equation}

while the velocity components would be:

\begin{equation}u\left(x,y\right) = \frac{\Gamma}{2\pi}\frac{y}{x^2+y^2} \qquad v\left(x,y\right) = -\frac{\Gamma}{2\pi}\frac{x}{x^2+y^2}\end{equation}

This vortex flow is irrotational everywhere, except at the vortex center, where it is infinite. The strength of the point vortex is equal to the circulation $\Gamma$ around it.

## Let's compute a vortex

The set-up is the same as before: we load our favorite libraries, and we create a grid of points to evaluate the velocity field.

```python
import numpy
import math
from matplotlib import pyplot
# embed the figures into the notebook
%matplotlib inline
```

```python
N = 50                                # Number of points in each direction
x_start, x_end = -2.0, 2.0            # x-direction boundaries
y_start, y_end = -1.0, 1.0            # y-direction boundaries
x = numpy.linspace(x_start, x_end, N)    # computes a 1D-array for x
y = numpy.linspace(y_start, y_end, N)    # computes a 1D-array for y
X, Y = numpy.meshgrid(x, y)              # generates a mesh grid
```

Give your vortex a strength $\Gamma=5$ and place it at the center of your domain:

```python
gamma = 5.0                      # strength of the vortex
x_vortex, y_vortex = 0.0, 0.0    # location of the vortex
```

We will define two functions,

* `get_velocity_vortex()` and
* `get_stream_function_vortex()`,

to compute the velocity components and the stream function on our Cartesian grid, given the strength and the location of the vortex. Then, we will use our custom functions to evaluate everything on the grid points. Let's write those functions first.

```python
def get_velocity_vortex(strength, xv, yv, X, Y):
    """
    Returns the velocity field generated by a vortex.

    Parameters
    ----------
    strength: float
        Strength of the vortex.
    xv: float
        x-coordinate of the vortex.
    yv: float
        y-coordinate of the vortex.
    X: 2D Numpy array of floats
        x-coordinate of the mesh points.
    Y: 2D Numpy array of floats
        y-coordinate of the mesh points.

    Returns
    -------
    u: 2D Numpy array of floats
        x-component of the velocity vector field.
    v: 2D Numpy array of floats
        y-component of the velocity vector field.
    """
    u = +strength / (2 * math.pi) * (Y - yv) / ((X - xv)**2 + (Y - yv)**2)
    v = -strength / (2 * math.pi) * (X - xv) / ((X - xv)**2 + (Y - yv)**2)

    return u, v
```

```python
def get_stream_function_vortex(strength, xv, yv, X, Y):
    """
    Returns the stream-function generated by a vortex.

    Parameters
    ----------
    strength: float
        Strength of the vortex.
    xv: float
        x-coordinate of the vortex.
    yv: float
        y-coordinate of the vortex.
    X: 2D Numpy array of floats
        x-coordinate of the mesh points.
    Y: 2D Numpy array of floats
        y-coordinate of the mesh points.

    Returns
    -------
    psi: 2D Numpy array of floats
        The stream-function.
    """
    psi = strength / (4 * math.pi) * numpy.log((X - xv)**2 + (Y - yv)**2)

    return psi
```

An now, call the functions with the vortex strength and position, plus the coordinates of the evaluation grid, to get the velocity and streamfunction of the vortex.

```python
# compute the velocity field on the mesh grid
u_vortex, v_vortex = get_velocity_vortex(gamma, x_vortex, y_vortex, X, Y)

# compute the stream-function on the mesh grid
psi_vortex = get_stream_function_vortex(gamma, x_vortex, y_vortex, X, Y)
```

We are now able to visualize the streamlines of a vortex, and they look like concentric circles around the vortex center, as expected.

```python
# plot the streamlines
width = 10
height = (y_end - y_start) / (x_end - x_start) * width
pyplot.figure(figsize=(width, height))
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.streamplot(X, Y, u_vortex, v_vortex,
                  density=2, linewidth=1, arrowsize=1, arrowstyle='->')
pyplot.scatter(x_vortex, y_vortex, color='#CD2305', s=80, marker='o');
```

## Vortex & Sink

For fun, let's use our superposition powers. Add a vortex to a sink, using two new functions to compute the velocity components and the stream function of the sink, and adding to those of a vortex (remember that the sink can be easily replaced by a source by just changing the sign of the strength).

```python
strength_sink = -1.0            # strength of the sink
x_sink, y_sink = 0.0, 0.0       # location of the sink
```

```python
def get_velocity_sink(strength, xs, ys, X, Y):
    """
    Returns the velocity field generated by a sink.

    Parameters
    ----------
    strength: float
        Strength of the sink.
    xs: float
        x-coordinate of the sink.
    ys: float
        y-coordinate of the sink.
    X: 2D Numpy array of floats
        x-coordinate of the mesh points.
    Y: 2D Numpy array of floats
        y-coordinate of the mesh points.

    Returns
    -------
    u: 2D Numpy array of floats
        x-component of the velocity vector field.
    v: 2D Numpy array of floats
        y-component of the velocity vector field.
    """
    u = strength / (2 * math.pi) * (X - xs) / ((X - xs)**2 + (Y - ys)**2)
    v = strength / (2 * math.pi) * (Y - ys) / ((X - xs)**2 + (Y - ys)**2)

    return u, v
```

```python
def get_stream_function_sink(strength, xs, ys, X, Y):
    """
    Returns the stream-function generated by a sink.

    Parameters
    ----------
    strength: float
        Strength of the sink.
    xs: float
        x-coordinate of the sink.
    ys: float
        y-coordinate of the sink.
    X: 2D Numpy array of floats
        x-coordinate of the mesh points.
    Y: 2D Numpy array of floats
        y-coordinate of the mesh points.

    Returns
    -------
    psi: 2D Numpy array of floats
        The stream-function.
    """
    psi = strength / (2 * math.pi) * numpy.arctan2((Y - ys), (X - xs))

    return psi
```

```python
# compute the velocity field on the mesh grid
u_sink, v_sink = get_velocity_sink(strength_sink, x_sink, y_sink, X, Y)

# compute the stream-function on the mesh grid
psi_sink = get_stream_function_sink(strength_sink, x_sink, y_sink, X, Y)
```

Now, let's visualize the streamlines of the vortex-sink, and admire our artistic creation:

```python
# superposition of the sink and the vortex
u = u_vortex + u_sink
v = v_vortex + v_sink
psi = psi_vortex + psi_sink

# plot the streamlines
width = 10
height = (y_end - y_start) / (x_end - x_start) * width
pyplot.figure(figsize=(width, height))
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.streamplot(X, Y, u, v, density=2, linewidth=1, arrowsize=1, arrowstyle='->')
pyplot.scatter(x_vortex, y_vortex, color='#CD2305', s=80, marker='o');
```

Very cool op-art. (And a good model for your typical bath-tub vortex.) But is all this useful? Yes!

First, you will take your training wheels off and—on your own—compute the flow of an [infinite row of vortices](05_Lesson05_InfiniteRowOfVortices.ipynb). Your mission, should you choose to accept it, is in the next notebook.

After that, we will learn about the connection between a vortex, and the force of lift. This turns out to be very important in aerodynamics!

## What's this "irrotational" vortex thing?

I know what you are thinking.

What does it mean that the vortex is *irrotational*? Surely if there's a vortex, there's rotation!

You are not crazy. It's natural to think this way, but the potential vortex is a flow where streamlines are circular, yet fluid elements do not rotate around themselves—they just go around the circular path.

This classic video will help you understand ... just watch the 25 seconds of video after time 4m 25s, and see a "vorticity meter" go around a free vortex without rotating itself.

```python
from IPython.display import YouTubeVideo
from datetime import timedelta

start=int(timedelta(hours=0, minutes=4, seconds=25).total_seconds())

YouTubeVideo("loCLkcYEWD4", start=start)
```

Remember: vorticity measures the local angular velocity of each fluid element. If the fluid elements go around a circular path, but do not spin themselves, there is no vorticity!

This animation from [Wikipedia](http://en.wikipedia.org/wiki/Vortex#Irrotational_vortices) helps further illustrate what happens in an irrotational vortex: the orange markers with a line across them are going around in circles, but they are not themselves rotating (notice the white lines keep their orientation).

## Next ...

The next IPython Notebook of the *AeroPython* series presents the exercise ["Infinite row of vortices,"](05_Lesson05_InfiniteRowOfVortices.ipynb) as independent student work.

---
Please ignore the cell below. It just loads our style for the notebook.

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

<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');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Alegreya Sans', sans-serif;
    font-style:regular;
    font-weight: 200;
    font-size: 50pt;
    line-height: 100%;
    color:#CD2305;
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Fenix', serif;
    font-size: 22pt;
    line-height: 100%;
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Fenix', serif;
    margin-top:12px;
	font-size: 16pt;
    margin-bottom: 3px;
    font-style: regular;
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Fenix', serif;
    font-size: 2pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Alegreya Sans', sans-serif;
    font-weight: 300;
    font-size: 16pt;
    color: #CD2305;
    font-style: italic;
    margin-bottom: .5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'Source Code Pro', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/

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


# Detector simulation
In this notebook we load a track dataset generated by `edep-sim` and we calculate the ADC counts corresponding to each pixel. The result is exported to a HDF5 file.

```python
# This is need so you can import larndsim without doing python setup.py install
import os,sys,inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0,parentdir)
```

```python
from larndsim import consts
import importlib
importlib.reload(consts)

consts.load_detector_properties("../larndsim/detector_properties/singlecube.yaml","../larndsim/pixel_layouts/layout-singlecube.yaml")
```

```python
from math import ceil

from larndsim import quenching, drifting, detsim, pixels_from_track, fee

importlib.reload(detsim)
importlib.reload(drifting)
importlib.reload(quenching)
importlib.reload(pixels_from_track)
importlib.reload(fee)

import h5py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import matplotlib.cm as cm
from mpl_toolkits.axes_grid1 import make_axes_locatable
from mpl_toolkits.mplot3d import Axes3D
import mpl_toolkits.mplot3d.art3d as art3d

import numpy as np
import numba as nb

from numba import cuda
```

### Dataset import

First of all we load the `edep-sim` output. For this sample we need to invert $z$ and $y$ axes.

```python
with h5py.File('edepsim_1M.h5', 'r') as f:
    tracks = np.array(f['segments'])

y_start = np.copy(tracks['y_start'] )
y_end = np.copy(tracks['y_end'])
y = np.copy(tracks['y'])

tracks['y_start'] = np.copy(tracks['z_start'])
tracks['y_end'] = np.copy(tracks['z_end'])
tracks['y'] = np.copy(tracks['z'])

tracks['z_start'] = y_start
tracks['z_end'] = y_end
tracks['z'] = y
```

```python
selected_tracks = tracks[:100]
```

### Quenching and drifting
We calculate the number of electrons after recombination (`quenching` module) and the position and number of electrons after drifting (`drifting` module).

```python
threadsperblock = 256
blockspergrid = ceil(selected_tracks.shape[0] / threadsperblock)
quenching.quench[blockspergrid,threadsperblock](selected_tracks, consts.box)
drifting.drift[blockspergrid,threadsperblock](selected_tracks)
```

We find the pixels intersected by the projection of the tracks on the anode plane using the Bresenham's algorithm. We also take into account the neighboring pixels, due to the transverse diffusion of the charges.

```python
longest_pix = ceil(max(selected_tracks["dx"])/consts.pixel_size[0])
max_radius = ceil(max(selected_tracks["tran_diff"])*5/consts.pixel_size[0])

MAX_PIXELS = (longest_pix*4+6)*max_radius*2
MAX_ACTIVE_PIXELS = longest_pix*2

active_pixels = np.full((selected_tracks.shape[0], MAX_ACTIVE_PIXELS, 2), -1, dtype=np.int32)
neighboring_pixels = np.full((selected_tracks.shape[0], MAX_PIXELS, 2), -1, dtype=np.int32)
n_pixels_list = np.zeros(shape=(selected_tracks.shape[0]))

threadsperblock = 128
blockspergrid = ceil(selected_tracks.shape[0] / threadsperblock)
pixels_from_track.get_pixels[blockspergrid,threadsperblock](selected_tracks,
                                                            active_pixels,
                                                            neighboring_pixels,
                                                            n_pixels_list,
                                                            max_radius+1)
```

```python
shapes = neighboring_pixels.shape
joined = neighboring_pixels.reshape(shapes[0]*shapes[1],2)
unique_pix = np.unique(joined, axis=0)
unique_pix = unique_pix[(unique_pix[:,0] != -1) & (unique_pix[:,1] != -1),:] # This array contains a unique list of involved pixels
```

### Charge distribution calculation
Here we calculate the current induced by each track on the pixels, taking into account longitudinal and transverse diffusion. The track segment is parametrized as:
\begin{align}
x'(r') &=x_s + \frac{\Delta x}{\Delta r}r'\\
y'(r') &=y_s + \frac{\Delta y}{\Delta r}r'\\
z'(r') &=z_s + \frac{\Delta z}{\Delta r}r',
\end{align}
where $\Delta r$ is the segment length. Here we assume $z$ as the drift direction.
The diffused charge distribution is calculated with the following integral:
\begin{equation}
\rho(x,y,z) = \frac{Q}{\sqrt{(2\pi)^3}\sigma_x\sigma_y\sigma_z\Delta r}\exp\left[-\frac{(x-x_s)^2}{2\sigma_x^2}-\frac{(y-y_s)^2}{2\sigma_y^2}-\frac{(z-z_s)^2}{2\sigma_z^2}\right]\int^{r'=\Delta r}_{r'=0}dr'\exp[-(ar'^2+br')],
\end{equation}
where
\begin{align}
a &= \left[\left(\frac{\Delta x}{\Delta r}\right)^2\frac{1}{2\sigma_x^2} + \left(\frac{\Delta y}{\Delta r}\right)^2\frac{1}{2\sigma_y^2} + \left(\frac{\Delta z}{\Delta r}\right)^2\frac{1}{2\sigma_z^2} \right]\\
b &= -\left[\frac{(x-x_s)}{\sigma_x^2}\frac{\Delta x}{\Delta r}+
\frac{(y-y_s)}{\sigma_y^2}\frac{\Delta y}{\Delta r}+
\frac{(z-z_s)}{\sigma_z^2}\frac{\Delta z}{\Delta r}\right].
\end{align}

The simmetry of the transverse diffusion along the track allows to take a slice on the $xy$ plane and solve the integral once at a fixed $z$ coordinate (e.g. at $z_{m} = (z_s+z_e)/2$) and re-use it at other $z$ coordinates away from the endpoints (where $\rho(x,y,z)$ varies along $z$ so must be calculated at each $z$).

```python
# Here we build a map between tracks and event IDs
unique_eventIDs = np.unique(selected_tracks['eventID'])
event_id_map = np.zeros_like(selected_tracks['eventID'])
for iev, evID in enumerate(selected_tracks['eventID']):
    event_id_map[iev] = np.where(evID == unique_eventIDs)[0]
d_event_id_map = cuda.to_device(event_id_map)

# Here we find the longest signal in time and we store an array with the start in time of each track
max_length = np.array([0])
track_starts = np.empty(selected_tracks.shape[0])
d_track_starts = cuda.to_device(track_starts)
threadsperblock = 128
blockspergrid = ceil(selected_tracks.shape[0] / threadsperblock)
detsim.time_intervals[blockspergrid,threadsperblock](d_track_starts, max_length,  d_event_id_map, selected_tracks)

# Here we calculate the induced current on each pixel
signals = np.zeros((selected_tracks.shape[0],
                    neighboring_pixels.shape[1],
                    max_length[0]), dtype=np.float32)
threadsperblock = (4,4,4)
blockspergrid_x = ceil(signals.shape[0] / threadsperblock[0])
blockspergrid_y = ceil(signals.shape[1] / threadsperblock[1])
blockspergrid_z = ceil(signals.shape[2] / threadsperblock[2])
blockspergrid = (blockspergrid_x, blockspergrid_y, blockspergrid_z)
d_signals = cuda.to_device(signals)

detsim.tracks_current[blockspergrid,threadsperblock](d_signals,
                                                     neighboring_pixels,
                                                     selected_tracks)

# Here we create a map between tracks and index in the unique pixel array
pixel_index_map = np.full((selected_tracks.shape[0], neighboring_pixels.shape[1]), -1)

for itr in range(neighboring_pixels.shape[0]):
    for ipix in range(neighboring_pixels.shape[1]):
        pID = neighboring_pixels[itr][ipix]
        if pID[0] >= 0 and pID[1] >= 0:
            try:
                index = np.where((unique_pix[:,0] == pID[0]) & (unique_pix[:,1] == pID[1]))
            except IndexError:
                print(index,"More pixels than maximum value")
            pixel_index_map[itr,ipix] = index[0]

# Here we combine the induced current on the same pixels by different tracks
d_pixel_index_map = cuda.to_device(pixel_index_map)
threadsperblock = (8,8,8)
blockspergrid_x = ceil(d_signals.shape[0] / threadsperblock[0])
blockspergrid_y = ceil(d_signals.shape[1] / threadsperblock[1])
blockspergrid_z = ceil(d_signals.shape[2] / threadsperblock[2])
blockspergrid = (blockspergrid_x, blockspergrid_y, blockspergrid_z)
pixels_signals = np.zeros((len(unique_pix),len(consts.time_ticks)*len(unique_eventIDs)*2))
d_pixels_signals = cuda.to_device(pixels_signals)
detsim.sum_pixel_signals[blockspergrid,threadsperblock](d_pixels_signals,
                                                        d_signals,
                                                        d_track_starts,
                                                        d_pixel_index_map)

currents = np.sum(d_pixels_signals,axis=1)*consts.t_sampling/consts.e_charge
```

### 3D event display

```python
cmap = cm.Spectral_r
norm = mpl.colors.Normalize(vmin=0, vmax=256)
m = cm.ScalarMappable(norm=norm, cmap=cmap)

cmap = cm.viridis
norm_curr = mpl.colors.LogNorm(vmin=1, vmax=max(currents))
m_curr = cm.ScalarMappable(norm=norm_curr, cmap=cmap)
fig = plt.figure(figsize=(7,7))
ax = fig.add_subplot(111, projection='3d')

for it,t in enumerate(selected_tracks):
    if it == 0:
        ax.plot((t["x_start"], t["x_end"]),
            (t["z_start"], t["z_end"]),
            (t["y_start"], t["y_end"]),
            c='r',
            lw=1,
            alpha=1,
            zorder=10,
            label='Geant4 detector segment')
    else:
        ax.plot((t["x_start"], t["x_end"]),
                (t["z_start"], t["z_end"]),
                (t["y_start"], t["y_end"]),
                c='r',
                lw=1,
                alpha=1,
                zorder=9999)

    ax.plot((t["x_start"], t["x_end"]),
            (consts.module_borders[0][2][0], consts.module_borders[0][2][0]),
            (t["y_start"], t["y_end"]),
            c='r',
            lw=1,
            ls=':',
            alpha=1,
            zorder=9999)

for ip, p in enumerate(unique_pix):
    x_rect, y_rect = detsim.get_pixel_coordinates(p)
    pixel_plane = int(p[0] // consts.n_pixels[0])
    row = int(pixel_plane // 4)
    column = int(pixel_plane % 4)
    if currents[ip] > 0:
        rect = plt.Rectangle((x_rect-consts.pixel_size[0]/2, y_rect-consts.pixel_size[0]/2),
                             consts.pixel_size[0], consts.pixel_size[1],
                             linewidth=0.1, fc=m_curr.to_rgba(currents[ip]),
                             edgecolor='white', label=('Pixel' if ip == 5 else ''))
        ax.add_patch(rect)
        art3d.pathpatch_2d_to_3d(rect, z=consts.module_borders[pixel_plane][2][0], zdir="y")

# ax.set_ylim(consts.module_borders[pixel_plane][2][0],50)
ax.set_xlim(consts.module_borders[0][1][0],consts.module_borders[0][1][1])
ax.set_ylim(consts.module_borders[0][2][0],consts.module_borders[0][2][1])
ax.set_zlim(consts.module_borders[0][2][0],consts.module_borders[0][2][1])

ax.set_box_aspect((1, 1, 1))
ax.grid(False)
ax.xaxis.set_major_locator(plt.MaxNLocator(3))
ax.yaxis.set_major_locator(plt.MaxNLocator(3))
ax.view_init(20, 55)

ax.set_ylabel("z [cm]")
ax.set_xlabel("x [cm]")
ax.set_zlabel("y [cm]")
_ = plt.colorbar(m_curr,fraction=0.035, pad=0.05,label='Induced current integral [# electrons]')

```

### Electronics response and digitization
Here we simulate the electronics response (the self-triggering cycle) and the signal digitization.

```python
time_ticks = np.linspace(0,len(unique_eventIDs)*consts.time_interval[1]*2,d_pixels_signals.shape[1]+1)
integral_list = np.zeros((d_pixels_signals.shape[0], fee.MAX_ADC_VALUES))
adc_ticks_list = np.zeros((d_pixels_signals.shape[0], fee.MAX_ADC_VALUES))
TPB = 32
BPG = ceil(d_pixels_signals.shape[0] / TPB)
rng_states = cuda.random.create_xoroshiro128p_states(TPB * BPG, seed=0)

fee.get_adc_values[BPG,TPB](d_pixels_signals,
                            time_ticks,
                            integral_list,
                            adc_ticks_list,
                            0,
                            rng_states)
adc_list = fee.digitize(integral_list)
```

### 2D event display with induced current and ADC counts

```python
fig,ax = plt.subplots(1,2,figsize=(14,6.5))

for ip, p in enumerate(unique_pix):
    x_rect, y_rect = detsim.get_pixel_coordinates(p)
    pixel_plane = int(p[0] // consts.n_pixels[0])
    c = currents[ip]
    if c >=1:
        rect = plt.Rectangle((x_rect-consts.pixel_size[0]/2, y_rect-consts.pixel_size[0]/2),
                             consts.pixel_size[0], consts.pixel_size[1],
                             linewidth=0.2, fc=m_curr.to_rgba(c),
                             edgecolor='grey')
        ax[0].add_patch(rect)

    a = adc_list[ip][adc_list[ip]>fee.digitize(0)]
    if len(a):
        rect = plt.Rectangle((x_rect-consts.pixel_size[0]/2, y_rect-consts.pixel_size[0]/2),
                             consts.pixel_size[0], consts.pixel_size[1],
                             linewidth=0.2, fc=m.to_rgba(np.sum(a)),
                             edgecolor='grey')
        ax[1].add_patch(rect)

for it,t in enumerate(selected_tracks):
    ax[0].plot((t["x_start"], t["x_end"]),
            (t["y_start"], t["y_end"]),
            c='r',
            lw=1.25,
            ls=':',
            alpha=1,
            zorder=10)
    ax[1].plot((t["x_start"], t["x_end"]),
            (t["y_start"], t["y_end"]),
            c='r',
            lw=1.25,
            ls=':',
            alpha=1,
            zorder=10)
    ax[0].scatter((t["x_start"], t["x_end"]),
                  (t["y_start"], t["y_end"]),
                  c='r', s=1, zorder=99999)
    ax[1].scatter((t["x_start"], t["x_end"]),
                  (t["y_start"], t["y_end"]),
                  c='r', s=1, zorder=99999)

ax[0].set_aspect("equal")
ax[1].set_aspect("equal")
ax[0].set_xlabel("x [cm]")
ax[1].set_xlabel("x [cm]")
ax[0].set_ylabel("y [cm]")

divider0 = make_axes_locatable(ax[1])
cax0 = divider0.append_axes("right", size="7%", pad=0.07)
fig.colorbar(m, ax=ax[1], cax=cax0, label='ADC counts sum')

divider1 = make_axes_locatable(ax[0])
cax1 = divider1.append_axes("right", size="7%", pad=0.07)
fig.colorbar(m_curr, ax=ax[0], cax=cax1, label='Induced current integral [# electrons]')

plt.subplots_adjust(hspace=0.5)
fig.savefig("currentadc.pdf")
```

### Export result
As a last step we backtrack the ADC counts to the Geant4 tracks and we export the result in a HDF5 file.

```python
track_pixel_map = np.full((unique_pix.shape[0],5),-1)
backtracked_id = np.full((adc_list.shape[0], adc_list.shape[1], track_pixel_map.shape[1]), -1)

detsim.get_track_pixel_map(track_pixel_map, unique_pix, neighboring_pixels)
detsim.backtrack_adcs(selected_tracks, adc_list, adc_ticks_list, track_pixel_map, event_id_map, backtracked_id)
```

```python
importlib.reload(fee)
pc = fee.export_to_hdf5(adc_list, adc_ticks_list, unique_pix, backtracked_id, "test.h5")
```

    Writing to HDF5...: 100%|██████████| 1542/1542 [00:00<00:00, 34690.44it/s]


Finally, we write the code file itself.  Most of the text below will appear literally in the file.  Statements in single curly braces will be substituted by the `format` statement at the end of the string; double curly braces will just appear as literal braces in the output.

In the following, the ODE system directly integrates the quaternion rotors describing the spin directions and orbital angular velocity vector.  This allows us to reduce the number of variables in the system and automatically satisfy certain constraints, which should lead to better numerical behavior.

Note that in this formulation, `Phi` is a completely unnecessary variable.  It doesn't need to be computed because it is implict in the definition of the frame.  It is calculated here simply because it is easy to do, and might be handy to have around.  Of course, it could be derived from knowledge of the frame alone.

```python
# Always run this first
# NOTE: Do not define new basic variables in this notebook;
#       define them in Variables_Q.ipynb.  Use this notebook
#       to define new expressions built from those variables.

from __future__ import division # This needs to be here, even though it's in Variables.py
import sys
sys.path.insert(0, '..') # Look for modules in directory above this one
execfile('../Utilities/ExecNotebook.ipy')
UseQuaternions = True
from sympy import N
from sympy import Rational as frac # Rename for similarity to latex
execnotebook('../PNTerms/Variables_Q.ipynb')
from Utilities import CodeOutput
from textwrap import indent
```

    /home/dongze/.local/lib/python3.8/site-packages/IPython/nbformat.py:12: ShimWarning: The `IPython.nbformat` package has been deprecated since IPython 4.0. You should import from nbformat instead.
      warn("The `IPython.nbformat` package has been deprecated since IPython 4.0. "
    /home/dongze/.local/lib/python3.8/site-packages/nbformat/current.py:15: UserWarning: nbformat.current is deprecated.

    - use nbformat for read/write/validate public API
    - use nbformat.vX directly to composing notebooks of a particular version

      warnings.warn("""nbformat.current is deprecated.

```python
HighestOrder=frac(8,2)
with open('PNEvolution.py', 'w') as f :
    f.write("""# File produced automatically by PNCodeGen.ipynb
from scipy.integrate import solve_ivp
import numpy as np
from numpy import dot, cross, log, sqrt, pi
from numpy import euler_gamma as EulerGamma
from numba import jit, njit, float64, boolean
from numba.experimental import jitclass
from scipy.interpolate import InterpolatedUnivariateSpline as Spline
from scipy.special import zeta
import quaternionic

qmul = njit(quaternionic.algebra.multiply)
qexp=njit(quaternionic.algebra.exp)
qconj=njit(quaternionic.algebra.conj)
qinverse=njit(quaternionic.algebra.reciprocal)

@njit(cache=True)
def mul(A,B):
    C=np.empty(4)
    qmul(A,B,C)
    return C

@njit(cache=True)
def exp(A):
    B=np.empty(4)
    qexp(A,B)
    return B

@njit(cache=True)
def conjugate(A):
    B=np.empty(4)
    qconj(A,B)
    return B

@njit(cache=True)
def inverse(A):
    B=np.empty(4)
    qinverse(A,B)
    return B

@njit(cache=True)
def FrameFromAngularVelocity_2D_Integrand(rfrak_x, rfrak_y, Omega):
    rfrakMag = np.sqrt(rfrak_x*rfrak_x+rfrak_y*rfrak_y)
    rfrakDot_x = Omega[0]/2.0
    rfrakDot_y = Omega[1]/2.0
    if np.abs(np.sin(rfrakMag)) > 1e-12 and np.abs(np.cos(rfrakMag)) > 1e-12:
        omega_v = (Omega[0]*(-rfrak_y/rfrakMag)+Omega[1]*(rfrak_x/rfrakMag))*np.tan(rfrakMag)-Omega[2]
        Omega[0] += -omega_v*np.sin(2*rfrakMag)*(-rfrak_y/rfrakMag)
        Omega[1] += -omega_v*np.sin(2*rfrakMag)*(rfrak_x/rfrakMag)
        Omega[2] +=  omega_v*np.cos(2*rfrakMag)
        dotTerm = (rfrak_x*Omega[0]+rfrak_y*Omega[1])/(rfrakMag*rfrakMag)
        cotTerm = rfrakMag/(2*np.tan(rfrakMag))
        rfrakDot_x = (Omega[0] - rfrak_x*dotTerm)*cotTerm + rfrak_x*dotTerm/2. - 0.5*Omega[2]*rfrak_y
        rfrakDot_y = (Omega[1] - rfrak_y*dotTerm)*cotTerm + rfrak_y*dotTerm/2. + 0.5*Omega[2]*rfrak_x
    return rfrakDot_x, rfrakDot_y

@njit(cache=True)
def FrameFromAngularVelocityIntegrand(rfrak, Omega):
    rfrakMag = np.sqrt(rfrak[0] * rfrak[0] + rfrak[1] * rfrak[1] + rfrak[2] * rfrak[2])
    OmegaMag = np.sqrt(Omega[0] * Omega[0] + Omega[1] * Omega[1] + Omega[2] * Omega[2])
    # If the matrix is really close to the identity, return
    if rfrakMag < 1e-12*OmegaMag:
        return np.array([Omega[0] / 2.0, Omega[1] / 2.0, Omega[2] / 2.0])
    # If the matrix is really close to singular, it's equivalent to the identity, so return
    if np.abs(np.sin(rfrakMag)) < 1e-12:
        return np.array([Omega[0] / 2.0, Omega[1] / 2.0, Omega[2] / 2.0])
    OmegaOver2 = np.array([Omega[0] / 2.0, Omega[1] / 2.0, Omega[2] / 2.0])
    rfrakHat = np.array([rfrak[0] / rfrakMag, rfrak[1] / rfrakMag, rfrak[2] / rfrakMag])
    return ((OmegaOver2 - rfrakHat * np.dot(rfrakHat, OmegaOver2)) * (rfrakMag / np.tan(rfrakMag))
        + rfrakHat * np.dot(rfrakHat, OmegaOver2) + np.cross(OmegaOver2, rfrak))
""")
    PNOrbitalEvolutionOrder=HighestOrder
    PNOrbitalEvolutionOrderString=str(N(PNOrbitalEvolutionOrder, 2)).replace('.','p')
    %cd -q ../PNTerms
    execnotebook('OrbitalEvolution.ipynb')
    ExpressionsForMemberFunctions = CodeOutput.CodeConstructor(PNVariables, PrecessionVelocities)
    execnotebook('AngularMomentum.ipynb')
    %cd -q -
    PrecessionVelocities.AddDerivedVariable('OrbitalAngularMomentum',
        AngularMomentumExpression(PNOrder=PNOrbitalEvolutionOrder),
        datatype=ellHat.datatype)
    CodeConstructor.AddDependencies(PrecessionVelocities)
    f.write("""
ConsSpec=[{ConsSpec},('EvolveSpin1',boolean),('EvolveSpin2',boolean)]
@jitclass(ConsSpec)
class Cons:
    def __init__(self,{ConsArgu},EvolveSpin1,EvolveSpin2):
{ConsInit}
        self.EvolveSpin1=EvolveSpin1
        self.EvolveSpin2=EvolveSpin2

VarsSpec=[{VarsSpec}]
@jitclass(VarsSpec)
class Vars:
    def __init__(self,{VarsArgu}):
{VarsInit}

@njit(cache=True)
def Initialization(Cons, {InputArguments}):
{Initializations}
    Cons.EvolveSpin1=np.linalg.norm(mul(Cons.S_chi1,conjugate(Cons.S_chi1)))>1e-8
    Cons.EvolveSpin2=np.linalg.norm(mul(Cons.S_chi2,conjugate(Cons.S_chi2)))>1e-8
""".format(ConsSpec=CodeConstructor.ConsSpec(),
           ConsArgu=CodeConstructor.ConsArgu(),
           ConsInit=CodeConstructor.ConsInit(),
           VarsSpec=CodeConstructor.VarsSpec(),
           VarsArgu=CodeConstructor.VarsArgu(),
           VarsInit=CodeConstructor.VarsInit(),
           InputArguments=CodeConstructor.CppInputArguments(11),
           Initializations=CodeConstructor.CppInitializations(4)))
    lenCons=0
    lenVars=0
    for atom in CodeConstructor.Atoms:
        if atom.constant:
            lenCons+=1
        elif not atom.constant:
            lenVars+=1
    Constructor=CodeConstructor

    for PNOrbitalEvolutionOrder in [frac(n,2) for n in range(0,2*int(HighestOrder)+1)]:
        print("Working on {0} PN...".format(PNOrbitalEvolutionOrder))
        PNOrbitalEvolutionOrderString = str(N(PNOrbitalEvolutionOrder, 2)).replace('.','p')
        %cd -q ../PNTerms
        execnotebook('OrbitalEvolution.ipynb')
        ExpressionsForMemberFunctions = CodeOutput.CodeConstructor(PNVariables, PrecessionVelocities)
        execnotebook('AngularMomentum.ipynb')
        %cd -q -
        PrecessionVelocities.AddDerivedVariable('OrbitalAngularMomentum',
            AngularMomentumExpression(PNOrder=PNOrbitalEvolutionOrder),
            datatype=ellHat.datatype)
        CodeConstructor.AddDependencies(PrecessionVelocities)
        # Start the class, write the declarations, initializations, etc.
        f.write("""
@njit(cache=True)
def Recalculate_{PNOrbitalEvolutionOrderString}(Cons,Vars,y):
    Vars.v = np.array([y[0]])
    Vars.rfrak_chi1 = np.array([y[1],y[2]])
    Vars.rfrak_chi2 = np.array([y[3],y[4]])
    Vars.rfrak_frame = np.array([y[5],y[6],y[7]])
{Evaluations}

{MemberFunctions}
""".format(InputArguments=CodeConstructor.CppInputArguments(11),
           PNOrbitalEvolutionOrderString=PNOrbitalEvolutionOrderString,
           Initializations=CodeConstructor.CppInitializations(4),
           Declarations=CodeConstructor.CppDeclarations(2),
           Evaluations=CodeConstructor.CppEvaluations(4),
           MemberFunctions=CodeOutput.CodeConstructor(PNVariables, PrecessionVelocities).CppExpressionsAsFunctions(2,PNOrbitalEvolutionOrderString)))
        for n,Expressions in zip([1], [T1Expressions]):
            f.write("""
@njit(cache=True)
def TaylorT{n}_{PNOrbitalEvolutionOrderString}(Cons,Vars):
{Computations}
    dydt=np.zeros(8)
    [dydt[5],dydt[6],dydt[7]] = FrameFromAngularVelocityIntegrand(Vars.rfrak_frame, OmegaVec_{PNOrbitalEvolutionOrderString}(Cons,Vars)[1:])
    dydt[0] = dvdt_T{n}[0]
    if(Cons.EvolveSpin1):
        dydt[1], dydt[2]=FrameFromAngularVelocity_2D_Integrand(Vars.rfrak_chi1[0], Vars.rfrak_chi1[1],(mul(mul(inverse(Cons.S_chi1),OmegaVec_chiVec_1_{PNOrbitalEvolutionOrderString}(Cons,Vars)),Cons.S_chi1))[1:])
    else:
        dydt[1], dydt[2] = 0.0, 0.0
    if(Cons.EvolveSpin2):
        dydt[3], dydt[4]=FrameFromAngularVelocity_2D_Integrand(Vars.rfrak_chi2[0], Vars.rfrak_chi2[1],(mul(mul(inverse(Cons.S_chi2),OmegaVec_chiVec_2_{PNOrbitalEvolutionOrderString}(Cons,Vars)),Cons.S_chi2))[1:])
    else:
        dydt[3], dydt[4] = 0.0, 0.0
    return dydt
""".format(n=n,
           PNOrbitalEvolutionOrderString=PNOrbitalEvolutionOrderString,
           Computations=CodeConstructor.CppEvaluateExpressions()))
        for n,Expressions in zip([4], [T4Expressions]):
            f.write("""
@njit(cache=True)
def TaylorT{n}_{PNOrbitalEvolutionOrderString}(Cons,Vars):
{Computations}
    dydt=np.zeros(8)
    [dydt[5],dydt[6],dydt[7]] = FrameFromAngularVelocityIntegrand(Vars.rfrak_frame, OmegaVec_{PNOrbitalEvolutionOrderString}(Cons,Vars)[1:])
    dydt[0] = dvdt_T{n}[0]
    if(Cons.EvolveSpin1):
        dydt[1], dydt[2]=FrameFromAngularVelocity_2D_Integrand(Vars.rfrak_chi1[0], Vars.rfrak_chi1[1],(mul(mul(inverse(Cons.S_chi1),OmegaVec_chiVec_1_{PNOrbitalEvolutionOrderString}(Cons,Vars)),Cons.S_chi1))[1:])
    else:
        dydt[1], dydt[2] = 0.0, 0.0
    if(Cons.EvolveSpin2):
        dydt[3], dydt[4]=FrameFromAngularVelocity_2D_Integrand(Vars.rfrak_chi2[0], Vars.rfrak_chi2[1],(mul(mul(inverse(Cons.S_chi2),OmegaVec_chiVec_2_{PNOrbitalEvolutionOrderString}(Cons,Vars)),Cons.S_chi2))[1:])
    else:
        dydt[3], dydt[4] = 0.0, 0.0
    return dydt
""".format(n=n,
           PNOrbitalEvolutionOrderString=PNOrbitalEvolutionOrderString,
           Computations=CodeOutput.CodeConstructor(PNVariables, Expressions).CppEvaluateT45(NumTerms4, DenTerms4, Constructor)))
        for n,Expressions in zip([5], [T5Expressions]):
            f.write("""
@njit(cache=True)
def TaylorT{n}_{PNOrbitalEvolutionOrderString}(Cons,Vars):
{Computations}
    dydt=np.zeros(8)
    [dydt[5],dydt[6],dydt[7]] = FrameFromAngularVelocityIntegrand(Vars.rfrak_frame, OmegaVec_{PNOrbitalEvolutionOrderString}(Cons,Vars)[1:])
    dydt[0] = dvdt_T{n}[0]
    if(Cons.EvolveSpin1):
        dydt[1], dydt[2]=FrameFromAngularVelocity_2D_Integrand(Vars.rfrak_chi1[0], Vars.rfrak_chi1[1],(mul(mul(inverse(Cons.S_chi1),OmegaVec_chiVec_1_{PNOrbitalEvolutionOrderString}(Cons,Vars)),Cons.S_chi1))[1:])
    else:
        dydt[1], dydt[2] = 0.0, 0.0
    if(Cons.EvolveSpin2):
        dydt[3], dydt[4]=FrameFromAngularVelocity_2D_Integrand(Vars.rfrak_chi2[0], Vars.rfrak_chi2[1],(mul(mul(inverse(Cons.S_chi2),OmegaVec_chiVec_2_{PNOrbitalEvolutionOrderString}(Cons,Vars)),Cons.S_chi2))[1:])
    else:
        dydt[3], dydt[4] = 0.0, 0.0
    return dydt
""".format(n=n,
           PNOrbitalEvolutionOrderString=PNOrbitalEvolutionOrderString,
           Computations=CodeOutput.CodeConstructor(PNVariables, Expressions).CppEvaluateT45(NumTerms5, DenTerms5, Constructor)))

        # Write the external interfaces to the ODE stepper
    Ty=''
    for n,Expressions in zip([1,4,5], [T1Expressions,T4Expressions,T5Expressions]):
        if len(Ty)!=0:
            Ty+=','
        Ty+='\n'+',\n'.join(['            {0}:TaylorT{1}_{2}'.format(i*10+n,n,str(N(i/2, 2)).replace('.','p')) for i in range(1+2*int(HighestOrder))])
    f.write("""
class PNEv:
    def Integrand(t,y):
        PNEv.Recalculate.get(2*PNEv.PNEvolutionOrder)(PNEv.Cons,PNEv.Vars,y)
        dydt=PNEv.Taylor.get(PNEv.TaylorTn+20*PNEv.PNEvolutionOrder)(PNEv.Cons,PNEv.Vars)
        if PNEv.Vars.v>=1.0 and PNEv.NotForward:
            print("Beyond domain of PN validity, this is a good way to terminate.")
            PNEv.terminal1=False
        if dydt[0]<1.0e-12 and PNEv.NotForward:
            print("v is decreasing, which is not an uncommon way to stop.")
            PNEv.terminal2=False
        return dydt

    def Evolution(xHat_i, yHat_i, zHat_i, M1_i, M2_i, v_i, S_chi1_i, S_chi2_i, rfrak_frame,
        t_PNStart=False, t_PNEnd=False, PNEvolutionOrder=3.5, TaylorTn=1, StepsPerOrbit=32, ForwardInTime=True, tol=1e-8, MinStep=1e-7):
        # Initialization of constants
        PNEv.terminal1=True
        PNEv.terminal2=True
        PNEv.NotForward=True
        PNEv.PNEvolutionOrder=PNEvolutionOrder
        PNEv.TaylorTn=TaylorTn
        PNEv.Recalculate={{{ReCal}}}
        PNEv.Taylor={{{Ty}}}
        z=np.array([0.0])
        PNEv.Cons=Cons({ConsInput},True,True)
        PNEv.Vars=Vars({VarsInput})
        Initialization(PNEv.Cons,xHat_i, yHat_i, zHat_i, M1_i, M2_i, v_i, S_chi1_i, S_chi2_i, rfrak_frame)

        def terminate(t,y):
            return 1.0*PNEv.terminal1*PNEv.terminal2
        terminate.terminal=True
        TMerger=5.0/(256.0*PNEv.Cons.nu*v_i**8)
        TEnd=TMerger
        if t_PNEnd:
            TEnd=t_PNEnd
        time=[0.0]
        while time[-1]<TEnd and 2*PNEv.Cons.M*(256*PNEv.Cons.nu*(TMerger-time[-1])/5)**(3/8)/StepsPerOrbit>MinStep:
            time.append(time[-1]+(2*PNEv.Cons.M*(256*PNEv.Cons.nu*(TMerger-time[-1])/5)**(3/8)/StepsPerOrbit)[0])
        time=np.delete(time, -1)

        # Integrate
        yy=solve_ivp(PNEv.Integrand, [time[0],time[-1]], [v_i,0.0,
            0.0,0.0,0.0,rfrak_frame[0],rfrak_frame[1],rfrak_frame[2]], method='DOP853',
            t_eval=time, dense_output=True, events=terminate, rtol=tol, atol=tol)
        if ForwardInTime:
            PNEv.NotForward=False
            time=[0.0]
            TStart=-3*TMerger
            if t_PNStart:
                TStart=t_PNStart
            while time[-1]>TStart:
                time.append(time[-1]-(2*PNEv.Cons.M*(256*PNEv.Cons.nu*(TMerger-time[-1])/5)**(3/8)/StepsPerOrbit)[0])
            yyForward=solve_ivp(PNEv.Integrand, [time[0],time[-1]], [v_i,0.0,
                0.0,0.0,0.0,rfrak_frame[0],rfrak_frame[1],rfrak_frame[2]], method='DOP853',
                t_eval=time, dense_output=True, rtol=tol, atol=tol)
            yy.t=np.append(yyForward.t[1:][::-1],yy.t)
            data=np.empty((8,len(yy.t)))
            for i in range(8):
                data[i]=np.append(yyForward.y[i][1:][::-1],yy.y[i])
            yy.y=data

        return yy
""".format(ConsInput=','.join(['z' for i in range(lenCons)]),
           VarsInput=','.join(['z' for i in range(lenVars)]),
           InputArguments=CodeConstructor.CppInputArguments(11),
           ReCal=',\n'.join(['            {0}:Recalculate_{1}'.format(i,str(N(i/2, 2)).replace('.','p')) for i in range(1+2*int(HighestOrder))]),
           Ty=Ty,
           PNOrbitalEvolutionOrderString=PNOrbitalEvolutionOrderString))

# Calculate waveform modes
ellMin=2
ellMax=8
def Express(atom,CodeConstructor):
    A=str(atom)
    for i in CodeConstructor.Atoms:
        try:
            if i.constant:
                j=0
                while j!=-1 and j+len(str(i))<=len(A):
                    j=A.find(str(i),j)
                    if j!=-1:
                        if j+len(str(i)) ==len(A) or not (A[j+len(str(i))].isalnum() or A[j+len(str(i))]=='_'):
                            B=A[0:j]+'Cons.{0}'.format(i)+A[j+len(str(i)):]
                            A=B
                            j=j+5+len(str(i))
                        else:
                            j=j+1
            elif not i.constant:
                j=0
                while j!=-1 and j+len(str(i))<=len(A):
                    j=A.find(str(i),j)
                    if j!=-1:
                        if j+len(str(i)) ==len(A) or not (A[j+len(str(i))].isalnum() or A[j+len(str(i))]=='_'):
                            if j==0:
                                B=A[0:j]+'Vars.{0}'.format(i)+A[j+len(str(i)):]
                                A=B
                                j=j+5+len(str(i))
                            elif not (A[j-1].isalnum() or A[j-1]=='_'):
                                B=A[0:j]+'Vars.{0}'.format(i)+A[j+len(str(i)):]
                                A=B
                                j=j+5+len(str(i))
                            else:
                                j=j+1
                        else:
                            j=j+1
        except:
            pass
    return A

InputArgumentsEvolution=CodeConstructor.CppInputArguments(11)
execnotebook('../PNTerms/WaveformModes.ipynb')
WaveformModeTerms = [WaveformModes_NoSpin, WaveformModes_Spin_Symm, WaveformModes_Spin_Asymm]
for Term in WaveformModeTerms:
    PNVariables.update(Term)
# For some reason I have to both overwrite these variables and pop them...
PNVariables.AddVariable('nHat', constant=True, fundamental=True,
                 substitution_atoms=[], substitution='Quaternions::xHat', datatype='Quaternions.Quaternion');
PNVariables.AddVariable('lambdaHat', constant=True, fundamental=True,
                substitution_atoms=[], substitution='Quaternions::yHat', datatype='Quaternions.Quaternion');
PNVariables.AddVariable('ellHat', constant=True, fundamental=True,
                substitution_atoms=[], substitution='Quaternions::zHat', datatype='Quaternions.Quaternion');
PNVariables.pop(nHat);
PNVariables.pop(lambdaHat);
PNVariables.pop(ellHat);

PNVariables.AddDerivedVariable('chiVec1', substitution_atoms=[chiVec1], substitution='mul(mul(mul(S_chi1,R_S1),zHat),mul(conjugate(R_S1),conjugate(S_chi1)))');
PNVariables.AddDerivedVariable('chiVec2', substitution_atoms=[chiVec2], substitution='mul(mul(mul(S_chi2,R_S2),zHat),mul(conjugate(R_S2),conjugate(S_chi2)))');
PNVariables.AddDerivedVariable('chi1_n', substitution_atoms=[chiVec1,nHat], substitution='np.array([chiVec1[1]])');
PNVariables.AddDerivedVariable('chi1_lambda', substitution_atoms=[chiVec1,lambdaHat], substitution='np.array([chiVec1[2]])');
PNVariables.AddDerivedVariable('chi1_ell', substitution_atoms=[chiVec1,ellHat], substitution='np.array([chiVec1[3]])');
PNVariables.AddDerivedVariable('chi2_n', substitution_atoms=[chiVec2,nHat], substitution='np.array([chiVec2[1]])');
PNVariables.AddDerivedVariable('chi2_lambda', substitution_atoms=[chiVec2,lambdaHat], substitution='np.array([chiVec2[2]])');
PNVariables.AddDerivedVariable('chi2_ell', substitution_atoms=[chiVec2,ellHat], substitution='np.array([chiVec2[3]])');

with open('PNWaveformModes.py', 'w') as f :
    f.write("""# File produced automatically by PNCodeGen.ipynb
import numpy as np
from numpy import dot, log, sqrt, pi
from numpy import euler_gamma as EulerGamma
from numba import jit, njit, float64, complex128
from numba.experimental import jitclass
import quaternionic

qmul = njit(quaternionic.algebra.multiply)
qexp=njit(quaternionic.algebra.exp)
qconj=njit(quaternionic.algebra.conj)
I=1j

@njit(cache=True)
def mul(A,B):
    C=np.empty(4)
    qmul(A,B,C)
    return C

@njit(cache=True)
def exp(A):
    B=np.empty(4)
    qexp(A,B)
    return B

@njit(cache=True)
def conjugate(A):
    B=np.empty(4)
    qconj(A,B)
    return B
""")

    PNWaveformModeOrder=HighestOrder
    PNWaveformModeOrderString = str(N(PNWaveformModeOrder,2)).replace('.','p')
    ModeExpressions0 = PNCollection()
    LM = [[ell,m] for ell in range(ellMin,ellMax+1) for m in range(-ell,ell+1)]
    for ell in range(ellMin, ellMax+1):
        for m in range(0, ell+1):
            Symm = (SymmetricWaveformModes([ell,m],PNOrder=PNWaveformModeOrder)).subs(log(v), logv)
            Asymm = (AsymmetricWaveformModes([ell,m],PNOrder=PNWaveformModeOrder)).subs(log(v), logv)
            ModeExpressions0.AddDerivedVariable('rhOverM_{0}_{1}_Symm'.format(ell,m),
                                                Symm, datatype='double')
            ModeExpressions0.AddDerivedVariable('rhOverM_{0}_{1}_Asymm'.format(ell,m),
                                               Asymm, datatype='double')
    CodeConstructor0 = CodeOutput.CodeConstructor(PNVariables, ModeExpressions0)
    f.write("""
ConsSpec=[{ConsSpec}]
@jitclass(ConsSpec)
class Constants:
    def __init__(self,{ConsArgu}):
{ConsInit}

VarsSpec=[{VarsSpec}]
@jitclass(VarsSpec)
class Variables:
    def __init__(self,{VarsArgu}):
{VarsInit}

@njit(cache=True)
def Initialization(Cons, {InputArguments}):
{Initializations}
""".format(ConsSpec=CodeConstructor0.ConsSpec(),
           ConsArgu=CodeConstructor0.ConsArgu(),
           ConsInit=CodeConstructor0.ConsInit(),
           VarsSpec=CodeConstructor0.VarsSpec(),
           VarsArgu=CodeConstructor0.VarsArgu(),
           VarsInit=CodeConstructor0.VarsInit(),
           InputArguments=InputArgumentsEvolution,
           Initializations=CodeConstructor0.CppInitializationModes(4)))
    lenConsReal=0
    lenVarsReal=0
    lenConsComplex=0
    lenVarsComplex=0
    for atom in CodeConstructor0.Atoms:
        if atom.constant and str(atom).find('hHat')==-1:
            lenConsReal+=1
        elif not atom.constant and str(atom).find('hHat')==-1:
            lenVarsReal+=1
        elif atom.constant and str(atom).find('hHat')!=-1:
            lenConsComplex+=1
        elif not atom.constant and str(atom).find('hHat')!=-1:
            lenVarsComplex+=1

    for PNWaveformModeOrder in [frac(n,2) for n in range(0,2*int(HighestOrder)+1)]:
        print("Working on {0} PN...".format(PNWaveformModeOrder))
        PNWaveformModeOrderString = str(N(PNWaveformModeOrder,2)).replace('.','p')
        ModeExpressions = PNCollection()
        LM = [[ell,m] for ell in range(ellMin,ellMax+1) for m in range(-ell,ell+1)]
        lenLM=len(LM)
        Evaluations = []
        for ell in range(ellMin, ellMax+1):
            for m in range(0, ell+1):
                Symm = (SymmetricWaveformModes([ell,m],PNOrder=PNWaveformModeOrder)).subs(log(v), logv)
                Asymm = (AsymmetricWaveformModes([ell,m],PNOrder=PNWaveformModeOrder)).subs(log(v), logv)
                ModeExpressions.AddDerivedVariable('rhOverM_{0}_{1}_Symm'.format(ell,m),
                                                   Symm, datatype='double')
                ModeExpressions.AddDerivedVariable('rhOverM_{0}_{1}_Asymm'.format(ell,m),
                                                   Asymm, datatype='double')
                code1 = "    Symm = {0}".format(Express(sympy.pycode(Symm),CodeConstructor0))
                code2 = "    Asymm = {0}".format(Express(sympy.pycode(Asymm),CodeConstructor0))
                code3 = "    modes[{2}] = (Symm + Asymm)[0]".format(ell, m, LM.index([ell,m]))
                code4 = "    modes[{2}] = ({3}np.conjugate(Symm - Asymm))[0]".format(ell, -m, LM.index([ell,-m]),
                                                                               ('' if ((ell%2)==0) else '-'))
                try:
                    if int(sympy.pycode(Symm))==0 and int(sympy.pycode(Asymm))==0:
                        pass
                except:
                    Evaluations.append("    # (ell, m) = ({0}, +/- {1})".format(ell, m))
                    Evaluations.append(code1)
                    Evaluations.append(code2)
                    Evaluations.append(code3)
                    if m!=0:
                        Evaluations.append(code4)
        CodeConstructor = CodeOutput.CodeConstructor(PNVariables, ModeExpressions)
        #[print(atom) for atom in CodeConstructor.Atoms if atom.constant]

        # Start the class, write the declarations, initializations, etc.
        f.write("""
@njit(cache=True)
def Recalculate_{PNWaveformModeOrderString}(Cons,Vars,y):
    Vars.v = np.array([y[0]])
    Vars.rfrak_chi1 = np.array([y[1],y[2]])
    Vars.rfrak_chi2 = np.array([y[3],y[4]])
    Vars.rfrak_frame = np.array([y[5],y[6],y[7]])
{Evaluations}

@njit(cache=True)
def Modes_{PNWaveformModeOrderString}(modes,Cons,Vars):
{Computations}
""".format(PNWaveformModeOrderString=PNWaveformModeOrderString,
           Evaluations=CodeConstructor.CppEvaluationModes(4),
           Computations='\n'.join(Evaluations)))

    f.write("""
def Modes(xHat_i, yHat_i, zHat_i, M1_i, M2_i, v_i, S_chi1_i, S_chi2_i, rfrak_frame, y, PNWaveformModeOrder=3.5) :
    Recalculate={{{ReCal}}}
    WaveformModes={{{ModeFunction}}}
    ModeData=np.zeros((len(y[0]),{lenLM}), dtype=complex)
    zz=np.array([0.0])
    z=np.array([0.0*1j])
    Cons=Constants({ConsInput})
    Vars=Variables({VarsInput})
    Initialization(Cons, xHat_i, yHat_i, zHat_i, M1_i, M2_i, v_i, S_chi1_i, S_chi2_i, rfrak_frame)
    for i in range(len(y[0])):
        Recalculate.get(2*PNWaveformModeOrder)(Cons,Vars,y[:,i])
        WaveformModes.get(2*PNWaveformModeOrder)(ModeData[i,:],Cons,Vars)
    return ModeData, [{ellMin},{ellMax}]
""".format(ReCal=',\n'.join(['        {0}:Recalculate_{1}'.format(i,str(N(i/2, 2)).replace('.','p')) for i in range(1+2*int(HighestOrder))]),
           ModeFunction=',\n'.join(['        {0}:Modes_{1}'.format(i,str(N(i/2, 2)).replace('.','p')) for i in range(1+2*int(HighestOrder))]),
           ConsInput=','.join(['zz' for i in range(lenConsReal)])+','+','.join(['z' for i in range(lenConsComplex)]),
           VarsInput=','.join(['zz' for i in range(lenVarsReal)])+','+','.join(['z' for i in range(lenVarsComplex)]),
           PNWaveformModeOrderString=PNWaveformModeOrderString,
           InputArgumentsEvolution=InputArgumentsEvolution,
           lenLM=lenLM,
           ellMin=ellMin,
           ellMax=ellMax))

print("All done")
```

    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 0 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 1/2 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 1 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 3/2 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 2 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 5/2 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 3 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 7/2 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 4 PN...
    WARNING: Absorption term in EnergyAbsorption.ipynb temporarily disabled.
    Working on 0 PN...
    Working on 1/2 PN...
    Working on 1 PN...
    Working on 3/2 PN...
    Working on 2 PN...
    Working on 5/2 PN...
    Working on 3 PN...
    Working on 7/2 PN...
    Working on 4 PN...
    All done

```python

```

```python

```

```python

```

```python

```

```python

```


# Computationally Visualizing Crystals Pt. 2
 Christina C. Lee, github: albi3ro

Category: General

Prerequisites: Crystal Shapes

Time to one-up the Bravais lattice from Part 1.  Many beautiful lattices don't adhere to the "every site the same" policy.  They still repeat, but just take a little bit longer to get around to doing so.

Take the Kagome Lattice, named for the Japanese style of basket weaving, below,

 Wikimedia commons

If we look at the stars at the center of triangles, we can recognize a point triangular Bravais lattice.  Now each of those stars stands for a grouping of three sites in a Unit Cell.  According to Chem Wiki, a unit cell is:
>A unit cell is the most basic and least volume consuming repeating structure of any solid. It is used to visually simplify the crystalline patterns solids arrange themselves in.

Now I chose these triangles to be be the unit cells above and in my computational representation below, but can you think of any other ways to represent the unit cell?

Turns out, there isn't a unique way.  We can go further and define the Wigner-Seitz unit cell, which uses the Bravais translations to pick out just ONE of the various possible definitions.

In my line of work though, we often use either the easiest to write down, or the one that has the symmetries we want.

### Introducing Some Lattice Options

You saw Kagome above.

The options I've put in now are:
* 2D
  * honeycomb
  * kagome
  * shuriken aka しゅりけん, 手裏剣, or Square-Kagome
* 3D
  * diamond
  * pyrochlore

The ones implemented here, except for diamond, are frustrated lattices that I work with in my research. Honeycomb is well known in condensed matter physics for being the structure of graphene, an extremely important material right now, though I work with it in terms of the Kitaev spin model.  Kagome and Pyrochlore are also popular models within my community.  The shuriken lattice is more uncommon, but gaining ground in the frustration community.

####  Japanese Shurikens- a type of ninja fighting star

 By kaex0r (http://www.flickr.com/photos/kaex0r414/191765028/) [CC BY 2.0 (http://creativecommons.org/licenses/by/2.0)], via Wikimedia Commons

```julia
# importing our packages
using Plots
gr()
```

    Plots.GRBackend()

```julia
lattice="shuriken";

Nx=3;
Ny=3;
Nz=1;
```

```julia
# A cell to just evaluate
# This one sets the unit vectors (a,b,c) for the different unit cells
# Can you guess what a lattice will look like by looking at the vectors?
if(lattice=="honeycomb") #also the graphite lattice
    d=2;
    Ncell=2;
    unit=[[0 0 0]
        [cos(π/6) sin(π/6) 0]];
    a=[sqrt(3),0,0];
    b=[sqrt(3)/2,3/2,0];
    c=[0,0,1];
elseif(lattice=="kagome" || lattice=="かごめ")
    d=2;
    Ncell=3;
    unit=[[0 0 0]
          [1 0 0]
        [.5 sqrt(3)/2 0]];
    a=[2,0,0];       #Look familiar? Checkout pt from Pt. 1
    b=[1, sqrt(3), 0];
    c=[0,0,1];
elseif(lattice=="shuriken"|| lattice=="しゅりけん" || lattice=="手裏剣")
    d=2;
    Ncell=6;
    unit=[[0 0 0]
          [.5 0 0]
          [0 .5 0]
          [.5 .5 0]
        [.5+.25*sqrt(3) .25 0]
        [.25 .5+.25*sqrt(3) 0]];
    a=[.5+.5*sqrt(3),0,0];
    b=[0,.5+.5*sqrt(3),0];
    c=[0,0,1];
elseif(lattice=="diamond")
    d=3;
    Ncell=2;
    unit=[[0 0 0]
          [.25 .25 .25]];
    a=[.5,.5,0];    #Look familiar? Checkout fcc from Pt.1
    b=[.5,0,.5];
    c=[0,.5,.5];
elseif(lattice=="pyrochlore")
    d=3;
    Ncell=4;
    unit=[[0 0 0]
        [.25 .25 0]
        [.25 0 .25]
        [0 .25 .25]];
    a=[.5,.5,0];
    b=[.5,0,.5];
    c=[0,.5,.5];

else
    println("Please have a correct lattice")
end
"Cell finished"
```

    "Cell finished"

### Connections to Bravais Lattices

If look at some of the comments above, and checkout the basis vectors from Crystal Shapes, like pt,
        \begin{equation}
            a=[1,0,0]\;\;\;\;\;\;\;\;\; b=[.5,\frac{\sqrt{3}}{2},0],
        \end{equation}
you'll notice they're the same except for a scaling factor.  This has to be true, since only 14 different patterns tile 3D space uniquely.

```julia
# Another cell to just evaluate
# Here we set up some numbers and matrices for our computation
N=Nx*Ny*Nz*Ncell;    #The total number of sites
aM=transpose(repeat(a,outer=[1,Ncell]));
bM=transpose(repeat(b,outer=[1,Ncell*Nx])); #these allow us to copy an entire row or layer at once
cM=transpose(repeat(c,outer=[1,Ncell*Nx*Ny]));

X=Array{Float64}(undef,N,3);  #where we store the positions
"Cell finished"
```

    "Cell finished"

```julia
# Another cell to just evaluate
# Here we are actually calculating the positions for every site
for i in 1:Nx    #for the first row
    X[Ncell*i-Ncell+1:Ncell*i,:]=unit.+(i-1)*aM;
end

for j in 2:Ny    #copying the first row into the first layer
    X[Ncell*Nx*(j-1).+(1:Ncell*Nx),:]=X[1:Ncell*Nx,:].+(j-1)*bM;
end

for j in 2:Nz    #copying the first layer into the entire cube
    X[Ncell*Ny*Nx*(j-1).+(1:Ncell*Nx*Ny),:]=X[1:Ncell*Nx*Ny,:].+(j-1)*cM;
end
"Cell finished"
```

    "Cell finished"

```julia
scatter(X[:,1],X[:,2])
# scatter(X[:,1],X[:,2],X[:,3])
plot!(title=lattice,xlabel="X",ylabel="Y",zlabel="Z",legend=false)
```

### Perdy Pictures
From these plots, some 3D structures like the pyrochlore are hard to visualize.  So here's a nice graphic I made that might help a little bit more.
####  Pyrochlore

The honeycomb, like several other lattices you see around here, is bipartite.  You can see in my image that black sites are only next to white sites, and vice versa.  This property can make the system much easier to work with.

If you keep reading, Please Do!, these lattices will keep cropping up again and again.  I'll probably throw in some new ones as well.

Anyway, if you've had too many lattices, will move onto some Quantum Mechanics to look at atomic orbitals soon!

```julia

```