# Reinforcement Learning for Ion Trap Quantum Computers

This exercise is a short extension of the two Tutorials:

- **Ion Trap Reinforcement Learning Environment Tutorial**
- **Projective Simulation Tutorial**

Here we are going to employ the implemented Projective Simulation (PS) agent to use short laser pulse sequences mapping an initially unentangled state $|000\rangle$ onto a GHZ-like state:

\begin{align}
|\mathrm{GHZ}\rangle = \frac{1}{\sqrt{d}}\sum_{i=0}^{d-1}|iii\rangle.\nonumber
\end{align}

We will consider three qutrits, i.e., $d=3$ for simplicity but you may choose to extend this at your own leisure.

More formally, we do not want to find GHZ states exactly but those states which are maximally entangled. We consider $n$ $d$-level states to be maximally entangled if they have a *Schmidt rank vector* (SRV) of $(d,...,d)$ where the $i$th entry is the rank of the reduced density matrix $\rho_i=\mathrm{tr}_{\bar{i}}(\rho)$ where $\bar{i}$ is the complement of $\{i\}$ in $\{1,...,n\}$.

Luckily, you don't really have to take care of this since this is already the default settings of the environment which we are going to load now:

```python
from ENV.IonTrap_env import IonTrapEnv
```

That was easy. According to the docs in the `init` method, the class allows the following kwargs:

* `num_ions` (int): The number of ions. Defaults to 3.
* `dim` (int): The local (odd) dimension of an ion. Defaults to 3.
* `goal` (list): List of SRVs that are rewarded. Defaults to `[[3,3,3]]`.
* `phases` (dict): The phases defining the laser gate set. Defaults to `{'pulse_angles': [np.pi/2], 'pulse_phases': [0, np.pi/2, np.pi/6], 'ms_phases': [-np.pi/2]}`
* `max_steps` (int): The maximum number of allowed time steps. Defaults to 10.

If you want to change anything you need to provide kwargs in form of a `dict` with the desired arguments as follows `IonTrapEnv(**{ 'max_steps': 20 })`.
Indeed, let us submit a small change. Since this is just supposed to be a small scale test, let us reduce the number of allowed phases and therefore, the number of possible actions.

```python
import numpy as np
KWARGS = {'phases': {'pulse_angles': [np.pi/2], 'pulse_phases': [np.pi/2], 'ms_phases': [-np.pi/2]}}
env = IonTrapEnv(**KWARGS)
```

Next, we need to get the PS agent and the ECM:

```python
from PS.agent.Universal_Agent import UniversalAgent
from PS.ecm.Universal_ECM import UniversalECM
```

For the initialisation we read through the docs:

Agent:

* `ECM` (object): Episodic compositional memory (ECM). The brain of the agent.
* `actions` (np.ndarray): An array of possible actions. Specified by the environment.
* `adj_matrix` (np.ndarray): Adjancency matrix representing the structure of the default decision tree.

ECM:

* `gamma_damping` (float): The damping (or gamma) parameter. Set to zero if the environment doesn't change in time. Defaults to 0.
* `eta_glow_damping` (float): glow parameter. Defaults to 0.1.
* `beta` (float): softmax parameter. Defaults to 1.

We don't know the actions and the adjancency matrix at this point, but want to keep all the other default parameters. Let's at first initialize the adjancency matrix. For now a two layered clip network is enough, later you can try other structures. I have a little task here.

__TASK:__

Initialize the adjancency matrix for the following decision tree. Use the PS Tutorial for help.

Tipp: The size of the matrix is (number actions + 1, number actions + 1)

```python

```

__SOLUTION:__

```python
num_actions = len(env.actions)

adj_matrix = np.zeros((num_actions + 1, num_actions + 1))
adj_matrix[0][list(range(1, num_actions + 1))] = 1
```

Now we can ask the environment what the actions are and initialize the agent accordingly:

```python
actions = env.actions

ecm = UniversalECM()
agent = UniversalAgent(ECM=ecm, actions=actions, adj_matrix=adj_matrix)

```

Fantastic, we have everything ready for a first run. Let's do that. The interaction between an environment and an agent is standardized through the [*openAI* `gym`](https://github.com/openai/gym) environments. In terms of code, we can imagine the interaction to go as follows,

Indeed, every reinforcement learning environment should provide at least two methods:

* `reset()`: Resets the environment to its initial state. *Returns* the initial observation.
* `step(action)`: Performs an action (given by an action index) on the environment. *Returns* the new observation, an associated reward and a bool value `done` which indicates whether a terminal state has been reached.

The agent on the other hand, supports the following two main methods:

* `predict(observation)` (here: `step(observation)`): Given an observation, the agent predicts an action. *Returns* an action index.
* `learn(reward)`: Uses the current reward to update internal network.

Knowing that the `IonTrapEnv` has been built according to this standard and the agent features the two methods above, we can start coding the interaction between agent and environment:

```python
# data set for performance evaluation
DATA_STEPS = []
# maximum number of episodes
NUM_EPISODES = 500

for i in range(NUM_EPISODES):
    # initial observation from environment
    observation = env.reset()
    #bool: whether or not the environment has finished the episode
    done = False
    #int: the current time step in this episode
    num_steps = 0
    action_seq = []
    while not done:
        # increment counter
        num_steps += 1
        # predict action
        action = agent.step(observation)
        action_seq.append(action)
        # perform action on environment and receive observation and reward
        observation, reward, done = env.step(action)
        # learn from reward
        agent.learn(reward)

        # gather statistics
        if done:
            DATA_STEPS.append(num_steps)

print(action_seq)

```

    [0, 4, 1, 0, 3, 5, 0]

And this is all the code that is needed to have an agent interact with our environment! In `DATA_STEPS` we have gathered the data that keeps track of the length of pulse sequences that generate GHZ-like states. We can use `matplotlib` to visualize the performance of the agent over time:

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

x_axis = np.arange(len(DATA_STEPS))

plt.plot(x_axis, DATA_STEPS)
plt.ylabel('Length of pulse sequence')
plt.xlabel('Episode')

```

We have witnessed an agent learning! The agent was able to push the gate sequences down to 5 laser pulses consisting of two Molmer-Sorensen gates and three single-ion laser pules.
Note that this is of course not conclusive because it is a single agent. Nevertheless, it has obviously learned and we can expect future agents to fare similarly. **Good work!**

Reinforcement Learning for Ion Traps.ipynb


# Tutorial 1: The Leaky Integrate-and-Fire (LIF) Neuron Model
**Week 2, Day 3: Biological Neuron Models**

**By Neuromatch Academy**

__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

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

---
# 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 ipywidgets import widgets

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

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

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

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

display(out)
```

## 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/W2D3_BiologicalNeuronModels/solutions/W2D3_Tutorial1_Solution_7a9c48e8.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 ipywidgets import widgets

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

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

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

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

display(out)
```

## 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/W2D3_BiologicalNeuronModels/solutions/W2D3_Tutorial1_Solution_1058324c.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/W2D3_BiologicalNeuronModels/solutions/W2D3_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/W2D3_BiologicalNeuronModels/solutions/W2D3_Tutorial1_Solution_030b7368.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 variation (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/W2D3_BiologicalNeuronModels/solutions/W2D3_Tutorial1_Solution_0125b1f6.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/W2D3_BiologicalNeuronModels/solutions/W2D3_Tutorial1_Solution_d3653711.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/W2D3_BiologicalNeuronModels/solutions/W2D3_Tutorial1_Solution_c6f1c4a2.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/W2D3_BiologicalNeuronModels/solutions/W2D3_Tutorial1_Solution_fc553243.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 ipywidgets import widgets

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

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

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

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

display(out)
```


```python
# Allow us to load `open_cp` without installing
import sys, os.path
sys.path.insert(0, os.path.abspath(".."))
```

# A grid based model SEPP model

In (1), an explicitly grid-based self-exciting point process (SEPP) model was considered.  The paper (1) is mostly concerned with two field-trials of the SEPP or epidemic-type aftershock sequence (ETAS) type prediction models.  There is no real discussion as to why a grid based model was chosen, compared to the earlier continuous space model using kernel density estimation.

### References:

1. Mohler et al. "Randomized Controlled Field Trials of Predictive Policing". Journal of the American Statistical Association (2015) DOI:10.1080/01621459.2015.1077710
2. Lewis, Mohler, "A Nonparametric EM algorithm for Multiscale Hawkes Processes", preprint (2011) see http://math.scu.edu/~gmohler/EM_paper.pdf

# The model

The area of interest is divided into a grid; 150m squares were used in (1).  To quote from (1):

> The size of the grid cells on which μ is defined can be estimated by
maximum likelihood and in general the optimum size of the grid
cell will decrease with increasing data. However, for a fixed area
flagged for patrol, a greater number of small hotspots are more
difficult to patrol than a small number of large hotspots. The
150 × 150 m hotspots were chosen in this study to be the size
of a city block in Foothill and were then held constant across all
of the experimental regions.

The model looks _at each grid cell individually_ and uses what we might recognise as a Hawkes process (in the original form studied by Hawkes, not the more general form).  In grid cell $n$, the conditional intensity $\lambda_n(t)$ is given by

\begin{equation} \lambda_n(t) = \mu_n + \sum_{t^i_n < t} \theta \omega e^{-\omega(t-t^i_n)}
\tag{eq 1}
\end{equation}

where here:

- $\mu_n$ is the background rate in cell $n$.
- $\omega$ and $\theta$ control the "near-repeat" behaviour.  Notice that these do not depend upon $n$.
- As always, we sum over events $t^i_n$ which have occurred before the time of interest $t$, and _which have occurred in the same grid cell_.

It seems surprising that inter-cell interactions are not considered at all in this model.

# Model fitting

I do not quite understand the paper (1) here.  The following is what I think _should_ have been written.

We make initial estimates for $\mu_n$ (perhaps setting each $\mu_n$ to be the same) and $\omega, \theta$.  Then we alternate the following two steps until convergence:

### Expectation step:

Compute the upper triangular "probability matrix":

$$ p^{n}_{ji} \propto \theta\omega e^{-\omega(t_n^i - t_n^j)}, \qquad p^n_{ii} \propto \mu_n $$

where $p^{n}_{ji}$ is the probability, in cell $n$, then event $j$ triggered event $i$.  We set $p^n_{ji} = 0$ if $j > i$ and then normalise so that each column of the matrix sums to $1$ (which is why in the above formula I only give what $p^n_{ji}$ is proportional to).

### Maximisation step:

We then compute the new parameters:

$$ \omega = \frac{\sum_n \sum_{i<j} p^n_{ij}}{\sum_n \sum_{i<j} p^n_{ij}(t_n^j - t_n^i)}, \qquad
\theta = \frac{\sum_n \sum_{i<j} p^n_{ij}}{\sum_n \sum_j 1}, \qquad \mu_n = \frac{\sum_i p^n_{ii}}{T} $$
where $T$ is the time window of data under consideration.

(Note here I have given an estimate for $\mu_n$, whereas (1) seems to sum over $n$ and give an estimate for $\mu$, the total rate across the study area, but does not specify a suitable estimate for $\mu_n$.)

# Simulation

Simulating a process satisfying (eq 1) can be accomplished in the same way as simulating a more general Hawkes-type process.  However, in the simple form we have here, we can find faster simulation methods.

1. We simulate the "branching process" form.  This can be done on a cell by cell basis.
2. So firstly we simulate a homogeneous Poisson process with intensity $\mu_n$.
3. Then each point $i$ gives rise to an independent inhomogeneous Poisson process with intensity $\theta\omega e^{-\omega(t-t_i)}$ where event $i$ occurred at time $t_i$.

To simulate the process in 3 we make use of the following result.  If $\lambda^*$ is the conditional intensity of a process, then the _compensator_ is
$$ \Lambda(t) = \int_0^t \lambda^*(s) \ ds. $$
Then we have the following useful result:

> If $(t_1,\cdots,t_k)$ is a realisation of time $[0,T]$ of a point process with conditional intensity $\lambda^*$ then $\{\Lambda(t_1),\cdots,\Lambda(t_k)\}$ is unit rate Poisson process.

The converse to this statement is also true.  So, if $\Lambda$ is invertible, then we may simulate a unit rate Poisson process $\{s_1,\cdots,s_k\}$ and then $t_i  = \Lambda^{-1}(s_i)$ will be a process with conditional intensity $\lambda^*$.

In our case, $\lambda^*(t) = \theta\omega e^{-\omega t}$ (translating so that the "trigger" event occurs at $t=0$) and so
$$ \Lambda(t) = \theta \int_0^t \omega e^{-\omega s} \ ds = \theta \Big[ -e^{-\omega s} \Big]_0^t
= \theta\big( 1- e^{-\omega t} \big). $$
This has inverse $\Lambda^{-1}(s) = \omega^{-1} \log(\theta / (\theta - s) )$ defined for $0\leq s < \theta$. So

- Simulate a unit rate Poisson process $\{s_1,\cdots,s_k\}$ on $[0,\theta)$ and then set $t_i = \Lambda^{-1}(s_i)$ to get the "after-shock" events.
- This allows us to think about the parameters $\theta$ and $\omega$.  $\theta$ controls the rate of the process-- we expect an average of $\theta$ events.  $\omega$ controls the scale of the resulting process.

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import datetime
import open_cp
```

## Simulate some test data

We lay down a grid, set a random background rate in each cell, simulate the process, and then add some noise to generate x,y coordinates in each grid cell.  This is obviously a bit artifical, as in real data, it is likely that the background rate in a cell will be related to the background rate in neighbouring cells.

```python
import open_cp.sources.sepp as source_sepp

rates = np.random.random(size=(10,10))
simulation = source_sepp.GridHawkesProcess(rates, 0.5, 10)
points = simulation.sample_to_randomised_grid(0, 365, grid_size=50)
time_unit = source_sepp.make_time_unit(datetime.timedelta(days=1))
timed_points = source_sepp.scale_to_real_time(points, datetime.datetime(2017,1,1),
    time_unit)
```

```python
fig, ax = plt.subplots(ncols=2, figsize=(16,7))

ax[0].scatter(timed_points.xcoords, timed_points.ycoords, alpha=0.1)
ax[0].set_title("Space location of simulated data")
ax[0].set(xlim=[0,500], ylim=[0,500])

times = timed_points.times_datetime()
ax[1].scatter(times, timed_points.xcoords, alpha=0.1)
ax[1].set_xlim([datetime.datetime(2017,1,1), datetime.datetime(2018,1,1)])
ax[1].set_title("Date against x location")
fig.autofmt_xdate()
None
```

# Train the model

Remember that we rescaled time.
- We sample initially with an implied unit of "days", and then convert to minutes; one day = 1440 minutes
- The aftershock rate was initially 10, so a mean of 0.1 days which becomes 144 minutes
- So the rate we look for is 1/144 = 0.00694...

```python
import open_cp.seppexp as sepp
```

```python
region = open_cp.RectangularRegion(xmin=0, xmax=500, ymin=0, ymax=500)
trainer = sepp.SEPPTrainer(region, grid_size=50)
trainer.data = timed_points
predictor = trainer.train(iterations=50)
print("Predicted omega={}, theta={}".format(predictor.omega, predictor.theta))
```

    Predicted omega=0.006955745919879276, theta=0.5006558130258035

```python
fig, ax = plt.subplots(ncols=1, figsize=(8,6))
ax.plot([0,1], [0,1], linewidth=1, color="r")
ax.scatter(rates.ravel(), predictor.mu.ravel()*1440)
ax.set(xlim=[0,1], ylim=[0,np.max(predictor.mu)*1440*1.1])
ax.set(xlabel="Actual background rate", ylabel="Predicted background rate")
ax.set_title("Predicted background rates")
None
```

# Make predictions

Once trained, we have an estimate of the background rate in each grid cell, and the global parameters $\theta, \omega$ describing the triggering / aftershock behaviour.  These can then be applied to any data fitting the same grid pattern (though of course the data should be compatible!)

```python
predictor.data = timed_points
dates = [datetime.datetime(2018,1,1), datetime.datetime(2018,1,2)]
predictions = [predictor.predict(date) for date in dates]
background_prediction = predictor.background_prediction()
```

```python
fig, ax = plt.subplots(ncols=3, figsize=(16,6))
for a in ax:
    a.set(xlim=[region.xmin, region.xmax], ylim=[region.ymin, region.ymax])

for pred, date, a in zip(predictions, dates, ax):
    m = a.pcolormesh(*pred.mesh_data(), pred.intensity_matrix, cmap="Blues")
    a.set_title("Prediction for {}".format(date))
    fig.colorbar(m, ax=a)

m2 = ax[2].pcolormesh(*background_prediction.mesh_data(), background_prediction.intensity_matrix, cmap="Blues")
ax[2].set_title("Background rate")
fig.colorbar(m2, ax=ax[2])
None
```

As with the other SEPP model, we see that "aftershocks" dominate the prediction, but as we predict further into the future, we converge on the background rate.

A more realistic "prediction" method would be to simulate future events using the model, and then compute the intensity based on this simulation, perhaps finally taking an average over many simulations.

```python

```

examples/Self-exciting point processes 2.ipynb


# Tutorial 1: Geometric view of data
**Week 1, Day 5: Dimensionality Reduction**

**By Neuromatch Academy**

__Content creators:__ Alex Cayco Gajic, John Murray

__Content reviewers:__ Roozbeh Farhoudi, Matt Krause, Spiros Chavlis, Richard Gao, Michael Waskom, Siddharth Suresh, Natalie Schaworonkow, Ella Batty

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

---
# Tutorial Objectives

*Estimated timing of tutorial: 50 minutes*

In this notebook we'll explore how multivariate data can be represented in different orthonormal bases. This will help us build intuition that will be helpful in understanding PCA in the following tutorial.

Overview:
 - Generate correlated multivariate data.
 - Define an arbitrary orthonormal basis.
 - Project the data onto the new basis.

```python
# @title Tutorial slides

# @markdown These are the slides for the videos in all tutorials today
from IPython.display import IFrame
IFrame(src=f"https://mfr.ca-1.osf.io/render?url=https://osf.io/kaq2x/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
```

```python
# @title Video 1: Geometric view of data
from ipywidgets import widgets

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

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

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

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

display(out)
```

---
# Setup

```python
# 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'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
# @title Plotting Functions

def plot_data(X):
  """
  Plots bivariate data. Includes a plot of each random variable, and a scatter
  plot of their joint activity. The title indicates the sample correlation
  calculated from the data.

  Args:
    X (numpy array of floats) :   Data matrix each column corresponds to a
                                  different random variable

  Returns:
    Nothing.
  """

  fig = plt.figure(figsize=[8, 4])
  gs = fig.add_gridspec(2, 2)
  ax1 = fig.add_subplot(gs[0, 0])
  ax1.plot(X[:, 0], color='k')
  plt.ylabel('Neuron 1')
  plt.title('Sample var 1: {:.1f}'.format(np.var(X[:, 0])))
  ax1.set_xticklabels([])
  ax2 = fig.add_subplot(gs[1, 0])
  ax2.plot(X[:, 1], color='k')
  plt.xlabel('Sample Number')
  plt.ylabel('Neuron 2')
  plt.title('Sample var 2: {:.1f}'.format(np.var(X[:, 1])))
  ax3 = fig.add_subplot(gs[:, 1])
  ax3.plot(X[:, 0], X[:, 1], '.', markerfacecolor=[.5, .5, .5],
           markeredgewidth=0)
  ax3.axis('equal')
  plt.xlabel('Neuron 1 activity')
  plt.ylabel('Neuron 2 activity')
  plt.title('Sample corr: {:.1f}'.format(np.corrcoef(X[:, 0], X[:, 1])[0, 1]))
  plt.show()

def plot_basis_vectors(X, W):
  """
  Plots bivariate data as well as new basis vectors.

  Args:
    X (numpy array of floats) :   Data matrix each column corresponds to a
                                  different random variable
    W (numpy array of floats) :   Square matrix representing new orthonormal
                                  basis each column represents a basis vector

  Returns:
    Nothing.
  """

  plt.figure(figsize=[4, 4])
  plt.plot(X[:, 0], X[:, 1], '.', color=[.5, .5, .5], label='Data')
  plt.axis('equal')
  plt.xlabel('Neuron 1 activity')
  plt.ylabel('Neuron 2 activity')
  plt.plot([0, W[0, 0]], [0, W[1, 0]], color='r', linewidth=3,
           label='Basis vector 1')
  plt.plot([0, W[0, 1]], [0, W[1, 1]], color='b', linewidth=3,
           label='Basis vector 2')
  plt.legend()
  plt.show()

def plot_data_new_basis(Y):
  """
  Plots bivariate data after transformation to new bases.
  Similar to plot_data but with colors corresponding to projections onto
  basis 1 (red) and basis 2 (blue). The title indicates the sample correlation
  calculated from the data.

  Note that samples are re-sorted in ascending order for the first
  random variable.

  Args:
    Y (numpy array of floats):   Data matrix in new basis each column
                                 corresponds to a different random variable

  Returns:
    Nothing.
  """
  fig = plt.figure(figsize=[8, 4])
  gs = fig.add_gridspec(2, 2)
  ax1 = fig.add_subplot(gs[0, 0])
  ax1.plot(Y[:, 0], 'r')
  plt.xlabel
  plt.ylabel('Projection \n basis vector 1')
  plt.title('Sample var 1: {:.1f}'.format(np.var(Y[:, 0])))
  ax1.set_xticklabels([])
  ax2 = fig.add_subplot(gs[1, 0])
  ax2.plot(Y[:, 1], 'b')
  plt.xlabel('Sample number')
  plt.ylabel('Projection \n basis vector 2')
  plt.title('Sample var 2: {:.1f}'.format(np.var(Y[:, 1])))
  ax3 = fig.add_subplot(gs[:, 1])
  ax3.plot(Y[:, 0], Y[:, 1], '.', color=[.5, .5, .5])
  ax3.axis('equal')
  plt.xlabel('Projection basis vector 1')
  plt.ylabel('Projection basis vector 2')
  plt.title('Sample corr: {:.1f}'.format(np.corrcoef(Y[:, 0], Y[:, 1])[0, 1]))
  plt.show()
```

---
# Section 1: Generate correlated multivariate data

```python
# @title Video 2: Multivariate data
from ipywidgets import widgets

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

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

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

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

display(out)
```

This video describes the covariance matrix and the multivariate normal distribution.

 Click here for text recap of video

To gain intuition, we will first use a simple model to generate multivariate data. Specifically, we will draw random samples from a *bivariate normal distribution*. This is an extension of the one-dimensional normal distribution to two dimensions, in which each $x_i$ is marginally normal with mean $\mu_i$ and variance $\sigma_i^2$:

\begin{align}
x_i \sim \mathcal{N}(\mu_i,\sigma_i^2).
\end{align}

Additionally, the joint distribution for $x_1$ and $x_2$ has a specified correlation coefficient $\rho$. Recall that the correlation coefficient is a normalized version of the covariance, and ranges between -1 and +1:

\begin{align}
\rho = \frac{\text{cov}(x_1,x_2)}{\sqrt{\sigma_1^2 \sigma_2^2}}.
\end{align}

For simplicity, we will assume that the mean of each variable has already been subtracted, so that $\mu_i=0$ for both $i=1$ and $i=2$. The remaining parameters can be summarized in the covariance matrix, which for two dimensions has the following form:

\begin{align}
{\bf \Sigma} =
\begin{pmatrix}
 \text{var}(x_1) & \text{cov}(x_1,x_2) \\
 \text{cov}(x_1,x_2) &\text{var}(x_2)
\end{pmatrix}.
\end{align}

In general, $\bf \Sigma$ is a symmetric matrix with the variances $\text{var}(x_i) = \sigma_i^2$ on the diagonal, and the covariances on the off-diagonal. Later, we will see that the covariance matrix plays a key role in PCA.

## Coding Exercise 1: Draw samples from a distribution

We have provided code to draw random samples from a zero-mean bivariate normal distribution with a specified covariance matrix (`get_data`). Throughout this tutorial, we'll imagine these samples represent the activity (firing rates) of two recorded neurons on different trials. Fill in the function below to calculate the covariance matrix given the desired variances and correlation coefficient. The covariance can be found by rearranging the equation above:

\begin{align}
\text{cov}(x_1,x_2) = \rho \sqrt{\sigma_1^2 \sigma_2^2}.
\end{align}

```python
# @markdown Execute this cell to get helper function `get_data`

def get_data(cov_matrix):
  """
  Returns a matrix of 1000 samples from a bivariate, zero-mean Gaussian.

  Note that samples are sorted in ascending order for the first random variable

  Args:
    cov_matrix (numpy array of floats): desired covariance matrix

  Returns:
    (numpy array of floats) : samples from the bivariate Gaussian, with each
                              column corresponding to a different random
                              variable
  """

  mean = np.array([0, 0])
  X = np.random.multivariate_normal(mean, cov_matrix, size=1000)
  indices_for_sorting = np.argsort(X[:, 0])
  X = X[indices_for_sorting, :]

  return X

help(get_data)
```

```python
def calculate_cov_matrix(var_1, var_2, corr_coef):
  """
  Calculates the covariance matrix based on the variances and correlation
  coefficient.

  Args:
    var_1 (scalar)          : variance of the first random variable
    var_2 (scalar)          : variance of the second random variable
    corr_coef (scalar)      : correlation coefficient

  Returns:
    (numpy array of floats) : covariance matrix
  """

  #################################################
  ## TODO for students: calculate the covariance matrix
  # Fill out function and remove
  raise NotImplementedError("Student excercise: calculate the covariance matrix!")
  #################################################

  # Calculate the covariance from the variances and correlation
  cov = ...

  cov_matrix = np.array([[var_1, cov], [cov, var_2]])

  return cov_matrix

# Set parameters
np.random.seed(2020)  # set random seed
variance_1 = 1
variance_2 = 1
corr_coef = 0.8

# Compute covariance matrix
cov_matrix = calculate_cov_matrix(variance_1, variance_2, corr_coef)

# Generate data with this covariance matrix
X = get_data(cov_matrix)

# Visualize
plot_data(X)
```

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

*Example output:*

## Interactive Demo 1: Correlation effect on data

We'll use the function you just completed but now we can change the correlation coefficient via slider. You should get a feel for how changing the correlation coefficient affects the geometry of the simulated data.

1.   What effect do negative correlation coefficient values have?
2.   What correlation coefficient results in a circular data cloud?

Note that we sort the samples according to neuron 1's firing rate, meaning the plot of neuron 1 firing rate over sample number looks clean and pretty unchanging when compared to neuron 2.

```python
# @markdown Execute this cell to enable widget

def _calculate_cov_matrix(var_1, var_2, corr_coef):

  # Calculate the covariance from the variances and correlation
  cov = corr_coef * np.sqrt(var_1 * var_2)

  cov_matrix = np.array([[var_1, cov], [cov, var_2]])

  return cov_matrix

@widgets.interact(corr_coef = widgets.FloatSlider(value=.2, min=-1, max=1, step=0.1))
def visualize_correlated_data(corr_coef=0):
  variance_1 = 1
  variance_2 = 1

  # Compute covariance matrix
  cov_matrix = _calculate_cov_matrix(variance_1, variance_2, corr_coef)

  # Generate data with this covariance matrix
  X = get_data(cov_matrix)

  # Visualize
  plot_data(X)
```

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

---
# Section 2: Define a new orthonormal basis

*Estimated timing to here from start of tutorial: 20 min*

```python
# @title Video 3: Orthonormal bases
from ipywidgets import widgets

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

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

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

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

display(out)
```

This video shows that data can be represented in many ways using different bases. It also explains how to check if your favorite basis is orthonormal.

 Click here for text recap of video

Next, we will define a new orthonormal basis of vectors ${\bf u} = [u_1,u_2]$ and ${\bf w} = [w_1,w_2]$. As we learned in the video, two vectors are orthonormal if:

1.   They are orthogonal (i.e., their dot product is zero):
\begin{align}
{\bf u\cdot w} = u_1 w_1 + u_2 w_2 = 0
\end{align}
2.   They have unit length:
\begin{align}
||{\bf u} || = ||{\bf w} || = 1
\end{align}

In two dimensions, it is easy to make an arbitrary orthonormal basis. All we need is a random vector ${\bf u}$, which we have normalized. If we now define the second basis vector to be ${\bf w} = [-u_2,u_1]$, we can check that both conditions are satisfied:
\begin{align}
{\bf u\cdot w} = - u_1 u_2 + u_2 u_1 = 0
\end{align}
and
\begin{align}
{|| {\bf w} ||} = \sqrt{(-u_2)^2 + u_1^2} = \sqrt{u_1^2 + u_2^2} = 1,
\end{align}
where we used the fact that ${\bf u}$ is normalized. So, with an arbitrary input vector, we can define an orthonormal basis, which we will write in matrix by stacking the basis vectors horizontally:

\begin{align}
{{\bf W} } =
\begin{pmatrix}
 u_1 & w_1 \\
 u_2 & w_2
\end{pmatrix}.
\end{align}

## Coding Exercise 2: Find an orthonormal basis

In this exercise you will fill in the function below to define an orthonormal basis, given a single arbitrary 2-dimensional vector as an input.

**Steps**
* Modify the function `define_orthonormal_basis` to first normalize the first basis vector $\bf u$.
* Then complete the function by finding a basis vector $\bf w$ that is orthogonal to $\bf u$.
* Test the function using initial basis vector ${\bf u} = [3,1]$. Plot the resulting basis vectors on top of the data scatter plot using the function `plot_basis_vectors`. (For the data, use  $\sigma_1^2 =1$, $\sigma_2^2 =1$, and $\rho = .8$).

```python
def define_orthonormal_basis(u):
  """
  Calculates an orthonormal basis given an arbitrary vector u.

  Args:
    u (numpy array of floats) : arbitrary 2-dimensional vector used for new
                                basis

  Returns:
    (numpy array of floats)   : new orthonormal basis
                                columns correspond to basis vectors
  """

  #################################################
  ## TODO for students: calculate the orthonormal basis
  # Fill out function and remove
  raise NotImplementedError("Student excercise: implement the orthonormal basis function")
  #################################################

  # Normalize vector u
  u = ...

  # Calculate vector w that is orthogonal to w
  w = ...

  # Put in matrix form
  W = np.column_stack([u, w])

  return W

# Set up parameters
np.random.seed(2020)  # set random seed
variance_1 = 1
variance_2 = 1
corr_coef = 0.8
u = np.array([3, 1])

# Compute covariance matrix
cov_matrix = calculate_cov_matrix(variance_1, variance_2, corr_coef)

# Generate data
X = get_data(cov_matrix)

# Get orthonomal basis
W = define_orthonormal_basis(u)

# Visualize
plot_basis_vectors(X, W)
```

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

*Example output:*

---
# Section 3: Project data onto new basis

*Estimated timing to here from start of tutorial: 35 min*

```python
# @title Video 4: Change of basis
from ipywidgets import widgets

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

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

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

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

display(out)
```

Finally, we will express our data in the new basis that we have just found. Since $\bf W$ is orthonormal, we can project the data into our new basis using simple matrix multiplication :

\begin{align}
{\bf Y = X W}.
\end{align}

We will explore the geometry of the transformed data $\bf Y$ as we vary the choice of basis.

## Coding Exercise 3: Change to orthonormal basis
In this exercise you will fill in the function below to change data to a orthonormal basis.

**Steps**
* Complete the function `change_of_basis` to project the data onto the new basis.
* Plot the projected data using the function `plot_data_new_basis`.
* What happens to the correlation coefficient in the new basis? Does it increase or decrease?
* What happens to variance?

```python
def change_of_basis(X, W):
  """
  Projects data onto new basis W.

  Args:
    X (numpy array of floats) : Data matrix each column corresponding to a
                                different random variable
    W (numpy array of floats) : new orthonormal basis columns correspond to
                                basis vectors

  Returns:
    (numpy array of floats)    : Data matrix expressed in new basis
  """

  #################################################
  ## TODO for students: project the data onto o new basis W
  # Fill out function and remove
  raise NotImplementedError("Student excercise: implement change of basis")
  #################################################

  # Project data onto new basis described by W
  Y = ...

  return Y

# Project data to new basis
Y = change_of_basis(X, W)

# Visualize
plot_data_new_basis(Y)
```

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

*Example output:*

## Interactive Demo 3: Play with the basis vectors
To see what happens to the correlation as we change the basis vectors, run the cell below. The parameter $\theta$ controls the angle of $\bf u$ in degrees. Use the slider to rotate the basis vectors.

1.   What happens to the projected data as you rotate the basis?
2.   How does the correlation coefficient change? How does the variance of the projection onto each basis vector change?
3.   Are you able to find a basis in which the projected data is **uncorrelated**?

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

def refresh(theta=0):
  u = np.array([1, np.tan(theta * np.pi / 180)])
  W = define_orthonormal_basis(u)
  Y = change_of_basis(X, W)
  plot_basis_vectors(X, W)
  plot_data_new_basis(Y)

_ = widgets.interact(refresh, theta=(0, 90, 5))
```

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

---
# Summary

*Estimated timing of tutorial: 50 minutes*

- In this tutorial, we learned that multivariate data can be visualized as a cloud of points in a high-dimensional vector space. The geometry of this cloud is shaped by the covariance matrix.

- Multivariate data can be represented in a new orthonormal basis using the dot product. These new basis vectors correspond to specific mixtures of the original variables - for example, in neuroscience, they could represent different ratios of activation  across a population of neurons.

- The projected data (after transforming into the new basis) will generally have a different geometry from the original data. In particular, taking basis vectors that are aligned with the spread of cloud of points decorrelates the data.

* These concepts - covariance, projections, and orthonormal bases - are key for understanding PCA, which we be our focus in the next tutorial.

---
# Notation

\begin{align}
x_i &\quad \text{data point for dimension } i\\
\mu_i &\quad \text{mean along dimension } i\\
\sigma_i^2 &\quad \text{variance along dimension } i \\
\bf u, \bf w &\quad \text{orthonormal basis vectors}\\
\rho &\quad \text{correlation coefficient}\\
\bf \Sigma &\quad \text{covariance matrix}\\
\bf X &\quad \text{original data matrix}\\
\bf W &\quad \text{projection matrix}\\
\bf Y &\quad \text{transformed data}\\
\end{align}


```python
from scipy.integrate import odeint
from sympy.plotting import plot
from sympy import init_printing
import sympy
from sympy.abc import t
from sympy import Array, Sum, Indexed, IndexedBase, Idx
init_printing()
from sympy.abc import t # x is the independent variable
from sympy import Function, dsolve, Eq, Derivative, sin, cos, symbols, exp, pi, diff, Poly
from sympy.physics.units.systems import SI
from sympy.physics.units import length, mass, acceleration, force
from sympy.physics.units import gravitational_constant as G
from sympy.physics.units.systems.si import dimsys_SI
import sympy.physics.units as units
#import pprint as pps
import sympy
import sympy.physics.units.util as util
from dataclasses import dataclass
from sympy.simplify.radsimp import collect
from sympy.assumptions.refine import refine
from sympy import init_printing
from sympy.simplify.powsimp import powsimp
init_printing()
from scipy.integrate import odeint
from sympy.plotting import plot
from sympy import init_printing
import sympy
from sympy.abc import t
from sympy import Array, Sum, Indexed, IndexedBase, Idx
init_printing()
from sympy.abc import t # x is the independent variable
from sympy import Function, dsolve, Eq, Derivative, sin, cos, symbols, exp, pi, diff, Poly
from sympy import I
```

```python
from sympy import symbols
import sympy.physics.units as u

Q, q, E, omega_0, omega, m, P_abs, f = symbols("Q, q, E, omega_0, omega, m, P_abs, f")

# where m is the reduced mass.
```

### Equation 9 from Yang et al, integrated power absorbed by one virus

```python
P = 0.5 * (Q * (q * E)**2 * omega_0 * omega**2 ) / (Q**2 * m * (omega_0**2 - omega**2)**2 + (omega_0 * omega)**2 * m)

display(Eq(P_abs, P))
```

```python
freq = f * 2 * pi

#remember the angular frequency
```

Now assume we drive on-resonance.

$\omega = \omega_0$ (this isn't exactly right since max power vs max amplitude freq is slightly different, but this is a minor correction for low Q):

```python
P_1 = P.subs([(omega, freq), (omega_0, freq)])

display(Eq(P_abs, P_1))
```

whoops, had the wrong value for reduced mass!

```python
MDa = 1.66054e-21

f_ = 8.2e9 * u.Hz
m_ = 14.5 * MDa * u.kg # 60 MDa
E_ = 50.0 * u.volts / u.m
Q_ = 1.95 # dimensionless
q_ = 1e7 * 1.602e-19 * u.coulomb

P_2 = P_1.subs([(f, f_), (m, m_), (E, E_), (Q, Q_), (q, q_)])

P_3 = u.convert_to(P_2, u.watts).evalf()

P_3
```

```python
# cuvette_volume = 1e-3*u.liter

cuvette_volume_liters = 1e-6*u.liter # yang et al use, say, 1 microliter
# liu et al use "1 drop", which could be 50 microliters

# N = 1e7 #number of viruses per 1 mL cuvette

cuvette_volume = u.convert_to(cuvette_volume_liters, u.meter**3)

display(cuvette_volume)

# yang use 7.5*10^8 / mL - not clear how they concentrate, I guess the MOI was just high enough
N_m3 = (7.5*(10**14)) / (u.meter**3) # N/cubic m

# N_m3 = 1e9 * 1e6 / (u.meter**3)

N = N_m3 * cuvette_volume

P_4 = (P_3 * N)

P_4
```

```python
print(sympy.pretty(P_4 / (1e-6*u.watts)), "microwatts")
```

    3.78198538901213 microwatts

```python
medium_conductivity = 12 * u.S / u.m

cuvette_power = (0.5 * medium_conductivity * (E_)**2 * cuvette_volume)

cuvette_power = u.convert_to(cuvette_power, u.watts).evalf()

cuvette_power
```

```python
SNR = P_4 / cuvette_power

SNR
```

```python

```

```python

```


# Exercises to Lecture 3: Introduction to Python 3.6
By Dr. Anders S. Christensen
`anders.christensen @ unibas.ch`

## Exercise 3.1: If/Else statments
Yesterday, we used if and else statments to check conditions.

For example, we can check if a variable `a` is greater 10:

```
a = 100

if a > 10:
  print("a is greater than 10")
```

    a is greater than 10

### Question 3.1.1:

In this exercise, write a function named for example `check_number(n)` which determines whether a number is negative, zero, or positive, and prints the answer.

Verify with a couple of cases, that the code yields the correct

```
def check_number(n):

  if n < 0:
    print("negative")
  elif n == 0:
    print("zero")
  elif n > 0:
    print("positive")
  else:
    print("this shouldn't happen!")

# Check the output of the tests below to ensure that your code works
check_number(-1000.0)   # Code should print 'negative'
check_number(-69.0)     # Code should print 'negative'
check_number(0.0)       # Code should print 'zero'
check_number(1.0)       # Code should print 'positive'
check_number(420.0)     # Code should print 'positive'
```

    negative
    negative
    zero
    positive
    positive

## Exercise 3.2: When to stop a loop?

Sometimes `for`-loops can be terminated before they run to the end of `range()`.

In this example we wish to print all numbers where the square is smaller than 42.

However, we might not know when to stop the `for` loop beforehand. In this case, we can use `if`/`else` to check when the condition is met, and when it is we can use the `break` statement to stop the `for` loop.

```
# Print all integers that where the square is smaller than 42:

# Try all numbers up to 100
for i in range(100):

  # Check if the square is small enough
  if i**2 < 42:
    print(i, i**2)

  # Stop the loop if the test is False
  else:
    break
```

    0 0
    1 1
    2 4
    3 9
    4 16
    5 25
    6 36

The so-called "Basel problem" in mathematics shows that the reciprocal of the square numbers form a convergent series:

\begin{equation}
x=\sum_{n=1}^\infty \frac{1}{n^2}  = \frac{1}{1^2} + \frac{1}{2^2} + \frac{1}{3^2} \dots = \frac{\pi^2}{6}
\end{equation}

The problem is named after Leonhard Euler who lived here in in Basel and worked on the problem.

The code below uses 10,000 iterations of a `for` loop to calculate the result.

```
x = 0

# Do 10000 iterations
for i in range(1, 10000):

  # Calculate 1/n^2 and add it to x
  x = x + 1/i**2

print(x)
```

    1.6448340618480652

### Question 3.2.1:

As part of his proof, Euler calculated many terms of this series by hand in order to verify his work.

The sum of the series is approximately $1.645$. To achieve this accuracy, the numerical calculation of the sum can be stopped when $\frac{1}{n^2} < 0.001$, long before 10000 terms as in the above example.

*    In this question, rewrite the above code for the Basel problem, such that the calculation stops when the $\frac{1}{n^2}$ terms are smaller than $0.001$.

*     **Hint:** Use a `break` statement to stop the for loop.

```
x = 0

for i in range(1, 10000):

  # The value of each term in the series
  contribution = 1/i**2

  # Add the term to the sum
  x = x + contribution

  # Print step number and contribution
  print(i, contribution)

  # Check if contribution is sufficiently small to break
  if contribution < 0.001:
    break

print(x)

```

    1 1.0
    2 0.25
    3 0.1111111111111111
    4 0.0625
    5 0.04
    6 0.027777777777777776
    7 0.02040816326530612
    8 0.015625
    9 0.012345679012345678
    10 0.01
    11 0.008264462809917356
    12 0.006944444444444444
    13 0.005917159763313609
    14 0.00510204081632653
    15 0.0044444444444444444
    16 0.00390625
    17 0.0034602076124567475
    18 0.0030864197530864196
    19 0.002770083102493075
    20 0.0025
    21 0.0022675736961451248
    22 0.002066115702479339
    23 0.001890359168241966
    24 0.001736111111111111
    25 0.0016
    26 0.0014792899408284023
    27 0.0013717421124828531
    28 0.0012755102040816326
    29 0.0011890606420927466
    30 0.0011111111111111111
    31 0.001040582726326743
    32 0.0009765625
    1.6141672628279242

## Exercise 3.3: Matrices and Vectors with Numpy

In this exercises you'll write some code contatining matrix and vector operations with numpy.

### Question 3.3.1: Matrix-Matrix Product

In the code block below, you are give two matrices A ($3\times4$ matrix), and B ($4\times 4$ matrix).

```
import numpy as np

np.random.seed(2089)

A = np.random.random(size=(3,4))
B = np.random.random(size=(4,4))

print(A)
print(B)
```

    [[0.27643555 0.01508359 0.15503277 0.03788909]
     [0.85084514 0.43484643 0.37326361 0.66656812]
     [0.446939   0.84514875 0.15845176 0.20942867]]
    [[0.69338912 0.92076566 0.96558827 0.52373193]
     [0.53668218 0.41861072 0.8096089  0.61153309]
     [0.98087461 0.18415275 0.28496972 0.17557867]
     [0.3359122  0.07466268 0.12196524 0.01539081]]

Use numpy's `np.matmul()` function to calculate the matrix product `C`:

\begin{equation}
\mathbf{C} = \mathbf{A}\mathbf{B}
\end{equation}

**Hint:** The matrix product is not the *element-wise* product!

```
# Calculate the matrix product

C = np.matmul(A, B)

# OR

C = A @ B

print(C)
```

    [[0.36456762 0.29222512 0.32793553 0.18180583]
     [1.41337425 1.08396564 1.36128859 0.7873339 ]
     [0.98924986 0.81013024 1.18649597 0.78195668]]

Also answer: what are the dimensions of `C`? **3x4**

### Question 3.3.2: Dot products
Here you are give two vectors (i.e. 1D Numpy arrays), `p` and `q`:

```
np.random.seed(3001)

p = np.random.random(size=(10))
q = np.random.random(size=(10))

print(p)
print(q)
```

    [0.25680088 0.03751904 0.96212176 0.45024744 0.84860471 0.0059018
     0.65601696 0.088925   0.2746181  0.14881909]
    [0.93091507 0.2473442  0.61201234 0.39467275 0.31056861 0.63544632
     0.09354759 0.16572425 0.27644798 0.02436491]

Use a Numpy function to calculate the dot product `k`:

\begin{equation}
k = \mathbf{p} \cdot \mathbf{q}
\end{equation}

**Hint:** The dot product is not the *element-wise* product!

```
# Calculate the dot product, k

k = np.dot(p, q)

print(k)
```

    1.437820387314264

Also answer: Is `k` a scalar (a single number), a vector, or a matrix? `k` is a scalar.

## Exercise 3.4: Plotting a histogram
The code block below makes some "fake" data for the height of 100 people in units of [cm].

For now, you will use this to plot a histogram of the data. Before you start Question 3.4.1, make sure to run the code block below and inspect the output (which is a list named `heights` with the heights of 100 people).

```
import numpy as np
np.random.seed(2021)

heights = np.random.normal(loc=170, scale=10, size=100)
print(heights)
```

    [184.88609053 176.76010871 165.81548631 161.93479187 175.55875832
     162.94495714 181.30858262 176.45001837 171.06413743 174.2215483
     171.24206845 161.62046539 174.09015705 171.02751219 150.92277609
     181.00224305 155.97674939 167.74918733 156.63794034 173.03721514
     162.79841165 195.44914601 183.1729112  170.72630302 167.43891862
     171.38010408 181.47235985 183.76260758 165.27816033 175.24084896
     184.85107926 184.82432249 177.28130509 166.10351917 172.78893759
     170.519002   159.55256318 168.38492466 142.06469427 173.61648007
     172.40107579 174.77812285 172.08851935 179.17911635 155.88222156
     182.24235722 164.54342282 179.06748053 160.17382764 163.66838108
     178.2552024  157.15503144 166.52691216 159.22239253 179.72574949
     174.12191165 168.67913957 163.26649082 182.22221715 160.73586579
     184.24993801 168.75210065 177.0549192  177.19547504 171.41463935
     163.45551517 163.2789583  178.7036849  180.03245273 166.33550173
     181.28051682 177.92583802 152.49157607 161.91130552 155.87398187
     161.96990301 177.9828047  187.55996222 169.24495802 174.20176818
     171.56556065 169.4313872  164.6176642  179.01387327 172.24944205
     182.77788182 176.29268598 173.61891216 206.38725858 157.98779223
     156.6996715  182.61033357 163.51874447 192.45300018 159.87606085
     168.74061301 165.67583572 174.25937197 177.56636033 160.05145941]

### Questions 3.4.1:

In order to plot a histogram, you can use the function `plt.hist(list)` to make a histogram with matplotlib.pyplot.

It works similarly to the `plt.plot()` (lineplots) and `plt.scatter()` (scatterplots), which you have already encountered in these exercises.

If you are unsure, use Google or consult the manual for `plt.hist()` here:
https://matplotlib.org/3.1.1/gallery/statistics/hist.html

```
import matplotlib.pyplot as plt

# Insert the code here to plot a histogram of the list 'heights'
plt.hist(heights, color="red") # Note: additional color keyword

# Sample labels
plt.xlabel("Height [cm]")
plt.ylabel("Occurrences #")

plt.show()
```

### Question 3.4.2: Making a better plot

Similarly to Question 2.4.2 in the exercises from yesterday, add proper labels to the x- and y- axis to the histogram.

***Go back to the code in Questions 3.4.1 and add labels to the plot***

### Question 3.4.3: Changing colors

Your supervisor/professor only wants red histograms!

In this question, use Google, or other resources, to find out how to change the histogram from the default blue color to red color.

***Go back to the code in Questions 3.4.1 and change the color to red***

## Exercise 3.5: Matrix multiplication (harder)

After doing this exercise, I hope you will appreciate NumPy and numpy arrays.

In the example below, there are two random matrices defined, `A` and `B`.

The matrix product
\begin{equation}
\mathbf{C} = \mathbf{A}\mathbf{B}
\end{equation}
is calculated using the `np.matmul()` function:

```
import numpy as np
np.random.seed(2021)

A = np.random.random(size=(3,5))
B = np.random.random(size=(5,4))

C = np.matmul(A,B)

print(C)
```

    [[1.53849657 1.84097289 1.02377889 1.66341903]
     [1.33497702 1.73370231 0.7243805  1.49474161]
     [0.82711575 1.17116457 0.62390613 1.05523344]]

### Question 3.5.1:
Without NumPy, we would have to write our own code to calculate the matrix multiplication of `A` and `B`.

For this, the formula is:

\begin{equation}
C_{ik} = \sum_j A_{ij}B_{jk}
\end{equation}

In this question, you have to implement the above equation. That is, calculate the elements of `C` using this summation.

Some things to consider:

*   Use a triple loop over the indices $i$, $j$, and $k$
*   What are the ranges for the indices?
*   Use `np.zeros()` to initialize the `C` matrix

Print the result and compare to the result in the previous code box to verify that you did it correctly.

```
# Initialize the array C of the correct size
C = np.zeros((3,4))

# From the sizes of A and B, the ranges for i, j, and k
# must be 3, 5, and 4, respectively.

# Code the matrix multiplication below yourself
for i in range(3):
  for j in range(5):
    for k in range(4):

      C[i,k] = C[i,k] + A[i,j] * B[j,k]

# Finally, print C
print(C)

# Compare with true answer:
C_numpy = np.matmul(A, B)
print(C - C_numpy)
```

    [[1.53849657 1.84097289 1.02377889 1.66341903]
     [1.33497702 1.73370231 0.7243805  1.49474161]
     [0.82711575 1.17116457 0.62390613 1.05523344]]
    [[ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00]
     [-1.11022302e-16  0.00000000e+00 -1.11022302e-16  0.00000000e+00]]


# メモ
問題解決のためのアルゴリズムx数学が基礎からしっかり身につく本米田優峻
を読む
https://github.com/E869120/math-algorithm-book

```python
# 読み方が分からないのでとりあえず節末問題を眺めながら進もう

# 問題 2.1.1 以下の数のうち整数をすべて選べ、とか #=> 省略

# 問題 2.1.2 省略

# 問題 2.1.3 A_1, A_2, A_3 を入力し、A_1 A_2 A_3 を出力するプログラムを作成する。例 2 8 8 => 128
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;
int main () {
    vector<int> vec(3);
    for (int i = 0; i < 3; i++) cin >> vec.at(i);
    cout << vec.at(0) * vec.at(1) * vec.at(2) << endl; // 128
}
```

    Writing temp.cpp

```python
!g++ temp.cpp; echo 2 8 8 |./a.out #=> 128
```

    128

```python
# 問題 2.1.4.1 手計算で 0b1001 を 10 進法に変換する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;
int main () {
    cout << 0b1001 << endl; //=> 9
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    9

```python
# 問題 2.1.4.2 手計算で 127 を 2 進法、3 進法に変換する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

string convert10tob(int num, int base) {
    if (num == 0) return "";
    else return convert10tob(num / base, base) + to_string(num % base);
}
int main () {
    int num = 127;
    cout << convert10tob(num, 2) << endl; // 1111111
    cout << convert10tob(num, 3) << endl; // 11201
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

```python
# p.30
# 問題 2.2.1 1 万, 1 億、1 兆はそれぞれ 10 の何乗ですか => 4, 8, 12 なのだが
# 関連して思いついた問題 アボガドロ定数（6.02×10^23）を日本語で位取りして示せ
# 京が 16 乗、垓が 20 乗なので 600 垓
```

    Overwriting temp.cpp

```python
# 問題 2.2.2.1 sqrt(841) と 29^2 を計算せよ
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int main () {
    cout << pow(841, 0.5) << endl; // 29
    cout << pow(29, 2) << endl; // 841
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    29
    841

```python
# 問題 2.2.2.2 sqrt{5}{1024} (5 乗根) 、 4^5 を計算する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int main () {
    cout << pow(1024, 0.2) << endl; // 4
    cout << pow(4, 5) << endl; // 1024
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    4
    1024

```python
# 問題 2.2.3.1 13 AND 14, 13 OR 14, 13 XOR 14 をそれぞれ計算する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int main () {
    int a = 13, b = 14;
    cout << bitset<8>(a) << ", " << bitset<8>(b) << endl;
    cout << (a & b) << endl; // 12
    cout << (a | b) << endl; // 15
    cout << (a ^ b) << endl; // 3
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    00001101, 00001110
    12
    15
    3

```python
# 問題 2.2.4 n 個の整数の合計を 100 で割った mod を出力する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
	int n = 3;
	vector<int> vec{3, 7, 8};
	int ans = 0;

	ans = accumulate (vec.begin(), vec.end(), 0, plus<>());

	cout << ans % 100 << endl;
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    18

```python
# p.39
# 問題  2.3.1 f(x) = x^3 の時、 f(11), f(5), f(10) を求める
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int f (int x) {return pow(x, 3);}

int main() {
	cout << f (11) << endl;
	cout << f (5) << endl;
	cout << f (10) << endl;
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    1331
    125
    1000

```python
# 問題 2.3.2.1 log_2 {8} を計算する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    cout << log2(8) << endl;
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    3

```python
# 問題 2.3.2.2 100^1.5 を計算する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    cout << pow(100, 1.5) << endl;
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    1000

```python
# 問題 2.3.2.3 floor \left\lfloor 20.21 \right\rfloor, ceil \left\lceil 20.21 \right\rceil を求める
%%latex
 \left\lfloor 20.21 \right\rfloor,  \left\lceil 20.21 \right\rceil
```

 \left\lfloor 20.21 \right\rfloor,  \left\lceil 20.21 \right\rceil

```python
# 問題 2.3.2.3 ⌊20.21⌋, ⌈20.21⌉ を求める
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    cout << floor(20.21) << endl;
    cout << ceil(20.21) << endl;
}
```

```python
!g++ temp.cpp; echo |./a.out
```

    20
    21

```python
# 問題 2.3.3.1 y = 2 * x + 3 のグラフを描く
from sympy.abc import *
from sympy import *
y = 2 * x + 3
p = plot(y);
```

```python
# 問題 2.3.3.2 y = 10 ^ x のグラフを描く
from sympy.abc import *
from sympy import *
y = 10 ** x
p = plot(y,(x,-1,1.2));
```

```python
# 問題 2.3.3.3 y = log_4 {x} のグラフを描く
from sympy.abc import *
from sympy import *
y = log(x)/log(4)
p = plot(y, (x,0.1,4.2));
```

```python
# 問題 2.3.3.4 y = log_2 {x} / 2 のグラフを描く
from sympy.abc import *
from sympy import *
y = log(x)/log(2) / 2
p = plot(y, (x,0.1,4.2));
```

```python
# 問題 2.3.4.1 f(x) = 2^x とする時、 f(20) の値を計算する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

double f (double x) {return pow(2, x);}

int main() {
    cout << f(20) << endl;
}
```

    Writing temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    1.04858e+06

```python
# 問題 2.3.4.2 2^10 = 1024 は 1000 と近いことから 2^20 の値がおよそ 10^6 であることを確認する
# 2^20 => 2^10 * 2^10 => おおよそ 1000 * 1000 => 10^6
```

```python
# 問題 2.3.5.1 関数 g(x) = log_10{x} とする時、g(1000000) の値を計算する
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

double g (double x) {return (log(x)/log(10));}

int main() {
    cout << g(1000000) << endl; // 6
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    6

```python
# 問題 2.3.5.2 対数関係の公式を使って、log_2{16N} - log_2{N} の値を計算する。
# ただし、N は正の整数とする
# 与式 = log_2{16} + log_2{N} - log_2{N}
# = 4
```

```python
# 問題 2.3.6 マグニチュードが 1 増えるたびにエネルギーは 32 倍 (正確には sqrt{1000} 倍) になる
# ここでは 32 倍ということにする。その時、
# マグニチュード 6.0 は 5.0 の何倍か => 32 倍
# マグニチュード 7.3 は 5.3 の何倍か => 32*32 => 1024 倍
# マグニチュード 9.0 は 7.2 の何倍か => 32^1.8 => 512 倍
```

```python
# 問題 2.3.7 正の整数 x を 2 進法で表した時の桁数を y とします。 y を x の式で表せ
# 1 桁 => {0, 1}, 2 => {2, 3}, 3 => {4, 5, 6, 7}. 4 => {8, 9, 10, 11, 12, 13, 14, 15}
# y = floor(log_2{x}) + 1
```

```python
# 問題 2.3.7 次の条件を満たす関数 f(x) を 1 つ見つける
# * すべての実数 a について、0 < f(a) < 1 である
# * f(x) は単調増加な関数である
# f(x) = 1 / {1 + 2^{-1}}
# シグモイド関数
# など
```

```python
# p.53
# 問題 2.4.1.1 計算量を O 記法で表す T_1(N) = 2021 N^3 + 1225 N^2 => O(N^3)
# 問題 2.4.1.2 計算量を O 記法で表す T_2(N) = 4 N + log N => O(N)
# 問題 2.4.1.3 計算量を O 記法で表す T_3(N) = 2^N + N^100 => O(2^N)
# 問題 2.4.1.4 計算量を O 記法で表す T_4(N) = N! + 100^N => O(N!)
```

# いまここ

```python
# 問題 2.4.2 次のプログラムの計算量を O 記法で表す => O(N^2)
# for (int i = 1; i <= N; i++) {
#     for (int j = 1; j <= N * 100; j++) {
#         cout << i << " " << j << endl;
#     }
# }
```

```python
# 問題 2.4.3 log_2{N} と log_10{N} は高々定数倍の違いしかない事を証明する
# log_2{N} / log_2{10} = log_10{N}  => 証明終わり
```

```python
# 問題 2.4.4 次の表を完成させる
%%html
<table border="1">
<thead>
<tr><th>計算回数</th><th>実行時間目安</th><th>N log N</th><th>N^2</th><th>2^N</th></tr>
</thead>
<tbody>
<tr><td>10^6 回以内</td><td>0.001 秒以下</td><td>N <= 60000</td><td>N <= 1000</td><td>N <= 20</td></tr>
<tr><td>10^7 回以内</td><td>0.01 秒以下</td><td>N <= 500000</td><td>N <= 3000</td><td>N <= 23</td></tr>
<tr><td>10^8 回以内</td><td>0.1 秒以下</td><td>N <= 4000000</td><td>N <= 10000</td><td>N <= 26</td></tr>
<tr><td>10^9 回以内</td><td>1 秒以下</td><td>N <= 40000000</td><td>N <= 30000</td><td>N <= 30</td></tr>
</tbody>
</table>
```

計算回数	実行時間目安	N log N	N^2	2^N

10^6 回以内	0.001 秒以下	N <= 60000	N <= 1000	N <= 20

10^7 回以内	0.01 秒以下	N <= 500000	N <= 3000	N <= 23

10^8 回以内	0.1 秒以下	N <= 4000000	N <= 10000	N <= 26

10^9 回以内	1 秒以下	N <= 40000000	N <= 30000	N <= 30

```python
# 問題 2.4.5 アルゴリズム D を実装したプログラムを N = 10, 12, 14, 16, 18, 20 で実行してみたところ
# 実行時間は 0.001 秒、0.006 秒、0.049 秒、0.447 秒、4.025 秒、36.189 秒になった。
# アルゴリズム D の計算量はどの程度か。
# => 下記のプログラムで結果のオーダーは合っているので O(3^N) でどうか。
%%writefile temp.cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    vector<double> vec = {10,12,14,16,18,20};
    vector<double> result;
    transform(vec.begin(), vec.end(), back_inserter(result), [](double x){return 0.001 * pow(3,x - 10);});
    for (int i = 0; i < result.size(); i++) {
        if (i != 0) cout << ", ";
        cout << result.at(i);
    }
    cout << endl;
}
```

    Overwriting temp.cpp

```python
!g++ temp.cpp; echo |./a.out
```

    0.001, 0.009, 0.081, 0.729, 6.561, 59.049

```python
# 問題 2.4.6 100000 程度の単語が辞書順に並べられている時、
# どのような方法を使えば効率的に単語を検索できるか考える
# => 二分探索法
```

```python
# p.62
# 問題 2.5.1 次の 2 つの値をそれぞれ計算する
%%latex
\displaystyle
\sum _{i = 1}^{100} i  \\
\\
\sum _{i = 1}^3 \sum _{j = 1}^3 ij
```

\displaystyle
\sum _{i = 1}^{100} i  \\
\\
\sum _{i = 1}^3 \sum _{j = 1}^3 ij

# EOF