From b16fc68910c3eb5831664d84de75d8a177ec46a2 Mon Sep 17 00:00:00 2001 From: Spiros Chavlis Date: Mon, 3 Jul 2023 14:13:07 +0300 Subject: [PATCH 1/7] Update _config.yml --- book/_config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/book/_config.yml b/book/_config.yml index d12c700..94ea7f4 100644 --- a/book/_config.yml +++ b/book/_config.yml @@ -30,6 +30,7 @@ parse: - amsmath - dollarmath - html_image + - substitution sphinx: config: html_show_copyright: false From 9b37a88d0f312e099770c7cadbbf0353dfc2af8c Mon Sep 17 00:00:00 2001 From: Spiros Chavlis Date: Mon, 3 Jul 2023 14:23:16 +0300 Subject: [PATCH 2/7] fixes --- .../W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb | 5420 +++++++++-------- 1 file changed, 2750 insertions(+), 2670 deletions(-) diff --git a/tutorials/W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb b/tutorials/W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb index f15b7ab..04c378a 100644 --- a/tutorials/W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb +++ b/tutorials/W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb @@ -1,2671 +1,2751 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "execution": {}, - "id": "view-in-github" - }, - "source": [ - "\"Open   \"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "# Tutorial 1: LIF Neuron Part II\n", - "\n", - "**Week 0, Day 2: Python Workshop 2**\n", - "\n", - "**By Neuromatch Academy**\n", - "\n", - "**Content creators:** Marco Brigham and the [CCNSS](https://www.ccnss.org/) team\n", - "\n", - "**Content reviewers:** Michael Waskom, Karolina Stosio, Spiros Chavlis\n", - "\n", - "**Production editors:** Ella Batty, Spiros Chavlis" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "## Tutorial objectives\n", - "We learned basic Python and NumPy concepts in the previous tutorial. These new and efficient coding techniques can be applied repeatedly in tutorials from the NMA course, and elsewhere.\n", - "\n", - "In this tutorial, we'll introduce spikes in our LIF neuron and evaluate the refractory period's effect in spiking dynamics!\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D2_T1\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "both", - "execution": {} - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Figure settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import ipywidgets as widgets # interactive display\n", - "%config InlineBackend.figure_format = 'retina'\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Helper functions\n", - "\n", - "t_max = 150e-3 # second\n", - "dt = 1e-3 # second\n", - "tau = 20e-3 # second\n", - "el = -60e-3 # milivolt\n", - "vr = -70e-3 # milivolt\n", - "vth = -50e-3 # milivolt\n", - "r = 100e6 # ohm\n", - "i_mean = 25e-11 # ampere\n", - "\n", - "\n", - "def plot_all(t_range, v, raster=None, spikes=None, spikes_mean=None):\n", - " \"\"\"\n", - " Plots Time evolution for\n", - " (1) multiple realizations of membrane potential\n", - " (2) spikes\n", - " (3) mean spike rate (optional)\n", - "\n", - " Args:\n", - " t_range (numpy array of floats)\n", - " range of time steps for the plots of shape (time steps)\n", - "\n", - " v (numpy array of floats)\n", - " membrane potential values of shape (neurons, time steps)\n", - "\n", - " raster (numpy array of floats)\n", - " spike raster of shape (neurons, time steps)\n", - "\n", - " spikes (dictionary of lists)\n", - " list with spike times indexed by neuron number\n", - "\n", - " spikes_mean (numpy array of floats)\n", - " Mean spike rate for spikes as dictionary\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - "\n", - " v_mean = np.mean(v, axis=0)\n", - " fig_w, fig_h = plt.rcParams['figure.figsize']\n", - " plt.figure(figsize=(fig_w, 1.5 * fig_h))\n", - "\n", - " ax1 = plt.subplot(3, 1, 1)\n", - " for j in range(n):\n", - " plt.scatter(t_range, v[j], color=\"k\", marker=\".\", alpha=0.01)\n", - " plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", - " plt.xticks([])\n", - " plt.ylabel(r'$V_m$ (V)')\n", - "\n", - " if raster is not None:\n", - " plt.subplot(3, 1, 2)\n", - " spikes_mean = np.mean(raster, axis=0)\n", - " plt.imshow(raster, cmap='Greys', origin='lower', aspect='auto')\n", - "\n", - " else:\n", - " plt.subplot(3, 1, 2, sharex=ax1)\n", - " for j in range(n):\n", - " times = np.array(spikes[j])\n", - " plt.scatter(times, j * np.ones_like(times), color=\"C0\", marker=\".\", alpha=0.2)\n", - "\n", - " plt.xticks([])\n", - " plt.ylabel('neuron')\n", - "\n", - " if spikes_mean is not None:\n", - " plt.subplot(3, 1, 3, sharex=ax1)\n", - " plt.plot(t_range, spikes_mean)\n", - " plt.xlabel('time (s)')\n", - " plt.ylabel('rate (Hz)')\n", - "\n", - " plt.tight_layout()\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 1: Histograms\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 1: Histograms\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'J24tne-IwvY'), ('Bilibili', 'BV1GC4y1h7Ex')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Histograms_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "
\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "Another important statistic is the sample [histogram](https://en.wikipedia.org/wiki/Histogram). For our LIF neuron it provides an approximate representation of the distribution of membrane potential $V_m(t)$ at time $t=t_k\\in[0,t_{max}]$. For $N$ realizations $V\\left(t_k\\right)$ and $J$ bins is given by:\n", - "\n", - "
\n", - "\\begin{equation}\n", - "N = \\sum_{j=1}^{J} m_j\n", - "\\end{equation}\n", - "
\n", - "\n", - "where $m_j$ is a function that counts the number of samples $V\\left(t_k\\right)$ that fall into bin $j$.\n", - "\n", - "The function `plt.hist(data, nbins)` plots an histogram of `data` in `nbins` bins. The argument `label` defines a label for `data` and `plt.legend()` adds all labels to the plot.\n", - "\n", - "```python\n", - "plt.hist(data, bins, label='my data')\n", - "plt.legend()\n", - "plt.show()\n", - "```\n", - "\n", - "The parameters `histtype='stepfilled'` and `linewidth=0` may improve histogram appearance (depending on taste). You can read more about [different histtype settings](https://matplotlib.org/gallery/statistics/histogram_histtypes.html).\n", - "\n", - "The function `plt.hist` returns the `pdf`, `bins`, and `patches` with the histogram bins, the edges of the bins, and the individual patches used to create the histogram.\n", - "\n", - "```python\n", - "pdf, bins, patches = plt.hist(data, bins)\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 2: Nano recap of histograms\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '71f1J98zj80'), ('Bilibili', 'BV1Zv411B7mD')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_histograms_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Coding Exercise 1: Plotting a histogram\n", - "\n", - "Plot an histogram of $J=50$ bins of $N=10000$ realizations of $V(t)$ for $t=t_{max}/10$ and $t=t_{max}$.\n", - "\n", - "We'll make a small correction in the definition of `t_range` to ensure increments of `dt` by using `np.arange` instead of `np.linspace`.\n", - "\n", - "```python\n", - "numpy.arange(start, stop, step)\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "#################################################\n", - "## TODO for students: fill out code to plot histogram ##\n", - "# Fill out code and comment or remove the next line\n", - "raise NotImplementedError(\"Student exercise: You need to plot histogram\")\n", - "#################################################\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize t_range, step_end, n, v_n, i and nbins\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 10000\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "nbins = 32\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r * i[:, step])\n", - "\n", - "# Initialize the figure\n", - "plt.figure()\n", - "plt.ylabel('Frequency')\n", - "plt.xlabel('$V_m$ (V)')\n", - "\n", - "# Plot a histogram at t_max/10 (add labels and parameters histtype='stepfilled' and linewidth=0)\n", - "plt.hist(...)\n", - "\n", - "# Plot a histogram at t_max (add labels and parameters histtype='stepfilled' and linewidth=0)\n", - "plt.hist(...)\n", - "\n", - "# Add legend\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize t_range, step_end, n, v_n, i and nbins\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 10000\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "nbins = 32\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r * i[:, step])\n", - "\n", - "# Initialize the figure\n", - "with plt.xkcd():\n", - " plt.figure()\n", - " plt.ylabel('Frequency')\n", - " plt.xlabel('$V_m$ (V)')\n", - "\n", - " # Plot a histogram at t_max/10 (add labels and parameters histtype='stepfilled' and linewidth=0)\n", - " plt.hist(v_n[:,int(step_end / 10)], nbins,\n", - " histtype='stepfilled', linewidth=0,\n", - " label = 't='+ str(t_max / 10) + 's')\n", - "\n", - " # Plot a histogram at t_max (add labels and parameters histtype='stepfilled' and linewidth=0)\n", - " plt.hist(v_n[:, -1], nbins,\n", - " histtype='stepfilled', linewidth=0,\n", - " label = 't='+ str(t_max) + 's')\n", - " # Add legend\n", - " plt.legend()\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Plotting_a_histogram_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 2: Dictionaries & introducing spikes\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 3: Dictionaries & introducing spikes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'ioKkiukDkNg'), ('Bilibili', 'BV1H54y1q7oS')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Dictionaries_&_introducing_spikes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "A spike takes place whenever $V(t)$ crosses $V_{th}$. In that case, a spike is recorded and $V(t)$ resets to $V_{reset}$ value. This is summarized in the *reset condition*:\n", - "\n", - "\\begin{equation}\n", - "V(t) = V_{reset}\\quad \\text{ if } V(t)\\geq V_{th}\n", - "\\end{equation}\n", - "\n", - "For more information about spikes or action potentials see [here](https://en.wikipedia.org/wiki/Action_potential) and [here](https://www.khanacademy.org/test-prep/mcat/organ-systems/neuron-membrane-potentials/a/neuron-action-potentials-the-creation-of-a-brain-signal).\n", - "\n", - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 4: Nano recap of dictionaries\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'nvpHtzuZggg'), ('Bilibili', 'BV1GC4y1h7hi')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_dictionaries_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Coding Exercise 2: Adding spiking to the LIF neuron\n", - "\n", - "Insert the reset condition, and collect the spike times of each realization in a dictionary variable `spikes`, with $N=500$.\n", - "\n", - "We've used `plt.plot` for plotting lines and also for plotting dots at `(x,y)` coordinates, which is a [scatter plot](https://en.wikipedia.org/wiki/Scatter_plot). From here on, we'll use use `plt.plot` for plotting lines and for scatter plots: `plt.scatter`.\n", - "\n", - "```python\n", - "plt.scatter(x, y, color=\"k\", marker=\".\")\n", - "```\n", - "\n", - "A *raster plot* represents spikes from multiple neurons by plotting dots at spike times from neuron `j` at plot height `j`, i.e.\n", - "\n", - "```python\n", - "plt.scatter(spike_times, j*np.ones_like(spike_times))\n", - "```\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "In this exercise, we use `plt.subplot` for multiple plots in the same figure. These plots can share the same `x` or `y` axis by specifying the parameter `sharex` or `sharey`. Add `plt.tight_layout()` at the end to automatically adjust subplot parameters to fit the figure area better. Please see the example below for a row of two plots sharing axis `y`.\n", - "\n", - "```python\n", - "# initialize the figure\n", - "plt.figure()\n", - "\n", - "# collect axis of 1st figure in ax1\n", - "ax1 = plt.subplot(1, 2, 1)\n", - "plt.plot(t_range, my_data_left)\n", - "plt.ylabel('ylabel')\n", - "\n", - "# share axis x with 1st figure\n", - "plt.subplot(1, 2, 2, sharey=ax1)\n", - "plt.plot(t_range, my_data_right)\n", - "\n", - "# automatically adjust subplot parameters to figure\n", - "plt.tight_layout()\n", - "plt.show()\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize spikes and spikes_n\n", - "spikes = {j: [] for j in range(n)}\n", - "spikes_n = np.zeros([step_end])\n", - "\n", - "#################################################\n", - "## TODO for students: add spikes to LIF neuron ##\n", - "# Fill out function and remove\n", - "raise NotImplementedError(\"Student exercise: add spikes to LIF neuron\")\n", - "#################################################\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Loop over simulations\n", - " for j in range(n):\n", - "\n", - " # Check if voltage above threshold\n", - " if v_n[j, step] >= vth:\n", - "\n", - " # Reset to reset voltage\n", - " v_n[j, step] = ...\n", - "\n", - " # Add this spike time\n", - " spikes[j] += ...\n", - "\n", - " # Add spike count to this step\n", - " spikes_n[step] += ...\n", - "\n", - "# Collect mean Vm and mean spiking rate\n", - "v_mean = np.mean(v_n, axis=0)\n", - "spikes_mean = spikes_n / n\n", - "\n", - "# Initialize the figure\n", - "plt.figure()\n", - "\n", - "# Plot simulations and sample mean\n", - "ax1 = plt.subplot(3, 1, 1)\n", - "for j in range(n):\n", - " plt.scatter(t_range, v_n[j], color=\"k\", marker=\".\", alpha=0.01)\n", - "plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", - "plt.ylabel('$V_m$ (V)')\n", - "\n", - "# Plot spikes\n", - "plt.subplot(3, 1, 2, sharex=ax1)\n", - "# for each neuron j: collect spike times and plot them at height j\n", - "for j in range(n):\n", - " times = ...\n", - " plt.scatter(...)\n", - "\n", - "plt.ylabel('neuron')\n", - "\n", - "# Plot firing rate\n", - "plt.subplot(3, 1, 3, sharex=ax1)\n", - "plt.plot(t_range, spikes_mean)\n", - "plt.xlabel('time (s)')\n", - "plt.ylabel('rate (Hz)')\n", - "\n", - "plt.tight_layout()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize spikes and spikes_n\n", - "spikes = {j: [] for j in range(n)}\n", - "spikes_n = np.zeros([step_end])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Loop over simulations\n", - " for j in range(n):\n", - "\n", - " # Check if voltage above threshold\n", - " if v_n[j, step] >= vth:\n", - "\n", - " # Reset to reset voltage\n", - " v_n[j, step] = vr\n", - "\n", - " # Add this spike time\n", - " spikes[j] += [t]\n", - "\n", - " # Add spike count to this step\n", - " spikes_n[step] += 1\n", - "\n", - "# Collect mean Vm and mean spiking rate\n", - "v_mean = np.mean(v_n, axis=0)\n", - "spikes_mean = spikes_n / n\n", - "\n", - "with plt.xkcd():\n", - " # Initialize the figure\n", - " plt.figure()\n", - "\n", - " # Plot simulations and sample mean\n", - " ax1 = plt.subplot(3, 1, 1)\n", - " for j in range(n):\n", - " plt.scatter(t_range, v_n[j], color=\"k\", marker=\".\", alpha=0.01)\n", - " plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", - " plt.ylabel('$V_m$ (V)')\n", - "\n", - " # Plot spikes\n", - " plt.subplot(3, 1, 2, sharex=ax1)\n", - " # for each neuron j: collect spike times and plot them at height j\n", - " for j in range(n):\n", - " times = np.array(spikes[j])\n", - " plt.scatter(times, j * np.ones_like(times), color=\"C0\", marker=\".\", alpha=0.2)\n", - "\n", - " plt.ylabel('neuron')\n", - "\n", - " # Plot firing rate\n", - " plt.subplot(3, 1, 3, sharex=ax1)\n", - " plt.plot(t_range, spikes_mean)\n", - " plt.xlabel('time (s)')\n", - " plt.ylabel('rate (Hz)')\n", - "\n", - " plt.tight_layout()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Adding_spiking_to_the_LIF_neuron_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 3: Boolean indexes\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 5: Boolean indexes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'G0C1v848I9Y'), ('Bilibili', 'BV1W54y1q7eh')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Boolean_indexes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "Numpy arrays can be indexed by boolean arrays to select a subset of elements (also works with lists of booleans).\n", - "\n", - "The boolean array itself can be initiated by a condition, as shown in the example below.\n", - "\n", - "```python\n", - "a = np.array([1, 2, 3])\n", - "b = a>=2\n", - "print(b)\n", - "--> [False True True]\n", - "\n", - "print(a[b])\n", - "--> [2 3]\n", - "\n", - "print(a[a>=2])\n", - "--> [2 3]\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 6: Nano recap of Boolean indexes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'dFPgO5wnyLc'), ('Bilibili', 'BV1W54y1q7gi')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_Boolean_indexes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Coding Exercise 3: Using Boolean indexing\n", - "\n", - "We can avoid looping all neurons in each time step by identifying with boolean arrays the indexes of neurons that spiked in the previous step.\n", - "\n", - "In the example below, `v_rest` is a boolean numpy array with `True` in each index of `v_n` with value `vr` at time index `step`:\n", - "\n", - "```python\n", - "v_rest = (v_n[:,step] == vr)\n", - "print(v_n[v_rest,step])\n", - " --> [vr, ..., vr]\n", - "```\n", - "\n", - "The function `np.where` returns indexes of boolean arrays with `True` values.\n", - "\n", - "You may use the helper function `plot_all` that implements the figure from the previous exercise." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "help(plot_all)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize spikes and spikes_n\n", - "spikes = {j: [] for j in range(n)}\n", - "spikes_n = np.zeros([step_end])\n", - "\n", - "#################################################\n", - "## TODO for students: use Boolean indexing ##\n", - "# Fill out function and remove\n", - "raise NotImplementedError(\"Student exercise: using Boolean indexing\")\n", - "#################################################\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = ...\n", - "\n", - " # Set relevant values of v_n to resting potential using spiked\n", - " v_n[spiked,step] = ...\n", - "\n", - " # Collect spike times\n", - " for j in np.where(spiked)[0]:\n", - " spikes[j] += [t]\n", - " spikes_n[step] += 1\n", - "\n", - "# Collect mean spiking rate\n", - "spikes_mean = spikes_n / n\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, spikes=spikes, spikes_mean=spikes_mean)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solutions\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize spikes and spikes_n\n", - "spikes = {j: [] for j in range(n)}\n", - "spikes_n = np.zeros([step_end])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to resting potential using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Collect spike times\n", - " for j in np.where(spiked)[0]:\n", - " spikes[j] += [t]\n", - " spikes_n[step] += 1\n", - "\n", - "# Collect mean spiking rate\n", - "spikes_mean = spikes_n / n\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, spikes=spikes, spikes_mean=spikes_mean)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Using_Boolean_indexing_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Coding Exercise 4: Making a binary raster plot\n", - "\n", - "A *binary raster plot* represents spike times as `1`s in a binary grid initialized with `0`s. We start with a numpy array `raster` of zeros with shape `(neurons, time steps)`, and represent a spike from neuron `5` at time step `20` as `raster(5,20)=1`, for example.\n", - "\n", - "The *binary raster plot* is much more efficient than the previous method by plotting the numpy array `raster` as an image:\n", - "\n", - "```python\n", - "plt.imshow(raster, cmap='Greys', origin='lower', aspect='auto')\n", - "```\n", - "\n", - "**Suggestions**\n", - "* At each time step:\n", - " * Initialize boolean numpy array `spiked` with $V_n(t)\\geq V_{th}$\n", - " * Set to `vr` indexes of `v_n` using `spiked`\n", - " * Set to `1` indexes of numpy array `raster` using `spiked`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "#################################################\n", - "## TODO for students: make a raster ##\n", - "# Fill out function and remove\n", - "raise NotImplementedError(\"Student exercise: make a raster \")\n", - "#################################################\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to v_reset using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Set relevant elements in raster to 1 using spiked\n", - " raster[spiked,step] = ...\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to v_reset using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Set relevant elements in raster to 1 using spiked\n", - " raster[spiked,step] = 1.\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Making_a_binary_raster_plot_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 4: Refractory period\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 7: Refractory period\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'KVNdbRY5-nY'), ('Bilibili', 'BV1MT4y1E79j')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Refractory_period_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "The absolute refractory period is a time interval in the order of a few milliseconds during which synaptic input will not lead to a 2nd spike, no matter how strong. This effect is due to the biophysics of the neuron membrane channels, and you can read more about absolute and relative refractory period [here](https://content.byui.edu/file/a236934c-3c60-4fe9-90aa-d343b3e3a640/1/module5/readings/refractory_periods.html) and [here](https://en.wikipedia.org/wiki/Refractory_period_(physiology)).\n", - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 8: Nano recap of refractory period\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'DOoftC0JU2k'), ('Bilibili', 'BV1pA411e7je')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_refractory_period_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Coding Exercise 5: Investigating refactory periods\n", - "Investigate the effect of (absolute) refractory period $t_{ref}$ on the evolution of output rate $\\lambda(t)$. Add refractory period $t_{ref}=10$ ms after each spike, during which $V(t)$ is clamped to $V_{reset}$.\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "#################################################\n", - "## TODO for students: add refactory period ##\n", - "# Fill out function and remove\n", - "raise NotImplementedError(\"Student exercise: add refactory period \")\n", - "#################################################\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "t_ref = 0.01\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to v_reset using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Set relevant elements in raster to 1 using spiked\n", - " raster[spiked,step] = 1.\n", - "\n", - " # Initialize boolean numpy array clamped using last_spike, t and t_ref\n", - " clamped = ...\n", - "\n", - " # Reset clamped neurons to vr using clamped\n", - " v_n[clamped,step] = ...\n", - "\n", - " # Update numpy array last_spike with time t for spiking neurons\n", - " last_spike[spiked] = ...\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "t_ref = 0.01\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to v_reset using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Set relevant elements in raster to 1 using spiked\n", - " raster[spiked,step] = 1.\n", - "\n", - " # Initialize boolean numpy array clamped using last_spike, t and t_ref\n", - " clamped = (last_spike + t_ref > t)\n", - "\n", - " # Reset clamped neurons to vr using clamped\n", - " v_n[clamped,step] = vr\n", - "\n", - " # Update numpy array last_spike with time t for spiking neurons\n", - " last_spike[spiked] = t\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Investigating_refractory_periods_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Interactive Demo 1: Random refractory period\n", - "In the following interactive demo, we will investigate the effect of random refractory periods. We will use random refactory periods $t_{ref}$ with\n", - "$t_{ref} = \\mu + \\sigma\\,\\mathcal{N}$, where $\\mathcal{N}$ is the [normal distribution](https://en.wikipedia.org/wiki/Normal_distribution), $\\mu=0.01$ and $\\sigma=0.007$.\n", - "\n", - "Refractory period samples `t_ref` of size `n` is initialized with `np.random.normal`. We clip negative values to `0` with boolean indexes. (Why?) You can double click the cell to see the hidden code.\n", - "\n", - "You can play with the parameters mu and sigma and visualize the resulting simulation.\n", - "What is the effect of different $\\sigma$ values?\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to enable the demo\n", - "\n", - "def random_ref_period(mu, sigma):\n", - " # set random number generator\n", - " np.random.seed(2020)\n", - "\n", - " # initialize step_end, t_range, n, v_n, syn and raster\n", - " t_range = np.arange(0, t_max, dt)\n", - " step_end = len(t_range)\n", - " n = 500\n", - " v_n = el * np.ones([n,step_end])\n", - " syn = i_mean * (1 + 0.1*(t_max/dt)**(0.5)*(2*np.random.random([n,step_end])-1))\n", - " raster = np.zeros([n,step_end])\n", - "\n", - " # initialize t_ref and last_spike\n", - " t_ref = mu + sigma*np.random.normal(size=n)\n", - " t_ref[t_ref<0] = 0\n", - " last_spike = -t_ref * np.ones([n])\n", - "\n", - " # loop time steps\n", - " for step, t in enumerate(t_range):\n", - " if step==0:\n", - " continue\n", - "\n", - " v_n[:,step] = v_n[:,step-1] + dt/tau * (el - v_n[:,step-1] + r*syn[:,step])\n", - "\n", - " # boolean array spiked indexes neurons with v>=vth\n", - " spiked = (v_n[:,step] >= vth)\n", - " v_n[spiked,step] = vr\n", - " raster[spiked,step] = 1.\n", - "\n", - " # boolean array clamped indexes refractory neurons\n", - " clamped = (last_spike + t_ref > t)\n", - " v_n[clamped,step] = vr\n", - " last_spike[spiked] = t\n", - "\n", - " # plot multiple realizations of Vm, spikes and mean spike rate\n", - " plot_all(t_range, v_n, raster)\n", - "\n", - " # plot histogram of t_ref\n", - " plt.figure(figsize=(8,4))\n", - " plt.hist(t_ref, bins=32, histtype='stepfilled', linewidth=0, color='C1')\n", - " plt.xlabel(r'$t_{ref}$ (s)')\n", - " plt.ylabel('count')\n", - " plt.tight_layout()\n", - "\n", - "_ = widgets.interact(random_ref_period, mu = (0.01, 0.05, 0.01), \\\n", - " sigma = (0.001, 0.02, 0.001))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Random_refractory_period_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 5: Using functions\n", - "Running key parts of your code inside functions improves your coding narrative by making it clearer and more flexible to future changes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 9: Functions\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'mkf8riqCjS4'), ('Bilibili', 'BV1sa4y1a7pq')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Functions_Video\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 10: Nano recap of functions\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '0An_NnVWY_Q'), ('Bilibili', 'BV1pz411v74H')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_functions_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Coding Exercise 6: Rewriting code with functions\n", - "We will now re-organize parts of the code from the previous exercise with functions. You need to complete the function `spike_clamp()` to update $V(t)$ and deal with spiking and refractoriness" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "def ode_step(v, i, dt):\n", - " \"\"\"\n", - " Evolves membrane potential by one step of discrete time integration\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential at previous time step of shape (neurons)\n", - "\n", - " v (numpy array of floats)\n", - " synaptic input at current time step of shape (neurons)\n", - "\n", - " dt (float)\n", - " time step increment\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential at current time step of shape (neurons)\n", - " \"\"\"\n", - " v = v + dt/tau * (el - v + r*i)\n", - "\n", - " return v\n", - "\n", - "def spike_clamp(v, delta_spike):\n", - " \"\"\"\n", - " Resets membrane potential of neurons if v>= vth\n", - " and clamps to vr if interval of time since last spike < t_ref\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - "\n", - " delta_spike (numpy array of floats)\n", - " interval of time since last spike of shape (neurons)\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - " spiked (numpy array of floats)\n", - " boolean array of neurons that spiked of shape (neurons)\n", - " \"\"\"\n", - "\n", - " ####################################################\n", - " ## TODO for students: complete spike_clamp\n", - " # Fill out function and remove\n", - " raise NotImplementedError(\"Student exercise: complete spike_clamp\")\n", - " ####################################################\n", - " # Boolean array spiked indexes neurons with v>=vth\n", - " spiked = ...\n", - " v[spiked] = ...\n", - "\n", - " # Boolean array clamped indexes refractory neurons\n", - " clamped = ...\n", - " v[clamped] = ...\n", - "\n", - " return v, spiked\n", - "\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "mu = 0.01\n", - "sigma = 0.007\n", - "t_ref = mu + sigma*np.random.normal(size=n)\n", - "t_ref[t_ref<0] = 0\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", - "\n", - " # Reset membrane potential and clamp\n", - " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", - "\n", - " # Update raster and last_spike\n", - " raster[spiked,step] = 1.\n", - " last_spike[spiked] = t\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "def ode_step(v, i, dt):\n", - " \"\"\"\n", - " Evolves membrane potential by one step of discrete time integration\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential at previous time step of shape (neurons)\n", - "\n", - " v (numpy array of floats)\n", - " synaptic input at current time step of shape (neurons)\n", - "\n", - " dt (float)\n", - " time step increment\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential at current time step of shape (neurons)\n", - " \"\"\"\n", - " v = v + dt/tau * (el - v + r*i)\n", - "\n", - " return v\n", - "\n", - "# to_remove solution\n", - "def spike_clamp(v, delta_spike):\n", - " \"\"\"\n", - " Resets membrane potential of neurons if v>= vth\n", - " and clamps to vr if interval of time since last spike < t_ref\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - "\n", - " delta_spike (numpy array of floats)\n", - " interval of time since last spike of shape (neurons)\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - " spiked (numpy array of floats)\n", - " boolean array of neurons that spiked of shape (neurons)\n", - " \"\"\"\n", - "\n", - " # Boolean array spiked indexes neurons with v>=vth\n", - " spiked = (v >= vth)\n", - " v[spiked] = vr\n", - "\n", - " # Boolean array clamped indexes refractory neurons\n", - " clamped = (t_ref > delta_spike)\n", - " v[clamped] = vr\n", - "\n", - " return v, spiked\n", - "\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "mu = 0.01\n", - "sigma = 0.007\n", - "t_ref = mu + sigma*np.random.normal(size=n)\n", - "t_ref[t_ref<0] = 0\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", - "\n", - " # Reset membrane potential and clamp\n", - " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", - "\n", - " # Update raster and last_spike\n", - " raster[spiked,step] = 1.\n", - " last_spike[spiked] = t\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Rewriting_code_with_functions_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 6: Using classes\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 11: Classes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'dGRESMoNPh0'), ('Bilibili', 'BV1hz411v7ne')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Classes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "Using classes helps with code reuse and reliability. Well-designed classes are like black boxes in that they receive inputs and provide expected outputs. The details of how the class processes inputs and produces outputs are unimportant.\n", - "\n", - "See additional details here: [A Beginner's Python Tutorial/Classes](https://en.wikibooks.org/wiki/A_Beginner%27s_Python_Tutorial/Classes)\n", - "\n", - "*Attributes* are variables internal to the class, and *methods* are functions internal to the class." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 12: Nano recap of classes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '4YNpMpVW2qs'), ('Bilibili', 'BV12V41167yu')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_classes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Coding Exercise 7: Making a LIF class\n", - "In this exercise we'll practice with Python classes by implementing `LIFNeurons`, a class that evolves and keeps state of multiple realizations of LIF neurons.\n", - "\n", - "Several attributes are used to keep state of our neurons:\n", - "\n", - "```python\n", - "self.v current membrane potential\n", - "self.spiked neurons that spiked\n", - "self.last_spike last spike time of each neuron\n", - "self.t running time of the simulation\n", - "self.steps simulation step\n", - "```\n", - "\n", - "There is a single method:\n", - "\n", - "```python\n", - "self.ode_step() performs single step discrete time integration\n", - " for provided synaptic current and dt\n", - "```\n", - "\n", - "Complete the spike and clamp part of method `self.ode_step` (should be similar to function `spike_and_clamp` seen before)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# Simulation class\n", - "class LIFNeurons:\n", - " \"\"\"\n", - " Keeps track of membrane potential for multiple realizations of LIF neuron,\n", - " and performs single step discrete time integration.\n", - " \"\"\"\n", - " def __init__(self, n, t_ref_mu=0.01, t_ref_sigma=0.002,\n", - " tau=20e-3, el=-60e-3, vr=-70e-3, vth=-50e-3, r=100e6):\n", - "\n", - " # Neuron count\n", - " self.n = n\n", - "\n", - " # Neuron parameters\n", - " self.tau = tau # second\n", - " self.el = el # milivolt\n", - " self.vr = vr # milivolt\n", - " self.vth = vth # milivolt\n", - " self.r = r # ohm\n", - "\n", - " # Initializes refractory period distribution\n", - " self.t_ref_mu = t_ref_mu\n", - " self.t_ref_sigma = t_ref_sigma\n", - " self.t_ref = self.t_ref_mu + self.t_ref_sigma * np.random.normal(size=self.n)\n", - " self.t_ref[self.t_ref<0] = 0\n", - "\n", - " # State variables\n", - " self.v = self.el * np.ones(self.n)\n", - " self.spiked = self.v >= self.vth\n", - " self.last_spike = -self.t_ref * np.ones([self.n])\n", - " self.t = 0.\n", - " self.steps = 0\n", - "\n", - "\n", - " def ode_step(self, dt, i):\n", - "\n", - " # Update running time and steps\n", - " self.t += dt\n", - " self.steps += 1\n", - "\n", - " # One step of discrete time integration of dt\n", - " self.v = self.v + dt / self.tau * (self.el - self.v + self.r * i)\n", - "\n", - " ####################################################\n", - " ## TODO for students: complete the `ode_step` method\n", - " # Fill out function and remove\n", - " raise NotImplementedError(\"Student exercise: complete the ode_step method\")\n", - " ####################################################\n", - "\n", - " # Spike and clamp\n", - " self.spiked = ...\n", - " self.v[self.spiked] = ...\n", - " self.last_spike[self.spiked] = ...\n", - " clamped = ...\n", - " self.v[clamped] = ...\n", - "\n", - " self.last_spike[self.spiked] = ...\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize neurons\n", - "neurons = LIFNeurons(n)\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Call ode_step method\n", - " neurons.ode_step(dt, i[:,step])\n", - "\n", - " # Log v_n and spike history\n", - " v_n[:,step] = neurons.v\n", - " raster[neurons.spiked, step] = 1.\n", - "\n", - "# Report running time and steps\n", - "print(f'Ran for {neurons.t:.3}s in {neurons.steps} steps.')\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Simulation class\n", - "class LIFNeurons:\n", - " \"\"\"\n", - " Keeps track of membrane potential for multiple realizations of LIF neuron,\n", - " and performs single step discrete time integration.\n", - " \"\"\"\n", - " def __init__(self, n, t_ref_mu=0.01, t_ref_sigma=0.002,\n", - " tau=20e-3, el=-60e-3, vr=-70e-3, vth=-50e-3, r=100e6):\n", - "\n", - " # Neuron count\n", - " self.n = n\n", - "\n", - " # Neuron parameters\n", - " self.tau = tau # second\n", - " self.el = el # milivolt\n", - " self.vr = vr # milivolt\n", - " self.vth = vth # milivolt\n", - " self.r = r # ohm\n", - "\n", - " # Initializes refractory period distribution\n", - " self.t_ref_mu = t_ref_mu\n", - " self.t_ref_sigma = t_ref_sigma\n", - " self.t_ref = self.t_ref_mu + self.t_ref_sigma * np.random.normal(size=self.n)\n", - " self.t_ref[self.t_ref<0] = 0\n", - "\n", - " # State variables\n", - " self.v = self.el * np.ones(self.n)\n", - " self.spiked = self.v >= self.vth\n", - " self.last_spike = -self.t_ref * np.ones([self.n])\n", - " self.t = 0.\n", - " self.steps = 0\n", - "\n", - "\n", - " def ode_step(self, dt, i):\n", - "\n", - " # Update running time and steps\n", - " self.t += dt\n", - " self.steps += 1\n", - "\n", - " # One step of discrete time integration of dt\n", - " self.v = self.v + dt / self.tau * (self.el - self.v + self.r * i)\n", - "\n", - " # Spike and clamp\n", - " self.spiked = (self.v >= self.vth)\n", - " self.v[self.spiked] = self.vr\n", - " self.last_spike[self.spiked] = self.t\n", - " clamped = (self.t_ref > self.t-self.last_spike)\n", - " self.v[clamped] = self.vr\n", - "\n", - " self.last_spike[self.spiked] = self.t\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize neurons\n", - "neurons = LIFNeurons(n)\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Call ode_step method\n", - " neurons.ode_step(dt, i[:,step])\n", - "\n", - " # Log v_n and spike history\n", - " v_n[:,step] = neurons.v\n", - " raster[neurons.spiked, step] = 1.\n", - "\n", - "# Report running time and steps\n", - "print(f'Ran for {neurons.t:.3}s in {neurons.steps} steps.')\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Making_a_LIF_class_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Summary\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 12: Last concepts & recap\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'h4mSJBPocPo'), ('Bilibili', 'BV1MC4y1h7eA')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Last_concepts_&_recap_Video\")" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "include_colab_link": true, - "name": "W0D2_Tutorial1", - "provenance": [], - "toc_visible": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "wFv9HHo6dxsV" + }, + "source": [ + "# Tutorial 1: LIF Neuron Part II\n", + "\n", + "**Week 0, Day 2: Python Workshop 2**\n", + "\n", + "**By Neuromatch Academy**\n", + "\n", + "**Content creators:** Marco Brigham and the [CCNSS](https://www.ccnss.org/) team\n", + "\n", + "**Content reviewers:** Michael Waskom, Karolina Stosio, Spiros Chavlis\n", + "\n", + "**Production editors:** Ella Batty, Spiros Chavlis" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "YrNzsl6VdxsW" + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "nEVfwEutdxsW" + }, + "source": [ + "---\n", + "## Tutorial objectives\n", + "\n", + "We learned basic Python and NumPy concepts in the previous tutorial. These new and efficient coding techniques can be applied repeatedly in tutorials from the NMA course, and elsewhere.\n", + "\n", + "In this tutorial, we'll introduce spikes in our LIF neuron and evaluate the refractory period's effect in spiking dynamics!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "XAg7NafOdxsW" + }, + "source": [ + "---\n", + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "u0mObsJHdxsW" + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D2_T1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "both", + "execution": {}, + "id": "ny3G5i-ndxsX" + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "UmxphSK9dxsX" + }, + "outputs": [], + "source": [ + "# @title Figure settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import ipywidgets as widgets # interactive display\n", + "%config InlineBackend.figure_format = 'retina'\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "eqAqy7_8dxsX" + }, + "outputs": [], + "source": [ + "# @title Helper functions\n", + "\n", + "t_max = 150e-3 # second\n", + "dt = 1e-3 # second\n", + "tau = 20e-3 # second\n", + "el = -60e-3 # milivolt\n", + "vr = -70e-3 # milivolt\n", + "vth = -50e-3 # milivolt\n", + "r = 100e6 # ohm\n", + "i_mean = 25e-11 # ampere\n", + "\n", + "\n", + "def plot_all(t_range, v, raster=None, spikes=None, spikes_mean=None):\n", + " \"\"\"\n", + " Plots Time evolution for\n", + " (1) multiple realizations of membrane potential\n", + " (2) spikes\n", + " (3) mean spike rate (optional)\n", + "\n", + " Args:\n", + " t_range (numpy array of floats)\n", + " range of time steps for the plots of shape (time steps)\n", + "\n", + " v (numpy array of floats)\n", + " membrane potential values of shape (neurons, time steps)\n", + "\n", + " raster (numpy array of floats)\n", + " spike raster of shape (neurons, time steps)\n", + "\n", + " spikes (dictionary of lists)\n", + " list with spike times indexed by neuron number\n", + "\n", + " spikes_mean (numpy array of floats)\n", + " Mean spike rate for spikes as dictionary\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + "\n", + " v_mean = np.mean(v, axis=0)\n", + " fig_w, fig_h = plt.rcParams['figure.figsize']\n", + " plt.figure(figsize=(fig_w, 1.5 * fig_h))\n", + "\n", + " ax1 = plt.subplot(3, 1, 1)\n", + " for j in range(n):\n", + " plt.scatter(t_range, v[j], color=\"k\", marker=\".\", alpha=0.01)\n", + " plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", + " plt.xticks([])\n", + " plt.ylabel(r'$V_m$ (V)')\n", + "\n", + " if raster is not None:\n", + " plt.subplot(3, 1, 2)\n", + " spikes_mean = np.mean(raster, axis=0)\n", + " plt.imshow(raster, cmap='Greys', origin='lower', aspect='auto')\n", + "\n", + " else:\n", + " plt.subplot(3, 1, 2, sharex=ax1)\n", + " for j in range(n):\n", + " times = np.array(spikes[j])\n", + " plt.scatter(times, j * np.ones_like(times), color=\"C0\", marker=\".\", alpha=0.2)\n", + "\n", + " plt.xticks([])\n", + " plt.ylabel('neuron')\n", + "\n", + " if spikes_mean is not None:\n", + " plt.subplot(3, 1, 3, sharex=ax1)\n", + " plt.plot(t_range, spikes_mean)\n", + " plt.xlabel('time (s)')\n", + " plt.ylabel('rate (Hz)')\n", + "\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "NJ9dKyBodxsY" + }, + "source": [ + "---\n", + "# Section 1: Histograms\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ycTJR187dxsY" + }, + "outputs": [], + "source": [ + "# @title Video 1: Histograms\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'J24tne-IwvY'), ('Bilibili', 'BV1GC4y1h7Ex')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "nz-ISqLTdxsY" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Histograms_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "x1tiEDGvdxsY" + }, + "source": [ + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "Another important statistic is the sample [histogram](https://en.wikipedia.org/wiki/Histogram). For our LIF neuron, it provides an approximate representation of the distribution of membrane potential $V_m(t)$ at time $t=t_k\\in[0,t_{max}]$. For $N$ realizations $V\\left(t_k\\right)$ and $J$ bins is given by:\n", + "\n", + "
\n", + "\\begin{equation}\n", + "N = \\sum_{j=1}^{J} m_j\n", + "\\end{equation}\n", + "
\n", + "\n", + "where $m_j$ is a function that counts the number of samples $V\\left(t_k\\right)$ that fall into bin $j$.\n", + "\n", + "The function `plt.hist(data, nbins)` plots an histogram of `data` in `nbins` bins. The argument `label` defines a label for `data` and `plt.legend()` adds all labels to the plot.\n", + "\n", + "```python\n", + "plt.hist(data, bins, label='my data')\n", + "plt.legend()\n", + "plt.show()\n", + "```\n", + "\n", + "The parameters `histtype='stepfilled'` and `linewidth=0` may improve histogram appearance (depending on taste). You can read more about [different histtype settings](https://matplotlib.org/gallery/statistics/histogram_histtypes.html).\n", + "\n", + "The function `plt.hist` returns the `pdf`, `bins`, and `patches` with the histogram bins, the edges of the bins, and the individual patches used to create the histogram.\n", + "\n", + "```python\n", + "pdf, bins, patches = plt.hist(data, bins)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "5oz5kUTZdxsZ" + }, + "outputs": [], + "source": [ + "# @title Video 2: Nano recap of histograms\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '71f1J98zj80'), ('Bilibili', 'BV1Zv411B7mD')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "9jQQkT_0dxsZ" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_histograms_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "EIIDPXaNdxsZ" + }, + "source": [ + "## Coding Exercise 1: Plotting a histogram\n", + "\n", + "Plot an histogram of $J=50$ bins of $N=10000$ realizations of $V(t)$ for $t=t_{max}/10$ and $t=t_{max}$.\n", + "\n", + "We'll make a small correction in the definition of `t_range` to ensure increments of `dt` by using `np.arange` instead of `np.linspace`.\n", + "\n", + "```python\n", + "numpy.arange(start, stop, step)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "fItxVGbjdxsZ" + }, + "outputs": [], + "source": [ + "#################################################\n", + "## TODO for students: fill out code to plot histogram ##\n", + "# Fill out code and comment or remove the next line\n", + "raise NotImplementedError(\"Student exercise: You need to plot histogram\")\n", + "#################################################\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize t_range, step_end, n, v_n, i and nbins\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 10000\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "nbins = 32\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r * i[:, step])\n", + "\n", + "# Initialize the figure\n", + "plt.figure()\n", + "plt.ylabel('Frequency')\n", + "plt.xlabel('$V_m$ (V)')\n", + "\n", + "# Plot a histogram at t_max/10 (add labels and parameters histtype='stepfilled' and linewidth=0)\n", + "plt.hist(...)\n", + "\n", + "# Plot a histogram at t_max (add labels and parameters histtype='stepfilled' and linewidth=0)\n", + "plt.hist(...)\n", + "\n", + "# Add legend\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "x4UQtJIJdxsZ" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize t_range, step_end, n, v_n, i and nbins\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 10000\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "nbins = 32\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r * i[:, step])\n", + "\n", + "# Initialize the figure\n", + "with plt.xkcd():\n", + " plt.figure()\n", + " plt.ylabel('Frequency')\n", + " plt.xlabel('$V_m$ (V)')\n", + "\n", + " # Plot a histogram at t_max/10 (add labels and parameters histtype='stepfilled' and linewidth=0)\n", + " plt.hist(v_n[:,int(step_end / 10)], nbins,\n", + " histtype='stepfilled', linewidth=0,\n", + " label = 't='+ str(t_max / 10) + 's')\n", + "\n", + " # Plot a histogram at t_max (add labels and parameters histtype='stepfilled' and linewidth=0)\n", + " plt.hist(v_n[:, -1], nbins,\n", + " histtype='stepfilled', linewidth=0,\n", + " label = 't='+ str(t_max) + 's')\n", + " # Add legend\n", + " plt.legend()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "Y5OLvElNdxsZ" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Plotting_a_histogram_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "n_IkDWiBdxsa" + }, + "source": [ + "---\n", + "# Section 2: Dictionaries & introducing spikes\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "oiVm4Jopdxsa" + }, + "outputs": [], + "source": [ + "# @title Video 3: Dictionaries & introducing spikes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'ioKkiukDkNg'), ('Bilibili', 'BV1H54y1q7oS')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "-Kw4Yxtkdxsa" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Dictionaries_&_introducing_spikes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "_-YxgwqOdxsa" + }, + "source": [ + "A spike occures whenever $V(t)$ crosses $V_{th}$. In that case, a spike is recorded, and $V(t)$ resets to $V_{reset}$ value. This is summarized in the *reset condition*:\n", + "\n", + "\\begin{equation}\n", + "V(t) = V_{reset}\\quad \\text{ if } V(t)\\geq V_{th}\n", + "\\end{equation}\n", + "\n", + "For more information about spikes or action potentials see [here](https://en.wikipedia.org/wiki/Action_potential) and [here](https://www.khanacademy.org/test-prep/mcat/organ-systems/neuron-membrane-potentials/a/neuron-action-potentials-the-creation-of-a-brain-signal).\n", + "\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "q4KoInt7dxsa" + }, + "outputs": [], + "source": [ + "# @title Video 4: Nano recap of dictionaries\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'nvpHtzuZggg'), ('Bilibili', 'BV1GC4y1h7hi')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ApfRaFCBdxsa" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_dictionaries_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "74mzXQFxdxsa" + }, + "source": [ + "## Coding Exercise 2: Adding spiking to the LIF neuron\n", + "\n", + "Insert the reset condition, and collect the spike times of each realization in a dictionary variable `spikes`, with $N=500$.\n", + "\n", + "We've used `plt.plot` for plotting lines and also for plotting dots at `(x,y)` coordinates, which is a [scatter plot](https://en.wikipedia.org/wiki/Scatter_plot). From here on, we'll use use `plt.plot` for plotting lines and for scatter plots: `plt.scatter`.\n", + "\n", + "```python\n", + "plt.scatter(x, y, color=\"k\", marker=\".\")\n", + "```\n", + "\n", + "A *raster plot* represents spikes from multiple neurons by plotting dots at spike times from neuron `j` at plot height `j`, i.e.\n", + "\n", + "```python\n", + "plt.scatter(spike_times, j*np.ones_like(spike_times))\n", + "```\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "In this exercise, we use `plt.subplot` for multiple plots in the same figure. These plots can share the same `x` or `y` axis by specifying the parameter `sharex` or `sharey`. Add `plt.tight_layout()` at the end to automatically adjust subplot parameters to fit the figure area better. Please see the example below for a row of two plots sharing axis `y`.\n", + "\n", + "```python\n", + "# initialize the figure\n", + "plt.figure()\n", + "\n", + "# collect axis of 1st figure in ax1\n", + "ax1 = plt.subplot(1, 2, 1)\n", + "plt.plot(t_range, my_data_left)\n", + "plt.ylabel('ylabel')\n", + "\n", + "# share axis x with 1st figure\n", + "plt.subplot(1, 2, 2, sharey=ax1)\n", + "plt.plot(t_range, my_data_right)\n", + "\n", + "# automatically adjust subplot parameters to figure\n", + "plt.tight_layout()\n", + "plt.show()\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "tsbGEihmdxsb" + }, + "outputs": [], + "source": [ + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize spikes and spikes_n\n", + "spikes = {j: [] for j in range(n)}\n", + "spikes_n = np.zeros([step_end])\n", + "\n", + "#################################################\n", + "## TODO for students: add spikes to LIF neuron ##\n", + "# Fill out function and remove\n", + "raise NotImplementedError(\"Student exercise: add spikes to LIF neuron\")\n", + "#################################################\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Loop over simulations\n", + " for j in range(n):\n", + "\n", + " # Check if voltage above threshold\n", + " if v_n[j, step] >= vth:\n", + "\n", + " # Reset to reset voltage\n", + " v_n[j, step] = ...\n", + "\n", + " # Add this spike time\n", + " spikes[j] += ...\n", + "\n", + " # Add spike count to this step\n", + " spikes_n[step] += ...\n", + "\n", + "# Collect mean Vm and mean spiking rate\n", + "v_mean = np.mean(v_n, axis=0)\n", + "spikes_mean = spikes_n / n\n", + "\n", + "# Initialize the figure\n", + "plt.figure()\n", + "\n", + "# Plot simulations and sample mean\n", + "ax1 = plt.subplot(3, 1, 1)\n", + "for j in range(n):\n", + " plt.scatter(t_range, v_n[j], color=\"k\", marker=\".\", alpha=0.01)\n", + "plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", + "plt.ylabel('$V_m$ (V)')\n", + "\n", + "# Plot spikes\n", + "plt.subplot(3, 1, 2, sharex=ax1)\n", + "# for each neuron j: collect spike times and plot them at height j\n", + "for j in range(n):\n", + " times = ...\n", + " plt.scatter(...)\n", + "\n", + "plt.ylabel('neuron')\n", + "\n", + "# Plot firing rate\n", + "plt.subplot(3, 1, 3, sharex=ax1)\n", + "plt.plot(t_range, spikes_mean)\n", + "plt.xlabel('time (s)')\n", + "plt.ylabel('rate (Hz)')\n", + "\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "lsjGq9uadxsb" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize spikes and spikes_n\n", + "spikes = {j: [] for j in range(n)}\n", + "spikes_n = np.zeros([step_end])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Loop over simulations\n", + " for j in range(n):\n", + "\n", + " # Check if voltage above threshold\n", + " if v_n[j, step] >= vth:\n", + "\n", + " # Reset to reset voltage\n", + " v_n[j, step] = vr\n", + "\n", + " # Add this spike time\n", + " spikes[j] += [t]\n", + "\n", + " # Add spike count to this step\n", + " spikes_n[step] += 1\n", + "\n", + "# Collect mean Vm and mean spiking rate\n", + "v_mean = np.mean(v_n, axis=0)\n", + "spikes_mean = spikes_n / n\n", + "\n", + "with plt.xkcd():\n", + " # Initialize the figure\n", + " plt.figure()\n", + "\n", + " # Plot simulations and sample mean\n", + " ax1 = plt.subplot(3, 1, 1)\n", + " for j in range(n):\n", + " plt.scatter(t_range, v_n[j], color=\"k\", marker=\".\", alpha=0.01)\n", + " plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", + " plt.ylabel('$V_m$ (V)')\n", + "\n", + " # Plot spikes\n", + " plt.subplot(3, 1, 2, sharex=ax1)\n", + " # for each neuron j: collect spike times and plot them at height j\n", + " for j in range(n):\n", + " times = np.array(spikes[j])\n", + " plt.scatter(times, j * np.ones_like(times), color=\"C0\", marker=\".\", alpha=0.2)\n", + "\n", + " plt.ylabel('neuron')\n", + "\n", + " # Plot firing rate\n", + " plt.subplot(3, 1, 3, sharex=ax1)\n", + " plt.plot(t_range, spikes_mean)\n", + " plt.xlabel('time (s)')\n", + " plt.ylabel('rate (Hz)')\n", + "\n", + " plt.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "IzlkaSZDdxsb" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Adding_spiking_to_the_LIF_neuron_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "CTiz2Z-Tdxsb" + }, + "source": [ + "---\n", + "# Section 3: Boolean indexes\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "pmW04_j2dxsb" + }, + "outputs": [], + "source": [ + "# @title Video 5: Boolean indexes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'G0C1v848I9Y'), ('Bilibili', 'BV1W54y1q7eh')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "8Fr72otvdxsb" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Boolean_indexes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "cU0QZwjEdxsb" + }, + "source": [ + "Boolean arrays can index NumPy arrays to select a subset of elements (also works with lists of booleans).\n", + "\n", + "The boolean array itself can be initiated by a condition, as shown in the example below.\n", + "\n", + "```python\n", + "a = np.array([1, 2, 3])\n", + "b = a>=2\n", + "print(b)\n", + "--> [False True True]\n", + "\n", + "print(a[b])\n", + "--> [2 3]\n", + "\n", + "print(a[a>=2])\n", + "--> [2 3]\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "5Lb7tI8zdxsb" + }, + "outputs": [], + "source": [ + "# @title Video 6: Nano recap of Boolean indexes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'dFPgO5wnyLc'), ('Bilibili', 'BV1W54y1q7gi')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "2NoklIbudxsc" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_Boolean_indexes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "x62OGFk8dxsc" + }, + "source": [ + "## Coding Exercise 3: Using Boolean indexing\n", + "\n", + "We can avoid looping all neurons in each time step by identifying with boolean arrays the indexes of neurons that spiked in the previous step.\n", + "\n", + "In the example below, `v_rest` is a boolean numpy array with `True` in each index of `v_n` with value `vr` at time index `step`:\n", + "\n", + "```python\n", + "v_rest = (v_n[:,step] == vr)\n", + "print(v_n[v_rest,step])\n", + " --> [vr, ..., vr]\n", + "```\n", + "\n", + "The function `np.where` returns indexes of boolean arrays with `True` values.\n", + "\n", + "You may use the helper function `plot_all` that implements the figure from the previous exercise." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "Jyl4b7Bddxsc" + }, + "outputs": [], + "source": [ + "help(plot_all)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "1ioRRAfYdxsc" + }, + "outputs": [], + "source": [ + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize spikes and spikes_n\n", + "spikes = {j: [] for j in range(n)}\n", + "spikes_n = np.zeros([step_end])\n", + "\n", + "#################################################\n", + "## TODO for students: use Boolean indexing ##\n", + "# Fill out function and remove\n", + "raise NotImplementedError(\"Student exercise: using Boolean indexing\")\n", + "#################################################\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = ...\n", + "\n", + " # Set relevant values of v_n to resting potential using spiked\n", + " v_n[spiked,step] = ...\n", + "\n", + " # Collect spike times\n", + " for j in np.where(spiked)[0]:\n", + " spikes[j] += [t]\n", + " spikes_n[step] += 1\n", + "\n", + "# Collect mean spiking rate\n", + "spikes_mean = spikes_n / n\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, spikes=spikes, spikes_mean=spikes_mean)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "rERQivhWdxsc" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize spikes and spikes_n\n", + "spikes = {j: [] for j in range(n)}\n", + "spikes_n = np.zeros([step_end])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to resting potential using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Collect spike times\n", + " for j in np.where(spiked)[0]:\n", + " spikes[j] += [t]\n", + " spikes_n[step] += 1\n", + "\n", + "# Collect mean spiking rate\n", + "spikes_mean = spikes_n / n\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, spikes=spikes, spikes_mean=spikes_mean)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "NGCU9IP6dxsc" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Using_Boolean_indexing_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "2FdA8HvKdxsk" + }, + "source": [ + "## Coding Exercise 4: Making a binary raster plot\n", + "\n", + "A *binary raster plot* represents spike times as `1`s in a binary grid initialized with `0`s. We start with a numpy array `raster` of zeros with shape `(neurons, time steps)`, and represent a spike from neuron `5` at time step `20` as `raster(5,20)=1`, for example.\n", + "\n", + "The *binary raster plot* is much more efficient than the previous method by plotting the numpy array `raster` as an image:\n", + "\n", + "```python\n", + "plt.imshow(raster, cmap='Greys', origin='lower', aspect='auto')\n", + "```\n", + "\n", + "**Suggestions**\n", + "* At each time step:\n", + " * Initialize boolean numpy array `spiked` with $V_n(t)\\geq V_{th}$\n", + " * Set to `vr` indexes of `v_n` using `spiked`\n", + " * Set to `1` indexes of numpy array `raster` using `spiked`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "WEbW_Toldxsk" + }, + "outputs": [], + "source": [ + "#################################################\n", + "## TODO for students: make a raster ##\n", + "# Fill out function and remove\n", + "raise NotImplementedError(\"Student exercise: make a raster \")\n", + "#################################################\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to v_reset using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Set relevant elements in raster to 1 using spiked\n", + " raster[spiked,step] = ...\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "l8kCiXlgdxsk" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to v_reset using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Set relevant elements in raster to 1 using spiked\n", + " raster[spiked,step] = 1.\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "HK372z5pdxsk" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Making_a_binary_raster_plot_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "LeBTKG9Cdxsk" + }, + "source": [ + "---\n", + "# Section 4: Refractory period\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "o4bGbMLMdxsk" + }, + "outputs": [], + "source": [ + "# @title Video 7: Refractory period\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'KVNdbRY5-nY'), ('Bilibili', 'BV1MT4y1E79j')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "_XKDSAxKdxsl" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Refractory_period_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "SZgjb2mvdxsl" + }, + "source": [ + "The absolute refractory period is a time interval in the order of a few milliseconds during which synaptic input will not lead to a 2nd spike, no matter how strong. This effect is due to the biophysics of the neuron membrane channels, and you can read more about absolute and relative refractory period [here](https://content.byui.edu/file/a236934c-3c60-4fe9-90aa-d343b3e3a640/1/module5/readings/refractory_periods.html) and [here](https://en.wikipedia.org/wiki/Refractory_period_(physiology)).\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "mgivzSyUdxsl" + }, + "outputs": [], + "source": [ + "# @title Video 8: Nano recap of refractory period\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'DOoftC0JU2k'), ('Bilibili', 'BV1pA411e7je')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "QAWP1By_dxsl" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_refractory_period_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "wzbvqMUfdxsl" + }, + "source": [ + "## Coding Exercise 5: Investigating refactory periods\n", + "\n", + "Investigate the effect of (absolute) refractory period $t_{ref}$ on the evolution of output rate $\\lambda(t)$. Add refractory period $t_{ref}=10$ ms after each spike, during which $V(t)$ is clamped to $V_{reset}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "_Bwf0hdmdxsl" + }, + "outputs": [], + "source": [ + "#################################################\n", + "## TODO for students: add refactory period ##\n", + "# Fill out function and remove\n", + "raise NotImplementedError(\"Student exercise: add refactory period \")\n", + "#################################################\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize t_ref and last_spike\n", + "t_ref = 0.01\n", + "last_spike = -t_ref * np.ones([n])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to v_reset using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Set relevant elements in raster to 1 using spiked\n", + " raster[spiked,step] = 1.\n", + "\n", + " # Initialize boolean numpy array clamped using last_spike, t and t_ref\n", + " clamped = ...\n", + "\n", + " # Reset clamped neurons to vr using clamped\n", + " v_n[clamped,step] = ...\n", + "\n", + " # Update numpy array last_spike with time t for spiking neurons\n", + " last_spike[spiked] = ...\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "uMf4A9Wadxsl" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize t_ref and last_spike\n", + "t_ref = 0.01\n", + "last_spike = -t_ref * np.ones([n])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to v_reset using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Set relevant elements in raster to 1 using spiked\n", + " raster[spiked,step] = 1.\n", + "\n", + " # Initialize boolean numpy array clamped using last_spike, t and t_ref\n", + " clamped = (last_spike + t_ref > t)\n", + "\n", + " # Reset clamped neurons to vr using clamped\n", + " v_n[clamped,step] = vr\n", + "\n", + " # Update numpy array last_spike with time t for spiking neurons\n", + " last_spike[spiked] = t\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "oeQlc83Xdxsm" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Investigating_refractory_periods_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "OmBgWmGedxsm" + }, + "source": [ + "## Interactive Demo 1: Random refractory period\n", + "\n", + "In the following interactive demo, we will investigate the effect of random refractory periods. We will use random refactory periods $t_{ref}$ with\n", + "$t_{ref} = \\mu + \\sigma\\,\\mathcal{N}$, where $\\mathcal{N}$ is the [normal distribution](https://en.wikipedia.org/wiki/Normal_distribution), $\\mu=0.01$ and $\\sigma=0.007$.\n", + "\n", + "Refractory period samples `t_ref` of size `n` is initialized with `np.random.normal`. We clip negative values to `0` with boolean indexes. (Why?) You can double-click the cell to see the hidden code.\n", + "\n", + "You can play with the parameters mu and sigma and visualize the resulting simulation. What is the effect of different $\\sigma$ values?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "Ay9Tk2zPdxsm" + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to enable the demo\n", + "\n", + "def random_ref_period(mu, sigma):\n", + " # set random number generator\n", + " np.random.seed(2020)\n", + "\n", + " # initialize step_end, t_range, n, v_n, syn and raster\n", + " t_range = np.arange(0, t_max, dt)\n", + " step_end = len(t_range)\n", + " n = 500\n", + " v_n = el * np.ones([n,step_end])\n", + " syn = i_mean * (1 + 0.1*(t_max/dt)**(0.5)*(2*np.random.random([n,step_end])-1))\n", + " raster = np.zeros([n,step_end])\n", + "\n", + " # initialize t_ref and last_spike\n", + " t_ref = mu + sigma*np.random.normal(size=n)\n", + " t_ref[t_ref<0] = 0\n", + " last_spike = -t_ref * np.ones([n])\n", + "\n", + " # loop time steps\n", + " for step, t in enumerate(t_range):\n", + " if step==0:\n", + " continue\n", + "\n", + " v_n[:,step] = v_n[:,step-1] + dt/tau * (el - v_n[:,step-1] + r*syn[:,step])\n", + "\n", + " # boolean array spiked indexes neurons with v>=vth\n", + " spiked = (v_n[:,step] >= vth)\n", + " v_n[spiked,step] = vr\n", + " raster[spiked,step] = 1.\n", + "\n", + " # boolean array clamped indexes refractory neurons\n", + " clamped = (last_spike + t_ref > t)\n", + " v_n[clamped,step] = vr\n", + " last_spike[spiked] = t\n", + "\n", + " # plot multiple realizations of Vm, spikes and mean spike rate\n", + " plot_all(t_range, v_n, raster)\n", + "\n", + " # plot histogram of t_ref\n", + " plt.figure(figsize=(8,4))\n", + " plt.hist(t_ref, bins=32, histtype='stepfilled', linewidth=0, color='C1')\n", + " plt.xlabel(r'$t_{ref}$ (s)')\n", + " plt.ylabel('count')\n", + " plt.tight_layout()\n", + "\n", + "_ = widgets.interact(random_ref_period, mu = (0.01, 0.05, 0.01), \\\n", + " sigma = (0.001, 0.02, 0.001))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "USvwwwetdxsm" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Random_refractory_period_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "wbrisIUbdxsm" + }, + "source": [ + "---\n", + "# Section 5: Using functions\n", + "Running key parts of your code inside functions improves your coding narrative by making it clearer and more flexible to future changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "Wbkh6UFGdxsn" + }, + "outputs": [], + "source": [ + "# @title Video 9: Functions\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'mkf8riqCjS4'), ('Bilibili', 'BV1sa4y1a7pq')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "KaF6pniedxsn" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Functions_Video\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "1nZFx72zdxsn" + }, + "outputs": [], + "source": [ + "# @title Video 10: Nano recap of functions\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '0An_NnVWY_Q'), ('Bilibili', 'BV1pz411v74H')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "YSoWNoG5dxso" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_functions_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "_oVXyOVDdxso" + }, + "source": [ + "## Coding Exercise 6: Rewriting code with functions\n", + "\n", + "We will now re-organize parts of the code from the previous exercise with functions. You need to complete the function `spike_clamp()` to update $V(t)$ and deal with spiking and refractoriness" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "H6vTyYwFdxso" + }, + "outputs": [], + "source": [ + "def ode_step(v, i, dt):\n", + " \"\"\"\n", + " Evolves membrane potential by one step of discrete time integration\n", + "\n", + " Args:\n", + " v (numpy array of floats)\n", + " membrane potential at previous time step of shape (neurons)\n", + "\n", + " i (numpy array of floats)\n", + " synaptic input at current time step of shape (neurons)\n", + "\n", + " dt (float)\n", + " time step increment\n", + "\n", + " Returns:\n", + " v (numpy array of floats)\n", + " membrane potential at current time step of shape (neurons)\n", + " \"\"\"\n", + " v = v + dt/tau * (el - v + r*i)\n", + "\n", + " return v\n", + "\n", + "\n", + "def spike_clamp(v, delta_spike):\n", + " \"\"\"\n", + " Resets membrane potential of neurons if v>= vth\n", + " and clamps to vr if interval of time since last spike < t_ref\n", + "\n", + " Args:\n", + " v (numpy array of floats)\n", + " membrane potential of shape (neurons)\n", + "\n", + " delta_spike (numpy array of floats)\n", + " interval of time since last spike of shape (neurons)\n", + "\n", + " Returns:\n", + " v (numpy array of floats)\n", + " membrane potential of shape (neurons)\n", + " spiked (numpy array of floats)\n", + " boolean array of neurons that spiked of shape (neurons)\n", + " \"\"\"\n", + "\n", + " ####################################################\n", + " ## TODO for students: complete spike_clamp\n", + " # Fill out function and remove\n", + " raise NotImplementedError(\"Student exercise: complete spike_clamp\")\n", + " ####################################################\n", + " # Boolean array spiked indexes neurons with v>=vth\n", + " spiked = ...\n", + " v[spiked] = ...\n", + "\n", + " # Boolean array clamped indexes refractory neurons\n", + " clamped = ...\n", + " v[clamped] = ...\n", + "\n", + " return v, spiked\n", + "\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize t_ref and last_spike\n", + "mu = 0.01\n", + "sigma = 0.007\n", + "t_ref = mu + sigma*np.random.normal(size=n)\n", + "t_ref[t_ref<0] = 0\n", + "last_spike = -t_ref * np.ones([n])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", + "\n", + " # Reset membrane potential and clamp\n", + " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", + "\n", + " # Update raster and last_spike\n", + " raster[spiked,step] = 1.\n", + " last_spike[spiked] = t\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "vPlBUtUpdxso" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "def ode_step(v, i, dt):\n", + " \"\"\"\n", + " Evolves membrane potential by one step of discrete time integration\n", + "\n", + " Args:\n", + " v (numpy array of floats)\n", + " membrane potential at previous time step of shape (neurons)\n", + "\n", + " i (numpy array of floats)\n", + " synaptic input at current time step of shape (neurons)\n", + "\n", + " dt (float)\n", + " time step increment\n", + "\n", + " Returns:\n", + " v (numpy array of floats)\n", + " membrane potential at current time step of shape (neurons)\n", + " \"\"\"\n", + " v = v + dt/tau * (el - v + r*i)\n", + "\n", + " return v\n", + "\n", + "\n", + "def spike_clamp(v, delta_spike):\n", + " \"\"\"\n", + " Resets membrane potential of neurons if v>= vth\n", + " and clamps to vr if interval of time since last spike < t_ref\n", + "\n", + " Args:\n", + " v (numpy array of floats)\n", + " membrane potential of shape (neurons)\n", + "\n", + " delta_spike (numpy array of floats)\n", + " interval of time since last spike of shape (neurons)\n", + "\n", + " Returns:\n", + " v (numpy array of floats)\n", + " membrane potential of shape (neurons)\n", + " spiked (numpy array of floats)\n", + " boolean array of neurons that spiked of shape (neurons)\n", + " \"\"\"\n", + "\n", + " # Boolean array spiked indexes neurons with v>=vth\n", + " spiked = (v >= vth)\n", + " v[spiked] = vr\n", + "\n", + " # Boolean array clamped indexes refractory neurons\n", + " clamped = (t_ref > delta_spike)\n", + " v[clamped] = vr\n", + "\n", + " return v, spiked\n", + "\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize t_ref and last_spike\n", + "mu = 0.01\n", + "sigma = 0.007\n", + "t_ref = mu + sigma*np.random.normal(size=n)\n", + "t_ref[t_ref<0] = 0\n", + "last_spike = -t_ref * np.ones([n])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", + "\n", + " # Reset membrane potential and clamp\n", + " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", + "\n", + " # Update raster and last_spike\n", + " raster[spiked,step] = 1.\n", + " last_spike[spiked] = t\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "JEBg55MZdxso" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Rewriting_code_with_functions_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "O3GugPeCdxsp" + }, + "source": [ + "---\n", + "# Section 6: Using classes\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "8BWkoeTddxsp" + }, + "outputs": [], + "source": [ + "# @title Video 11: Classes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'dGRESMoNPh0'), ('Bilibili', 'BV1hz411v7ne')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "CNAcLcLOdxsr" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Classes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "bY9swpA0dxss" + }, + "source": [ + "Using classes helps with code reuse and reliability. Well-designed classes are like black boxes, receiving inputs and providing expected outputs. The details of how the class processes inputs and produces outputs are unimportant.\n", + "\n", + "See additional details here: [A Beginner's Python Tutorial/Classes](https://en.wikibooks.org/wiki/A_Beginner%27s_Python_Tutorial/Classes)\n", + "\n", + "*Attributes* are variables internal to the class, and *methods* are functions internal to the class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "2PoK8siydxss" + }, + "outputs": [], + "source": [ + "# @title Video 12: Nano recap of classes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '4YNpMpVW2qs'), ('Bilibili', 'BV12V41167yu')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "U-ywaGZ_dxss" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_classes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "gr_s8q0zdxss" + }, + "source": [ + "## Coding Exercise 7: Making a LIF class\n", + "In this exercise we'll practice with Python classes by implementing `LIFNeurons`, a class that evolves and keeps state of multiple realizations of LIF neurons.\n", + "\n", + "Several attributes are used to keep state of our neurons:\n", + "\n", + "```python\n", + "self.v current membrane potential\n", + "self.spiked neurons that spiked\n", + "self.last_spike last spike time of each neuron\n", + "self.t running time of the simulation\n", + "self.steps simulation step\n", + "```\n", + "\n", + "There is a single method:\n", + "\n", + "```python\n", + "self.ode_step() performs single step discrete time integration\n", + " for provided synaptic current and dt\n", + "```\n", + "\n", + "Complete the spike and clamp part of method `self.ode_step` (should be similar to function `spike_and_clamp` seen before)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "Fjz_HLIKdxss" + }, + "outputs": [], + "source": [ + "# Simulation class\n", + "class LIFNeurons:\n", + " \"\"\"\n", + " Keeps track of membrane potential for multiple realizations of LIF neuron,\n", + " and performs single step discrete time integration.\n", + " \"\"\"\n", + " def __init__(self, n, t_ref_mu=0.01, t_ref_sigma=0.002,\n", + " tau=20e-3, el=-60e-3, vr=-70e-3, vth=-50e-3, r=100e6):\n", + "\n", + " # Neuron count\n", + " self.n = n\n", + "\n", + " # Neuron parameters\n", + " self.tau = tau # second\n", + " self.el = el # milivolt\n", + " self.vr = vr # milivolt\n", + " self.vth = vth # milivolt\n", + " self.r = r # ohm\n", + "\n", + " # Initializes refractory period distribution\n", + " self.t_ref_mu = t_ref_mu\n", + " self.t_ref_sigma = t_ref_sigma\n", + " self.t_ref = self.t_ref_mu + self.t_ref_sigma * np.random.normal(size=self.n)\n", + " self.t_ref[self.t_ref<0] = 0\n", + "\n", + " # State variables\n", + " self.v = self.el * np.ones(self.n)\n", + " self.spiked = self.v >= self.vth\n", + " self.last_spike = -self.t_ref * np.ones([self.n])\n", + " self.t = 0.\n", + " self.steps = 0\n", + "\n", + "\n", + " def ode_step(self, dt, i):\n", + "\n", + " # Update running time and steps\n", + " self.t += dt\n", + " self.steps += 1\n", + "\n", + " # One step of discrete time integration of dt\n", + " self.v = self.v + dt / self.tau * (self.el - self.v + self.r * i)\n", + "\n", + " ####################################################\n", + " ## TODO for students: complete the `ode_step` method\n", + " # Fill out function and remove\n", + " raise NotImplementedError(\"Student exercise: complete the ode_step method\")\n", + " ####################################################\n", + "\n", + " # Spike and clamp\n", + " self.spiked = ...\n", + " self.v[self.spiked] = ...\n", + " self.last_spike[self.spiked] = ...\n", + " clamped = ...\n", + " self.v[clamped] = ...\n", + "\n", + " self.last_spike[self.spiked] = ...\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize neurons\n", + "neurons = LIFNeurons(n)\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Call ode_step method\n", + " neurons.ode_step(dt, i[:,step])\n", + "\n", + " # Log v_n and spike history\n", + " v_n[:,step] = neurons.v\n", + " raster[neurons.spiked, step] = 1.\n", + "\n", + "# Report running time and steps\n", + "print(f'Ran for {neurons.t:.3}s in {neurons.steps} steps.')\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "y9UTOvgWdxst" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Simulation class\n", + "class LIFNeurons:\n", + " \"\"\"\n", + " Keeps track of membrane potential for multiple realizations of LIF neuron,\n", + " and performs single step discrete time integration.\n", + " \"\"\"\n", + " def __init__(self, n, t_ref_mu=0.01, t_ref_sigma=0.002,\n", + " tau=20e-3, el=-60e-3, vr=-70e-3, vth=-50e-3, r=100e6):\n", + "\n", + " # Neuron count\n", + " self.n = n\n", + "\n", + " # Neuron parameters\n", + " self.tau = tau # second\n", + " self.el = el # milivolt\n", + " self.vr = vr # milivolt\n", + " self.vth = vth # milivolt\n", + " self.r = r # ohm\n", + "\n", + " # Initializes refractory period distribution\n", + " self.t_ref_mu = t_ref_mu\n", + " self.t_ref_sigma = t_ref_sigma\n", + " self.t_ref = self.t_ref_mu + self.t_ref_sigma * np.random.normal(size=self.n)\n", + " self.t_ref[self.t_ref<0] = 0\n", + "\n", + " # State variables\n", + " self.v = self.el * np.ones(self.n)\n", + " self.spiked = self.v >= self.vth\n", + " self.last_spike = -self.t_ref * np.ones([self.n])\n", + " self.t = 0.\n", + " self.steps = 0\n", + "\n", + "\n", + " def ode_step(self, dt, i):\n", + "\n", + " # Update running time and steps\n", + " self.t += dt\n", + " self.steps += 1\n", + "\n", + " # One step of discrete time integration of dt\n", + " self.v = self.v + dt / self.tau * (self.el - self.v + self.r * i)\n", + "\n", + " # Spike and clamp\n", + " self.spiked = (self.v >= self.vth)\n", + " self.v[self.spiked] = self.vr\n", + " self.last_spike[self.spiked] = self.t\n", + " clamped = (self.t_ref > self.t-self.last_spike)\n", + " self.v[clamped] = self.vr\n", + "\n", + " self.last_spike[self.spiked] = self.t\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize neurons\n", + "neurons = LIFNeurons(n)\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Call ode_step method\n", + " neurons.ode_step(dt, i[:,step])\n", + "\n", + " # Log v_n and spike history\n", + " v_n[:,step] = neurons.v\n", + " raster[neurons.spiked, step] = 1.\n", + "\n", + "# Report running time and steps\n", + "print(f'Ran for {neurons.t:.3}s in {neurons.steps} steps.')\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "WsGDqK1Gdxst" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Making_a_LIF_class_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "HxigVxS5dxst" + }, + "source": [ + "---\n", + "# Summary\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "NefLjZPLdxst" + }, + "outputs": [], + "source": [ + "# @title Video 12: Last concepts & recap\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'h4mSJBPocPo'), ('Bilibili', 'BV1MC4y1h7eA')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "4lJhZj9idxst" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Last_concepts_&_recap_Video\")" + ] + } + ], + "metadata": { + "colab": { + "name": "W0D2_Tutorial1", + "provenance": [], + "toc_visible": true, + "include_colab_link": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file From 72dc7e028e5e4d967be87586348b84e9480d3260 Mon Sep 17 00:00:00 2001 From: Spiros Chavlis Date: Mon, 3 Jul 2023 14:43:45 +0300 Subject: [PATCH 3/7] fixes --- tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb | 4543 +++++++++--------- 1 file changed, 2312 insertions(+), 2231 deletions(-) diff --git a/tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb b/tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb index 5389114..23c637d 100644 --- a/tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb +++ b/tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb @@ -1,2232 +1,2313 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "execution": {}, - "id": "view-in-github" - }, - "source": [ - "\"Open   \"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "# Tutorial 1: Differentiation and Integration\n", - "\n", - "**Week 0, Day 4: Calculus**\n", - "\n", - "**By Neuromatch Academy**\n", - "\n", - "**Content creators:** John S Butler, Arvind Kumar with help from Ella Batty\n", - "\n", - "**Content reviewers:** Aderogba Bayo, Tessy Tom, Matt McCann\n", - "\n", - "**Production editors:** Matthew McCann, Ella Batty" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Tutorial Objectives\n", - "\n", - "*Estimated timing of tutorial: 80 minutes*\n", - "\n", - "In this tutorial, we will cover aspects of calculus that will be frequently used in the main NMA course. We assume that you have some familiarty with calculus, but may be a bit rusty or may not have done much practice. Specifically the objectives of this tutorial are\n", - "\n", - "* Get an intuitive understanding of derivative and integration operations\n", - "* Learn to calculate the derivatives of 1- and 2-dimensional functions/signals numerically\n", - "* Familiarize with the concept of neuron transfer function in 1- and 2-dimensions.\n", - "* Familiarize with the idea of numerical integration using Riemann sum\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Install dependencies\n", - "!pip install sympy --quiet" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D4_T1\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import scipy.optimize as opt # import root-finding algorithm\n", - "import sympy as sp # Python toolbox for symbolic maths\n", - "import matplotlib.pyplot as plt\n", - "from mpl_toolkits.mplot3d import Axes3D # Toolbox for rendring 3D figures\n", - "from mpl_toolkits import mplot3d # Toolbox for rendring 3D figures" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Figure Settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import ipywidgets as widgets # interactive display\n", - "from ipywidgets import interact\n", - "%config InlineBackend.figure_format = 'retina'\n", - "# use NMA plot style\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", - "my_layout = widgets.Layout()\n", - "\n", - "fig_w, fig_h = 12, 4.5\n", - "my_fontsize = 16\n", - "my_params = {'axes.labelsize': my_fontsize,\n", - " 'axes.titlesize': my_fontsize,\n", - " 'figure.figsize': [fig_w, fig_h],\n", - " 'font.size': my_fontsize,\n", - " 'legend.fontsize': my_fontsize-4,\n", - " 'lines.markersize': 8.,\n", - " 'lines.linewidth': 2.,\n", - " 'xtick.labelsize': my_fontsize-2,\n", - " 'ytick.labelsize': my_fontsize-2}\n", - "\n", - "plt.rcParams.update(my_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Plotting Functions\n", - "def move_sympyplot_to_axes(p, ax):\n", - " backend = p.backend(p)\n", - " backend.ax = ax\n", - " backend.process_series()\n", - " backend.ax.spines['right'].set_color('none')\n", - " backend.ax.spines['bottom'].set_position('zero')\n", - " backend.ax.spines['top'].set_color('none')\n", - " plt.close(backend.fig)\n", - "\n", - "\n", - "def plot_functions(function, show_derivative, show_integral):\n", - "\n", - " # For sympy we first define our symbolic variable\n", - " x, y, z, t, f = sp.symbols('x y z t f')\n", - "\n", - " # We define our function\n", - " if function == 'Linear':\n", - " f = -2*t\n", - " name = r'$-2t$'\n", - " elif function == 'Parabolic':\n", - " f = t**2\n", - " name = r'$t^2$'\n", - " elif function == 'Exponential':\n", - " f = sp.exp(t)\n", - " name = r'$e^t$'\n", - " elif function == 'Sine':\n", - " f = sp.sin(t)\n", - " name = r'$sin(t)$'\n", - " elif function == 'Sigmoid':\n", - " f = 1/(1 + sp.exp(-(t-5)))\n", - " name = r'$\\frac{1}{1+e^{-(t-5)}}$'\n", - "\n", - " if show_derivative and not show_integral:\n", - " # Calculate the derivative of sin(t) as a function of t\n", - " diff_f = sp.diff(f)\n", - " print('Derivative of', f, 'is ', diff_f)\n", - "\n", - " p1 = sp.plot(f, diff_f, show=False)\n", - " p1[0].line_color='r'\n", - " p1[1].line_color='b'\n", - " p1[0].label='Function'\n", - " p1[1].label='Derivative'\n", - " p1.legend=True\n", - " p1.title = 'Function = ' + name + '\\n'\n", - " p1.show()\n", - " elif show_integral and not show_derivative:\n", - "\n", - " int_f = sp.integrate(f)\n", - " int_f = int_f - int_f.subs(t, -10)\n", - " print('Integral of', f, 'is ', int_f)\n", - "\n", - "\n", - " p1 = sp.plot(f, int_f, show=False)\n", - " p1[0].line_color='r'\n", - " p1[1].line_color='g'\n", - " p1[0].label='Function'\n", - " p1[1].label='Integral'\n", - " p1.legend=True\n", - " p1.title = 'Function = ' + name + '\\n'\n", - " p1.show()\n", - "\n", - "\n", - " elif show_integral and show_derivative:\n", - "\n", - " diff_f = sp.diff(f)\n", - " print('Derivative of', f, 'is ', diff_f)\n", - "\n", - " int_f = sp.integrate(f)\n", - " int_f = int_f - int_f.subs(t, -10)\n", - " print('Integral of', f, 'is ', int_f)\n", - "\n", - " p1 = sp.plot(f, diff_f, int_f, show=False)\n", - " p1[0].line_color='r'\n", - " p1[1].line_color='b'\n", - " p1[2].line_color='g'\n", - " p1[0].label='Function'\n", - " p1[1].label='Derivative'\n", - " p1[2].label='Integral'\n", - " p1.legend=True\n", - " p1.title = 'Function = ' + name + '\\n'\n", - " p1.show()\n", - "\n", - " else:\n", - "\n", - " p1 = sp.plot(f, show=False)\n", - " p1[0].line_color='r'\n", - " p1[0].label='Function'\n", - " p1.legend=True\n", - " p1.title = 'Function = ' + name + '\\n'\n", - " p1.show()\n", - "\n", - "\n", - "def plot_alpha_func(t, f, df_dt):\n", - "\n", - " plt.figure()\n", - " plt.subplot(2,1,1)\n", - " plt.plot(t, f, 'r', label='Alpha function')\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('Voltage')\n", - " plt.title('Alpha function (f(t))')\n", - " #plt.legend()\n", - "\n", - " plt.subplot(2,1,2)\n", - " plt.plot(t, df_dt, 'b', label='Derivative')\n", - " plt.title('Derivative of alpha function')\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('df/dt')\n", - " #plt.legend()\n", - "\n", - "\n", - "def plot_charge_transfer(t, PSP, numerical_integral):\n", - "\n", - " fig, axes = plt.subplots(1, 2)\n", - "\n", - " axes[0].plot(t, PSP)\n", - " axes[0].set(xlabel = 't', ylabel = 'PSP')\n", - "\n", - " axes[1].plot(t, numerical_integral)\n", - " axes[1].set(xlabel = 't', ylabel = 'Charge Transferred')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 0: Introduction" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 1: Why do we care about calculus?\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'NZwfH_dG2wI'), ('Bilibili', 'BV1F44y1z7Uk')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Why_do_we_care_about_calculus_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 1: What is differentiation and integration?\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 2: A geometrical interpretation of differentiation and integration\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'uQjwr9RQaEs'), ('Bilibili', 'BV1sU4y1G7Ru')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_A_geometrical_interpretation_of_differentiation_and_integration_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "This video covers the definition of differentiation and integration, highlights the geometrical interpretation of each, and introduces the idea of eigenfunctions.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "Calculus is a part of mathematics concerned with **continous change**. There are two branches of calculus: differential calculus and integral calculus.\n", - "\n", - "Differentiation of a function $f(t)$ gives you the derivative of that function $\\frac{d(f(t))}{dt}$. A derivative captures how sensitive a function is to slight changes in the input for different ranges of inputs. Geometrically, the derivative of a function at a certain input is the slope of the function at that input. For example, as you drive, the distance traveled changes continuously with time. The derivative of the distance traveled with respect to time is the velocity of the vehicle at each point in time. The velocity tells you the rate of change of the distance traveled at different points in time. If you have slow velocity (a small derivative), the distance traveled doesn't change much for small changes in time. A high velocity (big derivative) means that the distance traveled changes a lot for small changes in time.\n", - "\n", - "The sign of the derivative of a function (or signal) tells whether the signal is increasing or decreasing. For a signal going through changes as a function of time, the derivative will become zero when the signal changes its direction of change (e.g. from increasing to decreasing). That is, at local minimum or maximum values, the slope of the signal will be zero. This property is used in optimizing problems. But we can also use it to find peaks in a signal.\n", - "\n", - "Integration can be thought of as the reverse of differentation. If we integrate the velocity with respect to time, we can calculate the distance traveled. By integrating a function, we are basically trying to find functions that would have the original one as their derivative. When we integrate a function, our integral will have an added unknown scalar constant, $C$.\n", - "For example, if\n", - "\n", - "\\begin{equation}\n", - "g(t) = 1.5t^2 + 4t - 1\n", - "\\end{equation}\n", - "\n", - "our integral function $f(t)$ will be:\n", - "\n", - "\\begin{equation}\n", - "f(t) = \\int g(t) dt = 0.5t^3 + 2t^2 - t + C\n", - "\\end{equation}\n", - "\n", - "This constant exists because the derivative of a constant is 0 so we cannot know what the constant should be. This is an indefinite integral. If we compute a definite integral, that is the integral between two limits of the input, we will not have this unknown constant and the integral of a function will capture the area under the curve of that function between those two limits.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 1: Geometrical understanding\n", - "\n", - "In the interactive demo below, you can pick different functions to examine in the drop down menu. You can then choose to show the derivative function and/or the integral function.\n", - "\n", - "For the integral, we have chosen the unknown constant $C$ such that the integral function at the left x-axis limit is $0$, as $f(t = -10) = 0$. So the integral will reflect the area under the curve starting from that position.\n", - "\n", - "For each function:\n", - "\n", - "* Examine just the function first. Discuss and predict what the derivative and integral will look like. Remember that derivative = slope of function, integral = area under curve from $t = -10$ to that t.\n", - "* Check the derivative - does it match your expectations?\n", - "* Check the integral - does it match your expectations?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to enable the widget\n", - "function_options = widgets.Dropdown(\n", - " options=['Linear', 'Exponential', 'Sine', 'Sigmoid'],\n", - " description='Function',\n", - " disabled=False,\n", - ")\n", - "\n", - "derivative = widgets.Checkbox(\n", - " value=False,\n", - " description='Show derivative',\n", - " disabled=False,\n", - " indent=False\n", - ")\n", - "\n", - "integral = widgets.Checkbox(\n", - " value=False,\n", - " description='Show integral',\n", - " disabled=False,\n", - " indent=False\n", - ")\n", - "\n", - "def on_value_change(change):\n", - " derivative.value = False\n", - " integral.value = False\n", - "\n", - "function_options.observe(on_value_change, names='value')\n", - "\n", - "interact(plot_functions, function = function_options, show_derivative = derivative, show_integral = integral);" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "In the demo above you may have noticed that the derivative and integral of the exponential function is same as the exponential function itself.\n", - "\n", - "Some functions like the exponential function, when differentiated or integrated, equal a scalar times the same function. This is a similar idea to eigenvectors of a matrix being those that, when multipled by the matrix, equal a scalar times themselves, as you saw yesterday!\n", - "\n", - "When\n", - "\n", - "\\begin{equation}\n", - "\\frac{d(f(t)}{dt} = a \\cdot f(t)\\text{,}\n", - "\\end{equation}\n", - "\n", - "we say that $f(t)$ is an **eigenfunction** for derivative operator, where $a$ is a scaling factor. Similarly, when\n", - "\n", - "\\begin{equation}\n", - "\\int f(t)dt = a \\cdot f(t)\\text{,}\n", - "\\end{equation}\n", - "\n", - "we say that $f(t)$ is an **eigenfunction** for integral operator.\n", - "\n", - "As you can imagine, working with eigenfunctions can make mathematical analysis easy." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Geometrical_understanding_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 2: Analytical & Numerical Differentiation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 3: Differentiation\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'sHogZISXGuQ'), ('Bilibili', 'BV14g41137d5')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Differentiation_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "\n", - "In this section, we will delve into how we actually find the derivative of a function, both analytically and numerically.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.1: Analytical Differentiation\n", - "\n", - "*Estimated timing to here from start of tutorial: 20 min*\n", - "\n", - "When we find the derivative analytically, we are finding the exact formula for the derivative function.\n", - "\n", - "To do this, instead of having to do some fancy math every time, we can often consult [an online resource](https://en.wikipedia.org/wiki/Differentiation_rules) for a list of common derivatives, in this case our trusty friend Wikipedia.\n", - "\n", - "If I told you to find the derivative of $f(t) = t^3$, you could consult that site and find in Section 2.1, that if $f(t) = t^n$, then $\\frac{d(f(t))}{dt} = nt^{n-1}$. So you would be able to tell me that the derivative of $f(t) = t^3$ is $\\frac{d(f(t))}{dt} = 3t^{2}$.\n", - "\n", - "This list of common derivatives often contains only very simple functions. Luckily, as we'll see in the next two sections, we can often break the derivative of a complex function down into the derivatives of more simple components." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Section 2.1.1: Product Rule\n", - "\n", - "Sometimes we encounter functions which are the product of two functions that both depend on the variable.\n", - "How do we take the derivative of such functions? For this we use the [Product Rule](https://en.wikipedia.org/wiki/Product_rule).\n", - "\n", - "\\begin{align}\n", - "f(t) &= u(t) \\cdot v(t) \\\\ \\\\\n", - "\\frac{d(f(t))}{dt} &= v \\cdot \\frac{du}{dt} + u \\cdot \\frac{dv}{dt}\n", - "\\end{align}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "#### Coding Exercise 2.1.1: Derivative of the postsynaptic potential alpha function\n", - "\n", - "Let's use the product rule to get the derivative of the post-synaptic potential alpha function. As we saw in Video 3, the shape of the postsynaptic potential is given by the so called alpha function:\n", - "\n", - "\\begin{equation}\n", - "f(t) = t \\cdot \\text{exp}\\left( -\\frac{t}{\\tau} \\right)\n", - "\\end{equation}\n", - "\n", - "Here $f(t)$ is a product of $t$ and $\\text{exp} \\left(-\\frac{t}{\\tau} \\right)$. So we can have $u(t) = t$ and $v(t) = \\text{exp} \\left( -\\frac{t}{\\tau} \\right)$ and use the product rule!\n", - "\n", - "We have defined $u(t)$ and $v(t)$ in the code below, in terms of the variable $t$ which is an array of time steps from 0 to 10. Define $\\frac{du}{dt}$ and $\\frac{dv}{dt}$, the compute the full derivative of the alpha function using the product rule. You can always consult wikipedia to figure out $\\frac{du}{dt}$ and $\\frac{dv}{dt}$!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "########################################################################\n", - "## TODO for students\n", - "## Complete all ... in code below and remove\n", - "raise NotImplementedError(\"Calculate the derivatives\")\n", - "########################################################################\n", - "\n", - "# Define time, time constant\n", - "t = np.arange(0, 10, .1)\n", - "tau = 0.5\n", - "\n", - "# Compute alpha function\n", - "f = t * np.exp(-t/tau)\n", - "\n", - "# Define u(t), v(t)\n", - "u_t = t\n", - "v_t = np.exp(-t/tau)\n", - "\n", - "# Define du/dt, dv/dt\n", - "du_dt = ...\n", - "dv_dt = ...\n", - "\n", - "# Define full derivative\n", - "df_dt = ...\n", - "\n", - "# Visualize\n", - "plot_alpha_func(t, f, df_dt)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Define time, time constant\n", - "t = np.arange(0, 10, .1)\n", - "tau = 0.5\n", - "\n", - "# Compute alpha function\n", - "f = t * np.exp(-t/tau)\n", - "\n", - "# Define u(t), v(t)\n", - "u_t = t\n", - "v_t = np.exp(-t/tau)\n", - "\n", - "# Define du/dt, dv/dt\n", - "du_dt = 1\n", - "dv_dt = -1/tau * np.exp(-t/tau)\n", - "\n", - "# Define full derivative\n", - "df_dt = u_t * dv_dt + v_t * du_dt\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_alpha_func(t, f, df_dt)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Derivative_of_the_postsynaptic_potential_alpha_function_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Section 2.1.2: Chain Rule\n", - "\n", - "Many times we encounter situations in which the variable $a$ is changing with time ($t$) and affecting another variable $r$. How can we estimate the derivative of $r$ with respect to $a$ i.e. $\\frac{dr}{da} = ?$\n", - "\n", - "To calculate $\\frac{dr}{da}$ we use the [Chain Rule](https://en.wikipedia.org/wiki/Chain_rule).\n", - "\n", - "\\begin{equation}\n", - "\\frac{dr}{da} = \\frac{dr}{dt}\\cdot\\frac{dt}{da}\n", - "\\end{equation}\n", - "\n", - "That is, we calculate the derivative of both variables with respect to t and divide that derivative of $r$ by that derivative of $a$.\n", - "\n", - "We can also use this formula to simplify taking derivatives of complex functions! We can make an arbitrary function t so that we can compute more simple derivatives and multiply, as we will see in this exercise." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "#### Math Exercise 2.1.2: Chain Rule\n", - "\n", - "Let's say that:\n", - "\n", - "\\begin{equation}\n", - "r(a) = e^{a^4 + 1}\n", - "\\end{equation}\n", - "\n", - "What is $\\frac{dr}{da}$? This is a more complex function so we can't simply consult a table of common derivatives. Can you use the chain rule to help?\n", - "\n", - "**Hint:** we didn't define t but you could set t equal to the function in the exponent." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "\n", - "We can set t equal to the exponent:\n", - "t = a^4 + 1\n", - "r(t) = e^t\n", - "\n", - "\n", - "Then:\n", - " dt/da = 4a^3\n", - " dr/dt = e^t\n", - "\n", - "Now we can use the chain rule:\n", - "dr/da = dr/dt * dt/da\n", - " = e^t(4a^3)\n", - " = 4a^3e^{a^4 + 1}\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Chain_Rule_Math_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Section 2.2.3: Derivatives in Python using SymPy\n", - "\n", - "There is a useful Python library for getting the analytical derivatives of functions: SymPy. We actually used this in Interactive Demo 1, under the hood.\n", - "\n", - "See the following cell for an example of setting up a sympy function and finding the derivative." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# For sympy we first define our symbolic variables\n", - "f, t = sp.symbols('f, t')\n", - "\n", - "# Function definition (sigmoid)\n", - "f = 1/(1 + sp.exp(-(t-5)))\n", - "\n", - "# Get the derivative\n", - "diff_f = sp.diff(f)\n", - "\n", - "# Print the resulting function\n", - "print('Derivative of', f, 'is ', diff_f)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.2: Numerical Differentiation\n", - "\n", - "*Estimated timing to here from start of tutorial: 30 min*\n", - "\n", - "Formally, the derivative of a function $\\mathcal{f}(x)$ at any value $a$ is given by the finite difference (FD) formula:\n", - "\n", - "\\begin{equation}\n", - "FD = \\frac{f(a+h) - f(a)}{h}\n", - "\\end{equation}\n", - "\n", - "As $h\\rightarrow 0$, the $FD$ approaches the actual value of the derivative. Let's check this.\n", - "\n", - "**Note:** The numerical estimate of the derivative will result\n", - "in a time series whose length is one short of the original time series." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 2.2: Numerical Differentiation of the Sine Function\n", - "\n", - "Below, we find the numerical derivative of the sine function for different values of $h$, and and compare the result the analytical solution.\n", - "\n", - "- What values of h result in more accurate numerical derivatives?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to enable the widget.*\n", - "def numerical_derivative_demo(h = 0.2):\n", - " # Now lets create a sequence of numbers which change according to the sine function\n", - " dt = 0.01\n", - " tx = np.arange(-10, 10, dt)\n", - " sine_fun = np.sin(tx)\n", - "\n", - " # symbolic diffrentiation tells us that the derivative of sin(t) is cos(t)\n", - " cos_fun = np.cos(tx)\n", - "\n", - " # Numerical derivative using difference formula\n", - " n_tx = np.arange(-10,10,h) # create new time axis\n", - " n_sine_fun = np.sin(n_tx) # calculate the sine function on the new time axis\n", - " sine_diff = (n_sine_fun[1:] - n_sine_fun[0:-1]) / h\n", - "\n", - " fig = plt.figure()\n", - " ax = plt.subplot(111)\n", - " plt.plot(tx, sine_fun, label='sine function')\n", - " plt.plot(tx, cos_fun, label='analytical derivative of sine')\n", - "\n", - " with plt.xkcd():\n", - " # notice that numerical derivative will have one element less\n", - " plt.plot(n_tx[0:-1], sine_diff, label='numerical derivative of sine')\n", - " plt.xlim([-10, 10])\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('f(x) or df(x)/dt')\n", - " ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.05),\n", - " ncol=3, fancybox=True)\n", - " plt.show()\n", - "\n", - "_ = widgets.interact(numerical_derivative_demo, h = (0.01, 0.5, .02))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "The smaller the value of $h$, the better the estimate of the derivative of the\n", - "function at $x=a$.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Numerical_Differentiation_of_the_Sine_Function_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.3: Transfer Function and Gain of a Neuron\n", - "\n", - "*Estimated timing to here from start of tutorial: 34 min*\n", - "\n", - "When we inject a constant current (DC) in a neuron, its firing rate changes as a function of strength of the injected current. This is called the **input-output transfer function** or just the *transfer function* or *I/O Curve* of the neuron. For most neurons this can be approximated by a sigmoid function e.g.,:\n", - "\n", - "\\begin{equation}\n", - "rate(I) = \\frac{1}{1+\\text{exp}(-a \\cdot (I-\\theta))} - \\frac{1}{\\text{exp}(a \\cdot \\theta)} + \\eta\n", - "\\end{equation}\n", - "\n", - "where $I$ is injected current, $rate$ is the neuron firing rate and $\\eta$ is noise (Gaussian noise with zero mean and $\\sigma$ standard deviation).\n", - "\n", - "*You will visit this equation in a different context in Week 3*\n", - "\n", - "The slope of a neurons input-output transfer function, i.e., $\\frac{d(r(I))}{dI}$, is called the **gain** of the neuron, as it tells how the neuron output will change if the input is changed. In other words, the slope of the transfer function tells us in which range of inputs the neuron output is most sensitive to changes in its input." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 2.3: Calculating the Transfer Function and Gain of a Neuron\n", - "\n", - "In the following demo, you can estimate the gain of the following neuron transfer function using numerical differentiaton. We will use our timestep as h. See the cell below for a function that computes the rate via the fomula above and then the gain using numerical differentiation. In the following cell, you can play with the parameters $a$ and $\\theta$ to change the shape of the transfer functon (and see the resulting gain function). You can also set $I_{mean}$ to see how the slope is computed for that value of I. In the left plot, the red vertical lines are the two values of the current being used to compute the slope, while the blue lines point to the corresponding ouput firing rates.\n", - "\n", - "Change the parameters of the neuron transfer function (i.e., $a$ and $\\theta$) and see if you can predict the value of $I$ for which the neuron has maximal slope and which parameter determines the peak value of the gain.\n", - "\n", - "1. Ensure you understand how the right plot relates to the left!\n", - "2. How does $\\theta$ affect the transfer function and gain?\n", - "3. How does $a$ affect the transfer function and gain?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "def compute_rate_and_gain(I, a, theta, current_timestep):\n", - " \"\"\" Compute rate and gain of neuron based on parameters\n", - "\n", - " Args:\n", - " I (ndarray): different possible values of the current\n", - " a (scalar): parameter of the transfer function\n", - " theta (scalar): parameter of the transfer function\n", - " current_timestep (scalar): the time we're using to take steps\n", - "\n", - " Returns:\n", - " (ndarray, ndarray): rate and gain for each possible value of I\n", - " \"\"\"\n", - "\n", - " # Compute rate\n", - " rate = (1+np.exp(-a*(I-theta)))**-1 - (1+np.exp(a*theta))**-1\n", - "\n", - " # Compute gain using a numerical derivative\n", - " gain = (rate[1:] - rate[0:-1])/current_timestep\n", - "\n", - " return rate, gain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to enable the widget\n", - "\n", - "def plot_rate_and_gain(a, theta, I_mean):\n", - "\n", - " current_timestep = 0.1\n", - "\n", - " # Compute I\n", - " I = np.arange(0, 8, current_timestep)\n", - "\n", - " rate, gain = compute_rate_and_gain(I, a, theta, current_timestep)\n", - " I_1 = I_mean - current_timestep/2\n", - " rate_1 = (1+np.exp(-a*(I_1-theta)))**-1 - (1+np.exp(a*theta))**-1\n", - " I_2 = I_mean + current_timestep/2\n", - " rate_2 = (1+np.exp(-a*(I_2-theta)))**-1 - (1+np.exp(a*theta))**-1\n", - "\n", - " input_range = I_2-I_1\n", - " output_range = rate_2 - rate_1\n", - "\n", - " # Visualize rate and gain\n", - " plt.subplot(1,2,1)\n", - " plt.plot(I,rate)\n", - " plt.plot([I_1,I_1],[0, rate_1],color='r')\n", - " plt.plot([0,I_1],[rate_1, rate_1],color='b')\n", - " plt.plot([I_2,I_2],[0, rate_2],color='r')\n", - " plt.plot([0,I_2],[rate_2, rate_2],color='b')\n", - " plt.xlim([0, 8])\n", - " low, high = plt.ylim()\n", - " plt.ylim([0, high])\n", - "\n", - " plt.xlabel('Injected current (au)')\n", - " plt.ylabel('Output firing rate (normalized)')\n", - " plt.title('Transfer function')\n", - "\n", - " plt.text(2, 1.3, 'Output-Input Ratio =' + str(np.round(1000*output_range/input_range)/1000), style='italic',\n", - " bbox={'facecolor': 'red', 'alpha': 0.5, 'pad': 10})\n", - " plt.subplot(1,2,2)\n", - " plt.plot(I[0:-1], gain)\n", - " plt.plot([I_mean, I_mean],[0,0.6],color='r')\n", - " plt.xlabel('Injected current (au)')\n", - " plt.ylabel('Gain')\n", - " plt.title('Gain')\n", - " plt.xlim([0, 8])\n", - " low, high = plt.ylim()\n", - " plt.ylim([0, high])\n", - "\n", - "_ = widgets.interact(plot_rate_and_gain, a = (0.5, 2.0, .02), theta=(1.2,4.0,0.1), I_mean= (0.5,8.0,0.1))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) $\\theta$ shifts the neuron transfer function along the x-axis -- reducing $theta$ moves the transfer function leftwards.\n", - "So changing $\\theta$ will affect the value of $I$ at which the neuron has the maximal slope.\n", - "Smaller the $\\theta$ smaller the value for maximum slope.\n", - "\n", - "2) $a$ controls the steepness of the neuron transfer function -- increasing $a$ makes the curve more steeper.\n", - "Therefore, $a$ determines the maximum value of the slope.\n", - "\n", - "Note: $a$ and $\\theta$ do not determine the maximum value of the transfer function itself.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Calculating_the_transfer_function_and_gain_of_a_neuron_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 3: Functions of Multiple Variables\n", - "\n", - "*Estimated timing to here from start of tutorial: 44 min*\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 4: Functions of multiple variables\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'Mp_uNNNiQAI'), ('Bilibili', 'BV1Ly4y1M77D')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Functions_of_multiple_variables_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "This video covers what partial derivatives are.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "In the previous section, you looked at function of single variable $t$ or $x$. In most cases, we encounter functions of multiple variables. For example, in the brain, the firing rate of a neuron is a function of both excitatory and inhibitory input rates. In the following, we will look into how to calculate derivatives of such functions.\n", - "\n", - "When we take the derrivative of a multivariable function with respect to one of the variables it is called the **partial derivative**. For example if we have a function:\n", - "\n", - "\\begin{align}\n", - "f(x,y) = x^2 + 2xy + y^2\n", - "\\end{align}\n", - "\n", - "The we can define the partial derivatives as\n", - "\n", - "\\begin{align}\n", - "\\frac{\\partial(f(x,y))}{\\partial x} = 2x + 2y + 0 \\\\\\\\\n", - "\\frac{\\partial(f(x,y))}{\\partial y} = 0 + 2x + 2y\n", - "\\end{align}\n", - "\n", - "In the above, the derivative of the last term ($y^2$) with respect to $x$ is zero because it does not change with respect to $x$. Similarly, the derivative of $x^2$ with respect to $y$ is also zero.\n", - "
\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "Just as with the derivatives we saw earlier, you can get partial derivatives through either an analytical method (finding an exact equation) or a numerical method (approximating)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 3: Visualize partial derivatives\n", - "\n", - "In the demo below, you can input any function of x and y and then visualize both the function and partial derivatives.\n", - "\n", - "We visualized the 2-dimensional function as a surface plot in which the values of the function are rendered as color. Yellow represents a high value and blue represents a low value. The height of the surface also shows the numerical value of the function. A more complete description of 2D surface plots and why we need them is located in Bonus Section 1.1. The first plot is that of our function. And the two bottom plots are the derivative surfaces with respect to $x$ and $y$ variables.\n", - "\n", - "1. Ensure you understand how the plots relate to each other - if not, review the above material\n", - "2. Can you come up with a function where the partial derivative with respect to x will be a linear plane and the derivative with respect to y will be more curvy?\n", - "3. What happens to the partial derivatives if there are no terms involving multiplying $x$ and $y$ together?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute this widget to enable the demo\n", - "\n", - "# Let's use sympy to calculate Partial derivatives of a function of 2-variables\n", - "@interact(f2d_string = 'x**2 + 2*x*y + y**2')\n", - "def plot_partial_derivs(f2d_string):\n", - " f, x, y = sp.symbols('f, x, y')\n", - "\n", - " f2d = eval(f2d_string)\n", - " f2d_dx = sp.diff(f2d,x)\n", - " f2d_dy = sp.diff(f2d,y)\n", - "\n", - " print('Partial derivative of ', f2d, 'with respect to x is', f2d_dx)\n", - " print('Partial derivative of ', f2d, 'with respect to y is', f2d_dy)\n", - "\n", - " p1 = sp.plotting.plot3d(f2d, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='f(x,y)',title='Our function')\n", - "\n", - " p2 = sp.plotting.plot3d(f2d_dx, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='df(x,y)/dx',title='Derivative w.r.t. x')\n", - "\n", - " p3 = sp.plotting.plot3d(f2d_dy, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='df(x,y)/dy',title='Derivative w.r.t. y')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) Change terms involving x to just be linear (a constant times x, or a constant times x times some function of y).\n", - "Change a term involving y to involve more than y or y^2. For example, x + 2*x*y + y**3\n", - "\n", - "2) If there are no terms involving both x and y, the partial derivative with respect to x will\n", - " just depend on x and the partial derivative with respect to y will just depend on y.\n", - "\"\"\";" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "To see an application of the numerical calculation of partial derivatives to understand a neuron driven by excitatory and inhibitory inputs, see Bonus Section 1!\n", - "\n", - "We will use the partial derivative several times in the course. For example partial derivative are used the calculate the Jacobian of a system of differential equations. The Jacobian is used to determine the dynamics and stability of a system. This will be introduced in the second week while studying the dynamics of excitatory and inhibitory population interactions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Visualize_partial_derivatives_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 4: Numerical Integration\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 5: Numerical Integration\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'cT0_CbD_h9Q'), ('Bilibili', 'BV1p54y1H7zt')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Numerical_Integration_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "This video covers numerical integration and specifically Riemann sums.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "Geometrically, integration is the area under the curve. This interpretation gives two formal ways to calculate the integral of a function numerically.\n", - "\n", - "**[Riemann sum](https://en.wikipedia.org/wiki/Riemann_sum)**:\n", - "If we wish to integrate a function $f(t)$ with respect to $t$, then first we divide the function into $n$ intervals of size $dt = a-b$, where $a$ is the starting of the interval. Thus, each interval gives a rectangle with height $f(a)$ and width $dt$. By summing the area of all the rectangles, we can approximate the area under the curve. As the size $dt$ approaches to zero, our estimate of the integral approcahes the analytical calculation. Essentially, the Riemann sum is cutting the region under the curve in vertical stripes, calculating area of the each stripe and summing them up.\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 4.1: Demonstration of the Riemann Sum\n", - "\n", - "*Estimated timing to here from start of tutorial: 60 min*\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 4.1: Riemann Sum vs. Analytical Integral with changing step size\n", - "\n", - "Below, we will compare numerical integration using the Riemann Sum with the analytical solution. You can change the interval size $dt$ using the slider.\n", - "\n", - "1. What values of dt result in the best numerical integration?\n", - "2. What is the downside of choosing that value of $dt$?\n", - "3. With large dt, why are we underestimating the integral (as opposed to overestimating?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Run this cell to enable the widget!\n", - "def riemann_sum_demo(dt = 0.5):\n", - " step_size = 0.1\n", - " min_val = 0.\n", - " max_val = 10.\n", - " tx = np.arange(min_val, max_val, step_size)\n", - "\n", - " # Our function\n", - " ftn = tx**2 - tx + 1\n", - " # And the integral analytical formula calculates using sympy\n", - " int_ftn = tx**3/3 - tx**2/2 + tx\n", - "\n", - " # Numerical integration of f(t) using Riemann Sum\n", - " n = int((max_val-min_val)/dt)\n", - " r_tx = np.zeros(n)\n", - " fun_value = np.zeros(n)\n", - " for ii in range(n):\n", - " a = min_val+ii*dt\n", - " fun_value[ii] = a**2 - a + 1\n", - " r_tx[ii] = a;\n", - "\n", - " # Riemann sum is just cumulative sum of the fun_value multiplied by the\n", - " r_sum = np.cumsum(fun_value)*dt\n", - " with plt.xkcd():\n", - " plt.figure(figsize=(20,5))\n", - " ax = plt.subplot(1,2,1)\n", - " plt.plot(tx,ftn,label='Function')\n", - "\n", - " for ii in range(n):\n", - " plt.plot([r_tx[ii], r_tx[ii], r_tx[ii]+dt, r_tx[ii]+dt], [0, fun_value[ii], fun_value[ii], 0] ,color='r')\n", - "\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('f(t)')\n", - " plt.title('f(t)')\n", - " plt.grid()\n", - "\n", - " plt.subplot(1,2,2)\n", - " plt.plot(tx,int_ftn,label='Analytical')\n", - " plt.plot(r_tx+dt,r_sum,color = 'r',label='Riemann Sum')\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('int(f(t))')\n", - " plt.title('Integral of f(t)')\n", - " plt.grid()\n", - " plt.legend()\n", - " plt.show()\n", - "\n", - "\n", - "_ = widgets.interact(riemann_sum_demo, dt = (0.1, 1., .02))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) The smallest value of dt results in the best numerical integration (most similar\n", - " to the analytical solution)\n", - "\n", - "2) We have to compute the area of more rectangles.\n", - "\n", - "3) Here we used the forward Riemann sum, and it results in an underestimate of the\n", - " integral for positive values of $T$ and underestimate for negative values of\n", - " $T$. We could also use the backward Riemann sum, and that will give an overestimate\n", - " of the integral for positive values of $T$.\n", - "\"\"\";" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "There are other methods of numerical integration, such as\n", - "**[Lebesgue integral](https://en.wikipedia.org/wiki/Lebesgue_integral)** and **Runge Kutta**. In the Lebesgue integral, we divide the area under the curve into horizontal stripes. That is, instead of the independent variable, the range of the function $f(t)$ is divided into small intervals. In any case, the Riemann sum is the basis of Euler's method of integration for solving ordinary differential equations - something you will do in a later tutorial today." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Riemann_Sum_vs_Analytical_Integral_with_changing_step_size_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 4.2: Neural Applications of Numerical Integration\n", - "\n", - "*Estimated timing to here from start of tutorial: 68 min*\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Coding Exercise 4.2: Calculating Charge Transfer with Excitatory Input\n", - "An incoming spike elicits a change in the post-synaptic membrane potential (PSP) which can be captured by the following function:\n", - "\n", - "\\begin{equation}\n", - "PSP(t) = J \\cdot t \\cdot \\text{exp}\\left(-\\frac{t-t_{sp}}{\\tau_{s}}\\right)\n", - "\\end{equation}\n", - "\n", - "where $J$ is the synaptic amplitude, $t_{sp}$ is the spike time and $\\tau_s$ is the synaptic time constant.\n", - "\n", - "Estimate the total charge transfered to the postsynaptic neuron during an PSP with amplitude $J=1.0$, $\\tau_s = 1.0$ and $t_{sp} = 1$ (that is the spike occured at $1$ ms). The total charge will be the integral of the PSP function." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "########################################################################\n", - "## TODO for students\n", - "## Complete all ... in code below and remove\n", - "raise NotImplementedError(\"Calculate the charge transfer\")\n", - "########################################################################\n", - "\n", - "# Set up parameters\n", - "J = 1\n", - "tau_s = 1\n", - "t_sp = 1\n", - "dt = .1\n", - "t = np.arange(0, 10, dt)\n", - "\n", - "# Code PSP formula\n", - "PSP = ...\n", - "\n", - "# Compute numerical integral\n", - "# We already have PSP at every time step (height of rectangles). We need to\n", - "#. multiply by width of rectangles (dt) to get areas\n", - "rectangle_areas = ...\n", - "\n", - "# Cumulatively sum rectangles (hint: use np.cumsum)\n", - "numerical_integral = ...\n", - "\n", - "# Visualize\n", - "plot_charge_transfer(t, PSP, numerical_integral)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set up parameters\n", - "J = 1\n", - "tau_s = 1\n", - "t_sp = 1\n", - "dt = .1\n", - "t = np.arange(0, 10, dt)\n", - "\n", - "# Code PSP formula\n", - "PSP = J * t * np.exp(- (t-t_sp)/tau_s)\n", - "\n", - "# Compute numerical integral\n", - "# We already have PSP at every time step (height of rectangles). We need to\n", - "#. multiply by width of rectangles (dt) to get areas\n", - "rectangle_areas = PSP *dt\n", - "\n", - "# Cumulatively sum rectangles (hint: use np.cumsum)\n", - "numerical_integral = np.cumsum(rectangle_areas)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_charge_transfer(t, PSP, numerical_integral)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "You can see from the figure that the total charge transferred is a little over 2.5." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Calculating_Charge_Transfer_with_Excitatory_Input_exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 5: Differentiation and Integration as Filtering Operations\n", - "\n", - "*Estimated timing to here from start of tutorial: 75 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 6: Filtering Operations\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '7_ZjlT2d174'), ('Bilibili', 'BV1Vy4y1M7oT')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Filtering_Operations_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "This video covers a different interpretation of differentiation and integration: viewing them as filtering operations.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "In the above, we used the notions that geometrically integration is the area under the curve and differentiation is the slope of the curve. There is another interpretation of these two operations.\n", - "\n", - "As we calculate the derivative of a function, we take the difference of adjacent values of the function. This results in the removal of common part between the two values. As a consequence, we end up removing the unchanging part of the signal. If we now think in terms of frequencies, differentiation removes low frequencies, or slow changes. That is, differentiation acts as a high pass filter.\n", - "\n", - "Integration does the opposite because in the estimation of an integral we keep adding adjacent values of the signal. So, again thinking in terms of frequencies, integration is akin to the removal of high frequencies or fast changes (low-pass filter). The shock absorbers in your bike are an example of integrators.\n", - "\n", - "We can see this behavior the demo below. Here we will not work with functions, but with signals. As such, functions and signals are the same. Just that in most cases our signals are measurements with respect to time." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to see visualization\n", - "h = 0.01\n", - "tx = np.arange(0,2,h)\n", - "noise_signal = np.random.uniform(0, 1, (len(tx)))*0.5\n", - "x1 = np.sin(0.5*np.pi*tx) + noise_signal # This will generate a 1 Hz sin wave\n", - "# In the signal x1 we have added random noise which contributs the high frequencies\n", - "\n", - "# Take the derivative equivalent of the signal i.e. subtract the adjacent values\n", - "x1_diff = (x1[1:] - x1[:-1])\n", - "\n", - "# Take the integration equivalent of the signal i.e. sum the adjacent values. And divide by 2 (take average essentially)\n", - "x1_integrate = (x1[1:] + x1[:-1])/2\n", - "\n", - "# Plotting code\n", - "plt.figure(figsize=(15,10))\n", - "plt.subplot(3,1,1)\n", - "plt.plot(tx,x1,label='Original Signal')\n", - "#plt.xlabel('Time (sec)')\n", - "plt.ylabel('Signal Value(au)')\n", - "plt.legend()\n", - "\n", - "plt.subplot(3,1,2)\n", - "plt.plot(tx[0:-1],x1_diff,label='Differentiated Signal')\n", - "# plt.xlabel('Time (sec)')\n", - "plt.ylabel('Differentiated Value(au)')\n", - "plt.legend()\n", - "\n", - "plt.subplot(3,1,3)\n", - "plt.plot(tx,x1,label='Original Signal')\n", - "plt.plot(tx[0:-1],x1_integrate,label='Integrate Signal')\n", - "plt.xlabel('Time (sec)')\n", - "plt.ylabel('Integrate Value(au)')\n", - "plt.legend()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "Notice how the differentiation operation amplifies the fast changes which were contributed by noise. By contrast, the integration operation supresses the fast changing noise. If we perform the same operation of averaging the adjancent samples on the orange trace, we will further smooth the signal. Such sums and subtractions form the basis of digital filters.\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Summary\n", - "\n", - "*Estimated timing of tutorial: 80 minutes*\n", - "\n", - "* Geometrically, integration is the area under the curve and differentiation is the slope of the function\n", - "* The concepts of slope and area can be easily extended to higher dimensions. We saw this when we took the derivative of a 2-dimensional transfer function of a neuron\n", - "* Numerical estimates of both derivatives and integrals require us to choose a time step $h$. The smaller the $h$, the better the estimate, but for small values of $h$, more computations are needed. So there is always some tradeoff.\n", - "* Partial derivatives are just the estimate of the slope along one of the many dimensions of the function. We can combine the slopes in different directions using vector sum to find the direction of the slope.\n", - "* Because the derivative of a function is zero at the local peak or trough, derivatives are used to solve optimization problems.\n", - "* When thinking of signal, integration operation is equivalent to smoothening the signals (i.e., remove fast changes)\n", - "* Differentiation operations remove slow changes and enhance high frequency content of a signal" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Bonus Section 1: Numerical calculation of partial derivatives\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Bonus Section 1.1: Understanding 2D plots\n", - "\n", - "Let's take the example of a neuron driven by excitatory and inhibitory inputs. Because this is for illustrative purposes, we will not go in the details of the numerical range of the input and output variables.\n", - "\n", - "In the function below, we assume that the firing rate of a neuron increases motonotically with an increase in excitation and decreases monotonically with an increase in inhibition. The inhibition is modelled as a subtraction. Like for the 1-dimensional transfer function, here we assume that we can approximate the transfer function as a sigmoid function.\n", - "\n", - "To evaluate the partial derivatives we can use the same numerical differentiation as before but now we apply it to each row and column separately." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to visualize the neuron firing rate surface\n", - "def sigmoid_function(x,a,theta):\n", - " '''\n", - " Population activation function.\n", - "\n", - " Expects:\n", - " x : the population input\n", - " a : the gain of the function\n", - " theta : the threshold of the function\n", - "\n", - " Returns:\n", - " the population activation response F(x) for input x\n", - " '''\n", - " # add the expression of f = F(x)\n", - " f = (1+np.exp(-a*(x-theta)))**-1 - (1+np.exp(a*theta))**-1\n", - "\n", - " return f\n", - "\n", - "# Neuron Transfer function\n", - "step_size = 0.1\n", - "exc_input = np.arange(2,9,step_size)\n", - "inh_input = np.arange(0,7,step_size)\n", - "exc_a = 1.2\n", - "exc_theta = 2.4\n", - "inh_a = 1.\n", - "inh_theta = 4.\n", - "\n", - "rate = np.zeros((len(exc_input),len(inh_input)))\n", - "\n", - "for ii in range(len(exc_input)):\n", - " for jj in range(len(inh_input)):\n", - " rate[ii,jj] = sigmoid_function(exc_input[ii],exc_a,exc_theta) - sigmoid_function(inh_input[jj],inh_a,inh_theta)*0.5\n", - "\n", - "with plt.xkcd():\n", - " X, Y = np.meshgrid(exc_input, inh_input)\n", - " fig = plt.figure(figsize=(12,12))\n", - " ax1 = fig.add_subplot(2,2,1)\n", - " lg_txt = 'Inhibition = ' + str(inh_input[0])\n", - " ax1.plot(exc_input,rate[:,0],label=lg_txt)\n", - " lg_txt = 'Inhibition = ' + str(inh_input[20])\n", - " ax1.plot(exc_input,rate[:,20],label=lg_txt)\n", - " lg_txt = 'Inhibition = ' + str(inh_input[40])\n", - " ax1.plot(exc_input,rate[:,40],label=lg_txt)\n", - " ax1.legend()\n", - " ax1.set_xlabel('Excitatory input (au)')\n", - " ax1.set_ylabel('Neuron output rate (au)');\n", - "\n", - " ax2 = fig.add_subplot(2,2,2)\n", - " lg_txt = 'Excitation = ' + str(exc_input[0])\n", - " ax2.plot(inh_input,rate[0,:],label=lg_txt)\n", - " lg_txt = 'Excitation = ' + str(exc_input[20])\n", - " ax2.plot(inh_input,rate[20,:],label=lg_txt)\n", - " lg_txt = 'Excitation = ' + str(exc_input[40])\n", - " ax2.plot(inh_input,rate[40,:],label=lg_txt)\n", - " ax2.legend()\n", - " ax2.set_xlabel('Inhibitory input (au)')\n", - " ax2.set_ylabel('Neuron output rate (au)');\n", - "\n", - " ax3 = fig.add_subplot(2, 1, 2, projection='3d')\n", - " surf= ax3.plot_surface(Y.T, X.T, rate, rstride=1, cstride=1,\n", - " cmap='viridis', edgecolor='none')\n", - " ax3.set_xlabel('Inhibitory input (au)')\n", - " ax3.set_ylabel('Excitatory input (au)')\n", - " ax3.set_zlabel('Neuron output rate (au)');\n", - " fig.colorbar(surf)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "In the **Top-Left** plot, we see how the neuron output rate increases as a function of excitatory input (e.g., the blue trace). However, as we increase inhibition, expectedly the neuron output decreases and the curve is shifted downwards. This constant shift in the curve suggests that the effect of inhibition is subtractive, and the amount of subtraction does not depend on the neuron output.\n", - "\n", - "We can alternatively see how the neuron output changes with respect to inhibition and study how excitation affects that. This is visualized in the **Top-Right** plot.\n", - "\n", - "This type of plotting is very intuitive, but it becomes very tedious to visualize when there are larger numbers of lines to be plotted. A nice solution to this visualization problem is to render the data as color, as surfaces, or both.\n", - "\n", - "This is what we have done in the plot on the bottom. The colormap on the right shows the output of the neuron as a function of inhibitory input and excitatory input. The output rate is shown both as height along the z-axis and as the color. Blue means low firing rate and yellow means high firing rate (see the color bar).\n", - "\n", - "In the above plot, the output rate of the neuron goes below zero. This is of course not physiological as neurons cannot have negative firing rates. In models, we either choose the operating point such that the output does not go below zero, or else we clamp the neuron output to zero if it goes below zero. You will learn about it more in Week 2." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Bonus Section 1.2: Numerical partial derivatives\n", - "\n", - "We can now compute the partial derivatives of our transfer function in response to excitatory and inhibitory input. We do so below!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell implement our neural transfer function, `plot_2d_neuron_transfer_function`, in respond to excitatory and inhibitory input\n", - "def plot_2d_neuron_transfer_function(exc_a, exc_theta, inh_a, inh_theta):\n", - " # Neuron Transfer Function\n", - " step_size = 0.1\n", - " exc_input = np.arange(1,10,step_size)\n", - " inh_input = np.arange(0,7,step_size)\n", - "\n", - " rate = np.zeros((len(exc_input),len(inh_input)))\n", - " for ii in range(len(exc_input)):\n", - " for jj in range(len(inh_input)):\n", - " rate[ii,jj] = sigmoid_function(exc_input[ii],exc_a,exc_theta) - sigmoid_function(inh_input[jj],inh_a,inh_theta)*0.5\n", - "\n", - " # Derivative with respect to excitatory input rate\n", - " rate_de = np.zeros((len(exc_input)-1,len(inh_input)))# this will have one row less than the rate matrix\n", - " for ii in range(len(inh_input)):\n", - " rate_de[:,ii] = (rate[1:,ii] - rate[0:-1,ii])/step_size\n", - "\n", - " # Derivative with respect to inhibitory input rate\n", - " rate_di = np.zeros((len(exc_input),len(inh_input)-1))# this will have one column less than the rate matrix\n", - " for ii in range(len(exc_input)):\n", - " rate_di[ii,:] = (rate[ii,1:] - rate[ii,0:-1])/step_size\n", - "\n", - "\n", - " X, Y = np.meshgrid(exc_input, inh_input)\n", - " fig = plt.figure(figsize=(20,8))\n", - " ax1 = fig.add_subplot(1, 3, 1, projection='3d')\n", - " surf1 = ax1.plot_surface(Y.T, X.T, rate, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", - " ax1.set_xlabel('Inhibitory input (au)')\n", - " ax1.set_ylabel('Excitatory input (au)')\n", - " ax1.set_zlabel('Neuron output rate (au)')\n", - " ax1.set_title('Rate as a function of Exc. and Inh');\n", - " ax1.view_init(45, 10)\n", - " fig.colorbar(surf1)\n", - "\n", - " Xde, Yde = np.meshgrid(exc_input[0:-1], inh_input)\n", - " ax2 = fig.add_subplot(1, 3, 2, projection='3d')\n", - " surf2 = ax2.plot_surface(Yde.T, Xde.T, rate_de, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", - " ax2.set_xlabel('Inhibitory input (au)')\n", - " ax2.set_ylabel('Excitatory input (au)')\n", - " ax2.set_zlabel('Neuron output rate (au)');\n", - " ax2.set_title('Derivative wrt Excitation');\n", - " ax2.view_init(45, 10)\n", - " fig.colorbar(surf2)\n", - "\n", - " Xdi, Ydi = np.meshgrid(exc_input, inh_input[:-1])\n", - " ax3 = fig.add_subplot(1, 3, 3, projection='3d')\n", - " surf3 = ax3.plot_surface(Ydi.T, Xdi.T, rate_di, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", - " ax3.set_xlabel('Inhibitory input (au)')\n", - " ax3.set_ylabel('Excitatory input (au)')\n", - " ax3.set_zlabel('Neuron output rate (au)');\n", - " ax3.set_title('Derivative wrt Inhibition');\n", - " ax3.view_init(15, -115)\n", - " fig.colorbar(surf3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "plot_2d_neuron_transfer_function(exc_a=1.2, exc_theta=2.4, inh_a=1, inh_theta=4)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "Is this what you expected? Change the parameters in the function to generate the 2-d transfer function of the neuron for different excitatory and inhibitory $a$ and $\\theta$ and test your intuitions Can you relate this shape of the partial derivative surface to the gain of the 1-d transfer-function of a neuron (Section 2)?\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "Here excitation and inhibition are interacting additively. That is, if you change $\\theta$ and $a$ for excitation,\n", - "it will not affect the derivative wrt to inhibition and vice versa.\n", - "\n", - "The effect of varying $\\theta$ and $a$ on the derivative wrt to excitation and inhibition is identical to what you for 1-D transfer function\n", - "$\\theta$ shifts the neuron transfer function along the x-axis -- reducing $theta$ moves the transfer function leftwards.\n", - "So by changing $\\theta$ for excitation you will shift the derivative surface left/right along the excitation axis. Same for inhibition.\n", - "\n", - "$a$ controls the steepness of the neuron transfer function -- increasing $a$ makes the curve more steeper.\n", - "So by changing $a$ for excitation you will vary the width of the notch of the derivative surface.\n", - "For inhibition, by varying $a$ you will vary the width of the ridge in the drivative surface.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Numerical_partial_derivatives_Bonus_Discussion\")" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [ - "nq-hVvP1MZqa", - "KCWR4wciMZqb", - "tcLiKp-AMZqc", - "8-bz4N8eMZqd", - "9-BbBcZPMZqf", - "L0_P-VNEMZqf", - "Yx7vP4zvMZqg", - "RACJEWwMMZqg", - "dXFmgh9UMZqh", - "pMQS0U54MZqi", - "ZLb9XPF_VvPm", - "c0oilOjOV0KZ" - ], - "include_colab_link": true, - "name": "W0D4_Tutorial1", - "provenance": [], - "toc_visible": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "TacT2M82f3ZW" + }, + "source": [ + "# Tutorial 1: Differentiation and Integration\n", + "\n", + "**Week 0, Day 4: Calculus**\n", + "\n", + "**By Neuromatch Academy**\n", + "\n", + "**Content creators:** John S Butler, Arvind Kumar with help from Ella Batty\n", + "\n", + "**Content reviewers:** Aderogba Bayo, Tessy Tom, Matt McCann\n", + "\n", + "**Production editors:** Matthew McCann, Spiros Chavlis, Ella Batty" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "W6VM3JDWf3ZY" + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "FwuOlEwFf3ZY" + }, + "source": [ + "---\n", + "# Tutorial Objectives\n", + "\n", + "*Estimated timing of tutorial: 80 minutes*\n", + "\n", + "In this tutorial, we will cover aspects of calculus that will be frequently used in the main NMA course. We assume that you have some familiarity with calculus but may be a bit rusty or may not have done much practice. Specifically, the objectives of this tutorial are\n", + "\n", + "* Get an intuitive understanding of derivative and integration operations\n", + "* Learn to calculate the derivatives of 1- and 2-dimensional functions/signals numerically\n", + "* Familiarize with the concept of the neuron transfer function in 1- and 2-dimensions.\n", + "* Familiarize with the idea of numerical integration using the Riemann sum" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "JNz5Ytinf3ZY" + }, + "source": [ + "---\n", + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "VuwJSe2wf3ZZ" + }, + "outputs": [], + "source": [ + "# @title Install dependencies\n", + "!pip install sympy --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "kuYTCN1Zf3ZZ" + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D4_T1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "Yuug4Cpwf3ZZ" + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import scipy.optimize as opt # import root-finding algorithm\n", + "import sympy as sp # Python toolbox for symbolic maths\n", + "import matplotlib.pyplot as plt\n", + "from mpl_toolkits.mplot3d import Axes3D # Toolbox for rendring 3D figures\n", + "from mpl_toolkits import mplot3d # Toolbox for rendring 3D figures" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "XUDhmZ4Kf3Za" + }, + "outputs": [], + "source": [ + "# @title Figure Settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import ipywidgets as widgets # interactive display\n", + "from ipywidgets import interact\n", + "%config InlineBackend.figure_format = 'retina'\n", + "# use NMA plot style\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", + "my_layout = widgets.Layout()\n", + "\n", + "fig_w, fig_h = 12, 4.5\n", + "my_fontsize = 16\n", + "my_params = {'axes.labelsize': my_fontsize,\n", + " 'axes.titlesize': my_fontsize,\n", + " 'figure.figsize': [fig_w, fig_h],\n", + " 'font.size': my_fontsize,\n", + " 'legend.fontsize': my_fontsize-4,\n", + " 'lines.markersize': 8.,\n", + " 'lines.linewidth': 2.,\n", + " 'xtick.labelsize': my_fontsize-2,\n", + " 'ytick.labelsize': my_fontsize-2}\n", + "\n", + "plt.rcParams.update(my_params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "k9_M_JXHf3Za" + }, + "outputs": [], + "source": [ + "# @title Plotting Functions\n", + "def move_sympyplot_to_axes(p, ax):\n", + " backend = p.backend(p)\n", + " backend.ax = ax\n", + " backend.process_series()\n", + " backend.ax.spines['right'].set_color('none')\n", + " backend.ax.spines['bottom'].set_position('zero')\n", + " backend.ax.spines['top'].set_color('none')\n", + " plt.close(backend.fig)\n", + "\n", + "\n", + "def plot_functions(function, show_derivative, show_integral):\n", + "\n", + " # For sympy we first define our symbolic variable\n", + " x, y, z, t, f = sp.symbols('x y z t f')\n", + "\n", + " # We define our function\n", + " if function == 'Linear':\n", + " f = -2*t\n", + " name = r'$-2t$'\n", + " elif function == 'Parabolic':\n", + " f = t**2\n", + " name = r'$t^2$'\n", + " elif function == 'Exponential':\n", + " f = sp.exp(t)\n", + " name = r'$e^t$'\n", + " elif function == 'Sine':\n", + " f = sp.sin(t)\n", + " name = r'$sin(t)$'\n", + " elif function == 'Sigmoid':\n", + " f = 1/(1 + sp.exp(-(t-5)))\n", + " name = r'$\\frac{1}{1+e^{-(t-5)}}$'\n", + "\n", + " if show_derivative and not show_integral:\n", + " # Calculate the derivative of sin(t) as a function of t\n", + " diff_f = sp.diff(f)\n", + " print('Derivative of', f, 'is ', diff_f)\n", + "\n", + " p1 = sp.plot(f, diff_f, show=False)\n", + " p1[0].line_color='r'\n", + " p1[1].line_color='b'\n", + " p1[0].label='Function'\n", + " p1[1].label='Derivative'\n", + " p1.legend=True\n", + " p1.title = 'Function = ' + name + '\\n'\n", + " p1.show()\n", + " elif show_integral and not show_derivative:\n", + "\n", + " int_f = sp.integrate(f)\n", + " int_f = int_f - int_f.subs(t, -10)\n", + " print('Integral of', f, 'is ', int_f)\n", + "\n", + "\n", + " p1 = sp.plot(f, int_f, show=False)\n", + " p1[0].line_color='r'\n", + " p1[1].line_color='g'\n", + " p1[0].label='Function'\n", + " p1[1].label='Integral'\n", + " p1.legend=True\n", + " p1.title = 'Function = ' + name + '\\n'\n", + " p1.show()\n", + "\n", + "\n", + " elif show_integral and show_derivative:\n", + "\n", + " diff_f = sp.diff(f)\n", + " print('Derivative of', f, 'is ', diff_f)\n", + "\n", + " int_f = sp.integrate(f)\n", + " int_f = int_f - int_f.subs(t, -10)\n", + " print('Integral of', f, 'is ', int_f)\n", + "\n", + " p1 = sp.plot(f, diff_f, int_f, show=False)\n", + " p1[0].line_color='r'\n", + " p1[1].line_color='b'\n", + " p1[2].line_color='g'\n", + " p1[0].label='Function'\n", + " p1[1].label='Derivative'\n", + " p1[2].label='Integral'\n", + " p1.legend=True\n", + " p1.title = 'Function = ' + name + '\\n'\n", + " p1.show()\n", + "\n", + " else:\n", + "\n", + " p1 = sp.plot(f, show=False)\n", + " p1[0].line_color='r'\n", + " p1[0].label='Function'\n", + " p1.legend=True\n", + " p1.title = 'Function = ' + name + '\\n'\n", + " p1.show()\n", + "\n", + "\n", + "def plot_alpha_func(t, f, df_dt):\n", + "\n", + " plt.figure()\n", + " plt.subplot(2,1,1)\n", + " plt.plot(t, f, 'r', label='Alpha function')\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('Voltage')\n", + " plt.title('Alpha function (f(t))')\n", + " #plt.legend()\n", + "\n", + " plt.subplot(2,1,2)\n", + " plt.plot(t, df_dt, 'b', label='Derivative')\n", + " plt.title('Derivative of alpha function')\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('df/dt')\n", + " #plt.legend()\n", + "\n", + "\n", + "def plot_charge_transfer(t, PSP, numerical_integral):\n", + "\n", + " fig, axes = plt.subplots(1, 2)\n", + "\n", + " axes[0].plot(t, PSP)\n", + " axes[0].set(xlabel = 't', ylabel = 'PSP')\n", + "\n", + " axes[1].plot(t, numerical_integral)\n", + " axes[1].set(xlabel = 't', ylabel = 'Charge Transferred')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "-5lr9lRsf3Za" + }, + "source": [ + "---\n", + "# Section 0: Introduction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "WW3hrs_Rf3Za" + }, + "outputs": [], + "source": [ + "# @title Video 1: Why do we care about calculus?\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'NZwfH_dG2wI'), ('Bilibili', 'BV1F44y1z7Uk')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "u9hhEvO4f3Zb" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Why_do_we_care_about_calculus_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "Qgo1fAhWf3Zb" + }, + "source": [ + "---\n", + "# Section 1: What is differentiation and integration?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "fqnzYQwNf3Zb" + }, + "outputs": [], + "source": [ + "# @title Video 2: A geometrical interpretation of differentiation and integration\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'uQjwr9RQaEs'), ('Bilibili', 'BV1sU4y1G7Ru')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "1u4lk-KPf3Zb" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_A_geometrical_interpretation_of_differentiation_and_integration_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "ezYZtWkAf3Zb" + }, + "source": [ + "This video covers the definition of differentiation and integration, highlights the geometrical interpretation of each, and introduces the idea of eigenfunctions.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "Calculus is a part of mathematics concerned with **continous change**. There are two branches of calculus: differential calculus and integral calculus.\n", + "\n", + "Differentiation of a function $f(t)$ gives you the derivative of that function $\\frac{d(f(t))}{dt}$. A derivative captures how sensitive a function is to slight changes in the input for different ranges of inputs. Geometrically, the derivative of a function at a certain input is the slope of the function at that input. For example, as you drive, the distance traveled changes continuously with time. The derivative of the distance traveled with respect to time is the velocity of the vehicle at each point in time. The velocity tells you the rate of change of the distance traveled at different points in time. If you have slow velocity (a small derivative), the distance traveled doesn't change much for small changes in time. A high velocity (big derivative) means that the distance traveled changes a lot for small changes in time.\n", + "\n", + "The sign of the derivative of a function (or signal) tells whether the signal is increasing or decreasing. For a signal going through changes as a function of time, the derivative will become zero when the signal changes its direction of change (e.g. from increasing to decreasing). That is, at local minimum or maximum values, the slope of the signal will be zero. This property is used in optimizing problems. But we can also use it to find peaks in a signal.\n", + "\n", + "Integration can be thought of as the reverse of differentation. If we integrate the velocity with respect to time, we can calculate the distance traveled. By integrating a function, we are basically trying to find functions that would have the original one as their derivative. When we integrate a function, our integral will have an added unknown scalar constant, $C$.\n", + "For example, if\n", + "\n", + "\\begin{equation}\n", + "g(t) = 1.5t^2 + 4t - 1\n", + "\\end{equation}\n", + "\n", + "our integral function $f(t)$ will be:\n", + "\n", + "\\begin{equation}\n", + "f(t) = \\int g(t) dt = 0.5t^3 + 2t^2 - t + C\n", + "\\end{equation}\n", + "\n", + "This constant exists because the derivative of a constant is 0 so we cannot know what the constant should be. This is an indefinite integral. If we compute a definite integral, that is the integral between two limits of the input, we will not have this unknown constant and the integral of a function will capture the area under the curve of that function between those two limits.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "kwh8lMCof3Zc" + }, + "source": [ + "### Interactive Demo 1: Geometrical understanding\n", + "\n", + "In the interactive demo below, you can pick different functions to examine in the drop-down menu. You can then choose to show the derivative function and/or the integral function.\n", + "\n", + "For the integral, we have chosen the unknown constant $C$ such that the integral function at the left x-axis limit is $0$, as $f(t = -10) = 0$. So the integral will reflect the area under the curve starting from that position.\n", + "\n", + "For each function:\n", + "\n", + "* Examine just the function first. Discuss and predict what the derivative and integral will look like. Remember that _derivative = slope of the function_, _integral = area under the curve from $t = -10$ to that $t$_.\n", + "* Check the derivative - does it match your expectations?\n", + "* Check the integral - does it match your expectations?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "N2Z0IyLXf3Zc" + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to enable the widget\n", + "function_options = widgets.Dropdown(\n", + " options=['Linear', 'Exponential', 'Sine', 'Sigmoid'],\n", + " description='Function',\n", + " disabled=False,\n", + ")\n", + "\n", + "derivative = widgets.Checkbox(\n", + " value=False,\n", + " description='Show derivative',\n", + " disabled=False,\n", + " indent=False\n", + ")\n", + "\n", + "integral = widgets.Checkbox(\n", + " value=False,\n", + " description='Show integral',\n", + " disabled=False,\n", + " indent=False\n", + ")\n", + "\n", + "def on_value_change(change):\n", + " derivative.value = False\n", + " integral.value = False\n", + "\n", + "function_options.observe(on_value_change, names='value')\n", + "\n", + "interact(plot_functions, function = function_options, show_derivative = derivative, show_integral = integral);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "7zF0I-hAf3Zc" + }, + "source": [ + "In the demo above, you may have noticed that the derivative and integral of the exponential function are the same as the exponential function itself.\n", + "\n", + "When differentiated or integrated, some functions, like the exponential function, equal scalar times the same function. This is a similar idea to eigenvectors of a matrix being those that, when multiplied by the matrix, equal scalar times themselves, as you saw yesterday!\n", + "\n", + "When\n", + "\n", + "\\begin{equation}\n", + "\\frac{d(f(t)}{dt} = a \\cdot f(t)\\text{,}\n", + "\\end{equation}\n", + "\n", + "we say that $f(t)$ is an **eigenfunction** for derivative operator, where $a$ is a scaling factor. Similarly, when\n", + "\n", + "\\begin{equation}\n", + "\\int f(t)dt = a \\cdot f(t)\\text{,}\n", + "\\end{equation}\n", + "\n", + "we say that $f(t)$ is an **eigenfunction** for integral operator.\n", + "\n", + "As you can imagine, working with eigenfunctions can make mathematical analysis easy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "EXA0mCAQf3Zc" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Geometrical_understanding_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "waVi1nTkf3Zc" + }, + "source": [ + "---\n", + "# Section 2: Analytical & Numerical Differentiation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "9EfXEFkxf3Zc" + }, + "outputs": [], + "source": [ + "# @title Video 3: Differentiation\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'sHogZISXGuQ'), ('Bilibili', 'BV14g41137d5')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "gp5v4UNZf3Zc" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Differentiation_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "SYVDa-JQf3Zc" + }, + "source": [ + "\n", + "In this section, we will delve into how we actually find the derivative of a function, both analytically and numerically.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "vdsryncaf3Zd" + }, + "source": [ + "When we find the derivative analytically, we obtain the exact formula for the derivative function.\n", + "\n", + "To do this, instead of having to do some fancy math every time, we can often consult [an online resource](https://en.wikipedia.org/wiki/Differentiation_rules) for a list of common derivatives, in this case, our trusty friend Wikipedia.\n", + "\n", + "If I told you to find the derivative of $f(t) = t^3$, you could consult that site and find in Section 2.1 that if $f(t) = t^n$, then $\\frac{d(f(t))}{dt} = nt^{n-1}$. So you would be able to tell me that the derivative of $f(t) = t^3$ is $\\frac{d(f(t))}{dt} = 3t^{2}$.\n", + "\n", + "This list of common derivatives often contains only very simple functions. Luckily, as we'll see in the next two sections, we can often break the derivative of a complex function down into the derivatives of more simple components." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "ptcwB2F6f3Zd" + }, + "source": [ + "### Section 2.1.1: Product Rule\n", + "\n", + "Sometimes we encounter functions which are the product of two functions that both depend on the variable.\n", + "How do we take the derivative of such functions? For this we use the [Product Rule](https://en.wikipedia.org/wiki/Product_rule).\n", + "\n", + "\\begin{align}\n", + "f(t) &= u(t) \\cdot v(t) \\\\ \\\\\n", + "\\frac{d(f(t))}{dt} &= v \\cdot \\frac{du}{dt} + u \\cdot \\frac{dv}{dt}\n", + "\\end{align}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "W_LkDLzlf3Zd" + }, + "source": [ + "#### Coding Exercise 2.1.1: Derivative of the postsynaptic potential alpha function\n", + "\n", + "Let's use the product rule to get the derivative of the postsynaptic potential alpha function. As we saw in Video 3, the shape of the postsynaptic potential is given by the so-called alpha function:\n", + "\n", + "\\begin{equation}\n", + "f(t) = t \\cdot \\text{exp}\\left( -\\frac{t}{\\tau} \\right)\n", + "\\end{equation}\n", + "\n", + "Here $f(t)$ is a product of $t$ and $\\text{exp} \\left(-\\frac{t}{\\tau} \\right)$. So we can have $u(t) = t$ and $v(t) = \\text{exp} \\left( -\\frac{t}{\\tau} \\right)$ and use the product rule!\n", + "\n", + "We have defined $u(t)$ and $v(t)$ in the code below for the variable $t$, an array of time steps from 0 to 10. Define $\\frac{du}{dt}$ and $\\frac{dv}{dt}$, then compute the full derivative of the alpha function using the product rule. You can always consult Wikipedia to figure out $\\frac{du}{dt}$ and $\\frac{dv}{dt}$!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "wQ9LmAOOf3Zd" + }, + "outputs": [], + "source": [ + "########################################################################\n", + "## TODO for students\n", + "## Complete all ... in code below and remove\n", + "raise NotImplementedError(\"Calculate the derivatives\")\n", + "########################################################################\n", + "\n", + "# Define time, time constant\n", + "t = np.arange(0, 10, .1)\n", + "tau = 0.5\n", + "\n", + "# Compute alpha function\n", + "f = t * np.exp(-t/tau)\n", + "\n", + "# Define u(t), v(t)\n", + "u_t = t\n", + "v_t = np.exp(-t/tau)\n", + "\n", + "# Define du/dt, dv/dt\n", + "du_dt = ...\n", + "dv_dt = ...\n", + "\n", + "# Define full derivative\n", + "df_dt = ...\n", + "\n", + "# Visualize\n", + "plot_alpha_func(t, f, df_dt)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "Q8PCjzi4f3Zd" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Define time, time constant\n", + "t = np.arange(0, 10, .1)\n", + "tau = 0.5\n", + "\n", + "# Compute alpha function\n", + "f = t * np.exp(-t/tau)\n", + "\n", + "# Define u(t), v(t)\n", + "u_t = t\n", + "v_t = np.exp(-t/tau)\n", + "\n", + "# Define du/dt, dv/dt\n", + "du_dt = 1\n", + "dv_dt = -1/tau * np.exp(-t/tau)\n", + "\n", + "# Define full derivative\n", + "df_dt = u_t * dv_dt + v_t * du_dt\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_alpha_func(t, f, df_dt)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "7Q2bnV20f3Zd" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Derivative_of_the_postsynaptic_potential_alpha_function_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "Kwbh_DR_f3Zd" + }, + "source": [ + "### Section 2.1.2: Chain Rule\n", + "\n", + "We often encounter situations in which the variable $a$ changes with time ($t$) and affects another variable $r$. How can we estimate the derivative of $r$ with respect to $a$, i.e., $\\frac{dr}{da} = ?$\n", + "\n", + "To calculate $\\frac{dr}{da}$ we use the [Chain Rule](https://en.wikipedia.org/wiki/Chain_rule).\n", + "\n", + "\\begin{equation}\n", + "\\frac{dr}{da} = \\frac{dr}{dt}\\cdot\\frac{dt}{da}\n", + "\\end{equation}\n", + "\n", + "We calculate the derivative of both variables with respect to $t$ and divide that derivative of $r$ by that derivative of $a$.\n", + "\n", + "We can also use this formula to simplify taking derivatives of complex functions! We can make an arbitrary function $t$ to compute more simple derivatives and multiply, as seen in this exercise." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "-oILTPd_f3Zd" + }, + "source": [ + "#### Math Exercise 2.1.2: Chain Rule\n", + "\n", + "Let's say that:\n", + "\n", + "\\begin{equation}\n", + "r(a) = e^{a^4 + 1}\n", + "\\end{equation}\n", + "\n", + "What is $\\frac{dr}{da}$? This is a more complex function, so we can't simply consult a table of common derivatives. Can you use the chain rule to help?\n", + "\n", + "**Hint:** We didn't define $t$, but you could set $t$ equal to the function in the exponent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "p2tAWNiZf3Zd" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "\n", + "We can set t equal to the exponent:\n", + "t = a^4 + 1\n", + "r(t) = e^t\n", + "\n", + "\n", + "Then:\n", + " dt/da = 4a^3\n", + " dr/dt = e^t\n", + "\n", + "Now we can use the chain rule:\n", + "dr/da = dr/dt * dt/da\n", + " = e^t(4a^3)\n", + " = 4a^3e^{a^4 + 1}\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "8c4VPwPYf3Ze" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Chain_Rule_Math_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "YEo06nXDf3Ze" + }, + "source": [ + "### Section 2.2.3: Derivatives in Python using SymPy\n", + "\n", + "There is a useful Python library for getting the analytical derivatives of functions: SymPy. We actually used this in Interactive Demo 1, under the hood.\n", + "\n", + "See the following cell for an example of setting up a SymPy function and finding the derivative." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "UXwn36dyf3Ze" + }, + "outputs": [], + "source": [ + "# For sympy we first define our symbolic variables\n", + "f, t = sp.symbols('f, t')\n", + "\n", + "# Function definition (sigmoid)\n", + "f = 1/(1 + sp.exp(-(t-5)))\n", + "\n", + "# Get the derivative\n", + "diff_f = sp.diff(f)\n", + "\n", + "# Print the resulting function\n", + "print('Derivative of', f, 'is ', diff_f)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "7gARkApDf3Ze" + }, + "source": [ + "## Section 2.2: Numerical Differentiation\n", + "\n", + "*Estimated timing to here from start of tutorial: 30 min*\n", + "\n", + "Formally, the derivative of a function $\\mathcal{f}(x)$ at any value $a$ is given by the finite difference (FD) formula:\n", + "\n", + "\\begin{equation}\n", + "FD = \\frac{f(a+h) - f(a)}{h}\n", + "\\end{equation}\n", + "\n", + "As $h\\rightarrow 0$, the $FD$ approaches the actual value of the derivative. Let's check this.\n", + "\n", + "**Note:** The numerical estimate of the derivative will result\n", + "in a time series whose length is one short of the original time series." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "a1D5KI6zf3Ze" + }, + "source": [ + "### Interactive Demo 2.2: Numerical Differentiation of the Sine Function\n", + "\n", + "Below, we find the numerical derivative of the sine function for different values of $h$ and compare the result to the analytical solution.\n", + "\n", + "* What values of $h$ result in more accurate numerical derivatives?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "kauWhSZFf3Ze" + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to enable the widget.*\n", + "def numerical_derivative_demo(h = 0.2):\n", + " # Now lets create a sequence of numbers which change according to the sine function\n", + " dt = 0.01\n", + " tx = np.arange(-10, 10, dt)\n", + " sine_fun = np.sin(tx)\n", + "\n", + " # symbolic diffrentiation tells us that the derivative of sin(t) is cos(t)\n", + " cos_fun = np.cos(tx)\n", + "\n", + " # Numerical derivative using difference formula\n", + " n_tx = np.arange(-10,10,h) # create new time axis\n", + " n_sine_fun = np.sin(n_tx) # calculate the sine function on the new time axis\n", + " sine_diff = (n_sine_fun[1:] - n_sine_fun[0:-1]) / h\n", + "\n", + " fig = plt.figure()\n", + " ax = plt.subplot(111)\n", + " plt.plot(tx, sine_fun, label='sine function')\n", + " plt.plot(tx, cos_fun, label='analytical derivative of sine')\n", + "\n", + " with plt.xkcd():\n", + " # notice that numerical derivative will have one element less\n", + " plt.plot(n_tx[0:-1], sine_diff, label='numerical derivative of sine')\n", + " plt.xlim([-10, 10])\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('f(x) or df(x)/dt')\n", + " ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.05),\n", + " ncol=3, fancybox=True)\n", + " plt.show()\n", + "\n", + "_ = widgets.interact(numerical_derivative_demo, h = (0.01, 0.5, .02))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "ffcSdzAMf3Zh" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "The smaller the value of $h$, the better the estimate of the derivative of the\n", + "function at $x=a$.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "pnDxCTuTf3Zi" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Numerical_Differentiation_of_the_Sine_Function_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "yLkazqeWf3Zi" + }, + "source": [ + "## Section 2.3: Transfer Function and Gain of a Neuron\n", + "\n", + "*Estimated timing to here from start of tutorial: 34 min*\n", + "\n", + "When we inject a constant current (DC) into a neuron, its firing rate changes as a function of the strength of the injected current. This is called the **input-output transfer function** or just the *transfer function* or *I/O Curve* of the neuron. For most neurons, this can be approximated by a sigmoid function, e.g.:\n", + "\n", + "\\begin{equation}\n", + "rate(I) = \\frac{1}{1+\\text{exp}(-a \\cdot (I-\\theta))} - \\frac{1}{\\text{exp}(a \\cdot \\theta)} + \\eta\n", + "\\end{equation}\n", + "\n", + "where $I$ is injected current, $rate$ is the neuron firing rate and $\\eta$ is noise (Gaussian noise with zero mean and $\\sigma$ standard deviation).\n", + "\n", + "*You will visit this equation in a different context in Week 3*\n", + "\n", + "The slope of a neuron input-output transfer function, i.e., $\\frac{d(r(I))}{dI}$, is called the **gain** of the neuron, as it tells how the neuron output will change if the input is changed. In other words, the slope of the transfer function tells us in which range of inputs the neuron output is most sensitive to changes in its input." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "c4UqcVL5f3Zi" + }, + "source": [ + "### Interactive Demo 2.3: Calculating the Transfer Function and Gain of a Neuron\n", + "\n", + "In the following demo, you can estimate the gain of the following neuron transfer function using numerical differentiation. We will use our timestep as h. See the cell below for a function that computes the rate via the formula above and then the gain using numerical differentiation. In the following cell, you can play with the parameters $a$ and $\\theta$ to change the shape of the transfer function (and see the resulting gain function). You can also set $I_{mean}$ to see how the slope is computed for that value of I. In the left plot, the red vertical lines are the two values of the current being used to calculate the slope, while the blue lines point to the corresponding output firing rates.\n", + "\n", + "Change the parameters of the neuron transfer function (i.e., $a$ and $\\theta$) and see if you can predict the value of $I$ for which the neuron has a maximal slope and which parameter determines the peak value of the gain.\n", + "\n", + "1. Ensure you understand how the right plot relates to the left!\n", + "2. How does $\\theta$ affect the transfer function and gain?\n", + "3. How does $a$ affect the transfer function and gain?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "0FW7LW6Kf3Zi" + }, + "outputs": [], + "source": [ + "def compute_rate_and_gain(I, a, theta, current_timestep):\n", + " \"\"\" Compute rate and gain of neuron based on parameters\n", + "\n", + " Args:\n", + " I (ndarray): different possible values of the current\n", + " a (scalar): parameter of the transfer function\n", + " theta (scalar): parameter of the transfer function\n", + " current_timestep (scalar): the time we're using to take steps\n", + "\n", + " Returns:\n", + " (ndarray, ndarray): rate and gain for each possible value of I\n", + " \"\"\"\n", + "\n", + " # Compute rate\n", + " rate = (1+np.exp(-a*(I-theta)))**-1 - (1+np.exp(a*theta))**-1\n", + "\n", + " # Compute gain using a numerical derivative\n", + " gain = (rate[1:] - rate[0:-1])/current_timestep\n", + "\n", + " return rate, gain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "If0V6Vzif3Zi" + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to enable the widget\n", + "\n", + "def plot_rate_and_gain(a, theta, I_mean):\n", + "\n", + " current_timestep = 0.1\n", + "\n", + " # Compute I\n", + " I = np.arange(0, 8, current_timestep)\n", + "\n", + " rate, gain = compute_rate_and_gain(I, a, theta, current_timestep)\n", + " I_1 = I_mean - current_timestep/2\n", + " rate_1 = (1+np.exp(-a*(I_1-theta)))**-1 - (1+np.exp(a*theta))**-1\n", + " I_2 = I_mean + current_timestep/2\n", + " rate_2 = (1+np.exp(-a*(I_2-theta)))**-1 - (1+np.exp(a*theta))**-1\n", + "\n", + " input_range = I_2-I_1\n", + " output_range = rate_2 - rate_1\n", + "\n", + " # Visualize rate and gain\n", + " plt.subplot(1,2,1)\n", + " plt.plot(I,rate)\n", + " plt.plot([I_1,I_1],[0, rate_1],color='r')\n", + " plt.plot([0,I_1],[rate_1, rate_1],color='b')\n", + " plt.plot([I_2,I_2],[0, rate_2],color='r')\n", + " plt.plot([0,I_2],[rate_2, rate_2],color='b')\n", + " plt.xlim([0, 8])\n", + " low, high = plt.ylim()\n", + " plt.ylim([0, high])\n", + "\n", + " plt.xlabel('Injected current (au)')\n", + " plt.ylabel('Output firing rate (normalized)')\n", + " plt.title('Transfer function')\n", + "\n", + " plt.text(2, 1.3, 'Output-Input Ratio =' + str(np.round(1000*output_range/input_range)/1000), style='italic',\n", + " bbox={'facecolor': 'red', 'alpha': 0.5, 'pad': 10})\n", + " plt.subplot(1,2,2)\n", + " plt.plot(I[0:-1], gain)\n", + " plt.plot([I_mean, I_mean],[0,0.6],color='r')\n", + " plt.xlabel('Injected current (au)')\n", + " plt.ylabel('Gain')\n", + " plt.title('Gain')\n", + " plt.xlim([0, 8])\n", + " low, high = plt.ylim()\n", + " plt.ylim([0, high])\n", + "\n", + "_ = widgets.interact(plot_rate_and_gain, a = (0.5, 2.0, .02), theta=(1.2,4.0,0.1), I_mean= (0.5,8.0,0.1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "eXgyVuUNf3Zj" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) $\\theta$ shifts the neuron transfer function along the x-axis -- reducing $theta$ moves the transfer function leftwards.\n", + "So changing $\\theta$ will affect the value of $I$ at which the neuron has the maximal slope.\n", + "Smaller the $\\theta$ smaller the value for maximum slope.\n", + "\n", + "2) $a$ controls the steepness of the neuron transfer function -- increasing $a$ makes the curve more steeper.\n", + "Therefore, $a$ determines the maximum value of the slope.\n", + "\n", + "Note: $a$ and $\\theta$ do not determine the maximum value of the transfer function itself.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "-0G8jJeef3Zj" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Calculating_the_transfer_function_and_gain_of_a_neuron_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "pUEKL3MNf3Zj" + }, + "source": [ + "---\n", + "# Section 3: Functions of Multiple Variables\n", + "\n", + "*Estimated timing to here from start of tutorial: 44 min*\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "aXy7S_1Wf3Zj" + }, + "outputs": [], + "source": [ + "# @title Video 4: Functions of multiple variables\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'Mp_uNNNiQAI'), ('Bilibili', 'BV1Ly4y1M77D')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "j1F7DU_5f3Zj" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Functions_of_multiple_variables_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "Ndww73njf3Zj" + }, + "source": [ + "This video covers what partial derivatives are.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "In the previous section, you looked at function of single variable $t$ or $x$. In most cases, we encounter functions of multiple variables. For example, in the brain, the firing rate of a neuron is a function of both excitatory and inhibitory input rates. In the following, we will look into how to calculate derivatives of such functions.\n", + "\n", + "When we take the derrivative of a multivariable function with respect to one of the variables it is called the **partial derivative**. For example if we have a function:\n", + "\n", + "\\begin{align}\n", + "f(x,y) = x^2 + 2xy + y^2\n", + "\\end{align}\n", + "\n", + "The we can define the partial derivatives as\n", + "\n", + "\\begin{align}\n", + "\\frac{\\partial(f(x,y))}{\\partial x} = 2x + 2y + 0 \\\\\\\\\n", + "\\frac{\\partial(f(x,y))}{\\partial y} = 0 + 2x + 2y\n", + "\\end{align}\n", + "\n", + "In the above, the derivative of the last term ($y^2$) with respect to $x$ is zero because it does not change with respect to $x$. Similarly, the derivative of $x^2$ with respect to $y$ is also zero.\n", + "
\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "FbUu-n2qf3Zk" + }, + "source": [ + "Just as with the derivatives we saw earlier, you can get partial derivatives through either an analytical method (finding an exact equation) or a numerical method (approximating)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "jEUEx5aKf3Zk" + }, + "source": [ + "### Interactive Demo 3: Visualize partial derivatives\n", + "\n", + "In the demo below, you can input any function of $x$ and $y$ and then visualize both the function and partial derivatives.\n", + "\n", + "We visualized the 2-dimensional function as a surface plot in which the function values are rendered as color. Yellow represents a high value, and blue represents a low value. The height of the surface also shows the numerical value of the function. A complete description of 2D surface plots and why we need them can be found in Bonus Section 1.1. The first plot is that of our function. And the two bottom plots are the derivative surfaces with respect to $x$ and $y$ variables.\n", + "\n", + "1. Ensure you understand how the plots relate to each other - if not, review the above material\n", + "2. Can you come up with a function where the partial derivative with respect to x will be a linear plane, and the derivative with respect to y will be more curvy?\n", + "3. What happens to the partial derivatives if no terms involve multiplying $x$ and $y$ together?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "T4zMt6wMf3Zk" + }, + "outputs": [], + "source": [ + "# @markdown Execute this widget to enable the demo\n", + "\n", + "# Let's use sympy to calculate Partial derivatives of a function of 2-variables\n", + "@interact(f2d_string = 'x**2 + 2*x*y + y**2')\n", + "def plot_partial_derivs(f2d_string):\n", + " f, x, y = sp.symbols('f, x, y')\n", + "\n", + " f2d = eval(f2d_string)\n", + " f2d_dx = sp.diff(f2d,x)\n", + " f2d_dy = sp.diff(f2d,y)\n", + "\n", + " print('Partial derivative of ', f2d, 'with respect to x is', f2d_dx)\n", + " print('Partial derivative of ', f2d, 'with respect to y is', f2d_dy)\n", + "\n", + " p1 = sp.plotting.plot3d(f2d, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='f(x,y)',title='Our function')\n", + "\n", + " p2 = sp.plotting.plot3d(f2d_dx, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='df(x,y)/dx',title='Derivative w.r.t. x')\n", + "\n", + " p3 = sp.plotting.plot3d(f2d_dy, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='df(x,y)/dy',title='Derivative w.r.t. y')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "cLwCCU7vf3Zk" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) Change terms involving x to just be linear (a constant times x, or a constant times x times some function of y).\n", + "Change a term involving y to involve more than y or y^2. For example, x + 2*x*y + y**3\n", + "\n", + "2) If there are no terms involving both x and y, the partial derivative with respect to x will\n", + " just depend on x and the partial derivative with respect to y will just depend on y.\n", + "\"\"\";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "vF1chLsVf3Zk" + }, + "source": [ + "To see an application of the numerical calculation of partial derivatives to understand a neuron driven by excitatory and inhibitory inputs, see Bonus Section 1!\n", + "\n", + "We will use the partial derivative several times in the course. For example partial derivative are used the calculate the Jacobian of a system of differential equations. The Jacobian is used to determine the dynamics and stability of a system. This will be introduced in the second week while studying the dynamics of excitatory and inhibitory population interactions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "Q0j7SA99f3Zk" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Visualize_partial_derivatives_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "Imv-FOE5f3Zl" + }, + "source": [ + "---\n", + "# Section 4: Numerical Integration\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "NATlkGfpf3Zl" + }, + "outputs": [], + "source": [ + "# @title Video 5: Numerical Integration\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'cT0_CbD_h9Q'), ('Bilibili', 'BV1p54y1H7zt')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "FGQ_3nGCf3Zl" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Numerical_Integration_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "B32nL6y9f3Zl" + }, + "source": [ + "This video covers numerical integration and specifically Riemann sums.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "Geometrically, integration is the area under the curve. This interpretation gives two formal ways to calculate the integral of a function numerically.\n", + "\n", + "**[Riemann sum](https://en.wikipedia.org/wiki/Riemann_sum)**:\n", + "If we wish to integrate a function $f(t)$ with respect to $t$, then first we divide the function into $n$ intervals of size $dt = a-b$, where $a$ is the starting of the interval. Thus, each interval gives a rectangle with height $f(a)$ and width $dt$. By summing the area of all the rectangles, we can approximate the area under the curve. As the size $dt$ approaches to zero, our estimate of the integral approcahes the analytical calculation. Essentially, the Riemann sum is cutting the region under the curve in vertical stripes, calculating area of the each stripe and summing them up.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "-yuPq7cIf3Zl" + }, + "source": [ + "## Section 4.1: Demonstration of the Riemann Sum\n", + "\n", + "*Estimated timing to here from start of tutorial: 60 min*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "FyqtNb9ff3Zl" + }, + "source": [ + "### Interactive Demo 4.1: Riemann Sum vs. Analytical Integral with changing step size\n", + "\n", + "Below, we will compare numerical integration using the Riemann Sum with the analytical solution. You can change the interval size $dt$ using the slider.\n", + "\n", + "1. What values of dt result in the best numerical integration?\n", + "2. What is the downside of choosing that value of $dt$?\n", + "3. With large dt, why are we underestimating the integral (as opposed to overestimating?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "DCIZCjVef3Zm" + }, + "outputs": [], + "source": [ + "# @markdown Run this cell to enable the widget!\n", + "def riemann_sum_demo(dt = 0.5):\n", + " step_size = 0.1\n", + " min_val = 0.\n", + " max_val = 10.\n", + " tx = np.arange(min_val, max_val, step_size)\n", + "\n", + " # Our function\n", + " ftn = tx**2 - tx + 1\n", + " # And the integral analytical formula calculates using sympy\n", + " int_ftn = tx**3/3 - tx**2/2 + tx\n", + "\n", + " # Numerical integration of f(t) using Riemann Sum\n", + " n = int((max_val-min_val)/dt)\n", + " r_tx = np.zeros(n)\n", + " fun_value = np.zeros(n)\n", + " for ii in range(n):\n", + " a = min_val+ii*dt\n", + " fun_value[ii] = a**2 - a + 1\n", + " r_tx[ii] = a;\n", + "\n", + " # Riemann sum is just cumulative sum of the fun_value multiplied by the\n", + " r_sum = np.cumsum(fun_value)*dt\n", + " with plt.xkcd():\n", + " plt.figure(figsize=(20,5))\n", + " ax = plt.subplot(1,2,1)\n", + " plt.plot(tx,ftn,label='Function')\n", + "\n", + " for ii in range(n):\n", + " plt.plot([r_tx[ii], r_tx[ii], r_tx[ii]+dt, r_tx[ii]+dt], [0, fun_value[ii], fun_value[ii], 0] ,color='r')\n", + "\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('f(t)')\n", + " plt.title('f(t)')\n", + " plt.grid()\n", + "\n", + " plt.subplot(1,2,2)\n", + " plt.plot(tx,int_ftn,label='Analytical')\n", + " plt.plot(r_tx+dt,r_sum,color = 'r',label='Riemann Sum')\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('int(f(t))')\n", + " plt.title('Integral of f(t)')\n", + " plt.grid()\n", + " plt.legend()\n", + " plt.show()\n", + "\n", + "\n", + "_ = widgets.interact(riemann_sum_demo, dt = (0.1, 1., .02))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "7EHfVa1wf3Zm" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) The smallest value of dt results in the best numerical integration (most similar\n", + " to the analytical solution)\n", + "\n", + "2) We have to compute the area of more rectangles.\n", + "\n", + "3) Here we used the forward Riemann sum, and it results in an underestimate of the\n", + " integral for positive values of $T$ and underestimate for negative values of\n", + " $T$. We could also use the backward Riemann sum, and that will give an overestimate\n", + " of the integral for positive values of $T$.\n", + "\"\"\";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "SZiFfEHxf3Zm" + }, + "source": [ + "There are other methods of numerical integration, such as\n", + "**[Lebesgue integral](https://en.wikipedia.org/wiki/Lebesgue_integral)** and **Runge Kutta**. In the Lebesgue integral, we divide the area under the curve into horizontal stripes. That is, instead of the independent variable, the range of the function $f(t)$ is divided into small intervals. In any case, the Riemann sum is the basis of Euler's integration method for solving ordinary differential equations - something you will do in a later tutorial today." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "_1MMFiUef3Zm" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Riemann_Sum_vs_Analytical_Integral_with_changing_step_size_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "alSysG_Jf3Zm" + }, + "source": [ + "## Section 4.2: Neural Applications of Numerical Integration\n", + "\n", + "*Estimated timing to here from start of tutorial: 68 min*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "4Ju_l1lLf3Zm" + }, + "source": [ + "### Coding Exercise 4.2: Calculating Charge Transfer with Excitatory Input\n", + "\n", + "An incoming spike elicits a change in the post-synaptic membrane potential (PSP), which can be captured by the following function:\n", + "\n", + "\\begin{equation}\n", + "PSP(t) = J \\cdot t \\cdot \\text{exp}\\left(-\\frac{t-t_{sp}}{\\tau_{s}}\\right)\n", + "\\end{equation}\n", + "\n", + "where $J$ is the synaptic amplitude, $t_{sp}$ is the spike time and $\\tau_s$ is the synaptic time constant.\n", + "\n", + "Estimate the total charge transferred to the postsynaptic neuron during a PSP with amplitude $J=1.0$, $\\tau_s = 1.0$ and $t_{sp} = 1$ (that is the spike occurred at $1$ ms). The total charge will be the integral of the PSP function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "DlOaqWFPf3Zm" + }, + "outputs": [], + "source": [ + "########################################################################\n", + "## TODO for students\n", + "## Complete all ... in code below and remove\n", + "raise NotImplementedError(\"Calculate the charge transfer\")\n", + "########################################################################\n", + "\n", + "# Set up parameters\n", + "J = 1\n", + "tau_s = 1\n", + "t_sp = 1\n", + "dt = .1\n", + "t = np.arange(0, 10, dt)\n", + "\n", + "# Code PSP formula\n", + "PSP = ...\n", + "\n", + "# Compute numerical integral\n", + "# We already have PSP at every time step (height of rectangles). We need to\n", + "#. multiply by width of rectangles (dt) to get areas\n", + "rectangle_areas = ...\n", + "\n", + "# Cumulatively sum rectangles (hint: use np.cumsum)\n", + "numerical_integral = ...\n", + "\n", + "# Visualize\n", + "plot_charge_transfer(t, PSP, numerical_integral)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "g3hXDdBYf3Zn" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set up parameters\n", + "J = 1\n", + "tau_s = 1\n", + "t_sp = 1\n", + "dt = .1\n", + "t = np.arange(0, 10, dt)\n", + "\n", + "# Code PSP formula\n", + "PSP = J * t * np.exp(- (t-t_sp)/tau_s)\n", + "\n", + "# Compute numerical integral\n", + "# We already have PSP at every time step (height of rectangles). We need to\n", + "#. multiply by width of rectangles (dt) to get areas\n", + "rectangle_areas = PSP *dt\n", + "\n", + "# Cumulatively sum rectangles (hint: use np.cumsum)\n", + "numerical_integral = np.cumsum(rectangle_areas)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_charge_transfer(t, PSP, numerical_integral)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "v5Lp4LbMf3Zn" + }, + "source": [ + "You can see from the figure that the total charge transferred is a little over 2.5." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "VM89npQgf3Zn" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Calculating_Charge_Transfer_with_Excitatory_Input_exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "Pm6yOD1Nf3Zn" + }, + "source": [ + "---\n", + "# Section 5: Differentiation and Integration as Filtering Operations\n", + "\n", + "*Estimated timing to here from start of tutorial: 75 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "vQ0ln-Tnf3Zn" + }, + "outputs": [], + "source": [ + "# @title Video 6: Filtering Operations\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '7_ZjlT2d174'), ('Bilibili', 'BV1Vy4y1M7oT')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "uNKjyPFEf3Zn" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Filtering_Operations_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "sHQvPYXIf3Zo" + }, + "source": [ + "This video covers a different interpretation of differentiation and integration: viewing them as filtering operations.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "In the above, we used the notions that geometrically integration is the area under the curve and differentiation is the slope of the curve. There is another interpretation of these two operations.\n", + "\n", + "As we calculate the derivative of a function, we take the difference of adjacent values of the function. This results in the removal of common part between the two values. As a consequence, we end up removing the unchanging part of the signal. If we now think in terms of frequencies, differentiation removes low frequencies, or slow changes. That is, differentiation acts as a high pass filter.\n", + "\n", + "Integration does the opposite because in the estimation of an integral we keep adding adjacent values of the signal. So, again thinking in terms of frequencies, integration is akin to the removal of high frequencies or fast changes (low-pass filter). The shock absorbers in your bike are an example of integrators.\n", + "\n", + "We can see this behavior the demo below. Here we will not work with functions, but with signals. As such, functions and signals are the same. Just that in most cases our signals are measurements with respect to time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "_kGtaT7mf3Zo" + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to see visualization\n", + "h = 0.01\n", + "tx = np.arange(0,2,h)\n", + "noise_signal = np.random.uniform(0, 1, (len(tx)))*0.5\n", + "x1 = np.sin(0.5*np.pi*tx) + noise_signal # This will generate a 1 Hz sin wave\n", + "# In the signal x1 we have added random noise which contributs the high frequencies\n", + "\n", + "# Take the derivative equivalent of the signal i.e. subtract the adjacent values\n", + "x1_diff = (x1[1:] - x1[:-1])\n", + "\n", + "# Take the integration equivalent of the signal i.e. sum the adjacent values. And divide by 2 (take average essentially)\n", + "x1_integrate = (x1[1:] + x1[:-1])/2\n", + "\n", + "# Plotting code\n", + "plt.figure(figsize=(15,10))\n", + "plt.subplot(3,1,1)\n", + "plt.plot(tx,x1,label='Original Signal')\n", + "#plt.xlabel('Time (sec)')\n", + "plt.ylabel('Signal Value(au)')\n", + "plt.legend()\n", + "\n", + "plt.subplot(3,1,2)\n", + "plt.plot(tx[0:-1],x1_diff,label='Differentiated Signal')\n", + "# plt.xlabel('Time (sec)')\n", + "plt.ylabel('Differentiated Value(au)')\n", + "plt.legend()\n", + "\n", + "plt.subplot(3,1,3)\n", + "plt.plot(tx,x1,label='Original Signal')\n", + "plt.plot(tx[0:-1],x1_integrate,label='Integrate Signal')\n", + "plt.xlabel('Time (sec)')\n", + "plt.ylabel('Integrate Value(au)')\n", + "plt.legend()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "rOTJkbSRf3Zo" + }, + "source": [ + "Notice how the differentiation operation amplifies the fast changes which were contributed by noise. By contrast, the integration operation suppresses the fast-changing noise. We will further smooth the signal if we perform the same operation of averaging the adjacent samples on the orange trace. Such sums and subtractions form the basis of digital filters." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "7dQWDg3Gf3Zo" + }, + "source": [ + "---\n", + "# Summary\n", + "\n", + "*Estimated timing of tutorial: 80 minutes*\n", + "\n", + "* Geometrically, integration is the area under the curve, and differentiation is the slope of the function\n", + "* The concepts of slope and area can be easily extended to higher dimensions. We saw this when we took the derivative of a 2-dimensional transfer function of a neuron\n", + "* Numerical estimates of both derivatives and integrals require us to choose a time step $h$. The smaller the $h$, the better the estimate, but more computations are needed for small values of $h$. So there is always some tradeoff\n", + "* Partial derivatives are just the estimate of the slope along one of the many dimensions of the function. We can combine the slopes in different directions using vector sum to find the direction of the slope\n", + "* Because the derivative of a function is zero at the local peak or trough, derivatives are used to solve optimization problems\n", + "* When thinking of signal, integration operation is equivalent to smoothening the signals (i.e., removing fast changes)\n", + "* Differentiation operations remove slow changes and enhance the high-frequency content of a signal" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "LCM50wunf3Zo" + }, + "source": [ + "---\n", + "# Bonus Section 1: Numerical calculation of partial derivatives\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "hcXseSHlf3Zo" + }, + "source": [ + "## Bonus Section 1.1: Understanding 2D plots\n", + "\n", + "Let's take the example of a neuron driven by excitatory and inhibitory inputs. Because this is for illustrative purposes, we will not go into the details of the numerical range of the input and output variables.\n", + "\n", + "In the function below, we assume that the firing rate of a neuron increases monotonically with an increase in excitation and decreases monotonically with an increase in inhibition. The inhibition is modeled as a subtraction. As for the 1-dimensional transfer function, we assume that we can approximate the transfer function as a sigmoid function.\n", + "\n", + "We can use the same numerical differentiation as before to evaluate the partial derivatives, but now we apply it to each row and column separately." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "5HLCH1G0f3Zo" + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to visualize the neuron firing rate surface\n", + "def sigmoid_function(x,a,theta):\n", + " '''\n", + " Population activation function.\n", + "\n", + " Expects:\n", + " x : the population input\n", + " a : the gain of the function\n", + " theta : the threshold of the function\n", + "\n", + " Returns:\n", + " the population activation response F(x) for input x\n", + " '''\n", + " # add the expression of f = F(x)\n", + " f = (1+np.exp(-a*(x-theta)))**-1 - (1+np.exp(a*theta))**-1\n", + "\n", + " return f\n", + "\n", + "# Neuron Transfer function\n", + "step_size = 0.1\n", + "exc_input = np.arange(2,9,step_size)\n", + "inh_input = np.arange(0,7,step_size)\n", + "exc_a = 1.2\n", + "exc_theta = 2.4\n", + "inh_a = 1.\n", + "inh_theta = 4.\n", + "\n", + "rate = np.zeros((len(exc_input),len(inh_input)))\n", + "\n", + "for ii in range(len(exc_input)):\n", + " for jj in range(len(inh_input)):\n", + " rate[ii,jj] = sigmoid_function(exc_input[ii],exc_a,exc_theta) - sigmoid_function(inh_input[jj],inh_a,inh_theta)*0.5\n", + "\n", + "with plt.xkcd():\n", + " X, Y = np.meshgrid(exc_input, inh_input)\n", + " fig = plt.figure(figsize=(12,12))\n", + " ax1 = fig.add_subplot(2,2,1)\n", + " lg_txt = 'Inhibition = ' + str(inh_input[0])\n", + " ax1.plot(exc_input,rate[:,0],label=lg_txt)\n", + " lg_txt = 'Inhibition = ' + str(inh_input[20])\n", + " ax1.plot(exc_input,rate[:,20],label=lg_txt)\n", + " lg_txt = 'Inhibition = ' + str(inh_input[40])\n", + " ax1.plot(exc_input,rate[:,40],label=lg_txt)\n", + " ax1.legend()\n", + " ax1.set_xlabel('Excitatory input (au)')\n", + " ax1.set_ylabel('Neuron output rate (au)');\n", + "\n", + " ax2 = fig.add_subplot(2,2,2)\n", + " lg_txt = 'Excitation = ' + str(exc_input[0])\n", + " ax2.plot(inh_input,rate[0,:],label=lg_txt)\n", + " lg_txt = 'Excitation = ' + str(exc_input[20])\n", + " ax2.plot(inh_input,rate[20,:],label=lg_txt)\n", + " lg_txt = 'Excitation = ' + str(exc_input[40])\n", + " ax2.plot(inh_input,rate[40,:],label=lg_txt)\n", + " ax2.legend()\n", + " ax2.set_xlabel('Inhibitory input (au)')\n", + " ax2.set_ylabel('Neuron output rate (au)');\n", + "\n", + " ax3 = fig.add_subplot(2, 1, 2, projection='3d')\n", + " surf= ax3.plot_surface(Y.T, X.T, rate, rstride=1, cstride=1,\n", + " cmap='viridis', edgecolor='none')\n", + " ax3.set_xlabel('Inhibitory input (au)')\n", + " ax3.set_ylabel('Excitatory input (au)')\n", + " ax3.set_zlabel('Neuron output rate (au)');\n", + " fig.colorbar(surf)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "WkpVNMRgf3Zp" + }, + "source": [ + "The **Top-Left** plot shows how the neuron output rate increases as a function of excitatory input (e.g., the blue trace). However, as we increase inhibition, expectedly, the neuron output decreases, and the curve is shifted downwards. This constant shift in the curve suggests that the effect of inhibition is subtractive, and the amount of subtraction does not depend on the neuron output.\n", + "\n", + "We can alternatively see how the neuron output changes with respect to inhibition and study how excitation affects that. This is visualized in the **Top-Right** plot.\n", + "\n", + "This type of plotting is very intuitive, but it becomes very tedious to visualize when there are larger numbers of lines to be plotted. A nice solution to this visualization problem is to render the data as color, as surfaces, or both.\n", + "\n", + "This is what we have done in the plot at the bottom. The color map on the right shows the neuron's output as a function of inhibitory and excitatory input. The output rate is shown both as height along the z-axis and as the color. Blue means a low firing rate, and yellow represents a high firing rate (see the color bar).\n", + "\n", + "In the above plot, the output rate of the neuron goes below zero. This is, of course, not physiological, as neurons cannot have negative firing rates. In models, we either choose the operating point so that the output does not go below zero, or we clamp the neuron output to zero if it goes below zero. You will learn about it more in Week 2." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "QTBofq-hf3Zp" + }, + "source": [ + "## Bonus Section 1.2: Numerical partial derivatives\n", + "\n", + "We can now compute the partial derivatives of our transfer function in response to excitatory and inhibitory input. We do so below!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "_ilQ5NfXf3Zp" + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell implement our neural transfer function, `plot_2d_neuron_transfer_function`, in respond to excitatory and inhibitory input\n", + "def plot_2d_neuron_transfer_function(exc_a, exc_theta, inh_a, inh_theta):\n", + " # Neuron Transfer Function\n", + " step_size = 0.1\n", + " exc_input = np.arange(1,10,step_size)\n", + " inh_input = np.arange(0,7,step_size)\n", + "\n", + " rate = np.zeros((len(exc_input),len(inh_input)))\n", + " for ii in range(len(exc_input)):\n", + " for jj in range(len(inh_input)):\n", + " rate[ii,jj] = sigmoid_function(exc_input[ii],exc_a,exc_theta) - sigmoid_function(inh_input[jj],inh_a,inh_theta)*0.5\n", + "\n", + " # Derivative with respect to excitatory input rate\n", + " rate_de = np.zeros((len(exc_input)-1,len(inh_input)))# this will have one row less than the rate matrix\n", + " for ii in range(len(inh_input)):\n", + " rate_de[:,ii] = (rate[1:,ii] - rate[0:-1,ii])/step_size\n", + "\n", + " # Derivative with respect to inhibitory input rate\n", + " rate_di = np.zeros((len(exc_input),len(inh_input)-1))# this will have one column less than the rate matrix\n", + " for ii in range(len(exc_input)):\n", + " rate_di[ii,:] = (rate[ii,1:] - rate[ii,0:-1])/step_size\n", + "\n", + "\n", + " X, Y = np.meshgrid(exc_input, inh_input)\n", + " fig = plt.figure(figsize=(20,8))\n", + " ax1 = fig.add_subplot(1, 3, 1, projection='3d')\n", + " surf1 = ax1.plot_surface(Y.T, X.T, rate, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", + " ax1.set_xlabel('Inhibitory input (au)')\n", + " ax1.set_ylabel('Excitatory input (au)')\n", + " ax1.set_zlabel('Neuron output rate (au)')\n", + " ax1.set_title('Rate as a function of Exc. and Inh');\n", + " ax1.view_init(45, 10)\n", + " fig.colorbar(surf1)\n", + "\n", + " Xde, Yde = np.meshgrid(exc_input[0:-1], inh_input)\n", + " ax2 = fig.add_subplot(1, 3, 2, projection='3d')\n", + " surf2 = ax2.plot_surface(Yde.T, Xde.T, rate_de, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", + " ax2.set_xlabel('Inhibitory input (au)')\n", + " ax2.set_ylabel('Excitatory input (au)')\n", + " ax2.set_zlabel('Neuron output rate (au)');\n", + " ax2.set_title('Derivative wrt Excitation');\n", + " ax2.view_init(45, 10)\n", + " fig.colorbar(surf2)\n", + "\n", + " Xdi, Ydi = np.meshgrid(exc_input, inh_input[:-1])\n", + " ax3 = fig.add_subplot(1, 3, 3, projection='3d')\n", + " surf3 = ax3.plot_surface(Ydi.T, Xdi.T, rate_di, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", + " ax3.set_xlabel('Inhibitory input (au)')\n", + " ax3.set_ylabel('Excitatory input (au)')\n", + " ax3.set_zlabel('Neuron output rate (au)');\n", + " ax3.set_title('Derivative wrt Inhibition');\n", + " ax3.view_init(15, -115)\n", + " fig.colorbar(surf3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "18k4QcXPf3Zp" + }, + "outputs": [], + "source": [ + "plot_2d_neuron_transfer_function(exc_a=1.2, exc_theta=2.4, inh_a=1, inh_theta=4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "Ycrutkqvf3Zp" + }, + "source": [ + "Is this what you expected? Change the parameters in the function to generate the 2-D transfer function of the neuron for different excitatory and inhibitory $a$ and $\\theta$ and test your intuitions. Can you relate this shape of the partial derivative surface to the gain of the 1-D transfer function of a neuron (Section 2)?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "RSlm7-54f3Zp" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "Here excitation and inhibition are interacting additively. That is, if you change $\\theta$ and $a$ for excitation,\n", + "it will not affect the derivative wrt to inhibition and vice versa.\n", + "\n", + "The effect of varying $\\theta$ and $a$ on the derivative wrt to excitation and inhibition is identical to what you for 1-D transfer function\n", + "$\\theta$ shifts the neuron transfer function along the x-axis -- reducing $theta$ moves the transfer function leftwards.\n", + "So by changing $\\theta$ for excitation you will shift the derivative surface left/right along the excitation axis. Same for inhibition.\n", + "\n", + "$a$ controls the steepness of the neuron transfer function -- increasing $a$ makes the curve more steeper.\n", + "So by changing $a$ for excitation you will vary the width of the notch of the derivative surface.\n", + "For inhibition, by varying $a$ you will vary the width of the ridge in the drivative surface.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "RSyrrZQqf3Zq" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Numerical_partial_derivatives_Bonus_Discussion\")" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "nq-hVvP1MZqa", + "KCWR4wciMZqb", + "tcLiKp-AMZqc", + "8-bz4N8eMZqd", + "9-BbBcZPMZqf", + "L0_P-VNEMZqf", + "Yx7vP4zvMZqg", + "RACJEWwMMZqg", + "dXFmgh9UMZqh", + "pMQS0U54MZqi", + "ZLb9XPF_VvPm", + "c0oilOjOV0KZ" + ], + "name": "W0D4_Tutorial1", + "provenance": [], + "toc_visible": true, + "include_colab_link": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file From ed6bc279589a88733656a79485c8438fc541a940 Mon Sep 17 00:00:00 2001 From: Spiros Chavlis Date: Mon, 3 Jul 2023 14:53:02 +0300 Subject: [PATCH 4/7] fixes --- tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb | 3711 +++++++++--------- 1 file changed, 1891 insertions(+), 1820 deletions(-) diff --git a/tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb b/tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb index 852bd30..f9c9feb 100644 --- a/tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb +++ b/tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb @@ -1,1821 +1,1892 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "74ca2e85", - "metadata": { - "colab_type": "text", - "execution": {}, - "id": "view-in-github" - }, - "source": [ - "\"Open   \"Open" - ] - }, - { - "cell_type": "markdown", - "id": "08842220", - "metadata": { - "execution": {} - }, - "source": [ - "# Tutorial 2: Differential Equations\n", - "\n", - "**Week 0, Day 4: Calculus**\n", - "\n", - "**By Neuromatch Academy**\n", - "\n", - "**Content creators:** John S Butler, Arvind Kumar with help from Rebecca Brady\n", - "\n", - "**Content reviewers:** Swapnil Kumar, Sirisha Sripada, Matthew McCann, Tessy Tom\n", - "\n", - "**Production editors:** Matthew McCann, Ella Batty" - ] - }, - { - "cell_type": "markdown", - "id": "gK0EQXTiH5W2", - "metadata": { - "execution": {} - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "id": "7d80998b", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Tutorial Objectives\n", - "\n", - "*Estimated timing of tutorial: 45 minutes*\n", - "\n", - "A great deal of neuroscience can be modelled using differential equations, from gating channels to single neurons to a network of neurons to blood flow, to behaviour. A simple way to think about differential equations is they are equations that describe how something changes.\n", - "\n", - "The most famous of these in neuroscience is the Nobel Prize winning Hodgkin Huxley equation, which describes a neuron by modelling the gating of each axon. But we will not start there; we will start a few steps back.\n", - "\n", - "Differential Equations are mathematical equations that describe how something like population or a neuron changes over time. The reason why differential equations are so useful is they can generalise a process such that one equation can be used to describe many different outcomes.\n", - "The general form of a first order differential equation is:\n", - "\n", - "\\begin{equation}\n", - "\\frac{d}{dt}y(t) = f\\left( t,y(t) \\right)\n", - "\\end{equation}\n", - "\n", - "which can be read as \"the change in a process $y$ over time $t$ is a function $f$ of time $t$ and itself $y$\". This might initially seem like a paradox as you are using a process $y$ you want to know about to describe itself, a bit like the MC Escher drawing of two hands painting [each other](https://en.wikipedia.org/wiki/Drawing_Hands). But that is the beauty of mathematics - this can be solved some of time, and when it cannot be solved exactly we can use numerical methods to estimate the answer (as we will see in the next tutorial).\n", - "\n", - "In this tutorial, we will see how __differential equations are motivated by observations of physical responses__. We will break down the population differential equation, then the integrate and fire model, which leads nicely into raster plots and frequency-current curves to rate models.\n", - "\n", - "**Steps:**\n", - "- Get an intuitive understanding of a linear population differential equation (humans, not neurons)\n", - "- Visualize the relationship between the change in population and the population\n", - "- Breakdown the Leaky Integrate and Fire (LIF) differential equation\n", - "- Code the exact solution of an LIF for a constant input\n", - "- Visualize and listen to the response of the LIF for different inputs" - ] - }, - { - "cell_type": "markdown", - "id": "kq1fWqhyNJ3i", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1-6UCje-fVs6", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D4_T2\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5985801", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d40abcd8", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Figure Settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import IPython.display as ipd\n", - "from matplotlib import gridspec\n", - "import ipywidgets as widgets # interactive display\n", - "%config InlineBackend.figure_format = 'retina'\n", - "# use NMA plot style\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", - "my_layout = widgets.Layout()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6XJo_I6_NbJg", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Plotting Functions\n", - "\n", - "def plot_dPdt(alpha=.3):\n", - " \"\"\" Plots change in population over time\n", - " Args:\n", - " alpha: Birth Rate\n", - " Returns:\n", - " A figure two panel figure\n", - " left panel: change in population as a function of population\n", - " right panel: membrane potential as a function of time\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " time=np.arange(0, 10 ,0.01)\n", - " fig = plt.figure(figsize=(12,4))\n", - " gs = gridspec.GridSpec(1, 2)\n", - "\n", - " ## dpdt as a fucntion of p\n", - " plt.subplot(gs[0])\n", - " plt.plot(np.exp(alpha*time), alpha*np.exp(alpha*time))\n", - " plt.xlabel(r'Population $p(t)$ (millions)')\n", - " plt.ylabel(r'$\\frac{d}{dt}p(t)=\\alpha p(t)$')\n", - "\n", - " ## p exact solution\n", - " plt.subplot(gs[1])\n", - " plt.plot(time, np.exp(alpha*time))\n", - " plt.ylabel(r'Population $p(t)$ (millions)')\n", - " plt.xlabel('time (years)')\n", - " plt.show()\n", - "\n", - "\n", - "def plot_V_no_input(V_reset=-75):\n", - " \"\"\"\n", - " Args:\n", - " V_reset: Reset Potential\n", - " Returns:\n", - " A figure two panel figure\n", - " left panel: change in membrane potential as a function of membrane potential\n", - " right panel: membrane potential as a function of time\n", - " \"\"\"\n", - " E_L=-75\n", - " tau_m=10\n", - " t=np.arange(0,100,0.01)\n", - " V= E_L+(V_reset-E_L)*np.exp(-(t)/tau_m)\n", - " V_range=np.arange(-90,0,1)\n", - " dVdt=-(V_range-E_L)/tau_m\n", - "\n", - " with plt.xkcd():\n", - " time=np.arange(0, 10, 0.01)\n", - " fig = plt.figure(figsize=(12, 4))\n", - " gs = gridspec.GridSpec(1, 2)\n", - "\n", - " plt.subplot(gs[0])\n", - " plt.plot(V_range,dVdt)\n", - " plt.hlines(0,min(V_range),max(V_range), colors='black', linestyles='dashed')\n", - " plt.vlines(-75, min(dVdt), max(dVdt), colors='black', linestyles='dashed')\n", - " plt.plot(V_reset,-(V_reset - E_L)/tau_m, 'o', label=r'$V_{reset}$')\n", - " plt.text(-50, 1, 'Positive')\n", - " plt.text(-50, -2, 'Negative')\n", - " plt.text(E_L - 1, max(dVdt), r'$E_L$')\n", - " plt.legend()\n", - " plt.xlabel('Membrane Potential V (mV)')\n", - " plt.ylabel(r'$\\frac{dV}{dt}=\\frac{-(V(t)-E_L)}{\\tau_m}$')\n", - "\n", - " plt.subplot(gs[1])\n", - " plt.plot(t,V)\n", - " plt.plot(t[0],V_reset,'o')\n", - " plt.ylabel(r'Membrane Potential $V(t)$ (mV)')\n", - " plt.xlabel('time (ms)')\n", - " plt.ylim([-95, -60])\n", - "\n", - " plt.show()\n", - "\n", - "\n", - "## LIF PLOT\n", - "def plot_IF(t, V,I,Spike_time):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " V : membrane Voltage\n", - " I : Input\n", - " Spike_time : Spike_times\n", - " Returns:\n", - " figure with three panels\n", - " top panel: Input as a function of time\n", - " middle panel: membrane potential as a function of time\n", - " bottom panel: Raster plot\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(12, 4))\n", - " gs = gridspec.GridSpec(3, 1, height_ratios=[1, 4, 1])\n", - "\n", - " # PLOT OF INPUT\n", - " plt.subplot(gs[0])\n", - " plt.ylabel(r'$I_e(nA)$')\n", - " plt.yticks(rotation=45)\n", - " plt.hlines(I,min(t),max(t),'g')\n", - " plt.ylim((2, 4))\n", - " plt.xlim((-50, 1000))\n", - "\n", - " # PLOT OF ACTIVITY\n", - " plt.subplot(gs[1])\n", - " plt.plot(t,V)\n", - " plt.xlim((-50, 1000))\n", - " plt.ylabel(r'$V(t)$(mV)')\n", - "\n", - " # PLOT OF SPIKES\n", - " plt.subplot(gs[2])\n", - " plt.ylabel(r'Spike')\n", - " plt.yticks([])\n", - " plt.scatter(Spike_time, 1 * np.ones(len(Spike_time)), color=\"grey\", marker=\".\")\n", - " plt.xlim((-50, 1000))\n", - " plt.xlabel('time(ms)')\n", - " plt.show()\n", - "\n", - "\n", - "## Plotting the differential Equation\n", - "def plot_dVdt(I=0):\n", - " \"\"\"\n", - " Args:\n", - " I : Input Current\n", - " Returns:\n", - " figure of change in membrane potential as a function of membrane potential\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " E_L = -75\n", - " tau_m = 10\n", - " V = np.arange(-85, 0, 1)\n", - " g_L = 10.\n", - " fig = plt.figure(figsize=(6, 4))\n", - "\n", - " plt.plot(V,(-(V-E_L) + I*10) / tau_m)\n", - " plt.hlines(0, min(V), max(V), colors='black', linestyles='dashed')\n", - " plt.xlabel('V (mV)')\n", - " plt.ylabel(r'$\\frac{dV}{dt}$')\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d88bf487", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Helper Functions\n", - "\n", - "## EXACT SOLUTION OF LIF\n", - "def Exact_Integrate_and_Fire(I,t):\n", - " \"\"\"\n", - " Args:\n", - " I : Input Current\n", - " t : time\n", - " Returns:\n", - " Spike : Spike Count\n", - " Spike_time : Spike time\n", - " V_exact : Exact membrane potential\n", - " \"\"\"\n", - "\n", - " Spike = 0\n", - " tau_m = 10\n", - " R = 10\n", - " t_isi = 0\n", - " V_reset = E_L = -75\n", - " V_exact = V_reset * np.ones(len(t))\n", - " V_th = -50\n", - " Spike_time = []\n", - "\n", - " for i in range(0, len(t)):\n", - "\n", - " V_exact[i] = E_L + R*I + (V_reset - E_L - R*I) * np.exp(-(t[i]-t_isi)/tau_m)\n", - "\n", - " # Threshold Reset\n", - " if V_exact[i] > V_th:\n", - " V_exact[i-1] = 0\n", - " V_exact[i] = V_reset\n", - " t_isi = t[i]\n", - " Spike = Spike+1\n", - " Spike_time = np.append(Spike_time, t[i])\n", - "\n", - " return Spike, Spike_time, V_exact" - ] - }, - { - "cell_type": "markdown", - "id": "O2Q5d9D2hLZq", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 0: Introduction" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "DIZfUH4rQNSR", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 1: Why do we care about differential equations?\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'LhX-mUd8lPo'), ('Bilibili', 'BV1v64y197bW')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "Or-35flNhTBo", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Why_do_we_care_about_differential_equations_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "AcwNWVdsQgLz", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 1: Population differential equation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "LRwtiFPVQ3Pz", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 2: Population differential equation\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'czgGyoUsRoQ'), ('Bilibili', 'BV1pg41137CU')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "WLss9oNLhSI-", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Population_differential_equation_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "286b4298", - "metadata": { - "execution": {} - }, - "source": [ - "This video covers our first example of a differential equation: a differential equation which models the change in population.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "To get an intuitive feel of a differential equations, we will start with a population differential equation, which models the change in population [1], that is human population not neurons, we will get to neurons later. Mathematically it is written like:\n", - "\\begin{align*}\n", - "\\\\\n", - "\\frac{d}{dt}\\,p(t) &= \\alpha p(t),\\\\\n", - "\\end{align*}\n", - "\n", - "where $p(t)$ is the population of the world and $\\alpha$ is a parameter representing birth rate.\n", - "\n", - "Another way of thinking about the models is that the equation\n", - "\\begin{align*}\n", - "\\\\\n", - "\\frac{d}{dt}\\,p(t) &= \\alpha p(t),\\\\\n", - "\\text{can be written as:}\\\\\n", - "\\text{\"Change in Population\"} &= \\text{ \"Birth rate times Current population.\"}\n", - "\\end{align*}\n", - "\n", - "The equation is saying something reasonable maybe not the perfect model but a good start.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "njGc4mc4ASXb", - "metadata": { - "execution": {} - }, - "source": [ - "### Think! 1.1: Interpretating the behavior of a linear population equation\n", - "Using the plot below of change of population $\\frac{d}{dt} p(t) $ as a function of population $p(t)$ with birth-rate $\\alpha=0.3$, discuss the following questions:\n", - "\n", - "1. Why is the population differential equation known as a linear differential equation?\n", - "2. How does population size affect the rate of change of the population?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e37083e", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute the code to plot the rate of change of population as a function of population\n", - "p = np.arange(0, 100, 0.1)\n", - "\n", - "with plt.xkcd():\n", - "\n", - " dpdt = 0.3*p\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(p, dpdt)\n", - " plt.xlabel(r'Population $p(t)$ (millions)')\n", - " plt.ylabel(r'$\\frac{d}{dt}p(t)=\\alpha p(t)$')\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "il7f6AWkXk0B", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. The plot of $\\frac{dp}{dt}$ is a line, which is why the differential\n", - " equation is known as a linear differential equation.\n", - "\n", - " 2. As the population increases, the change of population increases. A\n", - " population of 20 has a change of 6 while a population of 100 has a change of\n", - " 30. This makes sense - the larger the population the larger the change.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1whSBsnBhbAG", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Interpretating_the_behavior_of_a_linear_population_equation_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "id": "5846bbb5", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 1.1: Exact solution of the population equation" - ] - }, - { - "cell_type": "markdown", - "id": "uqcubpZNBIGl", - "metadata": { - "execution": {} - }, - "source": [ - "### Section 1.1.1: Initial condition\n", - "The linear population differential equation is known as an initial value differential equation because we need an initial population value to solve it. Here we will set our initial population at time $0$ to $1$:\n", - "\n", - "\\begin{equation}\n", - "p(0) = 1\n", - "\\end{equation}\n", - "\n", - "Different initial conditions will lead to different answers, but they will not change the differential equation. This is one of the strengths of a differential equation." - ] - }, - { - "cell_type": "markdown", - "id": "oYBK4NWYBL5t", - "metadata": { - "execution": {} - }, - "source": [ - "### Section 1.1.2: Exact Solution\n", - "To calculate the exact solution of a differential equation, we must integrate both sides. Instead of numerical integration (as you delved into in the last tutorial), we will first try to solve the differential equations using analytical integration. As with derivatives, we can find analytical integrals of simple equations by consulting [a list](https://en.wikipedia.org/wiki/Lists_of_integrals). We can then get integrals for more complex equations using some mathematical tricks - the harder the equation the more obscure the trick.\n", - "\n", - "The linear population equation\n", - "\\begin{equation}\n", - "\\frac{d}{dt}p(t) = \\alpha p(t), \\, p(0)=P_0\n", - "\\end{equation}\n", - "\n", - "has the exact solution:\n", - "\n", - "\\begin{equation}\n", - "p(t) = P_0 e^{\\alpha t}.\n", - "\\end{equation}\n", - "\n", - "The exact solution written in words is:\n", - "\n", - "\\begin{equation}\n", - "\\text{\"Population\"} = \\text{\"grows/declines exponentially as a function of time and birth rate\"}.\n", - "\\end{equation}\n", - "\n", - "Most differential equations do not have a known exact solution, so in the next tutorial on numerical methods we will show how the solution can be estimated.\n", - "\n", - "A small aside: a good deal of progress in mathematics was due to mathematicians writing taunting letters to each other saying they had a trick that could solve something better than everyone else. So do not worry too much about the tricks." - ] - }, - { - "cell_type": "markdown", - "id": "1257fcc0", - "metadata": { - "execution": {} - }, - "source": [ - "#### Example Exact Solution of the Population Equation\n", - "\n", - "Let's consider the population differential equation with a birth rate $\\alpha=0.3$:\n", - "\n", - "\\begin{equation}\n", - "\\frac{d}{dt}p(t) = 0.3p(t)\n", - "\\end{equation}\n", - "\n", - "with the initial condition\n", - "\n", - "\\begin{equation}\n", - "p(0)=1.\n", - "\\end{equation}\n", - "\n", - "It has an exact solution\n", - "\n", - "\\begin{equation}\n", - "p(t)=e^{0.3 t}.\n", - "\\end{equation}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eeb717b8", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute code to plot the exact solution\n", - "t = np.arange(0, 10, 0.1) # Time from 0 to 10 years in 0.1 steps\n", - "\n", - "with plt.xkcd():\n", - "\n", - " p = np.exp(0.3 * t)\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(t, p)\n", - " plt.ylabel('Population (millions)')\n", - " plt.xlabel('time (years)')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "792841c9", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 1.2: Parameters of the differential equation\n", - "\n", - "*Estimated timing to here from start of tutorial: 12 min*\n", - "\n", - "One of the goals when designing a differential equation is to make it generalisable. Which means that the differential equation will give reasonable solutions for different countries with different birth rates $\\alpha$.\n" - ] - }, - { - "cell_type": "markdown", - "id": "8zD98917MDtW", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 1.2: Parameter Change\n", - "\n", - "Play with the widget to see the relationship between $\\alpha$ and the population differential equation as a function of population (left-hand side), and the population solution as a function of time (right-hand side). Pay close attention to the transition point from positive to negative.\n", - "\n", - "How do changing parameters of the population equation affect the outcome?\n", - "\n", - "1. What happens when $\\alpha < 0$?\n", - "2. What happens when $\\alpha > 0$?\n", - "3. What happens when $\\alpha = 0$?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "490c0be0", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " alpha=widgets.FloatSlider(.3, min=-1., max=1., step=.1, layout=my_layout)\n", - ")\n", - "def Pop_widget(alpha):\n", - " plot_dPdt(alpha=alpha)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "KDgZUHS5YDQp", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. Negative values of alpha result in an exponential decrease to 0 a stable solution.\n", - " 2. Positive Values of alpha in an exponential increases to infinity.\n", - " 3. Alpha equal to 0 is a unique point known as an equilibrium point when the\n", - " dp/dt=0 and there is no change in population. This is known as a stable point.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1AgWDO58hnSv", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Parameter_change_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "c3a78544", - "metadata": { - "execution": {} - }, - "source": [ - "The population differential equation is an over-simplification and has some very obvious limitations:\n", - "1. Population growth is not exponential as there are limited number of resources so the population will level out at some point.\n", - "2. It does not include any external factors on the populations like weather, predators and preys.\n", - "\n", - "These kind of limitations can be addressed by extending the model.\n", - "\n", - "\n", - "While it might not seem that the population equation has direct relevance to neuroscience, a similar equation is used to describe the accumulation of evidence for decision making. This is known as the Drift Diffusion Model and you will see in more detail in the Linear System day in Neuromatch (W2D2).\n", - "\n", - "\n", - "Another differential equation that is similar to the population equation is the Leaky Integrate and Fire model which you may have seen in the python pre-course materials on W0D1 and W0D2. It will turn up later in Neuromatch as well. Below we will delve in the motivation of the differential equation." - ] - }, - { - "cell_type": "markdown", - "id": "ae82611f", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 2: The leaky integrate and fire (LIF) model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "hGBNAqnVVY3E", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 3: The leaky integrate and fire model\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'ZfWO6MLCa1s'), ('Bilibili', 'BV1rb4y1C79n')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eWa8PXDrh2NB", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_The_leaky_integrate_and_fire_model_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "4qgkjHOtWcEL", - "metadata": { - "execution": {} - }, - "source": [ - "This video covers the Leaky Integrate and Fire model (a linear differential equation which describes the membrane potential of a single neuron).\n", - "\n", - "
\n", - " Click here for text recap of full LIF equation from video \n", - "\n", - "The Leaky Integrate and Fire Model is a linear differential equation that describes the membrane potential ($V$) of a single neuron which was proposed by Louis Édouard Lapicque in 1907 [2].\n", - "\n", - "The subthreshold membrane potential dynamics of a LIF neuron is described by\n", - "\\begin{align}\n", - "\\tau_m\\frac{dV}{dt} = -(V-E_L) + R_mI\\,\n", - "\\end{align}\n", - "\n", - "\n", - "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is membrane resistance, and $I$ is the external input current.\n", - "\n", - "
\n", - "\n", - "In the next few sections, we will break down the full LIF equation and then build it back up to get an intuitive feel of the different facets of the differential equation.\n" - ] - }, - { - "cell_type": "markdown", - "id": "c05087c6", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.1: LIF without input\n", - "\n", - "*Estimated timing to here from start of tutorial: 18 min*\n", - "\n", - "As seen in the video, we will first model an LIF neuron without input, which results in the equation:\n", - "\n", - "\\begin{equation}\n", - "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}.\n", - "\\end{equation}\n", - "\n", - "where $\\tau_m$ is the time constant, $V$ is the membrane potential, and $E_L$ is the resting potential.\n", - "\n", - "
\n", - " Click here for further details (from video) \n", - "\n", - "Removing the input gives the equation\n", - "\n", - "\\begin{equation}\n", - "\\tau_m\\frac{dV}{dt} = -V+E_L,\n", - "\\end{equation}\n", - "\n", - "which can be written in words as:\n", - "\n", - "\\begin{align}\n", - "\\begin{matrix}\\text{\"Time constant multiplied by the} \\\\ \\text{change in membrane potential\"}\\end{matrix}&=\\begin{matrix}\\text{\"Minus Current} \\\\ \\text{membrane potential\"} \\end{matrix}+\n", - "\\begin{matrix}\\text{\"resting potential\"}\\end{matrix}.\\\\\n", - "\\end{align}\n", - "\n", - "\n", - "The equation can be re-arranged to look even more like the population equation:\n", - "\n", - "\\begin{align}\n", - "\\frac{dV}{dt} &= \\frac{-(V-E_L)}{\\tau_m}.\\\\\n", - "\\end{align}\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "XRl1cRt5ppJ1", - "metadata": { - "execution": {} - }, - "source": [ - "### Think! 2.1: Effect of membrane potential $V$ on the LIF model\n", - "\n", - "The plot the below shows the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ with the parameters set as:\n", - "* `E_L = -75`\n", - "* `V_reset = -50`\n", - "* `tau_m = 10.`\n", - "\n", - "1. What is the effect on $\\frac{dV}{dt}$ when $V>-75$ mV?\n", - "2. What is the effect on $\\frac{dV}{dt}$ when $V<-75$ mV\n", - "3. What is the effect on $\\frac{dV}{dt}$ when $V=-75$ mV?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8dd9906c", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to plot the relationship between dV/dt and V\n", - "# Parameter definition\n", - "E_L = -75\n", - "tau_m = 10\n", - "\n", - "# Range of Values of V\n", - "V = np.arange(-90, 0, 1)\n", - "dV = -(V - E_L) / tau_m\n", - "\n", - "with plt.xkcd():\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(V, dV)\n", - " plt.hlines(0, min(V), max(V), colors='black', linestyles='dashed')\n", - " plt.vlines(-75, min(dV), max(dV), colors='black', linestyles='dashed')\n", - "\n", - " plt.text(-50, 1, 'Positive')\n", - " plt.text(-50, -2, 'Negative')\n", - " plt.text(E_L, max(dV) + 1, r'$E_L$')\n", - " plt.xlabel(r'$V(t)$ (mV)')\n", - " plt.ylabel(r'$\\frac{dV}{dt}=\\frac{-(V-E_L)}{\\tau_m}$')\n", - " plt.ylim(-8, 2)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "h0MIi-MJYdk5", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. For $V>-75$ mV, the derivative is negative.\n", - " 2. For $V<-75$ mV, the derivative is positive.\n", - " 3. For $V=-75$ mV, the derivative is equal to $0$ is and a stable point when nothing changes.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "q0zy29uhh8lG", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Effect_of_membrane_potential_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "f7bb8882", - "metadata": { - "execution": {} - }, - "source": [ - "### Section 2.1.1: Exact Solution of the LIF model without input\n", - "\n", - "The LIF model has the exact solution:\n", - "\n", - "\\begin{equation}\n", - "V(t) = E_L+(V_{reset}-E_L)e^{\\frac{-t}{\\tau_m}}\n", - "\\end{equation}\n", - "\n", - "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, and $V_{reset}$ is the initial membrane potential.\n", - "\n", - "
\n", - " Click here for further details (from video) \n", - "\n", - "Similar to the population equation, we need an initial membrane potential at time $0$ to solve the LIF model.\n", - "\n", - "With this equation\n", - "\\begin{align}\n", - "\\frac{dV}{dt} &= \\frac{-(V-E_L)}{\\tau_m}\\,\\\\\n", - "V(0)&=V_{reset},\n", - "\\end{align}\n", - "where is $V_{reset}$ is called the reset potential.\n", - "\n", - "The LIF model has the exact solution:\n", - "\n", - "\\begin{align*}\n", - "V(t)=&\\ E_L+(V_{reset}-E_L)e^{\\frac{-t}{\\tau_m}}\\\\\n", - "\\text{ which can be written as: }\\\\\n", - "\\begin{matrix}\\text{\"Current membrane} \\\\ \\text{potential}\"\\end{matrix}=&\\text{\"Resting potential\"}+\\begin{matrix}\\text{\"Reset potential minus resting potential} \\\\ \\text{times exponential with rate one over time constant.\"}\\end{matrix}\\\\\n", - "\\end{align*}\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "31910ff4", - "metadata": { - "execution": {} - }, - "source": [ - "#### Interactive Demo 2.1.1: Initial Condition $V_{reset}$\n", - "This exercise is to get an intuitive feel of how the different initial conditions $V_{reset}$ impacts the differential equation of the LIF and the exact solution for the equation:\n", - "\n", - "\\begin{equation}\n", - "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}\n", - "\\end{equation}\n", - "\n", - "with the parameters set as:\n", - "* `E_L = -75,`\n", - "* `tau_m = 10.`\n", - "\n", - "The panel on the left-hand side plots the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ and right-hand side panel plots the exact solution $V$ as a function of time $t,$ the green dot in both panels is the reset potential $V_{reset}$.\n", - "\n", - "Pay close attention to when $V_{reset}=E_L=-75$mV.\n", - "\n", - "1. How does the solution look with initial values of $V_{reset} < -75$?\n", - "2. How does the solution look with initial values of $V_{reset} > -75$?\n", - "3. How does the solution look with initial values of $V_{reset} = -75$?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "03458759", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "#@markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " V_reset=widgets.FloatSlider(-77., min=-91., max=-61., step=2,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def V_reset_widget(V_reset):\n", - " plot_V_no_input(V_reset)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "IQmxKOuRaVGr", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. Initial Values of $V_{reset} < -75$ result in the solution increasing to\n", - " -75mV because $\\frac{dV}{dt} > 0$.\n", - " 2. Initial Values of $V_{reset} > -75$ result in the solution decreasing to\n", - " -75mV because $\\frac{dV}{dt} < 0$.\n", - " 3. Initial Values of $V_{reset} = -75$ result in a constant $V = -75$ mV\n", - " because $\\frac{dV}{dt} = 0$ (Stable point).\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "Q9OCCAmdiVYQ", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Initial_condition_Vreset_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "5dcd105e", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.2: LIF with input\n", - "*Estimated timing to here from start of tutorial: 24 min*\n", - "\n", - "We will re-introduce the input $I$ and membrane resistance $R_m$ giving the original equation:\n", - "\n", - "\\begin{equation}\n", - "\\tau_m\\frac{dV}{dt} = -(V-E_L) + \\color{blue}{R_mI}\n", - "\\end{equation}\n", - "\n", - "The input can be other neurons or sensory information." - ] - }, - { - "cell_type": "markdown", - "id": "9862281c", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 2.2: The Impact of Input\n", - "The interactive plot below manipulates $I$ in the differential equation.\n", - "\n", - "- With increasing input, how does the $\\frac{dV}{dt}$ change? How would this impact the solution?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc6ce7c7", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " I=widgets.FloatSlider(3., min=0., max=20., step=2,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(I):\n", - " plot_dVdt(I=I)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "HSNtznIwY6wA", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "dV/dt becomes bigger and less of it is below 0. This means the solution will\n", - "increase well beyond what is bioligically plausible.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2S4tiJYAidIt", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_The_impact_of_Input_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "968432a1", - "metadata": { - "execution": {} - }, - "source": [ - "### Section 2.2.1: LIF exact solution\n", - "\n", - "The LIF with a constant input has a known exact solution:\n", - "\\begin{equation}\n", - "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-t}{\\tau_m}}\n", - "\\end{equation}\n", - "\n", - "which is written as:\n", - "\n", - "\\begin{align*}\n", - "\\begin{matrix}\\text{\"Current membrane} \\\\ \\text{potential\"}\\end{matrix}=&\\text{\"Resting potential\"}+\\begin{matrix}\\text{\"Reset potential minus resting potential} \\\\ \\text{times exponential with rate one over time constant.\" }\\end{matrix}\\\\\n", - "\\end{align*}" - ] - }, - { - "cell_type": "markdown", - "id": "91b7c87a", - "metadata": { - "execution": {} - }, - "source": [ - "The plot below shows the exact solution of the membrane potential with the parameters set as:\n", - "* `V_reset = -75,`\n", - "* `E_L = -75,`\n", - "* `tau_m = 10,`\n", - "* `R_m = 10,`\n", - "* `I = 10.`\n", - "\n", - "Ask yourself, does the result make biological sense? If not, what would you change? We'll delve into this in the next section" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "db5816d4", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to see the exact solution\n", - "dt = 0.5\n", - "t_rest = 0\n", - "\n", - "t = np.arange(0, 1000, dt)\n", - "\n", - "tau_m = 10\n", - "R_m = 10\n", - "V_reset = E_L = -75\n", - "\n", - "I = 10\n", - "\n", - "V = E_L + R_m*I + (V_reset - E_L - R_m*I) * np.exp(-(t)/tau_m)\n", - "\n", - "with plt.xkcd():\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(t,V)\n", - " plt.ylabel('V (mV)')\n", - " plt.xlabel('time (ms)')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "0a31b8d1", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.3: Maths is one thing, but neuroscience matters\n", - "\n", - "*Estimated timing to here from start of tutorial: 30 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "CtWroVVASxG1", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 4: Adding firing to the LIF\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'rLQk-vXRaX0'), ('Bilibili', 'BV1gX4y1P7pZ')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "KljGW16eihTo", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Adding_firing_to_the_LIF_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "9f12c68e", - "metadata": { - "execution": {} - }, - "source": [ - "This video first recaps the introduction of input to the leaky integrate and fire model and then delves into how we add spiking behavior (or firing) to the model.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "While the mathematics of the exact solution is exact, it is not biologically valid as a neuron spikes and definitely does not plateau at a very positive value.\n", - "\n", - "To model the firing of a spike, we must have a threshold voltage $V_{th}$ such that if the voltage $V(t)$ goes above it, the neuron spikes\n", - "$$V(t)>V_{th}.$$\n", - "We must record the time of spike $t_{isi}$ and count the number of spikes\n", - "$$t_{isi}=t, $$\n", - "$$𝑆𝑝𝑖𝑘𝑒=𝑆𝑝𝑖𝑘𝑒+1.$$\n", - "Then reset the membrane voltage $V(t)$\n", - "$$V(t_{isi} )=V_{Reset}.$$\n", - "\n", - "To take into account the spike the exact solution becomes:\n", - "\\begin{align*}\n", - "V(t)=&\\ E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},&\\qquad V(t)V_{th}\\\\\n", - "Spike=&Spike+1,&\\\\\n", - "t_{isi}=&t,\\\\\n", - "\\end{align*}\n", - "while this does make the neuron spike, it introduces a discontinuity which is not as elegant mathematically as it could be, but it gets results so that is good.\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "acc43e8a", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 2.3.1: Input on spikes\n", - "This exercise show the relationship between firing rate and the Input for exact solution `V` of the LIF:\n", - "\n", - "\\begin{equation}\n", - "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},\n", - "\\end{equation}\n", - "\n", - "with the parameters set as:\n", - "* `V_reset = -75,`\n", - "* `E_L = -75,`\n", - "* `tau_m = 10,`\n", - "* `R_m = 10.`\n", - "\n", - "Below is a figure with three panels;\n", - "* the top panel is the input, $I,$\n", - "* the middle panel is the membrane potential $V(t)$. To illustrate the spike, $V(t)$ is set to $0$ and then reset to $-75$ mV when there is a spike.\n", - "* the bottom panel is the raster plot with each dot indicating a spike.\n", - "\n", - "First, as electrophysiologist normally listen to spikes when conducting experiments, listen to the music of the firing rate for a single value of $I$.\n", - "\n", - "**Note:** The audio doesn't work in some browsers so don't worry about it if you can't hear anything." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "55785e26", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to be able to hear the neuron\n", - "I = 3\n", - "t = np.arange(0, 1000, dt)\n", - "Spike, Spike_time, V = Exact_Integrate_and_Fire(I, t)\n", - "\n", - "plot_IF(t, V, I, Spike_time)\n", - "ipd.Audio(V, rate=len(V))" - ] - }, - { - "cell_type": "markdown", - "id": "54d5af0e", - "metadata": { - "execution": {} - }, - "source": [ - "Manipulate the input into the LIF to see the impact of input on the firing pattern (rate).\n", - "\n", - "* What is the effect of $I$ on spiking?\n", - "* Is this biologically valid?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ebf27a22", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " I=widgets.FloatSlider(3, min=2.0, max=4., step=.1,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(I):\n", - " Spike, Spike_time, V = Exact_Integrate_and_Fire(I, t)\n", - " plot_IF(t, V, I, Spike_time)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "GjwI3QF3ZLtY", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. As I increases, the number of spikes increases.\n", - " 2. No, as there is a limit to the number of spikes due to a refractory period,\n", - " which is not accounted for in this model.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ouprca8SipAs", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Input_on_spikes_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "c3cf4d7b", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.4 Firing Rate as a function of Input\n", - "\n", - "*Estimated timing to here from start of tutorial: 38 min*\n", - "\n", - "The firing frequency of a neuron plotted as a function of current is called an input-output curve (F–I curve). It is also known as a transfer function, which you came across in the previous tutorial. This function is one of the starting points for the rate model, which extends from modelling single neurons to the firing rate of a collection of neurons.\n", - "\n", - "By fitting this to a function, we can start to generalise the firing pattern of many neurons, which can be used to build rate models. This will be discussed later in Neuromatch." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "96bcf5e7", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown *Execture this cell to visualize the FI curve*\n", - "I_range = np.arange(2.0, 4.0, 0.1)\n", - "Spike_rate = np.ones(len(I_range))\n", - "\n", - "for i, I in enumerate(I_range):\n", - " Spike_rate[i], _, _ = Exact_Integrate_and_Fire(I, t)\n", - "\n", - "with plt.xkcd():\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(I_range,Spike_rate)\n", - " plt.xlabel('Input Current (nA)')\n", - " plt.ylabel('Spikes per Second (Hz)')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "58fdcbe8", - "metadata": { - "execution": {} - }, - "source": [ - "The LIF model is a very nice differential equation to start with in computational neuroscience as it has been used as a building block for many papers that simulate neuronal response.\n", - "\n", - "__Strengths of LIF model:__\n", - "+ Has an exact solution;\n", - "+ Easy to interpret;\n", - "+ Great to build network of neurons.\n", - "\n", - "__Weaknesses of the LIF model:__\n", - "- Spiking is a discontinuity;\n", - "- Abstraction from biology;\n", - "- Cannot generate different spiking patterns.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "697364ff", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Summary\n", - "\n", - "*Estimated timing of tutorial: 45 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "_Jmpnq0mSihx", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 5: Summary\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'VzwLAW5p4ao'), ('Bilibili', 'BV1jV411x7t9')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "VNxbJDK5iuFA", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Summary_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "x0H0zhTNWlop", - "metadata": { - "execution": {} - }, - "source": [ - "In this tutorial, we have seen two differential equations, the population differential equations and the leaky integrate and fire model.\n", - "\n", - "\n", - "We learned about:\n", - "* The motivation for differential equations.\n", - "* An intuitive relationship between the solution and the form of the differential equation.\n", - "* How different parameters of the differential equation impact the solution.\n", - "* The strengths and limitations of the simple differential equations.\n" - ] - }, - { - "cell_type": "markdown", - "id": "eaec6994", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "## Links to Neuromatch Computational Neuroscience Days\n", - "\n", - "Differential equations turn up in a number of different Neuromatch days:\n", - "* The LIF model is discussed in more details in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", - "* Drift Diffusion model which is a differential equation for decision making is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", - "* Systems of differential equations are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", - "\n", - "---\n", - "## References\n", - "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", - "\n", - "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", - "\n", - "\n", - "## Bibliography\n", - "1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.\n", - "2. Strogatz, S. (2014). Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2nd edition\n", - "\n", - "### Supplemental Popular Reading List\n", - "1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.\n", - "2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.\n", - "\n", - "### Popular Podcast\n", - "1. Strogatz, S. (Host). (2020), Joy of X https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine\n" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "include_colab_link": true, - "name": "W0D4_Tutorial2", - "provenance": [], - "toc_visible": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "id": "08842220", + "metadata": { + "execution": {}, + "id": "08842220" + }, + "source": [ + "# Tutorial 2: Differential Equations\n", + "\n", + "**Week 0, Day 4: Calculus**\n", + "\n", + "**By Neuromatch Academy**\n", + "\n", + "**Content creators:** John S Butler, Arvind Kumar with help from Rebecca Brady\n", + "\n", + "**Content reviewers:** Swapnil Kumar, Sirisha Sripada, Matthew McCann, Tessy Tom\n", + "\n", + "**Production editors:** Matthew McCann, Ella Batty" + ] + }, + { + "cell_type": "markdown", + "id": "gK0EQXTiH5W2", + "metadata": { + "execution": {}, + "id": "gK0EQXTiH5W2" + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "id": "7d80998b", + "metadata": { + "execution": {}, + "id": "7d80998b" + }, + "source": [ + "---\n", + "# Tutorial Objectives\n", + "\n", + "*Estimated timing of tutorial: 45 minutes*\n", + "\n", + "A great deal of neuroscience can be modeled using differential equations, from gating channels to single neurons to a network of neurons to blood flow to behavior. A simple way to think about differential equations is they are equations that describe how something changes.\n", + "\n", + "The most famous of these in neuroscience is the Nobel Prize-winning Hodgkin-Huxley equation, which describes a neuron by modeling the gating of each axon. But we will not start there; we will start a few steps back.\n", + "\n", + "Differential Equations are mathematical equations that describe how something like a population or a neuron changes over time. Differential equations are so useful because they can generalize a process such that one equation can be used to describe many different outcomes.\n", + "The general form of a first-order differential equation is:\n", + "\n", + "\\begin{equation}\n", + "\\frac{d}{dt}y(t) = f\\left( t,y(t) \\right)\n", + "\\end{equation}\n", + "\n", + "which can be read as \"the change in a process $y$ over time $t$ is a function $f$ of time $t$ and itself $y$\". This might initially seem like a paradox as you are using a process $y$ you want to know about to describe itself, a bit like the MC Escher drawing of two hands painting [each other](https://en.wikipedia.org/wiki/Drawing_Hands). But that is the beauty of mathematics - this can be solved sometimes, and when it cannot be solved exactly, we can use numerical methods to estimate the answer (as we will see in the next tutorial).\n", + "\n", + "In this tutorial, we will see how __differential equations are motivated by observations of physical responses__. We will break down the population differential equation, then the integrate and fire model, which leads nicely into raster plots and frequency-current curves to rate models.\n", + "\n", + "**Steps:**\n", + "- Get an intuitive understanding of a linear population differential equation (humans, not neurons)\n", + "- Visualize the relationship between the change in population and the population\n", + "- Breakdown the Leaky Integrate and Fire (LIF) differential equation\n", + "- Code the exact solution of a LIF for a constant input\n", + "- Visualize and listen to the response of the LIF for different inputs" + ] + }, + { + "cell_type": "markdown", + "id": "kq1fWqhyNJ3i", + "metadata": { + "execution": {}, + "id": "kq1fWqhyNJ3i" + }, + "source": [ + "---\n", + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1-6UCje-fVs6", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "1-6UCje-fVs6" + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D4_T2\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5985801", + "metadata": { + "execution": {}, + "id": "a5985801" + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d40abcd8", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "d40abcd8" + }, + "outputs": [], + "source": [ + "# @title Figure Settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import IPython.display as ipd\n", + "from matplotlib import gridspec\n", + "import ipywidgets as widgets # interactive display\n", + "%config InlineBackend.figure_format = 'retina'\n", + "# use NMA plot style\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", + "my_layout = widgets.Layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6XJo_I6_NbJg", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "6XJo_I6_NbJg" + }, + "outputs": [], + "source": [ + "# @title Plotting Functions\n", + "\n", + "def plot_dPdt(alpha=.3):\n", + " \"\"\" Plots change in population over time\n", + " Args:\n", + " alpha: Birth Rate\n", + " Returns:\n", + " A figure two panel figure\n", + " left panel: change in population as a function of population\n", + " right panel: membrane potential as a function of time\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " time=np.arange(0, 10 ,0.01)\n", + " fig = plt.figure(figsize=(12,4))\n", + " gs = gridspec.GridSpec(1, 2)\n", + "\n", + " ## dpdt as a fucntion of p\n", + " plt.subplot(gs[0])\n", + " plt.plot(np.exp(alpha*time), alpha*np.exp(alpha*time))\n", + " plt.xlabel(r'Population $p(t)$ (millions)')\n", + " plt.ylabel(r'$\\frac{d}{dt}p(t)=\\alpha p(t)$')\n", + "\n", + " ## p exact solution\n", + " plt.subplot(gs[1])\n", + " plt.plot(time, np.exp(alpha*time))\n", + " plt.ylabel(r'Population $p(t)$ (millions)')\n", + " plt.xlabel('time (years)')\n", + " plt.show()\n", + "\n", + "\n", + "def plot_V_no_input(V_reset=-75):\n", + " \"\"\"\n", + " Args:\n", + " V_reset: Reset Potential\n", + " Returns:\n", + " A figure two panel figure\n", + " left panel: change in membrane potential as a function of membrane potential\n", + " right panel: membrane potential as a function of time\n", + " \"\"\"\n", + " E_L=-75\n", + " tau_m=10\n", + " t=np.arange(0,100,0.01)\n", + " V= E_L+(V_reset-E_L)*np.exp(-(t)/tau_m)\n", + " V_range=np.arange(-90,0,1)\n", + " dVdt=-(V_range-E_L)/tau_m\n", + "\n", + " with plt.xkcd():\n", + " time=np.arange(0, 10, 0.01)\n", + " fig = plt.figure(figsize=(12, 4))\n", + " gs = gridspec.GridSpec(1, 2)\n", + "\n", + " plt.subplot(gs[0])\n", + " plt.plot(V_range,dVdt)\n", + " plt.hlines(0,min(V_range),max(V_range), colors='black', linestyles='dashed')\n", + " plt.vlines(-75, min(dVdt), max(dVdt), colors='black', linestyles='dashed')\n", + " plt.plot(V_reset,-(V_reset - E_L)/tau_m, 'o', label=r'$V_{reset}$')\n", + " plt.text(-50, 1, 'Positive')\n", + " plt.text(-50, -2, 'Negative')\n", + " plt.text(E_L - 1, max(dVdt), r'$E_L$')\n", + " plt.legend()\n", + " plt.xlabel('Membrane Potential V (mV)')\n", + " plt.ylabel(r'$\\frac{dV}{dt}=\\frac{-(V(t)-E_L)}{\\tau_m}$')\n", + "\n", + " plt.subplot(gs[1])\n", + " plt.plot(t,V)\n", + " plt.plot(t[0],V_reset,'o')\n", + " plt.ylabel(r'Membrane Potential $V(t)$ (mV)')\n", + " plt.xlabel('time (ms)')\n", + " plt.ylim([-95, -60])\n", + "\n", + " plt.show()\n", + "\n", + "\n", + "## LIF PLOT\n", + "def plot_IF(t, V,I,Spike_time):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " V : membrane Voltage\n", + " I : Input\n", + " Spike_time : Spike_times\n", + " Returns:\n", + " figure with three panels\n", + " top panel: Input as a function of time\n", + " middle panel: membrane potential as a function of time\n", + " bottom panel: Raster plot\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(12, 4))\n", + " gs = gridspec.GridSpec(3, 1, height_ratios=[1, 4, 1])\n", + "\n", + " # PLOT OF INPUT\n", + " plt.subplot(gs[0])\n", + " plt.ylabel(r'$I_e(nA)$')\n", + " plt.yticks(rotation=45)\n", + " plt.hlines(I,min(t),max(t),'g')\n", + " plt.ylim((2, 4))\n", + " plt.xlim((-50, 1000))\n", + "\n", + " # PLOT OF ACTIVITY\n", + " plt.subplot(gs[1])\n", + " plt.plot(t,V)\n", + " plt.xlim((-50, 1000))\n", + " plt.ylabel(r'$V(t)$(mV)')\n", + "\n", + " # PLOT OF SPIKES\n", + " plt.subplot(gs[2])\n", + " plt.ylabel(r'Spike')\n", + " plt.yticks([])\n", + " plt.scatter(Spike_time, 1 * np.ones(len(Spike_time)), color=\"grey\", marker=\".\")\n", + " plt.xlim((-50, 1000))\n", + " plt.xlabel('time(ms)')\n", + " plt.show()\n", + "\n", + "\n", + "## Plotting the differential Equation\n", + "def plot_dVdt(I=0):\n", + " \"\"\"\n", + " Args:\n", + " I : Input Current\n", + " Returns:\n", + " figure of change in membrane potential as a function of membrane potential\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " E_L = -75\n", + " tau_m = 10\n", + " V = np.arange(-85, 0, 1)\n", + " g_L = 10.\n", + " fig = plt.figure(figsize=(6, 4))\n", + "\n", + " plt.plot(V,(-(V-E_L) + I*10) / tau_m)\n", + " plt.hlines(0, min(V), max(V), colors='black', linestyles='dashed')\n", + " plt.xlabel('V (mV)')\n", + " plt.ylabel(r'$\\frac{dV}{dt}$')\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d88bf487", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "d88bf487" + }, + "outputs": [], + "source": [ + "# @title Helper Functions\n", + "\n", + "## EXACT SOLUTION OF LIF\n", + "def Exact_Integrate_and_Fire(I,t):\n", + " \"\"\"\n", + " Args:\n", + " I : Input Current\n", + " t : time\n", + " Returns:\n", + " Spike : Spike Count\n", + " Spike_time : Spike time\n", + " V_exact : Exact membrane potential\n", + " \"\"\"\n", + "\n", + " Spike = 0\n", + " tau_m = 10\n", + " R = 10\n", + " t_isi = 0\n", + " V_reset = E_L = -75\n", + " V_exact = V_reset * np.ones(len(t))\n", + " V_th = -50\n", + " Spike_time = []\n", + "\n", + " for i in range(0, len(t)):\n", + "\n", + " V_exact[i] = E_L + R*I + (V_reset - E_L - R*I) * np.exp(-(t[i]-t_isi)/tau_m)\n", + "\n", + " # Threshold Reset\n", + " if V_exact[i] > V_th:\n", + " V_exact[i-1] = 0\n", + " V_exact[i] = V_reset\n", + " t_isi = t[i]\n", + " Spike = Spike+1\n", + " Spike_time = np.append(Spike_time, t[i])\n", + "\n", + " return Spike, Spike_time, V_exact" + ] + }, + { + "cell_type": "markdown", + "id": "O2Q5d9D2hLZq", + "metadata": { + "execution": {}, + "id": "O2Q5d9D2hLZq" + }, + "source": [ + "---\n", + "# Section 0: Introduction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "DIZfUH4rQNSR", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "DIZfUH4rQNSR" + }, + "outputs": [], + "source": [ + "# @title Video 1: Why do we care about differential equations?\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'LhX-mUd8lPo'), ('Bilibili', 'BV1v64y197bW')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Or-35flNhTBo", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "Or-35flNhTBo" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Why_do_we_care_about_differential_equations_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "AcwNWVdsQgLz", + "metadata": { + "execution": {}, + "id": "AcwNWVdsQgLz" + }, + "source": [ + "---\n", + "# Section 1: Population differential equation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "LRwtiFPVQ3Pz", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "LRwtiFPVQ3Pz" + }, + "outputs": [], + "source": [ + "# @title Video 2: Population differential equation\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'czgGyoUsRoQ'), ('Bilibili', 'BV1pg41137CU')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "WLss9oNLhSI-", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "WLss9oNLhSI-" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Population_differential_equation_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "286b4298", + "metadata": { + "execution": {}, + "id": "286b4298" + }, + "source": [ + "This video covers our first example of a differential equation: a differential equation which models the change in population.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "To get an intuitive feel of a differential equations, we will start with a population differential equation, which models the change in population [1], that is human population not neurons, we will get to neurons later. Mathematically it is written like:\n", + "\\begin{align*}\n", + "\\\\\n", + "\\frac{d}{dt}\\,p(t) &= \\alpha p(t),\\\\\n", + "\\end{align*}\n", + "\n", + "where $p(t)$ is the population of the world and $\\alpha$ is a parameter representing birth rate.\n", + "\n", + "Another way of thinking about the models is that the equation\n", + "\\begin{align*}\n", + "\\\\\n", + "\\frac{d}{dt}\\,p(t) &= \\alpha p(t),\\\\\n", + "\\text{can be written as:}\\\\\n", + "\\text{\"Change in Population\"} &= \\text{ \"Birth rate times Current population.\"}\n", + "\\end{align*}\n", + "\n", + "The equation is saying something reasonable maybe not the perfect model but a good start.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "njGc4mc4ASXb", + "metadata": { + "execution": {}, + "id": "njGc4mc4ASXb" + }, + "source": [ + "### Think! 1.1: Interpretating the behavior of a linear population equation\n", + "\n", + "Using the plot below of change of population $\\frac{d}{dt} p(t) $ as a function of population $p(t)$ with birth-rate $\\alpha=0.3$, discuss the following questions:\n", + "\n", + "1. Why is the population differential equation known as a linear differential equation?\n", + "2. How does population size affect the rate of change of the population?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e37083e", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "8e37083e" + }, + "outputs": [], + "source": [ + "# @markdown Execute the code to plot the rate of change of population as a function of population\n", + "p = np.arange(0, 100, 0.1)\n", + "\n", + "with plt.xkcd():\n", + "\n", + " dpdt = 0.3*p\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(p, dpdt)\n", + " plt.xlabel(r'Population $p(t)$ (millions)')\n", + " plt.ylabel(r'$\\frac{d}{dt}p(t)=\\alpha p(t)$')\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "il7f6AWkXk0B", + "metadata": { + "execution": {}, + "id": "il7f6AWkXk0B" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. The plot of $\\frac{dp}{dt}$ is a line, which is why the differential\n", + " equation is known as a linear differential equation.\n", + "\n", + " 2. As the population increases, the change of population increases. A\n", + " population of 20 has a change of 6 while a population of 100 has a change of\n", + " 30. This makes sense - the larger the population the larger the change.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1whSBsnBhbAG", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "1whSBsnBhbAG" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Interpretating_the_behavior_of_a_linear_population_equation_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "id": "5846bbb5", + "metadata": { + "execution": {}, + "id": "5846bbb5" + }, + "source": [ + "## Section 1.1: Exact solution of the population equation" + ] + }, + { + "cell_type": "markdown", + "id": "uqcubpZNBIGl", + "metadata": { + "execution": {}, + "id": "uqcubpZNBIGl" + }, + "source": [ + "### Section 1.1.1: Initial condition\n", + "\n", + "The linear population differential equation is an initial value differential equation because we need an initial population value to solve it. Here we will set our initial population at time $0$ to $1$:\n", + "\n", + "\\begin{equation}\n", + "p(0) = 1\n", + "\\end{equation}\n", + "\n", + "Different initial conditions will lead to different answers but will not change the differential equation. This is one of the strengths of a differential equation." + ] + }, + { + "cell_type": "markdown", + "id": "oYBK4NWYBL5t", + "metadata": { + "execution": {}, + "id": "oYBK4NWYBL5t" + }, + "source": [ + "### Section 1.1.2: Exact Solution\n", + "\n", + "To calculate the exact solution of a differential equation, we must integrate both sides. Instead of numerical integration (as you delved into in the last tutorial), we will first try to solve the differential equations using analytical integration. As with derivatives, we can find analytical integrals of simple equations by consulting [a list](https://en.wikipedia.org/wiki/Lists_of_integrals). We can then get integrals for more complex equations using some mathematical tricks - the harder the equation, the more obscure the trick.\n", + "\n", + "The linear population equation\n", + "\\begin{equation}\n", + "\\frac{d}{dt}p(t) = \\alpha p(t), \\, p(0)=P_0\n", + "\\end{equation}\n", + "\n", + "has the exact solution:\n", + "\n", + "\\begin{equation}\n", + "p(t) = P_0 e^{\\alpha t}.\n", + "\\end{equation}\n", + "\n", + "The exact solution written in words is:\n", + "\n", + "\\begin{equation}\n", + "\\text{\"Population\"} = \\text{\"grows/declines exponentially as a function of time and birth rate\"}.\n", + "\\end{equation}\n", + "\n", + "Most differential equations do not have a known exact solution, so we will show how the solution can be estimated in the next tutorial on numerical methods.\n", + "\n", + "A small aside: a good deal of progress in mathematics was due to mathematicians writing taunting letters to each other, saying they had a trick to solve something better than everyone else. So do not worry too much about the tricks." + ] + }, + { + "cell_type": "markdown", + "id": "1257fcc0", + "metadata": { + "execution": {}, + "id": "1257fcc0" + }, + "source": [ + "#### Example Exact Solution of the Population Equation\n", + "\n", + "Let's consider the population differential equation with a birth rate $\\alpha=0.3$:\n", + "\n", + "\\begin{equation}\n", + "\\frac{d}{dt}p(t) = 0.3p(t)\n", + "\\end{equation}\n", + "\n", + "with the initial condition\n", + "\n", + "\\begin{equation}\n", + "p(0)=1.\n", + "\\end{equation}\n", + "\n", + "It has an exact solution\n", + "\n", + "\\begin{equation}\n", + "p(t)=e^{0.3 t}.\n", + "\\end{equation}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eeb717b8", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "eeb717b8" + }, + "outputs": [], + "source": [ + "# @markdown Execute code to plot the exact solution\n", + "t = np.arange(0, 10, 0.1) # Time from 0 to 10 years in 0.1 steps\n", + "\n", + "with plt.xkcd():\n", + "\n", + " p = np.exp(0.3 * t)\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(t, p)\n", + " plt.ylabel('Population (millions)')\n", + " plt.xlabel('time (years)')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "792841c9", + "metadata": { + "execution": {}, + "id": "792841c9" + }, + "source": [ + "## Section 1.2: Parameters of the differential equation\n", + "\n", + "*Estimated timing to here from start of tutorial: 12 min*\n", + "\n", + "One of the goals when designing a differential equation is to make it generalizable. This means that the differential equation will give reasonable solutions for different countries with different birth rates $\\alpha$." + ] + }, + { + "cell_type": "markdown", + "id": "8zD98917MDtW", + "metadata": { + "execution": {}, + "id": "8zD98917MDtW" + }, + "source": [ + "### Interactive Demo 1.2: Parameter Change\n", + "\n", + "Play with the widget to see the relationship between $\\alpha$ and the population differential equation as a function of population (left-hand side) and the population solution as a function of time (right-hand side). Pay close attention to the transition point from positive to negative.\n", + "\n", + "How do changing parameters of the population equation affect the outcome?\n", + "\n", + "1. What happens when $\\alpha < 0$?\n", + "2. What happens when $\\alpha > 0$?\n", + "3. What happens when $\\alpha = 0$?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "490c0be0", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "490c0be0" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " alpha=widgets.FloatSlider(.3, min=-1., max=1., step=.1, layout=my_layout)\n", + ")\n", + "def Pop_widget(alpha):\n", + " plot_dPdt(alpha=alpha)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "KDgZUHS5YDQp", + "metadata": { + "execution": {}, + "id": "KDgZUHS5YDQp" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. Negative values of alpha result in an exponential decrease to 0 a stable solution.\n", + " 2. Positive Values of alpha in an exponential increases to infinity.\n", + " 3. Alpha equal to 0 is a unique point known as an equilibrium point when the\n", + " dp/dt=0 and there is no change in population. This is known as a stable point.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1AgWDO58hnSv", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "1AgWDO58hnSv" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Parameter_change_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "c3a78544", + "metadata": { + "execution": {}, + "id": "c3a78544" + }, + "source": [ + "The population differential equation is an over-simplification and has some pronounced limitations:\n", + "1. Population growth is not exponential as there are limited resources, so the population will level out at some point.\n", + "2. It does not include any external factors on the populations, like weather, predators, and prey.\n", + "\n", + "These kinds of limitations can be addressed by extending the model.\n", + "\n", + "While it might not seem that the population equation has direct relevance to neuroscience, a similar equation is used to describe the accumulation of evidence for decision-making. This is known as the Drift Diffusion Model, and you will see it in more detail in the Linear System Day in Neuromatch (W2D2).\n", + "\n", + "Another differential equation similar to the population equation is the Leaky Integrate and Fire model, which you may have seen in the Python pre-course materials on W0D1 and W0D2. It will turn up later in Neuromatch as well. Below we will delve into the motivation of the differential equation." + ] + }, + { + "cell_type": "markdown", + "id": "ae82611f", + "metadata": { + "execution": {}, + "id": "ae82611f" + }, + "source": [ + "---\n", + "# Section 2: The leaky integrate and fire (LIF) model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hGBNAqnVVY3E", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "hGBNAqnVVY3E" + }, + "outputs": [], + "source": [ + "# @title Video 3: The leaky integrate and fire model\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'ZfWO6MLCa1s'), ('Bilibili', 'BV1rb4y1C79n')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eWa8PXDrh2NB", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "eWa8PXDrh2NB" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_The_leaky_integrate_and_fire_model_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "4qgkjHOtWcEL", + "metadata": { + "execution": {}, + "id": "4qgkjHOtWcEL" + }, + "source": [ + "This video covers the Leaky Integrate and Fire model (a linear differential equation which describes the membrane potential of a single neuron).\n", + "\n", + "
\n", + " Click here for text recap of full LIF equation from video \n", + "\n", + "The Leaky Integrate and Fire Model is a linear differential equation that describes the membrane potential ($V$) of a single neuron which was proposed by Louis Édouard Lapicque in 1907 [2].\n", + "\n", + "The subthreshold membrane potential dynamics of a LIF neuron is described by\n", + "\\begin{align}\n", + "\\tau_m\\frac{dV}{dt} = -(V-E_L) + R_mI\\,\n", + "\\end{align}\n", + "\n", + "\n", + "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is membrane resistance, and $I$ is the external input current.\n", + "\n", + "
\n", + "\n", + "In the next few sections, we will break down the full LIF equation and then build it back up to get an intuitive feel of the different facets of the differential equation.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c05087c6", + "metadata": { + "execution": {}, + "id": "c05087c6" + }, + "source": [ + "## Section 2.1: LIF without input\n", + "\n", + "*Estimated timing to here from start of tutorial: 18 min*\n", + "\n", + "As seen in the video, we will first model an LIF neuron without input, which results in the equation:\n", + "\n", + "\\begin{equation}\n", + "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}.\n", + "\\end{equation}\n", + "\n", + "where $\\tau_m$ is the time constant, $V$ is the membrane potential, and $E_L$ is the resting potential.\n", + "\n", + "
\n", + " Click here for further details (from video) \n", + "\n", + "Removing the input gives the equation\n", + "\n", + "\\begin{equation}\n", + "\\tau_m\\frac{dV}{dt} = -V+E_L,\n", + "\\end{equation}\n", + "\n", + "which can be written in words as:\n", + "\n", + "\\begin{align}\n", + "\\begin{matrix}\\text{\"Time constant multiplied by the} \\\\ \\text{change in membrane potential\"}\\end{matrix}&=\\begin{matrix}\\text{\"Minus Current} \\\\ \\text{membrane potential\"} \\end{matrix}+\n", + "\\begin{matrix}\\text{\"resting potential\"}\\end{matrix}.\\\\\n", + "\\end{align}\n", + "\n", + "\n", + "The equation can be re-arranged to look even more like the population equation:\n", + "\n", + "\\begin{align}\n", + "\\frac{dV}{dt} &= \\frac{-(V-E_L)}{\\tau_m}.\\\\\n", + "\\end{align}\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "XRl1cRt5ppJ1", + "metadata": { + "execution": {}, + "id": "XRl1cRt5ppJ1" + }, + "source": [ + "### Think! 2.1: Effect of membrane potential $V$ on the LIF model\n", + "\n", + "The plot the below shows the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ with the parameters set as:\n", + "* `E_L = -75`\n", + "* `V_reset = -50`\n", + "* `tau_m = 10.`\n", + "\n", + "1. What is the effect on $\\frac{dV}{dt}$ when $V>-75$ mV?\n", + "2. What is the effect on $\\frac{dV}{dt}$ when $V<-75$ mV\n", + "3. What is the effect on $\\frac{dV}{dt}$ when $V=-75$ mV?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dd9906c", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "8dd9906c" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to plot the relationship between dV/dt and V\n", + "# Parameter definition\n", + "E_L = -75\n", + "tau_m = 10\n", + "\n", + "# Range of Values of V\n", + "V = np.arange(-90, 0, 1)\n", + "dV = -(V - E_L) / tau_m\n", + "\n", + "with plt.xkcd():\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(V, dV)\n", + " plt.hlines(0, min(V), max(V), colors='black', linestyles='dashed')\n", + " plt.vlines(-75, min(dV), max(dV), colors='black', linestyles='dashed')\n", + "\n", + " plt.text(-50, 1, 'Positive')\n", + " plt.text(-50, -2, 'Negative')\n", + " plt.text(E_L, max(dV) + 1, r'$E_L$')\n", + " plt.xlabel(r'$V(t)$ (mV)')\n", + " plt.ylabel(r'$\\frac{dV}{dt}=\\frac{-(V-E_L)}{\\tau_m}$')\n", + " plt.ylim(-8, 2)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "h0MIi-MJYdk5", + "metadata": { + "execution": {}, + "id": "h0MIi-MJYdk5" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. For $V>-75$ mV, the derivative is negative.\n", + " 2. For $V<-75$ mV, the derivative is positive.\n", + " 3. For $V=-75$ mV, the derivative is equal to $0$ is and a stable point when nothing changes.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "q0zy29uhh8lG", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "q0zy29uhh8lG" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Effect_of_membrane_potential_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "f7bb8882", + "metadata": { + "execution": {}, + "id": "f7bb8882" + }, + "source": [ + "### Section 2.1.1: Exact Solution of the LIF model without input\n", + "\n", + "The LIF model has the exact solution:\n", + "\n", + "\\begin{equation}\n", + "V(t) = E_L+(V_{reset}-E_L)e^{\\frac{-t}{\\tau_m}}\n", + "\\end{equation}\n", + "\n", + "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, and $V_{reset}$ is the initial membrane potential.\n", + "\n", + "
\n", + " Click here for further details (from video) \n", + "\n", + "Similar to the population equation, we need an initial membrane potential at time $0$ to solve the LIF model.\n", + "\n", + "With this equation\n", + "\\begin{align}\n", + "\\frac{dV}{dt} &= \\frac{-(V-E_L)}{\\tau_m}\\,\\\\\n", + "V(0)&=V_{reset},\n", + "\\end{align}\n", + "where is $V_{reset}$ is called the reset potential.\n", + "\n", + "The LIF model has the exact solution:\n", + "\n", + "\\begin{align*}\n", + "V(t)=&\\ E_L+(V_{reset}-E_L)e^{\\frac{-t}{\\tau_m}}\\\\\n", + "\\text{ which can be written as: }\\\\\n", + "\\begin{matrix}\\text{\"Current membrane} \\\\ \\text{potential}\"\\end{matrix}=&\\text{\"Resting potential\"}+\\begin{matrix}\\text{\"Reset potential minus resting potential} \\\\ \\text{times exponential with rate one over time constant.\"}\\end{matrix}\\\\\n", + "\\end{align*}\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "31910ff4", + "metadata": { + "execution": {}, + "id": "31910ff4" + }, + "source": [ + "#### Interactive Demo 2.1.1: Initial Condition $V_{reset}$\n", + "\n", + "This exercise is to get an intuitive feel of how the different initial conditions $V_{reset}$ impact the differential equation of the LIF and the exact solution for the equation:\n", + "\n", + "\\begin{equation}\n", + "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}\n", + "\\end{equation}\n", + "\n", + "with the parameters set as:\n", + "* `E_L = -75,`\n", + "* `tau_m = 10.`\n", + "\n", + "The panel on the left-hand side plots the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ and the right-hand side panel plots the exact solution $V$ as a function of time $t,$ the green dot in both panels is the reset potential $V_{reset}$.\n", + "\n", + "Pay close attention to when $V_{reset}=E_L=-75$mV.\n", + "\n", + "1. How does the solution look with initial values of $V_{reset} < -75$?\n", + "2. How does the solution look with initial values of $V_{reset} > -75$?\n", + "3. How does the solution look with initial values of $V_{reset} = -75$?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03458759", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "03458759" + }, + "outputs": [], + "source": [ + "#@markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " V_reset=widgets.FloatSlider(-77., min=-91., max=-61., step=2,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def V_reset_widget(V_reset):\n", + " plot_V_no_input(V_reset)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "IQmxKOuRaVGr", + "metadata": { + "execution": {}, + "id": "IQmxKOuRaVGr" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. Initial Values of $V_{reset} < -75$ result in the solution increasing to\n", + " -75mV because $\\frac{dV}{dt} > 0$.\n", + " 2. Initial Values of $V_{reset} > -75$ result in the solution decreasing to\n", + " -75mV because $\\frac{dV}{dt} < 0$.\n", + " 3. Initial Values of $V_{reset} = -75$ result in a constant $V = -75$ mV\n", + " because $\\frac{dV}{dt} = 0$ (Stable point).\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Q9OCCAmdiVYQ", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "Q9OCCAmdiVYQ" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Initial_condition_Vreset_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "5dcd105e", + "metadata": { + "execution": {}, + "id": "5dcd105e" + }, + "source": [ + "## Section 2.2: LIF with input\n", + "\n", + "*Estimated timing to here from start of tutorial: 24 min*\n", + "\n", + "We will re-introduce the input $I$ and membrane resistance $R_m$ giving the original equation:\n", + "\n", + "\\begin{equation}\n", + "\\tau_m\\frac{dV}{dt} = -(V-E_L) + \\color{blue}{R_mI}\n", + "\\end{equation}\n", + "\n", + "The input can be other neurons or sensory information." + ] + }, + { + "cell_type": "markdown", + "id": "9862281c", + "metadata": { + "execution": {}, + "id": "9862281c" + }, + "source": [ + "### Interactive Demo 2.2: The Impact of Input\n", + "\n", + "The interactive plot below manipulates $I$ in the differential equation.\n", + "\n", + "- With increasing input, how does the $\\frac{dV}{dt}$ change? How would this impact the solution?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc6ce7c7", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "fc6ce7c7" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " I=widgets.FloatSlider(3., min=0., max=20., step=2,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(I):\n", + " plot_dVdt(I=I)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "HSNtznIwY6wA", + "metadata": { + "execution": {}, + "id": "HSNtznIwY6wA" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "dV/dt becomes bigger and less of it is below 0. This means the solution will\n", + "increase well beyond what is bioligically plausible.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2S4tiJYAidIt", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "2S4tiJYAidIt" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_The_impact_of_Input_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "968432a1", + "metadata": { + "execution": {}, + "id": "968432a1" + }, + "source": [ + "### Section 2.2.1: LIF exact solution\n", + "\n", + "The LIF with a constant input has a known exact solution:\n", + "\\begin{equation}\n", + "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-t}{\\tau_m}}\n", + "\\end{equation}\n", + "\n", + "which is written as:\n", + "\n", + "\\begin{align*}\n", + "\\begin{matrix}\\text{\"Current membrane} \\\\ \\text{potential\"}\\end{matrix}=&\\text{\"Resting potential\"}+\\begin{matrix}\\text{\"Reset potential minus resting potential} \\\\ \\text{times exponential with rate one over time constant.\" }\\end{matrix}\\\\\n", + "\\end{align*}" + ] + }, + { + "cell_type": "markdown", + "id": "91b7c87a", + "metadata": { + "execution": {}, + "id": "91b7c87a" + }, + "source": [ + "The plot below shows the exact solution of the membrane potential with the parameters set as:\n", + "* `V_reset = -75,`\n", + "* `E_L = -75,`\n", + "* `tau_m = 10,`\n", + "* `R_m = 10,`\n", + "* `I = 10.`\n", + "\n", + "Ask yourself, does the result make biological sense? If not, what would you change? We'll delve into this in the next section" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db5816d4", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "db5816d4" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to see the exact solution\n", + "dt = 0.5\n", + "t_rest = 0\n", + "\n", + "t = np.arange(0, 1000, dt)\n", + "\n", + "tau_m = 10\n", + "R_m = 10\n", + "V_reset = E_L = -75\n", + "\n", + "I = 10\n", + "\n", + "V = E_L + R_m*I + (V_reset - E_L - R_m*I) * np.exp(-(t)/tau_m)\n", + "\n", + "with plt.xkcd():\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(t,V)\n", + " plt.ylabel('V (mV)')\n", + " plt.xlabel('time (ms)')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0a31b8d1", + "metadata": { + "execution": {}, + "id": "0a31b8d1" + }, + "source": [ + "## Section 2.3: Maths is one thing, but neuroscience matters\n", + "\n", + "*Estimated timing to here from start of tutorial: 30 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "CtWroVVASxG1", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "CtWroVVASxG1" + }, + "outputs": [], + "source": [ + "# @title Video 4: Adding firing to the LIF\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'rLQk-vXRaX0'), ('Bilibili', 'BV1gX4y1P7pZ')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "KljGW16eihTo", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "KljGW16eihTo" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Adding_firing_to_the_LIF_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "9f12c68e", + "metadata": { + "execution": {}, + "id": "9f12c68e" + }, + "source": [ + "This video first recaps the introduction of input to the leaky integrate and fire model and then delves into how we add spiking behavior (or firing) to the model.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "While the mathematics of the exact solution is exact, it is not biologically valid as a neuron spikes and definitely does not plateau at a very positive value.\n", + "\n", + "To model the firing of a spike, we must have a threshold voltage $V_{th}$ such that if the voltage $V(t)$ goes above it, the neuron spikes\n", + "$$V(t)>V_{th}.$$\n", + "We must record the time of spike $t_{isi}$ and count the number of spikes\n", + "$$t_{isi}=t, $$\n", + "$$𝑆𝑝𝑖𝑘𝑒=𝑆𝑝𝑖𝑘𝑒+1.$$\n", + "Then reset the membrane voltage $V(t)$\n", + "$$V(t_{isi} )=V_{Reset}.$$\n", + "\n", + "To take into account the spike the exact solution becomes:\n", + "\\begin{align*}\n", + "V(t)=&\\ E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},&\\qquad V(t)V_{th}\\\\\n", + "Spike=&Spike+1,&\\\\\n", + "t_{isi}=&t,\\\\\n", + "\\end{align*}\n", + "while this does make the neuron spike, it introduces a discontinuity which is not as elegant mathematically as it could be, but it gets results so that is good.\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "acc43e8a", + "metadata": { + "execution": {}, + "id": "acc43e8a" + }, + "source": [ + "### Interactive Demo 2.3.1: Input on spikes\n", + "\n", + "This exercise shows the relationship between the firing rate and the Input for the exact solution `V` of the LIF:\n", + "\n", + "\\begin{equation}\n", + "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},\n", + "\\end{equation}\n", + "\n", + "with the parameters set as:\n", + "* `V_reset = -75,`\n", + "* `E_L = -75,`\n", + "* `tau_m = 10,`\n", + "* `R_m = 10.`\n", + "\n", + "Below is a figure with three panels;\n", + "* the top panel is the input, $I,$\n", + "* the middle panel is the membrane potential $V(t)$. To illustrate the spike, $V(t)$ is set to $0$ and then reset to $-75$ mV when there is a spike.\n", + "* the bottom panel is the raster plot with each dot indicating a spike.\n", + "\n", + "First, as electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the firing rate for a single value of $I$.\n", + "\n", + "**Note:** The audio doesn't work in some browsers so don't worry about it if you can't hear anything." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55785e26", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "55785e26" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to be able to hear the neuron\n", + "I = 3\n", + "t = np.arange(0, 1000, dt)\n", + "Spike, Spike_time, V = Exact_Integrate_and_Fire(I, t)\n", + "\n", + "plot_IF(t, V, I, Spike_time)\n", + "ipd.Audio(V, rate=len(V))" + ] + }, + { + "cell_type": "markdown", + "id": "54d5af0e", + "metadata": { + "execution": {}, + "id": "54d5af0e" + }, + "source": [ + "Manipulate the input into the LIF to see the impact of input on the firing pattern (rate).\n", + "\n", + "* What is the effect of $I$ on spiking?\n", + "* Is this biologically valid?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebf27a22", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ebf27a22" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " I=widgets.FloatSlider(3, min=2.0, max=4., step=.1,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(I):\n", + " Spike, Spike_time, V = Exact_Integrate_and_Fire(I, t)\n", + " plot_IF(t, V, I, Spike_time)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "GjwI3QF3ZLtY", + "metadata": { + "execution": {}, + "id": "GjwI3QF3ZLtY" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. As I increases, the number of spikes increases.\n", + " 2. No, as there is a limit to the number of spikes due to a refractory period,\n", + " which is not accounted for in this model.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ouprca8SipAs", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ouprca8SipAs" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Input_on_spikes_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "c3cf4d7b", + "metadata": { + "execution": {}, + "id": "c3cf4d7b" + }, + "source": [ + "## Section 2.4 Firing Rate as a function of Input\n", + "\n", + "*Estimated timing to here from start of tutorial: 38 min*\n", + "\n", + "The firing frequency of a neuron plotted as a function of current is called an input-output curve (F–I curve). It is also known as a transfer function, which you came across in the previous tutorial. This function is one of the starting points for the rate model, which extends from modeling single neurons to the firing rate of a collection of neurons.\n", + "\n", + "By fitting this to a function, we can start to generalize the firing pattern of many neurons, which can be used to build rate models. This will be discussed later in Neuromatch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96bcf5e7", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "96bcf5e7" + }, + "outputs": [], + "source": [ + "# @markdown *Execture this cell to visualize the FI curve*\n", + "I_range = np.arange(2.0, 4.0, 0.1)\n", + "Spike_rate = np.ones(len(I_range))\n", + "\n", + "for i, I in enumerate(I_range):\n", + " Spike_rate[i], _, _ = Exact_Integrate_and_Fire(I, t)\n", + "\n", + "with plt.xkcd():\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(I_range,Spike_rate)\n", + " plt.xlabel('Input Current (nA)')\n", + " plt.ylabel('Spikes per Second (Hz)')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "58fdcbe8", + "metadata": { + "execution": {}, + "id": "58fdcbe8" + }, + "source": [ + "The LIF model is a very nice differential equation to start with in computational neuroscience as it has been used as a building block for many papers that simulate neuronal response.\n", + "\n", + "__Strengths of LIF model:__\n", + "+ Has an exact solution;\n", + "+ Easy to interpret;\n", + "+ Great to build network of neurons.\n", + "\n", + "__Weaknesses of the LIF model:__\n", + "- Spiking is a discontinuity;\n", + "- Abstraction from biology;\n", + "- Cannot generate different spiking patterns." + ] + }, + { + "cell_type": "markdown", + "id": "697364ff", + "metadata": { + "execution": {}, + "id": "697364ff" + }, + "source": [ + "---\n", + "# Summary\n", + "\n", + "*Estimated timing of tutorial: 45 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "_Jmpnq0mSihx", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "_Jmpnq0mSihx" + }, + "outputs": [], + "source": [ + "# @title Video 5: Summary\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'VzwLAW5p4ao'), ('Bilibili', 'BV1jV411x7t9')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "VNxbJDK5iuFA", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "VNxbJDK5iuFA" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Summary_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "x0H0zhTNWlop", + "metadata": { + "execution": {}, + "id": "x0H0zhTNWlop" + }, + "source": [ + "In this tutorial, we have seen two differential equations, the population differential equations and the leaky integrate and fire model.\n", + "\n", + "We learned about the following:\n", + "* The motivation for differential equations.\n", + "* An intuitive relationship between the solution and the differential equation form.\n", + "* How different parameters of the differential equation impact the solution.\n", + "* The strengths and limitations of the simple differential equations." + ] + }, + { + "cell_type": "markdown", + "id": "eaec6994", + "metadata": { + "execution": {}, + "id": "eaec6994" + }, + "source": [ + "---\n", + "## Links to Neuromatch Computational Neuroscience Days\n", + "\n", + "Differential equations turn up in a number of different Neuromatch days:\n", + "* The LIF model is discussed in more details in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", + "* Drift Diffusion model which is a differential equation for decision making is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", + "* Systems of differential equations are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", + "\n", + "---\n", + "## References\n", + "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", + "\n", + "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", + "\n", + "\n", + "## Bibliography\n", + "1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.\n", + "2. Strogatz, S. (2014). Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2nd edition\n", + "\n", + "### Supplemental Popular Reading List\n", + "1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.\n", + "2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.\n", + "\n", + "### Popular Podcast\n", + "1. Strogatz, S. (Host). (2020), Joy of X https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine\n" + ] + } + ], + "metadata": { + "colab": { + "name": "W0D4_Tutorial2", + "provenance": [], + "toc_visible": true, + "include_colab_link": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file From fecd3e3317096e7ef2300b99d2a1677ebdc895b7 Mon Sep 17 00:00:00 2001 From: Spiros Chavlis Date: Mon, 3 Jul 2023 15:02:29 +0300 Subject: [PATCH 5/7] fixes --- tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb | 5276 +++++++++--------- 1 file changed, 2683 insertions(+), 2593 deletions(-) diff --git a/tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb b/tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb index a6621cf..d54dffc 100644 --- a/tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb +++ b/tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb @@ -1,2594 +1,2684 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "6c2d4b58", - "metadata": { - "colab_type": "text", - "execution": {}, - "id": "view-in-github" - }, - "source": [ - "\"Open   \"Open" - ] - }, - { - "cell_type": "markdown", - "id": "08842220", - "metadata": { - "execution": {} - }, - "source": [ - "# Tutorial 3: Numerical Methods\n", - "\n", - "**Week 0, Day 3: Calculus**\n", - "\n", - "**Content creators:** John S Butler, Arvind Kumar with help from Harvey McCone\n", - "\n", - "**Content reviewers:** Swapnil Kumar, Matthew McCann\n", - "\n", - "**Production editors:** Matthew McCann, Ella Batty" - ] - }, - { - "cell_type": "markdown", - "id": "v0oVvJHEMouH", - "metadata": { - "execution": {} - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "id": "7d80998b", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Tutorial Objectives\n", - "\n", - "*Estimated timing of tutorial: 70 minutes*\n", - "\n", - "While a great deal of neuroscience can be described by mathematics, a great deal of the mathematics used cannot be solved exactly. This might seem very odd that you can writing something in mathematics that cannot be immediately solved but that is the beauty and mystery of mathematics. To side step this issue we use Numerical Methods to estimate the solution.\n", - "\n", - "In this tutorial, we will look at the Euler method to estimate the solution of a few different differential equations: the population equation, the Leaky Integrate and Fire model and a simplified version of the Wilson-Cowan model which is a system of differential equations.\n", - "\n", - "**Steps:**\n", - "- Code the Euler estimate of the Population Equation;\n", - "- Investigate the impact of time step on the error of the numerical solution;\n", - "- Code the Euler estimate of the Leaky Integrate and Fire model for a constant input;\n", - "- Visualize and listen to the response of the integrate for different inputs;\n", - "- Apply the Euler method to estimate the solution of a system of differential equations.\n" - ] - }, - { - "cell_type": "markdown", - "id": "bwMygnJgMUp7", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "BrceoWcPfYlB", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D4_T3\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5985801", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d40abcd8", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Figure Settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import IPython.display as ipd\n", - "from matplotlib import gridspec\n", - "import ipywidgets as widgets # interactive display\n", - "from ipywidgets import Label\n", - "%config InlineBackend.figure_format = 'retina'\n", - "# use NMA plot style\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", - "my_layout = widgets.Layout()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "hwhtoyMMi7qj", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Plotting Functions\n", - "\n", - "time = np.arange(0, 1, 0.01)\n", - "\n", - "def plot_slope(dt):\n", - " \"\"\"\n", - " Args:\n", - " dt : time-step\n", - " Returns:\n", - " A figure of an exponential, the slope of the exponential and the derivative exponential\n", - " \"\"\"\n", - "\n", - " t = np.arange(0, 5+0.1/2, 0.1)\n", - "\n", - " with plt.xkcd():\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " # Exponential\n", - " p = np.exp(0.3*t)\n", - " plt.plot(t, p, label='y')\n", - " # slope\n", - " plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1+dt))],':og',label=r'$\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$')\n", - " # derivative\n", - " plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1))+dt*0.3*np.exp(0.3*(1))],'-k',label=r'$\\frac{dy}{dt}$')\n", - " plt.legend()\n", - " plt.plot(1+dt, np.exp(0.3*(1+dt)), 'og')\n", - " plt.ylabel('y')\n", - " plt.xlabel('t')\n", - " plt.show()\n", - "\n", - "\n", - "\n", - "def plot_StepEuler(dt):\n", - " \"\"\"\n", - " Args:\n", - " dt : time-step\n", - " Returns:\n", - " A figure of one step of the Euler method for an exponential growth function\n", - " \"\"\"\n", - "\n", - " t=np.arange(0, 1 + dt + 0.1 / 2, 0.1)\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(6,4))\n", - " p=np.exp(0.3*t)\n", - " plt.plot(t,p)\n", - " plt.plot([1,],[np.exp(0.3*1)],'og',label='Known')\n", - " plt.plot([1,1+dt],[np.exp(0.3*1),np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)],':g',label=r'Euler')\n", - " plt.plot(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),'or',label=r'Estimate $p_1$')\n", - " plt.plot(1+dt,p[-1],'bo',label=r'Exact $p(t_1)$')\n", - " plt.vlines(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),p[-1],colors='r', linestyles='dashed',label=r'Error $e_1$')\n", - " plt.text(1+dt+0.1,(np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)+p[-1])/2,r'$e_1$')\n", - " plt.legend()\n", - " plt.ylabel('Population (millions)')\n", - " plt.xlabel('time(years)')\n", - " plt.show()\n", - "\n", - "def visualize_population_approx(t, p):\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')\n", - "\n", - " plt.plot(t, p,':o', label='Euler Estimate')\n", - " plt.vlines(t, p, np.exp(0.3*t),\n", - " colors='r', linestyles='dashed', label=r'Error $e_k$')\n", - "\n", - " plt.ylabel('Population (millions)')\n", - " plt.legend()\n", - " plt.xlabel('Time (years)')\n", - " plt.show()\n", - "\n", - "## LIF PLOT\n", - "def plot_IF(t, V, I, Spike_time):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " V : membrane Voltage\n", - " I : Input\n", - " Spike_time : Spike_times\n", - " Returns:\n", - " figure with three panels\n", - " top panel: Input as a function of time\n", - " middle panel: membrane potential as a function of time\n", - " bottom panel: Raster plot\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(12,4))\n", - " gs = gridspec.GridSpec(3, 1, height_ratios=[1, 4, 1])\n", - " # PLOT OF INPUT\n", - " plt.subplot(gs[0])\n", - " plt.ylabel(r'$I_e(nA)$')\n", - " plt.yticks(rotation=45)\n", - " plt.plot(t,I,'g')\n", - " #plt.ylim((2,4))\n", - " plt.xlim((-50,1000))\n", - " # PLOT OF ACTIVITY\n", - " plt.subplot(gs[1])\n", - " plt.plot(t,V,':')\n", - " plt.xlim((-50,1000))\n", - " plt.ylabel(r'$V(t)$(mV)')\n", - " # PLOT OF SPIKES\n", - " plt.subplot(gs[2])\n", - " plt.ylabel(r'Spike')\n", - " plt.yticks([])\n", - " plt.scatter(Spike_time,1*np.ones(len(Spike_time)), color=\"grey\", marker=\".\")\n", - " plt.xlim((-50,1000))\n", - " plt.xlabel('time(ms)')\n", - " plt.show()\n", - "\n", - "def plot_rErI(t, r_E, r_I):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " r_E : excitation rate\n", - " r_I : inhibition rate\n", - "\n", - " Returns:\n", - " figure of r_I and r_E as a function of time\n", - "\n", - " \"\"\"\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(6,4))\n", - " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", - " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", - " plt.xlabel('time(ms)')\n", - " plt.legend()\n", - " plt.ylabel('Firing Rate (Hz)')\n", - " plt.show()\n", - "\n", - "\n", - "def plot_rErI_Simple(t, r_E, r_I):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " r_E : excitation rate\n", - " r_I : inhibition rate\n", - "\n", - " Returns:\n", - " figure with two panels\n", - " left panel: r_I and r_E as a function of time\n", - " right panel: r_I as a function of r_E with Nullclines\n", - "\n", - " \"\"\"\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(12,4))\n", - " gs = gridspec.GridSpec(1, 2)\n", - " # LEFT PANEL\n", - " plt.subplot(gs[0])\n", - " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", - " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", - " plt.xlabel('time(ms)')\n", - " plt.legend()\n", - " plt.ylabel('Firing Rate (Hz)')\n", - " # RIGHT PANEL\n", - " plt.subplot(gs[1])\n", - " plt.plot(r_E,r_I,'k:')\n", - " plt.plot(r_E[0],r_I[0],'go')\n", - "\n", - " plt.hlines(0,np.min(r_E),np.max(r_E),linestyles=\"dashed\",color='b',label=r'$\\frac{d}{dt}r_E=0$')\n", - " plt.vlines(0,np.min(r_I),np.max(r_I),linestyles=\"dashed\",color='r',label=r'$\\frac{d}{dt}r_I=0$')\n", - "\n", - " plt.legend(loc='upper left')\n", - "\n", - " plt.xlabel(r'$r_E$')\n", - " plt.ylabel(r'$r_I$')\n", - " plt.show()\n", - "\n", - "def plot_rErI_Matrix(t, r_E, r_I, Null_rE, Null_rI):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " r_E : excitation firing rate\n", - " r_I : inhibition firing rate\n", - " Null_rE: Nullclines excitation firing rate\n", - " Null_rI: Nullclines inhibition firing rate\n", - " Returns:\n", - " figure with two panels\n", - " left panel: r_I and r_E as a function of time\n", - " right panel: r_I as a function of r_E with Nullclines\n", - "\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(12,4))\n", - " gs = gridspec.GridSpec(1, 2)\n", - " plt.subplot(gs[0])\n", - " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", - " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", - " plt.xlabel('time(ms)')\n", - " plt.ylabel('Firing Rate (Hz)')\n", - " plt.legend()\n", - " plt.subplot(gs[1])\n", - " plt.plot(r_E,r_I,'k:')\n", - " plt.plot(r_E[0],r_I[0],'go')\n", - "\n", - " plt.plot(r_E,Null_rE,':',color='b',label=r'$\\frac{d}{dt}r_E=0$')\n", - " plt.plot(r_E,Null_rI,':',color='r',label=r'$\\frac{d}{dt}r_I=0$')\n", - " plt.legend(loc='best')\n", - " plt.xlabel(r'$r_E$')\n", - " plt.ylabel(r'$r_I$')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "SXJPvBQzik6K", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 1: Intro to the Euler method using the population differential equation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "DYKXWaR7iwl9", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 1: Intro to numerical methods for differential equations\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'NI8c80TA7IQ'), ('Bilibili', 'BV1gh411Y7gV')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "M3V5Kju3j7Ie", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Intro_to_numerical_methods_for_differential_equations_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "R9B9JKT-PvEn", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 1.1: Slope of line as approximation of derivative\n", - "\n", - "*Estimated timing to here from start of tutorial: 8 min*\n", - "\n", - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "The Euler method is one of the straight forward and elegant methods to approximate a differential. It was designed by [Leonhard Euler](https://en.wikipedia.org/wiki/Leonhard_Euler) (1707-1783).\n", - "Simply put we just replace the derivative in the differential equation by the formula for a line and re-arrange.\n", - "\n", - "The slope is the rate of change between two points. The formula for the slope of a line between the points $(t_0,y(t_0))$ and $(t_1,y(t_1))$ is given by:\n", - "$$ m=\\frac{y(t_1)-y(t_0)}{t_1-t_0}, $$\n", - "which can be written as\n", - "$$ m=\\frac{y_1-y_0}{t_1-t_0}, $$\n", - "or as\n", - "$$ m=\\frac{\\Delta y_0}{\\Delta t_0}, $$\n", - "where $\\Delta y_0=y_1-y_0$ and $\\Delta t_0=t_1-t_0$ or in words as\n", - "$$ m=\\frac{\\text{ Change in y} }{\\text{Change in t}}. $$\n", - "The slope can be used as an approximation of the derivative such that\n", - "$$ \\frac{d}{dt}y(t)\\approx \\frac{y(t_0+\\Delta t)-y(t_0)}{t_0+\\Delta t-t_0}=\\frac{y(t_0+dt)-y(t_0)}{\\Delta t}$$\n", - "where $\\Delta t$ is a time-step.\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "b667c36a", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 1.1: Slope of a Line\n", - "The plot below shows a function $y(t)$ in blue, its exact derivative $ \\frac{d}{dt}y(t)$ at $t_0=1$ in black, and the approximate derivative calculated using the slope formula $=\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$ for different time-steps sizes $\\Delta t$ in green.\n", - "\n", - "Interact with the widget to see how time-step impacts the accuracy of the slope which is the estimate of the derivative.\n", - "\n", - "- How does the size of $\\Delta t$ affect the approximation?\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ea4a4f78", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - "\n", - " dt=widgets.FloatSlider(1, min=0., max=4., step=.1,\n", - " layout=my_layout)\n", - "\n", - ")\n", - "\n", - "def Pop_widget(dt):\n", - " plot_slope(dt)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "gTgFRiMhc1qm", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " The larger the time-step $dt$, the worse job the formula of the slope of a\n", - " line does to approximate the derivative.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "FZ1SZPQwj9H4", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Slope_of_a_line_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "8f92ce83", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 1.2: Euler error for a single step\n", - "\n", - "*Estimated timing to here from start of tutorial: 12 min*\n", - "\n", - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "Linking with the previous tutorial, we will use the population differential equation to get an intuitive feel of using the Euler method to approximate the solution of a differential equation, as it has an exact solution with no discontinuities.\n", - "\n", - "The population differential equation is given by\n", - "\\begin{align*}\n", - "\\\\\n", - "\\frac{d}{dt}\\,p(t) &= \\alpha p(t)\\\\\\\\\n", - "p(0)&=p_0 \\quad \\text{Initial Condition}\n", - "\\end{align*}\n", - "\n", - "where $p(t)$ is the population at time $t$ and $\\alpha$ is a parameter representing birth rate. The exact solution of the differential equation is\n", - "$$ p(t)=p_0e^{\\alpha t}.$$\n", - "\n", - "To numerically estimate the population differential equation we replace the derivate with the slope of the line to get the discrete (not continuous) equation:\n", - "\n", - "\\begin{align*}\n", - "\\\\\n", - "\\frac{p_1-p_0}{t_1-t_0} &= \\alpha p_0\\\\\\\\\n", - "p(0)&=p_0 \\quad \\text{Initial Condition}\n", - "\\end{align*}\n", - "\n", - "where $p_1$ is the estimate of $p(t_1)$. Let $\\Delta t=t_1-t_0$ be the time-step and re-arrange the equation gives\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{p_1}&=\\color{green}{p_0}+\\color{blue}{\\Delta t} (\\color{blue}{\\alpha} \\color{green}{p_0})\n", - "\\end{align*}\n", - "\n", - "where $\\color{red}{p_1}$ is the unknown future, $\\color{green}{p_0}$ is the known current population, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter and $\\color{blue}{\\alpha}$ is the given birth rate parameter.\n", - "Another way to read the re-arranged discrete equation is:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{\\text{\"Future Population\"}}&=\\color{green}{\\text{ \"Current Population\"}}+\\color{blue}{\\text{ time-step}} \\times (\\color{blue}{\\text{ \"Birth rate}}\\times \\color{green}{\\text{ Current Population\"}}).\n", - "\\end{align*}\n", - "\n", - "So pretty much, we can use the current population and add a bit of the dynamics of the differential equation to predict the future. We will make millions... But wait there is always an error.\n", - "\n", - "The solution of the Euler method $p_1$ is an estimate of the exact solution $p(t_1)$ at $t_1$ which means there is a bit of error $e_1$ which gives the equation\n", - "\\begin{align*}\n", - "p(t_1)&=p_1+e_1\\\\\\\\\n", - "\\text{Rearranging}\\\\\\\\\n", - "e_1&=p(t_1)-p_1,\\\\\n", - "\\text{Error}&=\\text{Exact-Estimate}.\n", - "\\end{align*}\n", - "\n", - "Most of the time we do not know the exact answer $p(t_1)$ and hence the size of the error $e_1$ but for the population equation we have the exact solution $ p(t)=p_0e^{\\alpha}.$\n", - "\n", - "This means we can explore what the error looks like.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "vGxg-2k6k8hM", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 1.2: Euler error for a single step\n", - "Interact with the widget to see how the time-step $\\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.\n", - "\n", - "\n", - "1. What happens to the estimate $p_1$ as the time-step $\\Delta t$ increases?\n", - "\n", - "2. Is there a relationship between the size of $\\Delta t$ and $e_1$?\n", - "\n", - "3. How would you improve the error $e_1$?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e72c600b", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " dt=widgets.FloatSlider(1.5, min=0., max=4., step=.1,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(dt):\n", - " plot_StepEuler(dt)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "EoA90TrDdVWv", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. The larger the time-step $\\Delta t$ the more the estimate $p_1$ deviates\n", - " from the exact $p(t_1)$.\n", - "\n", - " 2. There is a linear relationship between $\\Delta t$ and $e_1$: double the time-step double the error.\n", - "\n", - " 3. Make more shorter time-steps\n", - "\"\"\";" - ] - }, - { - "cell_type": "markdown", - "id": "e78fc157", - "metadata": { - "execution": {} - }, - "source": [ - "The error $e_1$ from one time-step is known as the __local error__. For the Euler method the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $O(\\Delta t)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "qXS6Fkkkj-xD", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Euler_error_of_single_step_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "1yr4hKV7R0JJ", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 1.3: Taking more steps\n", - "\n", - "*Estimated timing to here from start of tutorial: 16 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "MZcqv_DoRsWJ", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 2: Taking more steps\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'cGsXHllGMVo'), ('Bilibili', 'BV135411T7QF')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "W8HbORAlkAWj", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Taking_more_steps_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "972a55c4", - "metadata": { - "execution": {} - }, - "source": [ - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "In the above exercise we saw that by increasing the time-step $\\Delta t$ the error between the estimate $p_1$ and the exact $p(t_1)$ increased. The largest error in the example above was for $\\Delta t=4$, meaning the first time point was at 1 year and the second was at 5 years (as 5 - 1 = 4).\n", - "\n", - "To decrease the error, we can divide the interval $[1, 5]$ into more steps using a smaller $\\Delta t$.\n", - "\n", - "In the plot below we use $\\Delta t=1$, which divides the interval into four segments\n", - "$$n=\\frac{5-1}{1}=4,$$\n", - "giving\n", - "$$ t_0=t[0]=1, \\ t_1=t[1]=2, \\ t_2=t[2]=3, \\ t_3=t[3]=4 \\ \\text{ and } t_4=t[4]=5. $$\n", - "This can be written as\n", - "$$ t[k]=1+k\\times1, \\text{ for } k=0,\\cdots 4, $$\n", - "and more generally as\n", - "$$ t[k]=t[k]+k\\times \\Delta t, \\text{ for } k=0,\\cdots n, $$\n", - "where $n$ is the number of steps.\n", - "\n", - "Using the Euler method, the continuous population differential equation is written as a series of discrete difference equations of the form:\n", - "\\begin{align*}\n", - "\\color{red}{p[k+1]}&=\\color{green}{p[k]}+\\color{blue}{\\Delta t} (\\color{blue}{\\alpha} \\color{green}{p[k]})\\\\\n", - "&\\text{for } k=0,1,\\cdots n-1\n", - "\\end{align*}\n", - "where $\\color{red}{p[k+1]}$ is the unknown estimate of the future population at $t[k+1]$, $\\color{green}{p[k]}$ is the known current population estimate at $t_k$, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter and $\\color{blue}{\\alpha}$ is the given birth rate parameter.\n", - "\n", - "\n", - "The Euler method can be applied to all first order differential equations of the form\n", - "\\begin{align*}\n", - "\\frac{d}{dt}y(t)&=f(t,y(t)),\\\\\n", - "y(t_{0})&=y_0,\\\\\n", - "\\end{align*}\n", - "on an interval $[a,b]$.\n", - "\n", - "Using the Euler method all differential equation can be written as discrete difference equations:\n", - "\\begin{align*}\n", - "\\frac{\\color{red}{y[k+1]}-\\color{green}{y[k]}}{\\color{blue}{\\Delta t}}&=f(\\color{blue}{t[k]},\\color{green}{y[k]}),\\\\\n", - "\\text{Re-arranging}\\\\\n", - "\\color{red}{y[k+1]}&=\\color{green}{y[k]}+\\color{blue}{\\Delta t}f(\\color{blue}{t[k]},\\color{green}{y[k]}),\\\\\n", - "&\\text{ for } k=0, \\cdots n-1,\\\\\n", - "y[0]&=\\color{green}{y_0},\\\\\n", - "\\end{align*}\n", - "where $\\color{red}{y[k+1]}$ is the unknown estimate at $t[k+1]$, $\\color{green}{y[k]}$ is the known at $t_k$, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter, $\\color{blue}{t[k]}$ is the time point and $f$ is the right hand side of the differential equation.\n", - "The discrete time steps are:\n", - "\\begin{align*}\n", - "\\color{blue}{t[k]}&=\\color{blue}{t[0]}+\\color{blue}{k}\\color{blue}{\\Delta t},\\\\\n", - "n&=\\frac{b-a}{\\Delta t}\\\\\n", - "&\\text{ for } k=0, \\cdots n.\\\\\n", - "\\end{align*}\n", - "Once again this can be simply put into words:\n", - "\\begin{align*}\n", - "\\color{red}{\\text{ \"Future\" }}&=\\color{green}{\\text{ \"Current Info\" }}+\\color{blue}{\\text{ time-step } }\\times\\text{ \"Dynamics of the system which is a function of } \\color{blue}{ \\text{ time }} \\text{ and }\\color{green}{ \\text{ Current Info.\" }}\n", - "\\end{align*}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d84f02ed", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to visualize time steps*\n", - "dt =1\n", - "t =np.arange(1, 5+dt/2, dt) # Time from 1 to 5 years in 0.1 steps\n", - "with plt.xkcd():\n", - " fig = plt.figure(figsize=(8, 2))\n", - " plt.plot(t, 0*t, ':o')\n", - " plt.plot(t[0] ,0*t[0],':go',label='Initial Condition')\n", - " plt.text(t[0]-0.1, 0*t[0]-0.03, r'$t_0$')\n", - " plt.text(t[1]-0.1, 0*t[0]-0.03, r'$t_1$')\n", - " plt.text(t[2]-0.1, 0*t[0]-0.03, r'$t_2$')\n", - " plt.text(t[3]-0.1, 0*t[0]-0.03, r'$t_3$')\n", - " plt.text(t[4]-0.1, 0*t[0]-0.03,r'$t_4$')\n", - " plt.text(t[0]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", - " plt.text(t[1]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", - " plt.text(t[2]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", - " plt.text(t[3]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", - " plt.yticks([])#plt.ylabel('Population (millions)')\n", - " plt.xlabel('time(years)')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "279ea532", - "metadata": { - "execution": {} - }, - "source": [ - "### Coding Exercise 1.3: Step, step, step\n", - "\n", - "Given the population differential equation:\n", - "\\begin{align*}\n", - "\\frac{d}{dt}\\,p(t) &= 0.3 p(t),\\\\\n", - "\\end{align*}\n", - "\n", - "and the initial condition:\n", - "\n", - "\\begin{align*}\n", - "p(t_0=1)&=e^{0.3},\n", - "\\end{align*}\n", - "\n", - "code the difference equation:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{p[k+1]}&=\\color{green}{p[k]}+\\color{blue}{\\Delta t} (\\color{blue}{0.3} \\color{green}{p[k]}),\\\\\n", - "\\color{green}{p[0]}&=e^{0.3},\\quad \\text{Initial Condition,}\\\\\n", - "&\\text{for } k=0,1,\\cdots 4,\\\\\n", - "\\end{align*}\n", - "\n", - "to estimate the population on the interval $[1,5]$ with a time-step $\\Delta t=1$, denoted by `dt` in code." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b872e66", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# Time step\n", - "dt = 1\n", - "\n", - "# Make time range from 1 to 5 years with step size dt\n", - "t = np.arange(1, 5+dt/2, dt)\n", - "\n", - "# Get number of steps\n", - "n = len(t)\n", - "\n", - "# Initialize p array\n", - "p = np.zeros(n)\n", - "p[0] = np.exp(0.3*t[0]) # initial condition\n", - "\n", - "# Loop over steps\n", - "for k in range(n-1):\n", - "\n", - " ########################################################################\n", - " ## TODO for students\n", - " ## Complete line of code and remove\n", - " raise NotImplementedError(\"Student exercise: calculate the population step for each time point\")\n", - " ########################################################################\n", - "\n", - " # Calculate the population step\n", - " p[k+1] = ...\n", - "\n", - "# Visualize\n", - "visualize_population_approx(t, p)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "LHAKHn_5UzLQ", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Time step\n", - "dt = 1\n", - "\n", - "# Make time range from 1 to 5 years with step size dt\n", - "t = np.arange(1, 5+dt/2, dt)\n", - "\n", - "# Get number of steps\n", - "n = len(t)\n", - "\n", - "# Initialize p array\n", - "p = np.zeros(n)\n", - "p[0] = np.exp(0.3*t[0]) # initial condition\n", - "\n", - "# Loop over steps\n", - "for k in range(n-1):\n", - "\n", - " # Calculate the population step\n", - " p[k+1] = p[k] + dt * 0.3 * p[k]\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " visualize_population_approx(t, p)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "Yvl3OBBPkCQr", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Step_step_step_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "id": "d9301447", - "metadata": { - "execution": {} - }, - "source": [ - "The error is smaller for 4 time-steps than taking one large time step from 1 to 5 but do note that the error is increasing for each step. This is known as __global error__ so the futher in time you want to predict, the larger the error.\n", - "You can read the theorems [here.](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)" - ] - }, - { - "cell_type": "markdown", - "id": "b419bf50", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 2: Euler method for the leaky integrate and fire\n", - "\n", - "\n", - "*Estimated timing to here from start of tutorial: 26 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "JOA7EzauxIV6", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 3: Leaky integrate and fire\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'T85OcgY7xjo'), ('Bilibili', 'BV1k5411T7by')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "GeAbFXiSkDEd", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Leaky_integrate_and_fire_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "Zjro4hXwxU8O", - "metadata": { - "execution": {} - }, - "source": [ - "
\n", - " Click here for text recap of video \n", - "\n", - "The Leaky Integrate and Fire (LIF) differential equation is:\n", - "\n", - "\\begin{align}\n", - "\\frac{dV}{dt} = \\frac{-(V-E_L) + R_mI(t)}{\\tau_m}\\,\n", - "\\end{align}\n", - "\n", - "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is leak resistance and $I(t)$ is the external input current.\n", - "\n", - "The solution of the LIF can be estimated by applying the Euler method to give the difference equation:\n", - "\n", - "\\begin{align}\n", - "\\frac{\\color{red}{V[k+1]}-\\color{green}{V[k]}}{\\color{blue}{\\Delta t}}=\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", - "\\end{align}\n", - "where $V[k]$ is the estimate of the membrane potential at time point $t[k]$.\n", - "Re-arranging the equation such that all the known terms are on the right gives:\n", - "\n", - "\\begin{align}\n", - "\\color{red}{V[k+1]}=\\color{green}{V[k]}+\\color{blue}{\\Delta t}\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", - "\\text{for } k=0\\cdots n-1,\n", - "\\end{align}\n", - "\n", - "where $\\color{red}{V[k+1]}$ is the unknown membrane potential at $t[k+1]$, $\\color{green}{V[k]} $ is known membrane potential, $\\color{blue}{E_L}$, $\\color{blue}{R_m}$ and $\\color{blue}{\\tau_m}$ are known parameters, $\\color{blue}{\\Delta t}$ is a chosen time-step and $I(t_k)$ is a function for an external input current." - ] - }, - { - "cell_type": "markdown", - "id": "1efb9d89", - "metadata": { - "execution": {} - }, - "source": [ - "## Coding Exercise 2: LIF and Euler\n", - "Code the difference equation for the LIF:\n", - "\n", - "\\begin{align}\n", - "\\color{red}{V[k+1]}=\\color{green}{V[k]}+\\color{blue}{\\Delta t}\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", - "\\text{for } k=0\\cdots n-1,\n", - "\\end{align}\n", - "\n", - "with the given parameters set as:\n", - "* `V_reset = -75,`\n", - "* `E_L = -75,`\n", - "* `tau_m = 10,`\n", - "* `R_m = 10.`\n", - "\n", - "We will then visualize the result.\n", - "The figure has three panels:\n", - "* the top panel is the sinusoidal input, $I$,\n", - "* the middle panel is the estimate membrane potential $V_k$. To illustrate a spike, $V_k$ is set to $0$ and then reset,\n", - "* the bottom panel is the raster plot with each dot indicating a spike." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4e7d29bb", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "def Euler_Integrate_and_Fire(I, time, dt):\n", - " \"\"\"\n", - " Args:\n", - " I: Input\n", - " time: time\n", - " dt: time-step\n", - " Returns:\n", - " Spike: Spike count\n", - " Spike_time: Spike times\n", - " V: membrane potential esitmated by the Euler method\n", - " \"\"\"\n", - "\n", - " Spike = 0\n", - " tau_m = 10\n", - " R_m = 10\n", - " t_isi = 0\n", - " V_reset = E_L = -75\n", - " n = len(time)\n", - " V = V_reset * np.ones(n)\n", - " V_th = -50\n", - " Spike_time = []\n", - "\n", - " for k in range(n-1):\n", - " #######################################################################\n", - " ## TODO for students: calculate the estimate solution of V at t[i+1]\n", - " ## Complete line of codes for dV and remove\n", - " ## Run the code in Section 5.1 or 5.2 to see the output!\n", - " raise NotImplementedError(\"Student exercise: calculate the estimate solution of V at t[i+1]\")\n", - " ########################################################################\n", - "\n", - " dV = ...\n", - " V[k+1] = V[k] + dt*dV\n", - "\n", - " # Discontinuity for Spike\n", - " if V[k] > V_th:\n", - " V[k] = 0\n", - " V[k+1] = V_reset\n", - " t_isi = time[k]\n", - " Spike = Spike + 1\n", - " Spike_time = np.append(Spike_time, time[k])\n", - "\n", - " return Spike, Spike_time, V\n", - "\n", - "# Set up time step and current\n", - "dt = 1\n", - "t = np.arange(0, 1000, dt)\n", - "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", - "\n", - "# Model integrate and fire neuron\n", - "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", - "\n", - "# Visualize\n", - "plot_IF(t, V,I,Spike_time)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "55785e26", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "def Euler_Integrate_and_Fire(I, time, dt):\n", - " \"\"\"\n", - " Args:\n", - " I: Input\n", - " time: time\n", - " dt: time-step\n", - " Returns:\n", - " Spike: Spike count\n", - " Spike_time: Spike times\n", - " V: membrane potential esitmated by the Euler method\n", - " \"\"\"\n", - "\n", - " Spike = 0\n", - " tau_m = 10\n", - " R_m = 10\n", - " t_isi = 0\n", - " V_reset = E_L = -75\n", - " n = len(time)\n", - " V = V_reset * np.ones(n)\n", - " V_th = -50\n", - " Spike_time = []\n", - "\n", - " for k in range(n-1):\n", - " dV = (-(V[k] - E_L) + R_m*I[k]) / tau_m\n", - " V[k+1] = V[k] + dt*dV\n", - "\n", - " # Discontinuity for Spike\n", - " if V[k] > V_th:\n", - " V[k] = 0\n", - " V[k+1] = V_reset\n", - " t_isi = time[k]\n", - " Spike = Spike + 1\n", - " Spike_time = np.append(Spike_time, time[k])\n", - "\n", - " return Spike, Spike_time, V\n", - "\n", - "# Set up time step and current\n", - "dt = 1\n", - "t = np.arange(0, 1000, dt)\n", - "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", - "\n", - "# Model integrate and fire neuron\n", - "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_IF(t, V,I,Spike_time)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ggAQjBCekEm7", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_LIF_and_Euler_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "id": "0d322dc9", - "metadata": { - "execution": {} - }, - "source": [ - "As electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the LIF neuron below. Note: this does not work on all browsers so just move on if you can't hear the audio." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cd8f60c2", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to visualize the LIF for sinusoidal input*\n", - "\n", - "dt = 1\n", - "t = np.arange(0, 1000, dt)\n", - "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", - "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", - "\n", - "plot_IF(t, V,I,Spike_time)\n", - "plt.show()\n", - "ipd.Audio(V, rate=1000/dt)" - ] - }, - { - "cell_type": "markdown", - "id": "L5o2liqz3bxi", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 3: Systems of differential equations\n", - "\n", - "\n", - "*Estimated timing to here from start of tutorial: 34 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "RkM3-RucX2c-", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 4: Systems of differential equations\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'A3Puozl9nEs'), ('Bilibili', 'BV1XV411s76a')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "x5wo7eFJkGBS", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Systems_of_differential_equations_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "dTy23IBPZxYG", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 3.1: Using Euler to approximate a simple system\n", - "\n", - "*Estimated timing to here from start of tutorial: 40 min*" - ] - }, - { - "cell_type": "markdown", - "id": "85df22b9", - "metadata": { - "execution": {} - }, - "source": [ - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "To get to grips with solving a system of differential equations using the Euler method, we will simplify the Wilson Cowan model, a set of equations that will be explored in more detail in the Dynamical Systems day.\n", - "Looking at systems of differential equations will also allow us to introduce the concept of a phase-plane plot which is a method of investigating how different processes interact.\n", - "\n", - "In the previous section we looked at the LIF model for a single neuron. We now model a collection of neurons using a differential equation which describes the firing rate of a population of neurons.\n", - "We will model the firing rate $r$ of two types of populations of neurons which interact, the excitation population firing rate $r_E$ and inhibition population firing rate $r_I$. These firing rates of neurons regulate each other by weighted connections $w$. The directed graph below illustrates this.\n", - "\n", - "Our system of differential equations is a linear version of the Wilson Cowan model. Consider the equations,\n", - "\n", - "\\begin{align}\n", - "\\tau_E \\frac{dr_E}{dt} &= w_{EE}r_E+w_{EI}r_I, \\\\\n", - "\\tau_I \\frac{dr_I}{dt} &= w_{IE}r_E+w_{II}r_I\n", - "\\end{align}\n", - "\n", - "$r_E(t)$ represents the average activation (or firing rate) of the excitatory population at time $t$, and $r_I(t)$ the activation (or firing rate) of the inhibitory population. The parameters $\\tau_E$ and $\\tau_I$ control the timescales of the dynamics of each population. Connection strengths are given by: $w_{EE}$ (E $\\rightarrow$ E), $w_{EI}$ (I $\\rightarrow$ E), $w_{IE}$ (E $\\rightarrow$ I), and $w_{II}$ (I $\\rightarrow$ I). The terms $w_{EI}$ and $w_{IE}$ represent connections from inhibitory to excitatory population and vice versa, respectively.\n", - "\n", - "To start we will further simplify the linear system of equations by setting $w_{EE}$ and $w_{II}$ to zero, we now have the equations\n", - "\n", - "\\begin{align}\n", - "\\tau_E \\frac{dr_E}{dt} &= w_{EI}r_I, \\\\\n", - "\\tau_I \\frac{dr_I}{dt} &= w_{IE}r_E, \\qquad (1)\n", - "\\end{align}\n", - "\n", - "where $\\tau_E=100$ and $\\tau_I=120$, no internal connection $w_{EE}=w_{II}=0$, and $w_{EI}=-1$ and $w_{IE}=1$,\n", - "with the initial conditions\n", - "\n", - "\\begin{align}\n", - "r_E(0)=30, \\\\\n", - "r_I(0)=20.\n", - "\\end{align}\n", - "\n", - "The solution can be approximated using the Euler method such that we have the difference equations:\n", - "\n", - "\\begin{align*}\n", - "\\frac{\\color{red}{r_E[k+1]}-\\color{green}{r_E[k]}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\frac{\\color{red}{r_I[k+1]}-\\color{green}{r_I[k]}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "\\end{align*}\n", - "where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t[k])$ and inhibition population $r_I(t[k])$ and $\\Delta t$ is the time-step.\n", - "\n", - "Re-arranging the equation such that all the known terms are on the right gives:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{r_E[k+1]}&=\\color{green}{r_E[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\color{red}{r_I[k+1]}&=\\color{green}{r_I[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "&\\text{ for } k=0, \\cdots , n-1,\\\\\\\\\n", - "r_E[0]&=30,\\\\\n", - "r_I[0]&=20.\\\\\n", - "\\end{align*}\n", - "\n", - "where $\\color{red}{r_E[k+1]}$ and $\\color{red}{r_I[k+1]}$ are unknown, $\\color{green}{r_E[k]} $ and $\\color{green}{r_I[k]} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$ and $\\color{blue}{\\tau_E}=100ms$ and $\\color{blue}{\\tau_I}=120ms$ are known parameters and $\\color{blue}{\\Delta t}=0.01ms$ is a chosen time-step.\n", - "
\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "0xhGLDhgdq1P", - "metadata": { - "execution": {} - }, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "067b5f6e", - "metadata": { - "execution": {} - }, - "source": [ - "### Coding Exercise 3.1: Euler on a Simple System\n", - "\n", - "Our difference equations are:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{r_E[k+1]}&=\\color{green}{r_E[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\color{red}{r_I[k+1]}&=\\color{green}{r_I[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "&\\text{ for } k=0, \\cdots , n-1,\\\\\\\\\n", - "r_E[0]&=30,\\\\\n", - "r_I[0]&=20.\\\\\n", - "\\end{align*}\n", - "\n", - "where $\\color{red}{r_E[k+1]}$ and $\\color{red}{r_I[k+1]}$ are unknown, $\\color{green}{r_E[k]} $ and $\\color{green}{r_I[k]} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$ and $\\color{blue}{\\tau_E}=100ms$ and $\\color{blue}{\\tau_I}=120ms$ are known parameters and $\\color{blue}{\\Delta t}=0.01ms$ is a chosen time-step.\n", - "__Code the difference equations to estimate $r_{E}$ and $r_{I}$__.\n", - "\n", - "Note that the equations have to estimated in the same `for` loop as they depend on each other." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e1514105", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "def Euler_Simple_Linear_System(t, dt):\n", - " \"\"\"\n", - " Args:\n", - " time: time\n", - " dt : time-step\n", - " Returns:\n", - " r_E : Excitation Firing Rate\n", - " r_I : Inhibition Firing Rate\n", - "\n", - " \"\"\"\n", - "\n", - " # Set up parameters\n", - " tau_E = 100\n", - " tau_I = 120\n", - " n = len(t)\n", - " r_I = np.zeros(n)\n", - " r_I[0] = 20\n", - " r_E = np.zeros(n)\n", - " r_E[0] = 30\n", - "\n", - " #######################################################################\n", - " ## TODO for students: calculate the estimate solutions of r_E and r_I at t[i+1]\n", - " ## Complete line of codes for dr_E and dr_I and remove\n", - " raise NotImplementedError(\"Student exercise: calculate the estimate solutions of r_E and r_I at t[i+1]\")\n", - " ########################################################################\n", - "\n", - " # Loop over time steps\n", - " for k in range(n-1):\n", - "\n", - " # Estimate r_e\n", - " dr_E = ...\n", - " r_E[k+1] = r_E[k] + dt*dr_E\n", - "\n", - " # Estimate r_i\n", - " dr_I = ...\n", - " r_I[k+1] = r_I[k] + dt*dr_I\n", - "\n", - " return r_E, r_I\n", - "\n", - "# Set up dt, t\n", - "dt = 0.1 # time-step\n", - "t = np.arange(0, 1000, dt)\n", - "\n", - "# Run Euler method\n", - "r_E, r_I = Euler_Simple_Linear_System(t, dt)\n", - "\n", - "# Visualize\n", - "plot_rErI(t, r_E, r_I)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "JnLfmtc5ZWXz", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "def Euler_Simple_Linear_System(t, dt):\n", - " \"\"\"\n", - " Args:\n", - " time: time\n", - " dt : time-step\n", - " Returns:\n", - " r_E : Excitation Firing Rate\n", - " r_I : Inhibition Firing Rate\n", - "\n", - " \"\"\"\n", - "\n", - " # Set up parameters\n", - " tau_E = 100\n", - " tau_I = 120\n", - " n = len(t)\n", - " r_I = np.zeros(n)\n", - " r_I[0] = 20\n", - " r_E = np.zeros(n)\n", - " r_E[0] = 30\n", - "\n", - " # Loop over time steps\n", - " for k in range(n-1):\n", - "\n", - " # Estimate r_e\n", - " dr_E = -r_I[k]/tau_E\n", - " r_E[k+1] = r_E[k] + dt*dr_E\n", - "\n", - " # Estimate r_i\n", - " dr_I = r_E[k]/tau_I\n", - " r_I[k+1] = r_I[k] + dt*dr_I\n", - "\n", - " return r_E, r_I\n", - "\n", - "# Set up dt, t\n", - "dt = 0.1 # time-step\n", - "t = np.arange(0, 1000, dt)\n", - "\n", - "# Run Euler method\n", - "r_E, r_I = Euler_Simple_Linear_System(t, dt)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_rErI(t, r_E, r_I)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ZGYMiHlAkHbV", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Euler_on_a_simple_system_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "id": "Fk-EvkAOImKR", - "metadata": { - "execution": {} - }, - "source": [ - "### Think! 3.1: Simple Euler solution to the Wilson Cowan model\n", - "\n", - "1. Is the simulation biologically plausible?\n", - "2. What is the effect of combined excitation and inhibition?\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "i3L1OGeUgZEe", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. The simulation is not biologically plausible as there are negative firing\n", - " rates but a mathematician could just say that it is firing rate relative to\n", - " baseline.\n", - " 2. Excitation and inhibition creates an oscillation.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "gyCVFYRHkIMH", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Simple_Euler_solution_to_the_Wilson_Cowan_model_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "id": "Cw-R4Yd4QCTH", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 3.2: Phase Plane Plot and Nullcline\n", - "\n", - "\n", - "*Estimated timing to here from start of tutorial: 50 min*\n", - "\n", - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "When there are two differential equations describing the interaction of two processes like excitation $r_{E}$ and inhibition $r_{I}$ that are dependent on each other they can be plotted as a function of each other, which is known as a phase plane plot. The phase plane plot can give insight give insight into the state of the system but more about that later in Neuromatch Academy.\n", - "\n", - "In the animated figure below, the panel of the left shows the excitation firing rate $r_E$ and the inhibition firing rate $r_I$ as a function of time. The panel on the right hand side is the phase plane plot which shows the inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$.\n", - "\n", - "An addition to the phase plane plot are the \"nullcline\". These lines plot when the rate of change $\\frac{d}{dt}$ of the variables is equal to $0$. We saw a variation of this for a single differential equation in the differential equation tutorial.\n", - "\n", - "As we have two differential equations we set $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0$ which gives the equations,\n", - "\n", - "\\begin{align}\n", - "0&= w_{EI}r_I, \\\\\n", - "0&= w_{IE}r_E,\\\\\n", - "\\end{align}\n", - "\n", - "these are a unique example as they are a vertical and horizontal line. Where the lines cross is the stable point which the $r_E(t)$ excitatory population and $r_I(t)$ the inhibitory population orbit around." - ] - }, - { - "cell_type": "markdown", - "id": "2ghDuf-YeG6z", - "metadata": { - "execution": {} - }, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "98f3c696", - "metadata": { - "execution": {} - }, - "source": [ - "### Think! 6.1: Discuss the Plots\n", - "\n", - "1. Which representation is more intuitive (and useful), the time plot or the phase plane plot?\n", - "2. Why do we only see one circle?\n", - "3. What do the quadrants represent?\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "036d3fb5", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute the code to plot the solution to the system\n", - "plot_rErI_Simple(t, r_E, r_I)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "Sfa1foAYhB13", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. Personal preference: both have their benefits\n", - " 2. The solution is stable.\n", - " 3. The quadrants are:\n", - " - Top right positive derivative for inhibition and negative for excitation (r_I increases, r_E decreases)\n", - " - top left negative derivative for both inhibition and excitation (r_I decrease, r_E decrease)\n", - " - bottom left negative derivative for inhibition and positive for excitation (r_I decrease, r_E increase)\n", - " - bottom right positive derivative for inhibition and excitation (r_I increase, r_E increase)\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "nDKgymrbkJJ9", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Discuss_the_Plots_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "id": "5a278386", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 3.3: Adding internal connections\n", - "\n", - "*Estimated timing to here from start of tutorial: 57 min*\n", - "\n", - "Building up the equations in the previous section we re-introduce internal connections $w_{EE}$, $w_{II}$. The two coupled differential equations, each representing the dynamics of the excitatory or inhibitory population are now:\n", - "\n", - "\\begin{align}\n", - "\\tau_E \\frac{dr_E}{dt} &=w_{EE}r_E +w_{EI}r_I, \\\\\n", - "\\tau_I \\frac{dr_I}{dt} &=w_{IE}r_E +w_{II}r_I ,\n", - "\\end{align}\n", - "\n", - "where $\\tau_E=100$ and $\\tau_I=120$, $w_{EE}=1$ and $w_{II}=-1$, and $w_{EI}=-5$ and $w_{IE}=0.6$,\n", - "with the initial conditions\n", - "\n", - "\\begin{align}\n", - "r_E(0)=30, \\\\\n", - "r_I(0)=20.\n", - "\\end{align}\n", - "\n", - "The solutions can be approximated using the Euler method such that the equations become:\n", - "\n", - "\\begin{align*}\n", - "\\frac{\\color{red}{rE_{k+1}}-\\color{green}{rE_k}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{EE}}\\color{green}{rE_k}+\\color{blue}{w_{EI}}\\color{green}{rI_k}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\frac{\\color{red}{rI_{k+1}}-\\color{green}{rI_k}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{II}}\\color{green}{rI_k}+\\color{blue}{w_{IE}}\\color{green}{rE_k}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "\\end{align*}\n", - "where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t_k)$ and inhibition population $r_I(t_K)$ and $\\Delta t$ is the time-step.\n", - "\n", - "Re-arranging the equation such that all the known terms are on the right gives:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{rE_{k+1}}&=\\color{green}{rE_k}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EE}}\\color{green}{rE_k}+\\color{blue}{w_{EI}}\\color{green}{rI_k}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\color{red}{rI_{k+1}}&=\\color{green}{rI_k}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{II}}\\color{green}{rI_k}+\\color{blue}{w_{IE}}\\color{green}{rE_k}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "&\\text{ for } k=0, \\cdots n-1,\\\\\\\\\n", - "rE_0&=30,\\\\\n", - "rI_0&=20.\\\\\n", - "\\end{align*}\n", - "\n", - "where $\\color{red}{rE_{k+1}}$ and $\\color{red}{rI_{k+1}}$ are unknown, $\\color{green}{rE_{k}} $ and $\\color{green}{rI_{k}} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$, $\\color{blue}{w_{EE}}=1$, $\\color{blue}{w_{II}}=-1$ and $\\color{blue}{\\tau_E}=100$ and $\\color{blue}{\\tau_I}=120$ are known parameters and $\\color{blue}{\\Delta t}=0.1$ is a chosen time-step." - ] - }, - { - "cell_type": "markdown", - "id": "byUkHQNsJDF-", - "metadata": { - "execution": {} - }, - "source": [ - "### Think! 3.3: Oscillations\n", - "\n", - "\n", - "The code below implements and visualizes the linear Wilson-Cowan model.\n", - "\n", - "1. What will happen to the oscillations if the time period is extended?\n", - "2. How would you control or predict the oscillations?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e00d88fb", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to visualize the Linear Willson-Cowan*\n", - "\n", - "def Euler_Linear_System_Matrix(t, dt, w_EE=1):\n", - " \"\"\"\n", - " Args:\n", - " time: time\n", - " dt: time-step\n", - " w_EE: Excitation to excitation weight\n", - " Returns:\n", - " r_E: Excitation Firing Rate\n", - " r_I: Inhibition Firing Rate\n", - " N_Er: Nullclines for drE/dt=0\n", - " N_Ir: Nullclines for drI/dt=0\n", - " \"\"\"\n", - "\n", - " tau_E = 100\n", - " tau_I = 120\n", - " n = len(t)\n", - " r_I = 20*np.ones(n)\n", - " r_E = 30*np.ones(n)\n", - " w_EI = -5\n", - " w_IE = 0.6\n", - " w_II = -1\n", - "\n", - " for k in range(n-1):\n", - "\n", - " # Calculate the derivatives of the E and I populations\n", - " drE = (w_EI*r_I[k] + w_EE*r_E[k]) / tau_E\n", - " r_E[k+1] = r_E[k] + dt*drE\n", - "\n", - " drI = (w_II*r_I[k] + w_IE*r_E[k]) / tau_I\n", - " r_I[k+1] = r_I[k] + dt*drI\n", - "\n", - "\n", - " N_Er = -w_EE / w_EI*r_E\n", - " N_Ir = -w_IE / w_II*r_E\n", - "\n", - " return r_E, r_I, N_Er, N_Ir\n", - "\n", - "\n", - "dt = 0.1 # time-step\n", - "t = np.arange(0, 1000, dt)\n", - "r_E, r_I, _, _ = Euler_Linear_System_Matrix(t, dt)\n", - "plot_rErI(t, r_E, r_I)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1ACUE5rriyyR", - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1. The oscillations start getting larger and larger which could in the end become uncontrollable\n", - "2. Looking at the shape of the spiral\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "z4gwxcvJkKKN", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Oscillations_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "id": "261b8f71", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 3.4 Phase Plane and Nullclines Part 2\n", - "\n", - "*Estimated timing to here from start of tutorial: 62 min*\n", - "\n", - "Like before, we have two differential equations so we can plot the results on a phase plane. We can also calculate the Nullclines when we set $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0$ which gives,\n", - "\\begin{align}\n", - "0&= w_{EE}r_E+w_{EI}r_I, \\\\\n", - "0&= w_{IE}r_E+w_{II}r_I,\n", - "\\end{align}\n", - "re-arranging as two lines\n", - "\\begin{align}\n", - "r_I&= -\\frac{w_{EE}}{w_{EI}}r_E, \\\\\n", - "r_I&= -\\frac{w_{IE}}{w_{II}}r_E,\n", - "\\end{align}\n", - "which crosses at the stable point.\n", - "\n", - "The panel on the left shows excitation firing rate $r_E$ and inhibition firing rate $r_I$ as a function of time. On the right side the phase plane plot shows inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$ with the Nullclines for $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0.$" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ca5f555", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown *Run this cell to visualize the phase plane*\n", - "dt = 0.1 # time-step\n", - "t = np.arange(0, 1000, dt)\n", - "r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt)\n", - "plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)" - ] - }, - { - "cell_type": "markdown", - "id": "4d220e96", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 3.4: A small change changes everything\n", - "\n", - "We will illustrate that even changing one parameter in a system of differential equations can have a large impact on the solutions of the excitation firing rate $r_E$ and inhibition firing rate $r_I$.\n", - "Interact with the widget below to change the size of $w_{EE}$.\n", - "\n", - "Take note of:\n", - "1. How the solution changes for positive and negative of $w_{EE}$. Pay close attention to the axis.\n", - "2. How would you maintain a stable oscillation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "42724aca", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " w_EE=widgets.FloatSlider(1, min=-1., max=2., step=.1,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(w_EE):\n", - " dt = 0.1 # time-step\n", - " t = np.arange(0,1000,dt)\n", - " r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt, w_EE)\n", - " plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "sH4Bpv7lkLFe", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Small_change_changes_everything_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "1268fd80", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Summary\n", - "\n", - "*Estimated timing of tutorial: 70 minutes*\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cEcFr2llcl-l", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 5: Summary\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '5UmGgboSc40'), ('Bilibili', 'BV1wM4y1g78M')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "uzv5CG0AkLwR", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Summary_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "-JZJWqTnitQx", - "metadata": { - "execution": {} - }, - "source": [ - "Using pretty much the formula for the slope of a line, the solution of differential equation can be estimated with reasonable accuracy.\n", - "This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution." - ] - }, - { - "cell_type": "markdown", - "id": "a307a8fa", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "## Links to Neuromatch Computational Neuroscience Days\n", - "\n", - "Differential equations turn up in a number of different Neuromatch days:\n", - "* The LIF model is discussed in more details in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", - "* Drift Diffusion model which is a differential equation for decision making is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", - "* Phase-plane plots are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", - "* The Wilson-Cowan model is discussed in [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", - "\n", - "## References\n", - "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", - "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", - "\n", - "\n", - "## Bibliography\n", - "1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.\n", - "2. Strogatz, S. (2014). Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2nd edition\n", - "\n", - "### Supplemental Popular Reading List\n", - "1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.\n", - "2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.\n", - "\n", - "### Popular Podcast\n", - "1. Strogatz, S. (Host). (2020), Joy of X https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine" - ] - }, - { - "cell_type": "markdown", - "id": "_bnKhPLwobY9", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Bonus" - ] - }, - { - "cell_type": "markdown", - "id": "3c9b3f6e", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "## Bonus Section 1: The 4th Order Runge-Kutta method\n", - "\n", - "Another popular numerical method to estimate the solution of differential equations of the general form:\n", - "\n", - "\\begin{align}\n", - "\\frac{d}{dt}y=f(t,y)\n", - "\\end{align}\n", - "\n", - "is the 4th Order Runge Kutta:\n", - "\\begin{align}\n", - "k_1 &= f(t_k,y_k)\\\\\n", - "k_2 &= f(t_k+\\frac{\\Delta t}{2},y_k+\\frac{\\Delta t}{2}k_1)\\\\\n", - "k_3 &= f(t_k+\\frac{\\Delta t}{2},y_k+\\frac{\\Delta t}{2}k_2)\\\\\n", - "k_4 &= f(t_k+\\Delta t,y_k+\\Delta tk_3)\\\\\n", - "y_{k+1} &= y_{k}+\\frac{\\Delta t}{6}(k_1+2k_2+2k_3+k_4)\\\\\n", - "\\end{align}\n", - "\n", - "for $k=0,1,\\cdots,n-1$,\n", - "which is more accurate than the Euler method.\n", - "\n", - "Given the population differential equation\n", - "\\begin{align*}\n", - "\\frac{d}{dt}\\,p(t) &= 0.3 p(t),\\\\\n", - "p(t_0=1)&=e^{0.3}\\quad \\text{Initial Condition},\n", - "\\end{align*}\n", - "\n", - "the 4th Runge Kutta difference equation is\n", - "\n", - "\\begin{align*}\n", - "k_1&=0.3\\color{green}{p[k]},\\\\\n", - "k_2&=0.3(\\color{green}{p[k]}+\\frac{\\Delta t}{2}k_1),\\\\\n", - "k_3&=0.3(\\color{green}{p[k]}+\\frac{\\Delta t}{2}k_2),\\\\\n", - "k_4&=0.3(\\color{green}{p[k]}+k_3),\\\\\n", - "\\color{red}{p_{k[k]}}&=\\color{green}{p[k]}+\\frac{\\color{blue}{\\Delta t}}{6}( \\color{green}{k_1+2k_2+2k_3+k_4})\\\\\n", - "\\end{align*}\n", - "\n", - "for $k=0,1,\\cdots 4$ to estimate the population for $\\Delta t=0.5$.\n", - "\n", - "The code is implemented below. Note how much more accurate the 4th Order Runge Kutta (yellow) is compared to the Euler Method (blue). The 4th order Runge Kutta is of order 4 which means that if you half the time-step $\\Delta t$ the error decreases by a factor of $\\Delta t^4$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7b936428", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to show the difference between the Runge Kutta Method and the Euler Method*\n", - "\n", - "\n", - "def RK4(dt=0.5):\n", - " t=np.arange(1, 5+dt/2, dt)\n", - " t_fine=np.arange(1, 5+0.1/2, 0.1)\n", - " n = len(t)\n", - " p = np.ones(n)\n", - " pRK4 = np.ones(n)\n", - " p[0] = np.exp(0.3*t[0])\n", - " pRK4[0] = np.exp(0.3*t[0])# Initial Condition\n", - "\n", - " with plt.xkcd():\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')\n", - "\n", - " for i in range(n-1):\n", - " dp = dt*0.3*p[i]\n", - " p[i+1] = p[i] + dp # Euler\n", - " k1 = 0.3*(pRK4[i])\n", - " k2 = 0.3*(pRK4[i] + dt/2*k1)\n", - " k3 = 0.3*(pRK4[i] + dt/2*k2)\n", - " k4 = 0.3*(pRK4[i] + dt*k3)\n", - " pRK4[i+1] = pRK4[i] + dt/6 *(k1 + 2*k2 + 2*k3 + k4)\n", - "\n", - " plt.plot(t_fine, np.exp(0.3*t_fine), label='Exact')\n", - " plt.plot(t, p,':ro', label='Euler Estimate')\n", - " plt.plot(t, pRK4,':co', label='4th Order Runge Kutta Estimate')\n", - "\n", - " plt.ylabel('Population (millions)')\n", - " plt.legend()\n", - " plt.xlabel('Time (years)')\n", - " plt.show()\n", - "\n", - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " dt=widgets.FloatSlider(0.5, min=.1, max=4., step=.1,\n", - " layout=my_layout)\n", - ")\n", - "def Pop_widget(dt):\n", - " RK4(dt)" - ] - }, - { - "cell_type": "markdown", - "id": "W7ziQulJL6Kl", - "metadata": { - "execution": {} - }, - "source": [ - "**Bonus Reference 1: A full course on numerical methods in Python**\n", - "\n", - "For a full course on Numerical Methods for differential Equations you can look [here](https://github.com/john-s-butler-dit/Numerical-Analysis-Python)." - ] - }, - { - "cell_type": "markdown", - "id": "jXpJytN7dgYn", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "## Bonus Section 2: Neural oscillations are a start toward understanding brain activity rather than the end\n", - "\n", - "The differential equations we have discussed above are all to simulate neuronal processes, another way differential equations can be used is to motivate experimental findings.\n", - "\n", - "A great deal of experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrain to a stimulus.\n", - "An issue with these studies is that there is not a consistent definition of what constitutes an oscillation. Right now it is a bit of I know one when I see one problem.\n", - "\n", - "\n", - "In an essay from May 2021 in PLOS Biology by Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally when studying oscillations. In the essay they propose that instead of thinking about the brain we should look at this question from the mathematical side, to motivate what can be defined as an oscillation.\n", - "\n", - "To do this they used Stuart–Landau equations, which is a system of differential equations\n", - "\\begin{align}\n", - "\\frac{dx}{dt} &= \\lambda x-\\omega y -\\gamma (x^2+y^2)x+s\\\\\n", - "\\frac{dy}{dt} &= \\lambda y+\\omega x -\\gamma (x^2+y^2)y\n", - "\\end{align}\n", - "\n", - "where $s$ is input to the system, and $\\lambda$, $\\omega$ and $\\gamma$ are parameters.\n", - "\n", - "The Stuart–Landau equations are a well described system of non-linear differential equations that generate oscillations. For their purpose, Dowling and Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.\n", - "In their paper, using the Stuart–Landau equations, they outline\n", - "* \"What is an oscillator?\"\n", - "* \"What an oscillator is not\"\n", - "* \"Not all that oscillates is an oscillator\"\n", - "* \"Not all oscillators are alike.\"\n", - "\n", - "The Euler form of the Stuart–Landau system of equations is:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{x_{k+1}}&=\\color{green}{x_k}+\\color{blue}{\\Delta t}\\big(\\lambda \\color{green}{x_k}-\\omega \\color{green}{y_k} -\\gamma (\\color{green}{x_k}^2+\\color{green}{y_k}^2)\\color{green}{x_k}+s\\big),\\\\\n", - "\\color{red}{y_{k+1}}&=\\color{green}{y_k}+\\color{blue}{\\Delta t}\\big(\\lambda \\color{green}{y_k}+\\omega \\color{green}{x_k} -\\gamma (\\color{green}{x_k}^2+\\color{green}{y_k}^2)\\color{green}{y_k} \\big),\\\\\n", - "&\\text{ for } k=0, \\cdots n-1,\\\\\n", - "x_0&=1,\\\\\n", - "y_0&=1,\\\\\n", - "\\end{align*}\n", - "\n", - "with $ \\Delta t=0.1/1000$ ms.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b9e30cc", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Helper functions\n", - "def plot_Stuart_Landa(t, x, y, s):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " x : x\n", - " y : y\n", - " s : input\n", - " Returns:\n", - " figure with two panels\n", - " top panel: Input as a function of time\n", - " bottom panel: x\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(14, 4))\n", - " gs = gridspec.GridSpec(2, 2, height_ratios=[1, 4], width_ratios=[4, 1])\n", - "\n", - " # PLOT OF INPUT\n", - " plt.subplot(gs[0])\n", - " plt.ylabel(r'$s$')\n", - " plt.plot(t, s, 'g')\n", - " #plt.ylim((2,4))\n", - "\n", - " # PLOT OF ACTIVITY\n", - " plt.subplot(gs[2])\n", - " plt.plot(t ,x)\n", - " plt.ylabel(r'x')\n", - " plt.xlabel(r't')\n", - " plt.subplot(gs[3])\n", - " plt.plot(x,y)\n", - " plt.plot(x[0], y[0],'go')\n", - " plt.xlabel(r'x')\n", - " plt.ylabel(r'y')\n", - " plt.show()\n", - "\n", - "\n", - "def Euler_Stuart_Landau(s,time,dt,lamba=0.1,gamma=1,k=25):\n", - " \"\"\"\n", - " Args:\n", - " I: Input\n", - " time: time\n", - " dt: time-step\n", - " \"\"\"\n", - "\n", - " n = len(time)\n", - " omega = 4 * 2*np.pi\n", - " x = np.zeros(n)\n", - " y = np.zeros(n)\n", - " x[0] = 1\n", - " y[0] = 1\n", - "\n", - " for i in range(n-1):\n", - " dx = lamba*x[i] - omega*y[i] - gamma*(x[i]*x[i] + y[i]*y[i])*x[i] + k*s[i]\n", - " x[i+1] = x[i] + dt*dx\n", - " dy = lamba*y[i] + omega*x[i] - gamma*(x[i]*x[i] + y[i]*y[i])*y[i]\n", - " y[i+1] = y[i] + dt*dy\n", - "\n", - " return x, y" - ] - }, - { - "cell_type": "markdown", - "id": "H5inj2EKJyqN", - "metadata": { - "execution": {} - }, - "source": [ - "### Bonus 2.1: What is an Oscillator?\n", - "Doelling & Assaneo (2021), using the Stuart–Landau system, show different possible states of an oscillator by manipulating the $\\lambda$ term in the equation.\n", - "From the paper: \"this qualitative change in behavior takes place at λ = 0: For λ < 0, the system decays to a stable equilibrium, while for λ > 0, it keeps oscillating.\"\n", - "\n", - "This illustrates an oscillations does not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\\alpha$ band oscillations in EEG the oscillations come and go." - ] - }, - { - "cell_type": "markdown", - "id": "5MyIOK9w_qjd", - "metadata": { - "execution": {} - }, - "source": [ - "#### Interactive Demo Bonus 2.1: Oscillator\n", - "\n", - "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency $\\omega$ set to $4\\times 2\\pi$, $\\gamma=1$ and $k=25$ over 3 seconds. The input to the system $s(t)$ is plotted in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$. You can manipulate $\\lambda$ to see how the oscillations change." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "LAnBGRU5crfG", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "dt=0.1/1000\n", - "t=np.arange(0, 3, dt)\n", - "\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " lamda=widgets.FloatSlider(1, min=-1., max=5., step=0.5,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(lamda):\n", - " s=np.zeros(len(t))\n", - " x,y=Euler_Stuart_Landau(s,t,dt,lamda)\n", - " plot_Stuart_Landa(t, x, y, s)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "PNtk7NRomDuQ", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Oscillator_Bonus_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "q2OMPLOoKGtr", - "metadata": { - "execution": {} - }, - "source": [ - "### Bonus 2.2 : Not all oscillators are alike" - ] - }, - { - "cell_type": "markdown", - "id": "NLOVdwN5_ypj", - "metadata": { - "execution": {} - }, - "source": [ - "#### Interactive Demo Bonus 2: Stuart-Landau System\n", - "\n", - "The plot below shows estimated solution of the Stuart–Landau system with a base frequency of 4Hz by stetting $\\omega$ to $4\\times 2\\pi$, $\\lambda=1$, $\\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\\sin(freq 2\\pi t) $ in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$.\n", - "\n", - "You can manipulate the frequency $freq$ of the input to see how the oscillations change and for frequencies $freq$ further and further from the base frequency of 4Hz the oscillations breaks down.\n", - "\n", - "This shoes that if you have an oscillating input into an oscillator with it does not have to respond by oscillating about could even breakdown. Hence the frequency of the input oscillation is important to the system.\n", - "So if you flash something at 50Hz, for example, the visual system might not follow the signal but that does not mean the visual system is not an oscillator it might just be the wrong frequency." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "XPuVG5f4clxt", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "dt=0.1/1000\n", - "t=np.arange(0, 3, dt)\n", - "\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " freq=widgets.FloatSlider(4, min=0.5, max=10., step=0.5,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(freq):\n", - " s = np.sin(freq * 2*np.pi * t)\n", - "\n", - " x, y = Euler_Stuart_Landau(s, t, dt, lamba=1, gamma=.1, k=50)\n", - " plot_Stuart_Landa(t, x, y, s)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "EC5oefXvmOtd", - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Stuart_Landau_System_Bonus_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "1cWYP3SNL1Mh", - "metadata": { - "execution": {} - }, - "source": [ - "**Bonus Reference 2**:\n", - "\n", - "Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234. doi: [10.1371/journal.pbio.3001234](https://doi.org/10.1371/journal.pbio.3001234)\n" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [ - "1cWYP3SNL1Mh" - ], - "include_colab_link": true, - "name": "W0D4_Tutorial3", - "provenance": [], - "toc_visible": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "id": "08842220", + "metadata": { + "execution": {}, + "id": "08842220" + }, + "source": [ + "# Tutorial 3: Numerical Methods\n", + "\n", + "**Week 0, Day 3: Calculus**\n", + "\n", + "**Content creators:** John S Butler, Arvind Kumar with help from Harvey McCone\n", + "\n", + "**Content reviewers:** Swapnil Kumar, Matthew McCann\n", + "\n", + "**Production editors:** Matthew McCann, Ella Batty" + ] + }, + { + "cell_type": "markdown", + "id": "v0oVvJHEMouH", + "metadata": { + "execution": {}, + "id": "v0oVvJHEMouH" + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "id": "7d80998b", + "metadata": { + "execution": {}, + "id": "7d80998b" + }, + "source": [ + "---\n", + "# Tutorial Objectives\n", + "\n", + "*Estimated timing of tutorial: 70 minutes*\n", + "\n", + "While a great deal of neuroscience can be described by mathematics, a great deal of the mathematics used cannot be solved exactly. This might seem very odd that you can writing something in mathematics that cannot be immediately solved but that is the beauty and mystery of mathematics. To side step this issue we use Numerical Methods to estimate the solution.\n", + "\n", + "In this tutorial, we will look at the Euler method to estimate the solution of a few different differential equations: the population equation, the Leaky Integrate and Fire model and a simplified version of the Wilson-Cowan model which is a system of differential equations.\n", + "\n", + "**Steps:**\n", + "- Code the Euler estimate of the Population Equation;\n", + "- Investigate the impact of time step on the error of the numerical solution;\n", + "- Code the Euler estimate of the Leaky Integrate and Fire model for a constant input;\n", + "- Visualize and listen to the response of the integrate for different inputs;\n", + "- Apply the Euler method to estimate the solution of a system of differential equations." + ] + }, + { + "cell_type": "markdown", + "id": "bwMygnJgMUp7", + "metadata": { + "execution": {}, + "id": "bwMygnJgMUp7" + }, + "source": [ + "---\n", + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "BrceoWcPfYlB", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "BrceoWcPfYlB" + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D4_T3\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5985801", + "metadata": { + "execution": {}, + "id": "a5985801" + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d40abcd8", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "d40abcd8" + }, + "outputs": [], + "source": [ + "# @title Figure Settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import IPython.display as ipd\n", + "from matplotlib import gridspec\n", + "import ipywidgets as widgets # interactive display\n", + "from ipywidgets import Label\n", + "%config InlineBackend.figure_format = 'retina'\n", + "# use NMA plot style\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", + "my_layout = widgets.Layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hwhtoyMMi7qj", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "hwhtoyMMi7qj" + }, + "outputs": [], + "source": [ + "# @title Plotting Functions\n", + "\n", + "time = np.arange(0, 1, 0.01)\n", + "\n", + "def plot_slope(dt):\n", + " \"\"\"\n", + " Args:\n", + " dt : time-step\n", + " Returns:\n", + " A figure of an exponential, the slope of the exponential and the derivative exponential\n", + " \"\"\"\n", + "\n", + " t = np.arange(0, 5+0.1/2, 0.1)\n", + "\n", + " with plt.xkcd():\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " # Exponential\n", + " p = np.exp(0.3*t)\n", + " plt.plot(t, p, label='y')\n", + " # slope\n", + " plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1+dt))],':og',label=r'$\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$')\n", + " # derivative\n", + " plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1))+dt*0.3*np.exp(0.3*(1))],'-k',label=r'$\\frac{dy}{dt}$')\n", + " plt.legend()\n", + " plt.plot(1+dt, np.exp(0.3*(1+dt)), 'og')\n", + " plt.ylabel('y')\n", + " plt.xlabel('t')\n", + " plt.show()\n", + "\n", + "\n", + "\n", + "def plot_StepEuler(dt):\n", + " \"\"\"\n", + " Args:\n", + " dt : time-step\n", + " Returns:\n", + " A figure of one step of the Euler method for an exponential growth function\n", + " \"\"\"\n", + "\n", + " t=np.arange(0, 1 + dt + 0.1 / 2, 0.1)\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(6,4))\n", + " p=np.exp(0.3*t)\n", + " plt.plot(t,p)\n", + " plt.plot([1,],[np.exp(0.3*1)],'og',label='Known')\n", + " plt.plot([1,1+dt],[np.exp(0.3*1),np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)],':g',label=r'Euler')\n", + " plt.plot(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),'or',label=r'Estimate $p_1$')\n", + " plt.plot(1+dt,p[-1],'bo',label=r'Exact $p(t_1)$')\n", + " plt.vlines(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),p[-1],colors='r', linestyles='dashed',label=r'Error $e_1$')\n", + " plt.text(1+dt+0.1,(np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)+p[-1])/2,r'$e_1$')\n", + " plt.legend()\n", + " plt.ylabel('Population (millions)')\n", + " plt.xlabel('time(years)')\n", + " plt.show()\n", + "\n", + "def visualize_population_approx(t, p):\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')\n", + "\n", + " plt.plot(t, p,':o', label='Euler Estimate')\n", + " plt.vlines(t, p, np.exp(0.3*t),\n", + " colors='r', linestyles='dashed', label=r'Error $e_k$')\n", + "\n", + " plt.ylabel('Population (millions)')\n", + " plt.legend()\n", + " plt.xlabel('Time (years)')\n", + " plt.show()\n", + "\n", + "## LIF PLOT\n", + "def plot_IF(t, V, I, Spike_time):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " V : membrane Voltage\n", + " I : Input\n", + " Spike_time : Spike_times\n", + " Returns:\n", + " figure with three panels\n", + " top panel: Input as a function of time\n", + " middle panel: membrane potential as a function of time\n", + " bottom panel: Raster plot\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(12,4))\n", + " gs = gridspec.GridSpec(3, 1, height_ratios=[1, 4, 1])\n", + " # PLOT OF INPUT\n", + " plt.subplot(gs[0])\n", + " plt.ylabel(r'$I_e(nA)$')\n", + " plt.yticks(rotation=45)\n", + " plt.plot(t,I,'g')\n", + " #plt.ylim((2,4))\n", + " plt.xlim((-50,1000))\n", + " # PLOT OF ACTIVITY\n", + " plt.subplot(gs[1])\n", + " plt.plot(t,V,':')\n", + " plt.xlim((-50,1000))\n", + " plt.ylabel(r'$V(t)$(mV)')\n", + " # PLOT OF SPIKES\n", + " plt.subplot(gs[2])\n", + " plt.ylabel(r'Spike')\n", + " plt.yticks([])\n", + " plt.scatter(Spike_time,1*np.ones(len(Spike_time)), color=\"grey\", marker=\".\")\n", + " plt.xlim((-50,1000))\n", + " plt.xlabel('time(ms)')\n", + " plt.show()\n", + "\n", + "def plot_rErI(t, r_E, r_I):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " r_E : excitation rate\n", + " r_I : inhibition rate\n", + "\n", + " Returns:\n", + " figure of r_I and r_E as a function of time\n", + "\n", + " \"\"\"\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(6,4))\n", + " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", + " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", + " plt.xlabel('time(ms)')\n", + " plt.legend()\n", + " plt.ylabel('Firing Rate (Hz)')\n", + " plt.show()\n", + "\n", + "\n", + "def plot_rErI_Simple(t, r_E, r_I):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " r_E : excitation rate\n", + " r_I : inhibition rate\n", + "\n", + " Returns:\n", + " figure with two panels\n", + " left panel: r_I and r_E as a function of time\n", + " right panel: r_I as a function of r_E with Nullclines\n", + "\n", + " \"\"\"\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(12,4))\n", + " gs = gridspec.GridSpec(1, 2)\n", + " # LEFT PANEL\n", + " plt.subplot(gs[0])\n", + " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", + " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", + " plt.xlabel('time(ms)')\n", + " plt.legend()\n", + " plt.ylabel('Firing Rate (Hz)')\n", + " # RIGHT PANEL\n", + " plt.subplot(gs[1])\n", + " plt.plot(r_E,r_I,'k:')\n", + " plt.plot(r_E[0],r_I[0],'go')\n", + "\n", + " plt.hlines(0,np.min(r_E),np.max(r_E),linestyles=\"dashed\",color='b',label=r'$\\frac{d}{dt}r_E=0$')\n", + " plt.vlines(0,np.min(r_I),np.max(r_I),linestyles=\"dashed\",color='r',label=r'$\\frac{d}{dt}r_I=0$')\n", + "\n", + " plt.legend(loc='upper left')\n", + "\n", + " plt.xlabel(r'$r_E$')\n", + " plt.ylabel(r'$r_I$')\n", + " plt.show()\n", + "\n", + "def plot_rErI_Matrix(t, r_E, r_I, Null_rE, Null_rI):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " r_E : excitation firing rate\n", + " r_I : inhibition firing rate\n", + " Null_rE: Nullclines excitation firing rate\n", + " Null_rI: Nullclines inhibition firing rate\n", + " Returns:\n", + " figure with two panels\n", + " left panel: r_I and r_E as a function of time\n", + " right panel: r_I as a function of r_E with Nullclines\n", + "\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(12,4))\n", + " gs = gridspec.GridSpec(1, 2)\n", + " plt.subplot(gs[0])\n", + " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", + " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", + " plt.xlabel('time(ms)')\n", + " plt.ylabel('Firing Rate (Hz)')\n", + " plt.legend()\n", + " plt.subplot(gs[1])\n", + " plt.plot(r_E,r_I,'k:')\n", + " plt.plot(r_E[0],r_I[0],'go')\n", + "\n", + " plt.plot(r_E,Null_rE,':',color='b',label=r'$\\frac{d}{dt}r_E=0$')\n", + " plt.plot(r_E,Null_rI,':',color='r',label=r'$\\frac{d}{dt}r_I=0$')\n", + " plt.legend(loc='best')\n", + " plt.xlabel(r'$r_E$')\n", + " plt.ylabel(r'$r_I$')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "SXJPvBQzik6K", + "metadata": { + "execution": {}, + "id": "SXJPvBQzik6K" + }, + "source": [ + "---\n", + "# Section 1: Intro to the Euler method using the population differential equation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "DYKXWaR7iwl9", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "DYKXWaR7iwl9" + }, + "outputs": [], + "source": [ + "# @title Video 1: Intro to numerical methods for differential equations\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'NI8c80TA7IQ'), ('Bilibili', 'BV1gh411Y7gV')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "M3V5Kju3j7Ie", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "M3V5Kju3j7Ie" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Intro_to_numerical_methods_for_differential_equations_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "R9B9JKT-PvEn", + "metadata": { + "execution": {}, + "id": "R9B9JKT-PvEn" + }, + "source": [ + "## Section 1.1: Slope of line as approximation of derivative\n", + "\n", + "*Estimated timing to here from start of tutorial: 8 min*\n", + "\n", + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "The Euler method is one of the straight forward and elegant methods to approximate a differential. It was designed by [Leonhard Euler](https://en.wikipedia.org/wiki/Leonhard_Euler) (1707-1783).\n", + "Simply put we just replace the derivative in the differential equation by the formula for a line and re-arrange.\n", + "\n", + "The slope is the rate of change between two points. The formula for the slope of a line between the points $(t_0,y(t_0))$ and $(t_1,y(t_1))$ is given by:\n", + "$$ m=\\frac{y(t_1)-y(t_0)}{t_1-t_0}, $$\n", + "which can be written as\n", + "$$ m=\\frac{y_1-y_0}{t_1-t_0}, $$\n", + "or as\n", + "$$ m=\\frac{\\Delta y_0}{\\Delta t_0}, $$\n", + "where $\\Delta y_0=y_1-y_0$ and $\\Delta t_0=t_1-t_0$ or in words as\n", + "$$ m=\\frac{\\text{ Change in y} }{\\text{Change in t}}. $$\n", + "The slope can be used as an approximation of the derivative such that\n", + "$$ \\frac{d}{dt}y(t)\\approx \\frac{y(t_0+\\Delta t)-y(t_0)}{t_0+\\Delta t-t_0}=\\frac{y(t_0+dt)-y(t_0)}{\\Delta t}$$\n", + "where $\\Delta t$ is a time-step.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "b667c36a", + "metadata": { + "execution": {}, + "id": "b667c36a" + }, + "source": [ + "### Interactive Demo 1.1: Slope of a Line\n", + "\n", + "The plot below shows a function $y(t)$ in blue, its exact derivative $ \\frac{d}{dt}y(t)$ at $t_0=1$ in black. The approximate derivative is calculated using the slope formula $=\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$ for different time-steps sizes $\\Delta t$ in green.\n", + "\n", + "Interact with the widget to see how time-step impacts the slope's accuracy, which is the derivative's estimate.\n", + "- How does the size of $\\Delta t$ affect the approximation?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea4a4f78", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ea4a4f78" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + "\n", + " dt=widgets.FloatSlider(1, min=0., max=4., step=.1,\n", + " layout=my_layout)\n", + "\n", + ")\n", + "\n", + "def Pop_widget(dt):\n", + " plot_slope(dt)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "gTgFRiMhc1qm", + "metadata": { + "execution": {}, + "id": "gTgFRiMhc1qm" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " The larger the time-step $dt$, the worse job the formula of the slope of a\n", + " line does to approximate the derivative.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "FZ1SZPQwj9H4", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "FZ1SZPQwj9H4" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Slope_of_a_line_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "8f92ce83", + "metadata": { + "execution": {}, + "id": "8f92ce83" + }, + "source": [ + "## Section 1.2: Euler error for a single step\n", + "\n", + "*Estimated timing to here from start of tutorial: 12 min*\n", + "\n", + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "Linking with the previous tutorial, we will use the population differential equation to get an intuitive feel of using the Euler method to approximate the solution of a differential equation, as it has an exact solution with no discontinuities.\n", + "\n", + "The population differential equation is given by\n", + "\\begin{align*}\n", + "\\\\\n", + "\\frac{d}{dt}\\,p(t) &= \\alpha p(t)\\\\\\\\\n", + "p(0)&=p_0 \\quad \\text{Initial Condition}\n", + "\\end{align*}\n", + "\n", + "where $p(t)$ is the population at time $t$ and $\\alpha$ is a parameter representing birth rate. The exact solution of the differential equation is\n", + "$$ p(t)=p_0e^{\\alpha t}.$$\n", + "\n", + "To numerically estimate the population differential equation we replace the derivate with the slope of the line to get the discrete (not continuous) equation:\n", + "\n", + "\\begin{align*}\n", + "\\\\\n", + "\\frac{p_1-p_0}{t_1-t_0} &= \\alpha p_0\\\\\\\\\n", + "p(0)&=p_0 \\quad \\text{Initial Condition}\n", + "\\end{align*}\n", + "\n", + "where $p_1$ is the estimate of $p(t_1)$. Let $\\Delta t=t_1-t_0$ be the time-step and re-arrange the equation gives\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{p_1}&=\\color{green}{p_0}+\\color{blue}{\\Delta t} (\\color{blue}{\\alpha} \\color{green}{p_0})\n", + "\\end{align*}\n", + "\n", + "where $\\color{red}{p_1}$ is the unknown future, $\\color{green}{p_0}$ is the known current population, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter and $\\color{blue}{\\alpha}$ is the given birth rate parameter.\n", + "Another way to read the re-arranged discrete equation is:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{\\text{\"Future Population\"}}&=\\color{green}{\\text{ \"Current Population\"}}+\\color{blue}{\\text{ time-step}} \\times (\\color{blue}{\\text{ \"Birth rate}}\\times \\color{green}{\\text{ Current Population\"}}).\n", + "\\end{align*}\n", + "\n", + "So pretty much, we can use the current population and add a bit of the dynamics of the differential equation to predict the future. We will make millions... But wait there is always an error.\n", + "\n", + "The solution of the Euler method $p_1$ is an estimate of the exact solution $p(t_1)$ at $t_1$ which means there is a bit of error $e_1$ which gives the equation\n", + "\\begin{align*}\n", + "p(t_1)&=p_1+e_1\\\\\\\\\n", + "\\text{Rearranging}\\\\\\\\\n", + "e_1&=p(t_1)-p_1,\\\\\n", + "\\text{Error}&=\\text{Exact-Estimate}.\n", + "\\end{align*}\n", + "\n", + "Most of the time we do not know the exact answer $p(t_1)$ and hence the size of the error $e_1$ but for the population equation we have the exact solution $ p(t)=p_0e^{\\alpha}.$\n", + "\n", + "This means we can explore what the error looks like.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "vGxg-2k6k8hM", + "metadata": { + "execution": {}, + "id": "vGxg-2k6k8hM" + }, + "source": [ + "### Interactive Demo 1.2: Euler error for a single step\n", + "\n", + "Interact with the widget to see how the time-step $\\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.\n", + "\n", + "1. What happens to the estimate $p_1$ as the time-step $\\Delta t$ increases?\n", + "2. Is there a relationship between the size of $\\Delta t$ and $e_1$?\n", + "3. How would you improve the error $e_1$?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e72c600b", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "e72c600b" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " dt=widgets.FloatSlider(1.5, min=0., max=4., step=.1,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(dt):\n", + " plot_StepEuler(dt)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "EoA90TrDdVWv", + "metadata": { + "execution": {}, + "id": "EoA90TrDdVWv" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. The larger the time-step $\\Delta t$ the more the estimate $p_1$ deviates\n", + " from the exact $p(t_1)$.\n", + "\n", + " 2. There is a linear relationship between $\\Delta t$ and $e_1$: double the time-step double the error.\n", + "\n", + " 3. Make more shorter time-steps\n", + "\"\"\";" + ] + }, + { + "cell_type": "markdown", + "id": "e78fc157", + "metadata": { + "execution": {}, + "id": "e78fc157" + }, + "source": [ + "The error $e_1$ from one time-step is known as the __local error__. For the Euler method, the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $\\mathcal{O}(\\Delta t)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "qXS6Fkkkj-xD", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "qXS6Fkkkj-xD" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Euler_error_of_single_step_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "1yr4hKV7R0JJ", + "metadata": { + "execution": {}, + "id": "1yr4hKV7R0JJ" + }, + "source": [ + "## Section 1.3: Taking more steps\n", + "\n", + "*Estimated timing to here from start of tutorial: 16 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "MZcqv_DoRsWJ", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "MZcqv_DoRsWJ" + }, + "outputs": [], + "source": [ + "# @title Video 2: Taking more steps\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'cGsXHllGMVo'), ('Bilibili', 'BV135411T7QF')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "W8HbORAlkAWj", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "W8HbORAlkAWj" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Taking_more_steps_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "972a55c4", + "metadata": { + "execution": {}, + "id": "972a55c4" + }, + "source": [ + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "In the above exercise we saw that by increasing the time-step $\\Delta t$ the error between the estimate $p_1$ and the exact $p(t_1)$ increased. The largest error in the example above was for $\\Delta t=4$, meaning the first time point was at 1 year and the second was at 5 years (as 5 - 1 = 4).\n", + "\n", + "To decrease the error, we can divide the interval $[1, 5]$ into more steps using a smaller $\\Delta t$.\n", + "\n", + "In the plot below we use $\\Delta t=1$, which divides the interval into four segments\n", + "$$n=\\frac{5-1}{1}=4,$$\n", + "giving\n", + "$$ t_0=t[0]=1, \\ t_1=t[1]=2, \\ t_2=t[2]=3, \\ t_3=t[3]=4 \\ \\text{ and } t_4=t[4]=5. $$\n", + "This can be written as\n", + "$$ t[k]=1+k\\times1, \\text{ for } k=0,\\cdots 4, $$\n", + "and more generally as\n", + "$$ t[k]=t[k]+k\\times \\Delta t, \\text{ for } k=0,\\cdots n, $$\n", + "where $n$ is the number of steps.\n", + "\n", + "Using the Euler method, the continuous population differential equation is written as a series of discrete difference equations of the form:\n", + "\\begin{align*}\n", + "\\color{red}{p[k+1]}&=\\color{green}{p[k]}+\\color{blue}{\\Delta t} (\\color{blue}{\\alpha} \\color{green}{p[k]})\\\\\n", + "&\\text{for } k=0,1,\\cdots n-1\n", + "\\end{align*}\n", + "where $\\color{red}{p[k+1]}$ is the unknown estimate of the future population at $t[k+1]$, $\\color{green}{p[k]}$ is the known current population estimate at $t_k$, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter and $\\color{blue}{\\alpha}$ is the given birth rate parameter.\n", + "\n", + "\n", + "The Euler method can be applied to all first order differential equations of the form\n", + "\\begin{align*}\n", + "\\frac{d}{dt}y(t)&=f(t,y(t)),\\\\\n", + "y(t_{0})&=y_0,\\\\\n", + "\\end{align*}\n", + "on an interval $[a,b]$.\n", + "\n", + "Using the Euler method all differential equation can be written as discrete difference equations:\n", + "\\begin{align*}\n", + "\\frac{\\color{red}{y[k+1]}-\\color{green}{y[k]}}{\\color{blue}{\\Delta t}}&=f(\\color{blue}{t[k]},\\color{green}{y[k]}),\\\\\n", + "\\text{Re-arranging}\\\\\n", + "\\color{red}{y[k+1]}&=\\color{green}{y[k]}+\\color{blue}{\\Delta t}f(\\color{blue}{t[k]},\\color{green}{y[k]}),\\\\\n", + "&\\text{ for } k=0, \\cdots n-1,\\\\\n", + "y[0]&=\\color{green}{y_0},\\\\\n", + "\\end{align*}\n", + "where $\\color{red}{y[k+1]}$ is the unknown estimate at $t[k+1]$, $\\color{green}{y[k]}$ is the known at $t_k$, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter, $\\color{blue}{t[k]}$ is the time point and $f$ is the right hand side of the differential equation.\n", + "The discrete time steps are:\n", + "\\begin{align*}\n", + "\\color{blue}{t[k]}&=\\color{blue}{t[0]}+\\color{blue}{k}\\color{blue}{\\Delta t},\\\\\n", + "n&=\\frac{b-a}{\\Delta t}\\\\\n", + "&\\text{ for } k=0, \\cdots n.\\\\\n", + "\\end{align*}\n", + "Once again this can be simply put into words:\n", + "\\begin{align*}\n", + "\\color{red}{\\text{ \"Future\" }}&=\\color{green}{\\text{ \"Current Info\" }}+\\color{blue}{\\text{ time-step } }\\times\\text{ \"Dynamics of the system which is a function of } \\color{blue}{ \\text{ time }} \\text{ and }\\color{green}{ \\text{ Current Info.\" }}\n", + "\\end{align*}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d84f02ed", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "d84f02ed" + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to visualize time steps*\n", + "dt =1\n", + "t =np.arange(1, 5+dt/2, dt) # Time from 1 to 5 years in 0.1 steps\n", + "with plt.xkcd():\n", + " fig = plt.figure(figsize=(8, 2))\n", + " plt.plot(t, 0*t, ':o')\n", + " plt.plot(t[0] ,0*t[0],':go',label='Initial Condition')\n", + " plt.text(t[0]-0.1, 0*t[0]-0.03, r'$t_0$')\n", + " plt.text(t[1]-0.1, 0*t[0]-0.03, r'$t_1$')\n", + " plt.text(t[2]-0.1, 0*t[0]-0.03, r'$t_2$')\n", + " plt.text(t[3]-0.1, 0*t[0]-0.03, r'$t_3$')\n", + " plt.text(t[4]-0.1, 0*t[0]-0.03,r'$t_4$')\n", + " plt.text(t[0]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", + " plt.text(t[1]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", + " plt.text(t[2]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", + " plt.text(t[3]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", + " plt.yticks([])#plt.ylabel('Population (millions)')\n", + " plt.xlabel('time(years)')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "279ea532", + "metadata": { + "execution": {}, + "id": "279ea532" + }, + "source": [ + "### Coding Exercise 1.3: Step, step, step\n", + "\n", + "Given the population differential equation:\n", + "\\begin{align*}\n", + "\\frac{d}{dt}\\,p(t) &= 0.3 p(t),\\\\\n", + "\\end{align*}\n", + "\n", + "and the initial condition:\n", + "\n", + "\\begin{align*}\n", + "p(t_0=1)&=e^{0.3},\n", + "\\end{align*}\n", + "\n", + "code the difference equation:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{p[k+1]}&=\\color{green}{p[k]}+\\color{blue}{\\Delta t} (\\color{blue}{0.3} \\color{green}{p[k]}),\\\\\n", + "\\color{green}{p[0]}&=e^{0.3},\\quad \\text{Initial Condition,}\\\\\n", + "&\\text{for } k=0,1,\\cdots 4,\\\\\n", + "\\end{align*}\n", + "\n", + "to estimate the population on the interval $[1,5]$ with a time-step $\\Delta t=1$, denoted by `dt` in code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b872e66", + "metadata": { + "execution": {}, + "id": "8b872e66" + }, + "outputs": [], + "source": [ + "# Time step\n", + "dt = 1\n", + "\n", + "# Make time range from 1 to 5 years with step size dt\n", + "t = np.arange(1, 5+dt/2, dt)\n", + "\n", + "# Get number of steps\n", + "n = len(t)\n", + "\n", + "# Initialize p array\n", + "p = np.zeros(n)\n", + "p[0] = np.exp(0.3*t[0]) # initial condition\n", + "\n", + "# Loop over steps\n", + "for k in range(n-1):\n", + "\n", + " ########################################################################\n", + " ## TODO for students\n", + " ## Complete line of code and remove\n", + " raise NotImplementedError(\"Student exercise: calculate the population step for each time point\")\n", + " ########################################################################\n", + "\n", + " # Calculate the population step\n", + " p[k+1] = ...\n", + "\n", + "# Visualize\n", + "visualize_population_approx(t, p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "LHAKHn_5UzLQ", + "metadata": { + "execution": {}, + "id": "LHAKHn_5UzLQ" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Time step\n", + "dt = 1\n", + "\n", + "# Make time range from 1 to 5 years with step size dt\n", + "t = np.arange(1, 5+dt/2, dt)\n", + "\n", + "# Get number of steps\n", + "n = len(t)\n", + "\n", + "# Initialize p array\n", + "p = np.zeros(n)\n", + "p[0] = np.exp(0.3*t[0]) # initial condition\n", + "\n", + "# Loop over steps\n", + "for k in range(n-1):\n", + "\n", + " # Calculate the population step\n", + " p[k+1] = p[k] + dt * 0.3 * p[k]\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " visualize_population_approx(t, p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Yvl3OBBPkCQr", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "Yvl3OBBPkCQr" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Step_step_step_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "id": "d9301447", + "metadata": { + "execution": {}, + "id": "d9301447" + }, + "source": [ + "The error is smaller for 4-time steps than taking one large time step from 1 to 5 but does note that the error is increasing for each step. This is known as __global error__ so the further in time you want to predict, the larger the error.\n", + "\n", + "You can read the theorems [here](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)." + ] + }, + { + "cell_type": "markdown", + "id": "b419bf50", + "metadata": { + "execution": {}, + "id": "b419bf50" + }, + "source": [ + "---\n", + "# Section 2: Euler method for the leaky integrate and fire\n", + "\n", + "\n", + "*Estimated timing to here from start of tutorial: 26 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "JOA7EzauxIV6", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "JOA7EzauxIV6" + }, + "outputs": [], + "source": [ + "# @title Video 3: Leaky integrate and fire\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'T85OcgY7xjo'), ('Bilibili', 'BV1k5411T7by')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "GeAbFXiSkDEd", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "GeAbFXiSkDEd" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Leaky_integrate_and_fire_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "Zjro4hXwxU8O", + "metadata": { + "execution": {}, + "id": "Zjro4hXwxU8O" + }, + "source": [ + "
\n", + " Click here for text recap of video \n", + "\n", + "The Leaky Integrate and Fire (LIF) differential equation is:\n", + "\n", + "\\begin{align}\n", + "\\frac{dV}{dt} = \\frac{-(V-E_L) + R_mI(t)}{\\tau_m}\\,\n", + "\\end{align}\n", + "\n", + "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is leak resistance and $I(t)$ is the external input current.\n", + "\n", + "The solution of the LIF can be estimated by applying the Euler method to give the difference equation:\n", + "\n", + "\\begin{align}\n", + "\\frac{\\color{red}{V[k+1]}-\\color{green}{V[k]}}{\\color{blue}{\\Delta t}}=\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", + "\\end{align}\n", + "where $V[k]$ is the estimate of the membrane potential at time point $t[k]$.\n", + "Re-arranging the equation such that all the known terms are on the right gives:\n", + "\n", + "\\begin{align}\n", + "\\color{red}{V[k+1]}=\\color{green}{V[k]}+\\color{blue}{\\Delta t}\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", + "\\text{for } k=0\\cdots n-1,\n", + "\\end{align}\n", + "\n", + "where $\\color{red}{V[k+1]}$ is the unknown membrane potential at $t[k+1]$, $\\color{green}{V[k]} $ is known membrane potential, $\\color{blue}{E_L}$, $\\color{blue}{R_m}$ and $\\color{blue}{\\tau_m}$ are known parameters, $\\color{blue}{\\Delta t}$ is a chosen time-step and $I(t_k)$ is a function for an external input current." + ] + }, + { + "cell_type": "markdown", + "id": "1efb9d89", + "metadata": { + "execution": {}, + "id": "1efb9d89" + }, + "source": [ + "## Coding Exercise 2: LIF and Euler\n", + "Code the difference equation for the LIF:\n", + "\n", + "\\begin{align}\n", + "\\color{red}{V[k+1]}=\\color{green}{V[k]}+\\color{blue}{\\Delta t}\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", + "\\text{for } k=0\\cdots n-1,\n", + "\\end{align}\n", + "\n", + "with the given parameters set as:\n", + "* `V_reset = -75,`\n", + "* `E_L = -75,`\n", + "* `tau_m = 10,`\n", + "* `R_m = 10.`\n", + "\n", + "We will then visualize the result.\n", + "The figure has three panels:\n", + "* the top panel is the sinusoidal input, $I$,\n", + "* the middle panel is the estimate membrane potential $V_k$. To illustrate a spike, $V_k$ is set to $0$ and then reset,\n", + "* the bottom panel is the raster plot with each dot indicating a spike." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e7d29bb", + "metadata": { + "execution": {}, + "id": "4e7d29bb" + }, + "outputs": [], + "source": [ + "def Euler_Integrate_and_Fire(I, time, dt):\n", + " \"\"\"\n", + " Args:\n", + " I: Input\n", + " time: time\n", + " dt: time-step\n", + " Returns:\n", + " Spike: Spike count\n", + " Spike_time: Spike times\n", + " V: membrane potential esitmated by the Euler method\n", + " \"\"\"\n", + "\n", + " Spike = 0\n", + " tau_m = 10\n", + " R_m = 10\n", + " t_isi = 0\n", + " V_reset = E_L = -75\n", + " n = len(time)\n", + " V = V_reset * np.ones(n)\n", + " V_th = -50\n", + " Spike_time = []\n", + "\n", + " for k in range(n-1):\n", + " #######################################################################\n", + " ## TODO for students: calculate the estimate solution of V at t[i+1]\n", + " ## Complete line of codes for dV and remove\n", + " ## Run the code in Section 5.1 or 5.2 to see the output!\n", + " raise NotImplementedError(\"Student exercise: calculate the estimate solution of V at t[i+1]\")\n", + " ########################################################################\n", + "\n", + " dV = ...\n", + " V[k+1] = V[k] + dt*dV\n", + "\n", + " # Discontinuity for Spike\n", + " if V[k] > V_th:\n", + " V[k] = 0\n", + " V[k+1] = V_reset\n", + " t_isi = time[k]\n", + " Spike = Spike + 1\n", + " Spike_time = np.append(Spike_time, time[k])\n", + "\n", + " return Spike, Spike_time, V\n", + "\n", + "# Set up time step and current\n", + "dt = 1\n", + "t = np.arange(0, 1000, dt)\n", + "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", + "\n", + "# Model integrate and fire neuron\n", + "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", + "\n", + "# Visualize\n", + "plot_IF(t, V,I,Spike_time)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55785e26", + "metadata": { + "execution": {}, + "id": "55785e26" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "def Euler_Integrate_and_Fire(I, time, dt):\n", + " \"\"\"\n", + " Args:\n", + " I: Input\n", + " time: time\n", + " dt: time-step\n", + " Returns:\n", + " Spike: Spike count\n", + " Spike_time: Spike times\n", + " V: membrane potential esitmated by the Euler method\n", + " \"\"\"\n", + "\n", + " Spike = 0\n", + " tau_m = 10\n", + " R_m = 10\n", + " t_isi = 0\n", + " V_reset = E_L = -75\n", + " n = len(time)\n", + " V = V_reset * np.ones(n)\n", + " V_th = -50\n", + " Spike_time = []\n", + "\n", + " for k in range(n-1):\n", + " dV = (-(V[k] - E_L) + R_m*I[k]) / tau_m\n", + " V[k+1] = V[k] + dt*dV\n", + "\n", + " # Discontinuity for Spike\n", + " if V[k] > V_th:\n", + " V[k] = 0\n", + " V[k+1] = V_reset\n", + " t_isi = time[k]\n", + " Spike = Spike + 1\n", + " Spike_time = np.append(Spike_time, time[k])\n", + "\n", + " return Spike, Spike_time, V\n", + "\n", + "# Set up time step and current\n", + "dt = 1\n", + "t = np.arange(0, 1000, dt)\n", + "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", + "\n", + "# Model integrate and fire neuron\n", + "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_IF(t, V,I,Spike_time)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ggAQjBCekEm7", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ggAQjBCekEm7" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_LIF_and_Euler_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "id": "0d322dc9", + "metadata": { + "execution": {}, + "id": "0d322dc9" + }, + "source": [ + "As electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the LIF neuron below. Note: this does not work on all browsers so just move on if you can't hear the audio." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd8f60c2", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "cd8f60c2" + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to visualize the LIF for sinusoidal input*\n", + "\n", + "dt = 1\n", + "t = np.arange(0, 1000, dt)\n", + "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", + "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", + "\n", + "plot_IF(t, V,I,Spike_time)\n", + "plt.show()\n", + "ipd.Audio(V, rate=1000/dt)" + ] + }, + { + "cell_type": "markdown", + "id": "L5o2liqz3bxi", + "metadata": { + "execution": {}, + "id": "L5o2liqz3bxi" + }, + "source": [ + "---\n", + "# Section 3: Systems of differential equations\n", + "\n", + "\n", + "*Estimated timing to here from start of tutorial: 34 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "RkM3-RucX2c-", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "RkM3-RucX2c-" + }, + "outputs": [], + "source": [ + "# @title Video 4: Systems of differential equations\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'A3Puozl9nEs'), ('Bilibili', 'BV1XV411s76a')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "x5wo7eFJkGBS", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "x5wo7eFJkGBS" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Systems_of_differential_equations_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "dTy23IBPZxYG", + "metadata": { + "execution": {}, + "id": "dTy23IBPZxYG" + }, + "source": [ + "## Section 3.1: Using Euler to approximate a simple system\n", + "\n", + "*Estimated timing to here from start of tutorial: 40 min*" + ] + }, + { + "cell_type": "markdown", + "id": "85df22b9", + "metadata": { + "execution": {}, + "id": "85df22b9" + }, + "source": [ + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "To get to grips with solving a system of differential equations using the Euler method, we will simplify the Wilson Cowan model, a set of equations that will be explored in more detail in the Dynamical Systems day.\n", + "Looking at systems of differential equations will also allow us to introduce the concept of a phase-plane plot which is a method of investigating how different processes interact.\n", + "\n", + "In the previous section we looked at the LIF model for a single neuron. We now model a collection of neurons using a differential equation which describes the firing rate of a population of neurons.\n", + "We will model the firing rate $r$ of two types of populations of neurons which interact, the excitation population firing rate $r_E$ and inhibition population firing rate $r_I$. These firing rates of neurons regulate each other by weighted connections $w$. The directed graph below illustrates this.\n", + "\n", + "Our system of differential equations is a linear version of the Wilson Cowan model. Consider the equations,\n", + "\n", + "\\begin{align}\n", + "\\tau_E \\frac{dr_E}{dt} &= w_{EE}r_E+w_{EI}r_I, \\\\\n", + "\\tau_I \\frac{dr_I}{dt} &= w_{IE}r_E+w_{II}r_I\n", + "\\end{align}\n", + "\n", + "$r_E(t)$ represents the average activation (or firing rate) of the excitatory population at time $t$, and $r_I(t)$ the activation (or firing rate) of the inhibitory population. The parameters $\\tau_E$ and $\\tau_I$ control the timescales of the dynamics of each population. Connection strengths are given by: $w_{EE}$ (E $\\rightarrow$ E), $w_{EI}$ (I $\\rightarrow$ E), $w_{IE}$ (E $\\rightarrow$ I), and $w_{II}$ (I $\\rightarrow$ I). The terms $w_{EI}$ and $w_{IE}$ represent connections from inhibitory to excitatory population and vice versa, respectively.\n", + "\n", + "To start we will further simplify the linear system of equations by setting $w_{EE}$ and $w_{II}$ to zero, we now have the equations\n", + "\n", + "\\begin{align}\n", + "\\tau_E \\frac{dr_E}{dt} &= w_{EI}r_I, \\\\\n", + "\\tau_I \\frac{dr_I}{dt} &= w_{IE}r_E, \\qquad (1)\n", + "\\end{align}\n", + "\n", + "where $\\tau_E=100$ and $\\tau_I=120$, no internal connection $w_{EE}=w_{II}=0$, and $w_{EI}=-1$ and $w_{IE}=1$,\n", + "with the initial conditions\n", + "\n", + "\\begin{align}\n", + "r_E(0)=30, \\\\\n", + "r_I(0)=20.\n", + "\\end{align}\n", + "\n", + "The solution can be approximated using the Euler method such that we have the difference equations:\n", + "\n", + "\\begin{align*}\n", + "\\frac{\\color{red}{r_E[k+1]}-\\color{green}{r_E[k]}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\frac{\\color{red}{r_I[k+1]}-\\color{green}{r_I[k]}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "\\end{align*}\n", + "where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t[k])$ and inhibition population $r_I(t[k])$ and $\\Delta t$ is the time-step.\n", + "\n", + "Re-arranging the equation such that all the known terms are on the right gives:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{r_E[k+1]}&=\\color{green}{r_E[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\color{red}{r_I[k+1]}&=\\color{green}{r_I[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "&\\text{ for } k=0, \\cdots , n-1,\\\\\\\\\n", + "r_E[0]&=30,\\\\\n", + "r_I[0]&=20.\\\\\n", + "\\end{align*}\n", + "\n", + "where $\\color{red}{r_E[k+1]}$ and $\\color{red}{r_I[k+1]}$ are unknown, $\\color{green}{r_E[k]} $ and $\\color{green}{r_I[k]} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$ and $\\color{blue}{\\tau_E}=100ms$ and $\\color{blue}{\\tau_I}=120ms$ are known parameters and $\\color{blue}{\\Delta t}=0.01ms$ is a chosen time-step.\n", + "
\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "0xhGLDhgdq1P", + "metadata": { + "execution": {}, + "id": "0xhGLDhgdq1P" + }, + "source": [ + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "067b5f6e", + "metadata": { + "execution": {}, + "id": "067b5f6e" + }, + "source": [ + "### Coding Exercise 3.1: Euler on a Simple System\n", + "\n", + "Our difference equations are:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{r_E[k+1]}&=\\color{green}{r_E[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\color{red}{r_I[k+1]}&=\\color{green}{r_I[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "&\\text{ for } k=0, \\cdots , n-1,\\\\\\\\\n", + "r_E[0]&=30,\\\\\n", + "r_I[0]&=20.\\\\\n", + "\\end{align*}\n", + "\n", + "where $\\color{red}{r_E[k+1]}$ and $\\color{red}{r_I[k+1]}$ are unknown, $\\color{green}{r_E[k]} $ and $\\color{green}{r_I[k]} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$ and $\\color{blue}{\\tau_E}=100ms$ and $\\color{blue}{\\tau_I}=120ms$ are known parameters and $\\color{blue}{\\Delta t}=0.01ms$ is a chosen time-step.\n", + "__Code the difference equations to estimate $r_{E}$ and $r_{I}$__.\n", + "\n", + "Note that the equations have to estimated in the same `for` loop as they depend on each other." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1514105", + "metadata": { + "execution": {}, + "id": "e1514105" + }, + "outputs": [], + "source": [ + "def Euler_Simple_Linear_System(t, dt):\n", + " \"\"\"\n", + " Args:\n", + " time: time\n", + " dt : time-step\n", + " Returns:\n", + " r_E : Excitation Firing Rate\n", + " r_I : Inhibition Firing Rate\n", + "\n", + " \"\"\"\n", + "\n", + " # Set up parameters\n", + " tau_E = 100\n", + " tau_I = 120\n", + " n = len(t)\n", + " r_I = np.zeros(n)\n", + " r_I[0] = 20\n", + " r_E = np.zeros(n)\n", + " r_E[0] = 30\n", + "\n", + " #######################################################################\n", + " ## TODO for students: calculate the estimate solutions of r_E and r_I at t[i+1]\n", + " ## Complete line of codes for dr_E and dr_I and remove\n", + " raise NotImplementedError(\"Student exercise: calculate the estimate solutions of r_E and r_I at t[i+1]\")\n", + " ########################################################################\n", + "\n", + " # Loop over time steps\n", + " for k in range(n-1):\n", + "\n", + " # Estimate r_e\n", + " dr_E = ...\n", + " r_E[k+1] = r_E[k] + dt*dr_E\n", + "\n", + " # Estimate r_i\n", + " dr_I = ...\n", + " r_I[k+1] = r_I[k] + dt*dr_I\n", + "\n", + " return r_E, r_I\n", + "\n", + "# Set up dt, t\n", + "dt = 0.1 # time-step\n", + "t = np.arange(0, 1000, dt)\n", + "\n", + "# Run Euler method\n", + "r_E, r_I = Euler_Simple_Linear_System(t, dt)\n", + "\n", + "# Visualize\n", + "plot_rErI(t, r_E, r_I)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "JnLfmtc5ZWXz", + "metadata": { + "execution": {}, + "id": "JnLfmtc5ZWXz" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "def Euler_Simple_Linear_System(t, dt):\n", + " \"\"\"\n", + " Args:\n", + " time: time\n", + " dt : time-step\n", + " Returns:\n", + " r_E : Excitation Firing Rate\n", + " r_I : Inhibition Firing Rate\n", + "\n", + " \"\"\"\n", + "\n", + " # Set up parameters\n", + " tau_E = 100\n", + " tau_I = 120\n", + " n = len(t)\n", + " r_I = np.zeros(n)\n", + " r_I[0] = 20\n", + " r_E = np.zeros(n)\n", + " r_E[0] = 30\n", + "\n", + " # Loop over time steps\n", + " for k in range(n-1):\n", + "\n", + " # Estimate r_e\n", + " dr_E = -r_I[k]/tau_E\n", + " r_E[k+1] = r_E[k] + dt*dr_E\n", + "\n", + " # Estimate r_i\n", + " dr_I = r_E[k]/tau_I\n", + " r_I[k+1] = r_I[k] + dt*dr_I\n", + "\n", + " return r_E, r_I\n", + "\n", + "# Set up dt, t\n", + "dt = 0.1 # time-step\n", + "t = np.arange(0, 1000, dt)\n", + "\n", + "# Run Euler method\n", + "r_E, r_I = Euler_Simple_Linear_System(t, dt)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_rErI(t, r_E, r_I)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ZGYMiHlAkHbV", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ZGYMiHlAkHbV" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Euler_on_a_simple_system_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "id": "Fk-EvkAOImKR", + "metadata": { + "execution": {}, + "id": "Fk-EvkAOImKR" + }, + "source": [ + "### Think! 3.1: Simple Euler solution to the Wilson Cowan model\n", + "\n", + "1. Is the simulation biologically plausible?\n", + "2. What is the effect of combined excitation and inhibition?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "i3L1OGeUgZEe", + "metadata": { + "execution": {}, + "id": "i3L1OGeUgZEe" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. The simulation is not biologically plausible as there are negative firing\n", + " rates but a mathematician could just say that it is firing rate relative to\n", + " baseline.\n", + " 2. Excitation and inhibition creates an oscillation.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "gyCVFYRHkIMH", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "gyCVFYRHkIMH" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Simple_Euler_solution_to_the_Wilson_Cowan_model_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "id": "Cw-R4Yd4QCTH", + "metadata": { + "execution": {}, + "id": "Cw-R4Yd4QCTH" + }, + "source": [ + "## Section 3.2: Phase Plane Plot and Nullcline\n", + "\n", + "\n", + "*Estimated timing to here from start of tutorial: 50 min*\n", + "\n", + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "When there are two differential equations describing the interaction of two processes like excitation $r_{E}$ and inhibition $r_{I}$ that are dependent on each other they can be plotted as a function of each other, which is known as a phase plane plot. The phase plane plot can give insight give insight into the state of the system but more about that later in Neuromatch Academy.\n", + "\n", + "In the animated figure below, the panel of the left shows the excitation firing rate $r_E$ and the inhibition firing rate $r_I$ as a function of time. The panel on the right hand side is the phase plane plot which shows the inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$.\n", + "\n", + "An addition to the phase plane plot are the \"nullcline\". These lines plot when the rate of change $\\frac{d}{dt}$ of the variables is equal to $0$. We saw a variation of this for a single differential equation in the differential equation tutorial.\n", + "\n", + "As we have two differential equations we set $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0$ which gives the equations,\n", + "\n", + "\\begin{align}\n", + "0&= w_{EI}r_I, \\\\\n", + "0&= w_{IE}r_E,\\\\\n", + "\\end{align}\n", + "\n", + "these are a unique example as they are a vertical and horizontal line. Where the lines cross is the stable point which the $r_E(t)$ excitatory population and $r_I(t)$ the inhibitory population orbit around." + ] + }, + { + "cell_type": "markdown", + "id": "2ghDuf-YeG6z", + "metadata": { + "execution": {}, + "id": "2ghDuf-YeG6z" + }, + "source": [ + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "98f3c696", + "metadata": { + "execution": {}, + "id": "98f3c696" + }, + "source": [ + "### Think! 6.1: Discuss the Plots\n", + "\n", + "1. Which representation is more intuitive (and useful), the time plot or the phase plane plot?\n", + "2. Why do we only see one circle?\n", + "3. What do the quadrants represent?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "036d3fb5", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "036d3fb5" + }, + "outputs": [], + "source": [ + "# @markdown Execute the code to plot the solution to the system\n", + "plot_rErI_Simple(t, r_E, r_I)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Sfa1foAYhB13", + "metadata": { + "execution": {}, + "id": "Sfa1foAYhB13" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. Personal preference: both have their benefits\n", + " 2. The solution is stable.\n", + " 3. The quadrants are:\n", + " - Top right positive derivative for inhibition and negative for excitation (r_I increases, r_E decreases)\n", + " - top left negative derivative for both inhibition and excitation (r_I decrease, r_E decrease)\n", + " - bottom left negative derivative for inhibition and positive for excitation (r_I decrease, r_E increase)\n", + " - bottom right positive derivative for inhibition and excitation (r_I increase, r_E increase)\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "nDKgymrbkJJ9", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "nDKgymrbkJJ9" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Discuss_the_Plots_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "id": "5a278386", + "metadata": { + "execution": {}, + "id": "5a278386" + }, + "source": [ + "## Section 3.3: Adding internal connections\n", + "\n", + "*Estimated timing to here from start of tutorial: 57 min*\n", + "\n", + "Building up the equations in the previous section we re-introduce internal connections $w_{EE}$, $w_{II}$. The two coupled differential equations, each representing the dynamics of the excitatory or inhibitory population are now:\n", + "\n", + "\\begin{align}\n", + "\\tau_E \\frac{dr_E}{dt} &=w_{EE}r_E +w_{EI}r_I, \\\\\n", + "\\tau_I \\frac{dr_I}{dt} &=w_{IE}r_E +w_{II}r_I ,\n", + "\\end{align}\n", + "\n", + "where $\\tau_E=100$ and $\\tau_I=120$, $w_{EE}=1$ and $w_{II}=-1$, and $w_{EI}=-5$ and $w_{IE}=0.6$,\n", + "with the initial conditions\n", + "\n", + "\\begin{align}\n", + "r_E(0)=30, \\\\\n", + "r_I(0)=20.\n", + "\\end{align}\n", + "\n", + "The solutions can be approximated using the Euler method such that the equations become:\n", + "\n", + "\\begin{align*}\n", + "\\frac{\\color{red}{rE_{k+1}}-\\color{green}{rE_k}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{EE}}\\color{green}{rE_k}+\\color{blue}{w_{EI}}\\color{green}{rI_k}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\frac{\\color{red}{rI_{k+1}}-\\color{green}{rI_k}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{II}}\\color{green}{rI_k}+\\color{blue}{w_{IE}}\\color{green}{rE_k}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "\\end{align*}\n", + "where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t_k)$ and inhibition population $r_I(t_K)$ and $\\Delta t$ is the time-step.\n", + "\n", + "Re-arranging the equation such that all the known terms are on the right gives:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{rE_{k+1}}&=\\color{green}{rE_k}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EE}}\\color{green}{rE_k}+\\color{blue}{w_{EI}}\\color{green}{rI_k}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\color{red}{rI_{k+1}}&=\\color{green}{rI_k}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{II}}\\color{green}{rI_k}+\\color{blue}{w_{IE}}\\color{green}{rE_k}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "&\\text{ for } k=0, \\cdots n-1,\\\\\\\\\n", + "rE_0&=30,\\\\\n", + "rI_0&=20.\\\\\n", + "\\end{align*}\n", + "\n", + "where $\\color{red}{rE_{k+1}}$ and $\\color{red}{rI_{k+1}}$ are unknown, $\\color{green}{rE_{k}} $ and $\\color{green}{rI_{k}} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$, $\\color{blue}{w_{EE}}=1$, $\\color{blue}{w_{II}}=-1$ and $\\color{blue}{\\tau_E}=100$ and $\\color{blue}{\\tau_I}=120$ are known parameters and $\\color{blue}{\\Delta t}=0.1$ is a chosen time-step." + ] + }, + { + "cell_type": "markdown", + "id": "byUkHQNsJDF-", + "metadata": { + "execution": {}, + "id": "byUkHQNsJDF-" + }, + "source": [ + "### Think! 3.3: Oscillations\n", + "\n", + "\n", + "The code below implements and visualizes the linear Wilson-Cowan model.\n", + "\n", + "1. What will happen to the oscillations if the time period is extended?\n", + "2. How would you control or predict the oscillations?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e00d88fb", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "e00d88fb" + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to visualize the Linear Willson-Cowan*\n", + "\n", + "def Euler_Linear_System_Matrix(t, dt, w_EE=1):\n", + " \"\"\"\n", + " Args:\n", + " time: time\n", + " dt: time-step\n", + " w_EE: Excitation to excitation weight\n", + " Returns:\n", + " r_E: Excitation Firing Rate\n", + " r_I: Inhibition Firing Rate\n", + " N_Er: Nullclines for drE/dt=0\n", + " N_Ir: Nullclines for drI/dt=0\n", + " \"\"\"\n", + "\n", + " tau_E = 100\n", + " tau_I = 120\n", + " n = len(t)\n", + " r_I = 20*np.ones(n)\n", + " r_E = 30*np.ones(n)\n", + " w_EI = -5\n", + " w_IE = 0.6\n", + " w_II = -1\n", + "\n", + " for k in range(n-1):\n", + "\n", + " # Calculate the derivatives of the E and I populations\n", + " drE = (w_EI*r_I[k] + w_EE*r_E[k]) / tau_E\n", + " r_E[k+1] = r_E[k] + dt*drE\n", + "\n", + " drI = (w_II*r_I[k] + w_IE*r_E[k]) / tau_I\n", + " r_I[k+1] = r_I[k] + dt*drI\n", + "\n", + "\n", + " N_Er = -w_EE / w_EI*r_E\n", + " N_Ir = -w_IE / w_II*r_E\n", + "\n", + " return r_E, r_I, N_Er, N_Ir\n", + "\n", + "\n", + "dt = 0.1 # time-step\n", + "t = np.arange(0, 1000, dt)\n", + "r_E, r_I, _, _ = Euler_Linear_System_Matrix(t, dt)\n", + "plot_rErI(t, r_E, r_I)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ACUE5rriyyR", + "metadata": { + "execution": {}, + "id": "1ACUE5rriyyR" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1. The oscillations start getting larger and larger which could in the end become uncontrollable\n", + "2. Looking at the shape of the spiral\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "z4gwxcvJkKKN", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "z4gwxcvJkKKN" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Oscillations_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "id": "261b8f71", + "metadata": { + "execution": {}, + "id": "261b8f71" + }, + "source": [ + "## Section 3.4 Phase Plane and Nullclines Part 2\n", + "\n", + "*Estimated timing to here from start of tutorial: 62 min*\n", + "\n", + "Like before, we have two differential equations so we can plot the results on a phase plane. We can also calculate the Nullclines when we set $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0$ which gives,\n", + "\\begin{align}\n", + "0&= w_{EE}r_E+w_{EI}r_I, \\\\\n", + "0&= w_{IE}r_E+w_{II}r_I,\n", + "\\end{align}\n", + "re-arranging as two lines\n", + "\\begin{align}\n", + "r_I&= -\\frac{w_{EE}}{w_{EI}}r_E, \\\\\n", + "r_I&= -\\frac{w_{IE}}{w_{II}}r_E,\n", + "\\end{align}\n", + "which crosses at the stable point.\n", + "\n", + "The panel on the left shows excitation firing rate $r_E$ and inhibition firing rate $r_I$ as a function of time. On the right side the phase plane plot shows inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$ with the Nullclines for $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0.$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ca5f555", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "3ca5f555" + }, + "outputs": [], + "source": [ + "# @markdown *Run this cell to visualize the phase plane*\n", + "dt = 0.1 # time-step\n", + "t = np.arange(0, 1000, dt)\n", + "r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt)\n", + "plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)" + ] + }, + { + "cell_type": "markdown", + "id": "4d220e96", + "metadata": { + "execution": {}, + "id": "4d220e96" + }, + "source": [ + "### Interactive Demo 3.4: A small change changes everything\n", + "\n", + "We will illustrate that even changing one parameter in a system of differential equations can have a large impact on the solutions of the excitation firing rate $r_E$ and inhibition firing rate $r_I$.\n", + "Interact with the widget below to change the size of $w_{EE}$.\n", + "\n", + "Take note of:\n", + "1. How the solution changes for positive and negative of $w_{EE}$. Pay close attention to the axis.\n", + "2. How would you maintain a stable oscillation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42724aca", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "42724aca" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " w_EE=widgets.FloatSlider(1, min=-1., max=2., step=.1,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(w_EE):\n", + " dt = 0.1 # time-step\n", + " t = np.arange(0,1000,dt)\n", + " r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt, w_EE)\n", + " plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "sH4Bpv7lkLFe", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "sH4Bpv7lkLFe" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Small_change_changes_everything_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "1268fd80", + "metadata": { + "execution": {}, + "id": "1268fd80" + }, + "source": [ + "---\n", + "# Summary\n", + "\n", + "*Estimated timing of tutorial: 70 minutes*\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cEcFr2llcl-l", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "cEcFr2llcl-l" + }, + "outputs": [], + "source": [ + "# @title Video 5: Summary\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '5UmGgboSc40'), ('Bilibili', 'BV1wM4y1g78M')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "uzv5CG0AkLwR", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "uzv5CG0AkLwR" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Summary_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "-JZJWqTnitQx", + "metadata": { + "execution": {}, + "id": "-JZJWqTnitQx" + }, + "source": [ + "Using the formula for the slope of a line, the solution of the differential equation can be estimated with reasonable accuracy. This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution." + ] + }, + { + "cell_type": "markdown", + "id": "a307a8fa", + "metadata": { + "execution": {}, + "id": "a307a8fa" + }, + "source": [ + "---\n", + "## Links to Neuromatch Computational Neuroscience Days\n", + "\n", + "Differential equations turn up on a number of different Neuromatch days:\n", + "* The LIF model is discussed in more detail in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", + "* Drift Diffusion model, which is a differential equation for decision making, is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", + "* Phase-plane plots are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", + "* The Wilson-Cowan model is discussed in [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", + "\n", + "## References\n", + "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", + "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", + "\n", + "\n", + "## Bibliography\n", + "1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.\n", + "2. Strogatz, S. (2014). Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2nd edition\n", + "\n", + "### Supplemental Popular Reading List\n", + "1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.\n", + "2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.\n", + "\n", + "### Popular Podcast\n", + "1. Strogatz, S. (Host). (2020), Joy of X https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine" + ] + }, + { + "cell_type": "markdown", + "id": "_bnKhPLwobY9", + "metadata": { + "execution": {}, + "id": "_bnKhPLwobY9" + }, + "source": [ + "---\n", + "# Bonus" + ] + }, + { + "cell_type": "markdown", + "id": "3c9b3f6e", + "metadata": { + "execution": {}, + "id": "3c9b3f6e" + }, + "source": [ + "---\n", + "## Bonus Section 1: The 4th Order Runge-Kutta method\n", + "\n", + "Another popular numerical method to estimate the solution of differential equations of the general form:\n", + "\n", + "\\begin{align}\n", + "\\frac{d}{dt}y=f(t,y)\n", + "\\end{align}\n", + "\n", + "is the 4th Order Runge Kutta:\n", + "\\begin{align}\n", + "k_1 &= f(t_k,y_k)\\\\\n", + "k_2 &= f(t_k+\\frac{\\Delta t}{2},y_k+\\frac{\\Delta t}{2}k_1)\\\\\n", + "k_3 &= f(t_k+\\frac{\\Delta t}{2},y_k+\\frac{\\Delta t}{2}k_2)\\\\\n", + "k_4 &= f(t_k+\\Delta t,y_k+\\Delta tk_3)\\\\\n", + "y_{k+1} &= y_{k}+\\frac{\\Delta t}{6}(k_1+2k_2+2k_3+k_4)\\\\\n", + "\\end{align}\n", + "\n", + "for $k=0,1,\\cdots,n-1$,\n", + "which is more accurate than the Euler method.\n", + "\n", + "Given the population differential equation\n", + "\\begin{align*}\n", + "\\frac{d}{dt}\\,p(t) &= 0.3 p(t),\\\\\n", + "p(t_0=1)&=e^{0.3}\\quad \\text{Initial Condition},\n", + "\\end{align*}\n", + "\n", + "the 4th Runge Kutta difference equation is\n", + "\n", + "\\begin{align*}\n", + "k_1&=0.3\\color{green}{p[k]},\\\\\n", + "k_2&=0.3(\\color{green}{p[k]}+\\frac{\\Delta t}{2}k_1),\\\\\n", + "k_3&=0.3(\\color{green}{p[k]}+\\frac{\\Delta t}{2}k_2),\\\\\n", + "k_4&=0.3(\\color{green}{p[k]}+k_3),\\\\\n", + "\\color{red}{p_{k[k]}}&=\\color{green}{p[k]}+\\frac{\\color{blue}{\\Delta t}}{6}( \\color{green}{k_1+2k_2+2k_3+k_4})\\\\\n", + "\\end{align*}\n", + "\n", + "for $k=0,1,\\cdots 4$ to estimate the population for $\\Delta t=0.5$.\n", + "\n", + "The code is implemented below. Note how much more accurate the 4th Order Runge Kutta (yellow) is compared to the Euler Method (blue). The 4th order Runge Kutta is of order 4 which means that if you half the time-step $\\Delta t$ the error decreases by a factor of $\\Delta t^4$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b936428", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "7b936428" + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to show the difference between the Runge Kutta Method and the Euler Method*\n", + "\n", + "\n", + "def RK4(dt=0.5):\n", + " t=np.arange(1, 5+dt/2, dt)\n", + " t_fine=np.arange(1, 5+0.1/2, 0.1)\n", + " n = len(t)\n", + " p = np.ones(n)\n", + " pRK4 = np.ones(n)\n", + " p[0] = np.exp(0.3*t[0])\n", + " pRK4[0] = np.exp(0.3*t[0])# Initial Condition\n", + "\n", + " with plt.xkcd():\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')\n", + "\n", + " for i in range(n-1):\n", + " dp = dt*0.3*p[i]\n", + " p[i+1] = p[i] + dp # Euler\n", + " k1 = 0.3*(pRK4[i])\n", + " k2 = 0.3*(pRK4[i] + dt/2*k1)\n", + " k3 = 0.3*(pRK4[i] + dt/2*k2)\n", + " k4 = 0.3*(pRK4[i] + dt*k3)\n", + " pRK4[i+1] = pRK4[i] + dt/6 *(k1 + 2*k2 + 2*k3 + k4)\n", + "\n", + " plt.plot(t_fine, np.exp(0.3*t_fine), label='Exact')\n", + " plt.plot(t, p,':ro', label='Euler Estimate')\n", + " plt.plot(t, pRK4,':co', label='4th Order Runge Kutta Estimate')\n", + "\n", + " plt.ylabel('Population (millions)')\n", + " plt.legend()\n", + " plt.xlabel('Time (years)')\n", + " plt.show()\n", + "\n", + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " dt=widgets.FloatSlider(0.5, min=.1, max=4., step=.1,\n", + " layout=my_layout)\n", + ")\n", + "def Pop_widget(dt):\n", + " RK4(dt)" + ] + }, + { + "cell_type": "markdown", + "id": "W7ziQulJL6Kl", + "metadata": { + "execution": {}, + "id": "W7ziQulJL6Kl" + }, + "source": [ + "**Bonus Reference 1: A full course on numerical methods in Python**\n", + "\n", + "For a full course on Numerical Methods for differential Equations you can look [here](https://github.com/john-s-butler-dit/Numerical-Analysis-Python)." + ] + }, + { + "cell_type": "markdown", + "id": "jXpJytN7dgYn", + "metadata": { + "execution": {}, + "id": "jXpJytN7dgYn" + }, + "source": [ + "---\n", + "## Bonus Section 2: Neural oscillations are a start toward understanding brain activity rather than the end\n", + "\n", + "The differential equations we have discussed above are all to simulate neuronal processes. Another way differential equations can be used is to motivate experimental findings.\n", + "\n", + "Many experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrees to a stimulus.\n", + "An issue with these studies is that there is no consistent definition of what constitutes an oscillation. Right now, it is a bit of I know one when I see one problem.\n", + "\n", + "In an essay from May 2021 in PLoS Biology, Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally when studying oscillations. The essay proposes that instead of thinking about the brain, we should look at this question from the mathematical side to motivate what can be defined as an oscillation.\n", + "\n", + "To do this, they used Stuart–Landau equations, which is a system of differential equations\n", + "\\begin{align}\n", + "\\frac{dx}{dt} &= \\lambda x-\\omega y -\\gamma (x^2+y^2)x+s\\\\\n", + "\\frac{dy}{dt} &= \\lambda y+\\omega x -\\gamma (x^2+y^2)y\n", + "\\end{align}\n", + "\n", + "where $s$ is input to the system, and $\\lambda$, $\\omega$ and $\\gamma$ are parameters.\n", + "\n", + "The Stuart–Landau equations are a well-described system of non-linear differential equations that generate oscillations. For their purpose, Dowling and Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.\n", + "In their paper, using the Stuart–Landau equations, they outline\n", + "* \"What is an oscillator?\"\n", + "* \"What an oscillator is not.\"\n", + "* \"Not all that oscillates is an oscillator.\"\n", + "* \"Not all oscillators are alike.\"\n", + "\n", + "The Euler form of the Stuart–Landau system of equations is:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{x_{k+1}}&=\\color{green}{x_k}+\\color{blue}{\\Delta t}\\big(\\lambda \\color{green}{x_k}-\\omega \\color{green}{y_k} -\\gamma (\\color{green}{x_k}^2+\\color{green}{y_k}^2)\\color{green}{x_k}+s\\big),\\\\\n", + "\\color{red}{y_{k+1}}&=\\color{green}{y_k}+\\color{blue}{\\Delta t}\\big(\\lambda \\color{green}{y_k}+\\omega \\color{green}{x_k} -\\gamma (\\color{green}{x_k}^2+\\color{green}{y_k}^2)\\color{green}{y_k} \\big),\\\\\n", + "&\\text{ for } k=0, \\cdots n-1,\\\\\n", + "x_0&=1,\\\\\n", + "y_0&=1,\\\\\n", + "\\end{align*}\n", + "\n", + "with $ \\Delta t=0.1/1000$ ms." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b9e30cc", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "5b9e30cc" + }, + "outputs": [], + "source": [ + "# @title Helper functions\n", + "def plot_Stuart_Landa(t, x, y, s):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " x : x\n", + " y : y\n", + " s : input\n", + " Returns:\n", + " figure with two panels\n", + " top panel: Input as a function of time\n", + " bottom panel: x\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(14, 4))\n", + " gs = gridspec.GridSpec(2, 2, height_ratios=[1, 4], width_ratios=[4, 1])\n", + "\n", + " # PLOT OF INPUT\n", + " plt.subplot(gs[0])\n", + " plt.ylabel(r'$s$')\n", + " plt.plot(t, s, 'g')\n", + " #plt.ylim((2,4))\n", + "\n", + " # PLOT OF ACTIVITY\n", + " plt.subplot(gs[2])\n", + " plt.plot(t ,x)\n", + " plt.ylabel(r'x')\n", + " plt.xlabel(r't')\n", + " plt.subplot(gs[3])\n", + " plt.plot(x,y)\n", + " plt.plot(x[0], y[0],'go')\n", + " plt.xlabel(r'x')\n", + " plt.ylabel(r'y')\n", + " plt.show()\n", + "\n", + "\n", + "def Euler_Stuart_Landau(s,time,dt,lamba=0.1,gamma=1,k=25):\n", + " \"\"\"\n", + " Args:\n", + " I: Input\n", + " time: time\n", + " dt: time-step\n", + " \"\"\"\n", + "\n", + " n = len(time)\n", + " omega = 4 * 2*np.pi\n", + " x = np.zeros(n)\n", + " y = np.zeros(n)\n", + " x[0] = 1\n", + " y[0] = 1\n", + "\n", + " for i in range(n-1):\n", + " dx = lamba*x[i] - omega*y[i] - gamma*(x[i]*x[i] + y[i]*y[i])*x[i] + k*s[i]\n", + " x[i+1] = x[i] + dt*dx\n", + " dy = lamba*y[i] + omega*x[i] - gamma*(x[i]*x[i] + y[i]*y[i])*y[i]\n", + " y[i+1] = y[i] + dt*dy\n", + "\n", + " return x, y" + ] + }, + { + "cell_type": "markdown", + "id": "H5inj2EKJyqN", + "metadata": { + "execution": {}, + "id": "H5inj2EKJyqN" + }, + "source": [ + "### Bonus 2.1: What is an Oscillator?\n", + "\n", + "Doelling & Assaneo (2021), using the Stuart–Landau system, show different possible states of an oscillator by manipulating the $\\lambda$ term in the equation.\n", + "\n", + "From the paper:\n", + "\n", + "> This qualitative change in behavior takes place at $\\lambda = 0$. For $\\lambda < 0$, the system decays to a stable equilibrium, while for $\\lambda > 0$, it keeps oscillating.\n", + "\n", + "This illustrates that oscillations do not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\\alpha$ band oscillations in EEG the oscillations come and go." + ] + }, + { + "cell_type": "markdown", + "id": "5MyIOK9w_qjd", + "metadata": { + "execution": {}, + "id": "5MyIOK9w_qjd" + }, + "source": [ + "#### Interactive Demo Bonus 2.1: Oscillator\n", + "\n", + "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency $\\omega$ set to $4\\times 2\\pi$, $\\gamma=1$ and $k=25$ over 3 seconds. The input to the system $s(t)$ is plotted in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$. You can manipulate $\\lambda$ to see how the oscillations change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "LAnBGRU5crfG", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "LAnBGRU5crfG" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "dt=0.1/1000\n", + "t=np.arange(0, 3, dt)\n", + "\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " lamda=widgets.FloatSlider(1, min=-1., max=5., step=0.5,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(lamda):\n", + " s=np.zeros(len(t))\n", + " x,y=Euler_Stuart_Landau(s,t,dt,lamda)\n", + " plot_Stuart_Landa(t, x, y, s)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "PNtk7NRomDuQ", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "PNtk7NRomDuQ" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Oscillator_Bonus_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "q2OMPLOoKGtr", + "metadata": { + "execution": {}, + "id": "q2OMPLOoKGtr" + }, + "source": [ + "### Bonus 2.2 : Not all oscillators are alike" + ] + }, + { + "cell_type": "markdown", + "id": "NLOVdwN5_ypj", + "metadata": { + "execution": {}, + "id": "NLOVdwN5_ypj" + }, + "source": [ + "#### Interactive Demo Bonus 2: Stuart-Landau System\n", + "\n", + "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency of 4Hz by setting $\\omega$ to $4\\times 2\\pi$, $\\lambda=1$, $\\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\\sin(freq 2\\pi t) $ in the top panel, $x$ as a function of time in the bottom panel and on the right the phase plane plot of $x$ and $y$.\n", + "\n", + "You can manipulate the frequency $freq$ of the input to see how the oscillations change, and for frequencies $freq$ further and further from the base frequency of 4Hz, the oscillations break down.\n", + "\n", + "This shows that if you have an oscillating input into an oscillator, it does not have to respond by oscillating about could even break down. Hence the frequency of the input oscillation is important to the system.\n", + "So if you flash something at 50Hz, for example, the visual system might not follow the signal, but that does not mean the visual system is not an oscillator. It might just be the wrong frequency." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "XPuVG5f4clxt", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "XPuVG5f4clxt" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "dt=0.1/1000\n", + "t=np.arange(0, 3, dt)\n", + "\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " freq=widgets.FloatSlider(4, min=0.5, max=10., step=0.5,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(freq):\n", + " s = np.sin(freq * 2*np.pi * t)\n", + "\n", + " x, y = Euler_Stuart_Landau(s, t, dt, lamba=1, gamma=.1, k=50)\n", + " plot_Stuart_Landa(t, x, y, s)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "EC5oefXvmOtd", + "metadata": { + "cellView": "form", + "execution": {}, + "id": "EC5oefXvmOtd" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Stuart_Landau_System_Bonus_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "1cWYP3SNL1Mh", + "metadata": { + "execution": {}, + "id": "1cWYP3SNL1Mh" + }, + "source": [ + "**Bonus Reference 2**:\n", + "\n", + "Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234. doi: [10.1371/journal.pbio.3001234](https://doi.org/10.1371/journal.pbio.3001234)" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "1cWYP3SNL1Mh" + ], + "name": "W0D4_Tutorial3", + "provenance": [], + "toc_visible": true, + "include_colab_link": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file From 6f3f71fcb009241d2f5e6bb79bb312304e53782e Mon Sep 17 00:00:00 2001 From: Spiros Chavlis Date: Mon, 3 Jul 2023 15:14:52 +0300 Subject: [PATCH 6/7] fixes --- .../W0D5_Statistics/W0D5_Tutorial1.ipynb | 3793 +++++++++-------- 1 file changed, 2077 insertions(+), 1716 deletions(-) diff --git a/tutorials/W0D5_Statistics/W0D5_Tutorial1.ipynb b/tutorials/W0D5_Statistics/W0D5_Tutorial1.ipynb index 79e9d79..d2de726 100644 --- a/tutorials/W0D5_Statistics/W0D5_Tutorial1.ipynb +++ b/tutorials/W0D5_Statistics/W0D5_Tutorial1.ipynb @@ -1,1717 +1,2078 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "execution": {}, - "id": "view-in-github" - }, - "source": [ - "\"Open   \"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "# Tutorial 1: Probability Distributions\n", - "\n", - "**Week 0, Day 5: Probability & Statistics**\n", - "\n", - "**By Neuromatch Academy**\n", - "\n", - "__Content creators:__ Ulrik Beierholm\n", - "\n", - "__Content reviewers:__ Natalie Schaworonkow, Keith van Antwerp, Anoop Kulkarni, Pooya Pakarian, Hyosub Kim\n", - "\n", - "__Production editors:__ Ethan Cheng, Ella Batty" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Tutorial Objectives\n", - "\n", - "We will cover the basic ideas from probability and statistics, as a reminder of what you have hopefully previously learned. These ideas will be important for almost every one of the following topics covered in the course.\n", - "\n", - "There are many additional topics within probability and statistics that we will not cover as they are not central to the main course. We also do not have time to get into a lot of details, but this should help you recall material you have previously encountered.\n", - "\n", - "\n", - "By completing the exercises in this tutorial, you should:\n", - "* get some intuition about how stochastic randomly generated data can be\n", - "* understand how to model data using simple probability distributions\n", - "* understand the difference between discrete and continuous probability distributions\n", - "* be able to plot a Gaussian distribution" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Setup\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D5_T1\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "both", - "execution": {} - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import scipy as sp\n", - "from scipy.stats import norm # the normal probability distribution" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Figure settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import ipywidgets as widgets # interactive display\n", - "from ipywidgets import interact, fixed, HBox, Layout, VBox, interactive, Label, interact_manual\n", - "%config InlineBackend.figure_format = 'retina'\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Plotting Functions\n", - "\n", - "def plot_random_sample(x, y, figtitle = None):\n", - " \"\"\" Plot the random sample between 0 and 1 for both the x and y axes.\n", - "\n", - " Args:\n", - " x (ndarray): array of x coordinate values across the random sample\n", - " y (ndarray): array of y coordinate values across the random sample\n", - " figtitle (str): title of histogram plot (default is no title)\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - " fig, ax = plt.subplots()\n", - " ax.set_xlabel('x')\n", - " ax.set_ylabel('y')\n", - " plt.xlim([-0.25, 1.25]) # set x and y axis range to be a bit less than 0 and greater than 1\n", - " plt.ylim([-0.25, 1.25])\n", - " plt.scatter(dataX, dataY)\n", - " if figtitle is not None:\n", - " fig.suptitle(figtitle, size=16)\n", - " plt.show()\n", - "\n", - "\n", - "def plot_random_walk(x, y, figtitle = None):\n", - " \"\"\" Plots the random walk within the range 0 to 1 for both the x and y axes.\n", - "\n", - " Args:\n", - " x (ndarray): array of steps in x direction\n", - " y (ndarray): array of steps in y direction\n", - " figtitle (str): title of histogram plot (default is no title)\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - " fig, ax = plt.subplots()\n", - " plt.plot(x,y,'b-o', alpha = 0.5)\n", - " plt.xlim(-0.1,1.1)\n", - " plt.ylim(-0.1,1.1)\n", - " ax.set_xlabel('x location')\n", - " ax.set_ylabel('y location')\n", - " plt.plot(x[0], y[0], 'go')\n", - " plt.plot(x[-1], y[-1], 'ro')\n", - "\n", - " if figtitle is not None:\n", - " fig.suptitle(figtitle, size=16)\n", - " plt.show()\n", - "\n", - "\n", - "def plot_hist(data, xlabel, figtitle = None, num_bins = None):\n", - " \"\"\" Plot the given data as a histogram.\n", - "\n", - " Args:\n", - " data (ndarray): array with data to plot as histogram\n", - " xlabel (str): label of x-axis\n", - " figtitle (str): title of histogram plot (default is no title)\n", - " num_bins (int): number of bins for histogram (default is 10)\n", - "\n", - " Returns:\n", - " count (ndarray): number of samples in each histogram bin\n", - " bins (ndarray): center of each histogram bin\n", - " \"\"\"\n", - " fig, ax = plt.subplots()\n", - " ax.set_xlabel(xlabel)\n", - " ax.set_ylabel('Count')\n", - " if num_bins is not None:\n", - " count, bins, _ = plt.hist(data, bins = num_bins)\n", - " else:\n", - " count, bins, _ = plt.hist(data, bins = np.arange(np.min(data)-.5, np.max(data)+.6)) # 10 bins default\n", - " if figtitle is not None:\n", - " fig.suptitle(figtitle, size=16)\n", - " plt.show()\n", - " return count, bins\n", - "\n", - "\n", - "def my_plot_single(x, px):\n", - " \"\"\"\n", - " Plots normalized Gaussian distribution\n", - "\n", - " Args:\n", - " x (numpy array of floats): points at which the likelihood has been evaluated\n", - " px (numpy array of floats): normalized probabilities for prior evaluated at each `x`\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - " if px is None:\n", - " px = np.zeros_like(x)\n", - "\n", - " fig, ax = plt.subplots()\n", - " ax.plot(x, px, '-', color='C2', linewidth=2, label='Prior')\n", - " ax.legend()\n", - " ax.set_ylabel('Probability')\n", - " ax.set_xlabel('Orientation (Degrees)')\n", - " plt.show()\n", - "\n", - "\n", - "def plot_gaussian_samples_true(samples, xspace, mu, sigma, xlabel, ylabel):\n", - " \"\"\" Plot a histogram of the data samples on the same plot as the gaussian\n", - " distribution specified by the give mu and sigma values.\n", - "\n", - " Args:\n", - " samples (ndarray): data samples for gaussian distribution\n", - " xspace (ndarray): x values to sample from normal distribution\n", - " mu (scalar): mean parameter of normal distribution\n", - " sigma (scalar): variance parameter of normal distribution\n", - " xlabel (str): the label of the x-axis of the histogram\n", - " ylabel (str): the label of the y-axis of the histogram\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - " fig, ax = plt.subplots()\n", - " ax.set_xlabel(xlabel)\n", - " ax.set_ylabel(ylabel)\n", - " # num_samples = samples.shape[0]\n", - "\n", - " count, bins, _ = plt.hist(samples, density=True)\n", - " plt.plot(xspace, norm.pdf(xspace, mu, sigma),'r-')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "\n", - "# Section 1: Stochasticity and randomness" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 1.1: Intro to Randomness\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 1: Stochastic World\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '-QwTPDp7-a8'), ('Bilibili', 'BV1sU4y1G7Qt')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Stochastic_World_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "\n", - "Before trying out different probability distributions, let's start with the simple uniform distribution, U(a,b), which assigns equal probability to any value between a and b.\n", - "\n", - "To show that we are drawing a random number $x$ from a uniform distribution with lower and upper bounds $a$ and $b$ we will use this notation:\n", - "$x \\sim \\mathcal{U}(a,b)$. Alternatively, we can say that all the potential values of $x$ are distributed as a uniform distribution between $a$ and $b$. $x$ here is a random variable: a variable whose value depends on the outcome of a random process." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Coding Exercise 1.1: Create randomness\n", - "\n", - "Numpy has many functions and capabilities related to randomness. We can draw random numbers from various probability distributions. For example, to draw 5 uniform numbers between 0 and 100, you would use `np.random.uniform(0, 100, size = (5,))`.\n", - "\n", - " We will use `np.random.seed` to set a specific seed for the random number generator. For example, `np.random.seed(0)` sets the seed as 0. By including this, we are actually making the random numbers reproducible, which may seem odd at first. Basically if we do the below code without that 0, we would get different random numbers every time we run it. By setting the seed to 0, we ensure we will get the same random numbers. There are lots of reasons we may want randomness to be reproducible. In NMA-world, it's so your plots will match the solution plots exactly!\n", - "\n", - "```python\n", - "np.random.seed(0)\n", - "random_nums = np.random.uniform(0, 100, size = (5,))\n", - "```\n", - "\n", - "Below, you will complete a function `generate_random_sample` that randomly generates `num_points` $x$ and $y$ coordinate values, all within the range 0 to 1. You will then generate 10 points and visualize." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "def generate_random_sample(num_points):\n", - " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", - " in the range [0, 1] using a random number generator object.\n", - "\n", - " Args:\n", - " num_points (int): number of points desired in random sample\n", - "\n", - " Returns:\n", - " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", - " and y coordinates of sampled points\n", - "\n", - " \"\"\"\n", - "\n", - " ###################################################################\n", - " ## TODO for students: Draw the uniform numbers\n", - " ## Fill out the following then remove\n", - " raise NotImplementedError(\"Student exercise: need to complete generate_random_sample\")\n", - " ###################################################################\n", - "\n", - " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", - " # both x and y\n", - " dataX = ...\n", - " dataY = ...\n", - "\n", - " return dataX, dataY\n", - "\n", - "# Set a seed\n", - "np.random.seed(0)\n", - "\n", - "# Set number of points to draw\n", - "num_points = 10\n", - "\n", - "# Draw random points\n", - "dataX, dataY = generate_random_sample(num_points)\n", - "\n", - "# Visualize\n", - "plot_random_sample(dataX, dataY, \"Random sample of 10 points\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "def generate_random_sample(num_points):\n", - " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", - " in the range [0, 1] using a random number generator object.\n", - "\n", - " Args:\n", - " num_points (int): number of points desired in random sample\n", - "\n", - " Returns:\n", - " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", - " and y coordinates of sampled points\n", - "\n", - " \"\"\"\n", - "\n", - " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", - " # both x and y\n", - " dataX = np.random.uniform(0, 1, size = (num_points,))\n", - " dataY = np.random.uniform(0, 1, size = (num_points,))\n", - "\n", - " return dataX, dataY\n", - "\n", - "# Set a seed\n", - "np.random.seed(0)\n", - "\n", - "# Set number of points to draw\n", - "num_points = 10\n", - "\n", - "# Draw random points\n", - "dataX, dataY = generate_random_sample(num_points)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_random_sample(dataX, dataY, \"Random sample of 10 points\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Create_Randomness_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 1.1: Random Sample Generation from Uniform Distribution\n", - "In practice this may not look very uniform, although that is of course part of the randomness! Uniform randomness does not mean smoothly uniform. When we have very little data it can be hard to see the distribution.\n", - "\n", - "Below, you can adjust the number of points sampled with a slider. Does it look more uniform now? Try increasingly large numbers of sampled points." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "#@markdown Make sure you execute this cell to enable the widget!\n", - "\n", - "def generate_random_sample(num_points):\n", - " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", - " in the range [0, 1] using a random number generator object.\n", - "\n", - " Args:\n", - " num_points (int): number of points desired in random sample\n", - "\n", - " Returns:\n", - " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", - " and y coordinates of sampled points\n", - "\n", - " \"\"\"\n", - "\n", - " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", - " # both x and y\n", - " dataX = np.random.uniform(0, 1, size = (num_points,))\n", - " dataY = np.random.uniform(0, 1, size = (num_points,))\n", - "\n", - " return dataX, dataY\n", - "\n", - "@widgets.interact\n", - "def gen_and_plot_random_sample(num_points = widgets.SelectionSlider(options=[(\"%g\"%i,i) for i in np.arange(0, 500, 10)])):\n", - "\n", - " dataX, dataY = generate_random_sample(num_points)\n", - " fig, ax = plt.subplots()\n", - " ax.set_xlabel('x')\n", - " ax.set_ylabel('y')\n", - " plt.xlim([-0.25, 1.25])\n", - " plt.ylim([-0.25, 1.25])\n", - " plt.scatter(dataX, dataY)\n", - " fig.suptitle(\"Random sample of \" + str(num_points) + \" points\", size=16)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Random_Sample_Generation_from_Uniform_Distribution_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 1.2: Random walk\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 2: Random walk\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'Tz9gjHcqj5k'), ('Bilibili', 'BV11U4y1G7Bu')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Random_Walk_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "Stochastic models can be used to create models of behaviour. As an example, imagine that a rat is placed inside a novel environment, a box. We could try and model its exploration behaviour by assuming that for each time step it takes a random uniformly sampled step in any direction (simultaneous random step in x direction and random step in y direction)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Coding Exercise 1.2: Modeling a random walk\n", - "\n", - "\n", - "Use the `generate_random_sample` function from above to obtain the random steps the rat takes at each time step and complete the generate_random_walk function below. For plotting, the box will be represented graphically as the unit square enclosed by the points $(0, 0)$ and $(1, 1)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "def generate_random_walk(num_steps, step_size):\n", - " \"\"\" Generate the points of a random walk within a 1 X 1 box.\n", - "\n", - " Args:\n", - " num_steps (int): number of steps in the random walk\n", - " step_size (float): how much each random step size is weighted\n", - "\n", - " Returns:\n", - " x, y (ndarray, ndarray): the (x, y) locations reached at each time step of the walk\n", - "\n", - " \"\"\"\n", - " x = np.zeros(num_steps + 1)\n", - " y = np.zeros(num_steps + 1)\n", - "\n", - " ###################################################################\n", - " ## TODO for students: Collect random step values with function from before\n", - " ## Fill out the following then remove\n", - " raise NotImplementedError(\"Student exercise: need to complete generate_random_walk\")\n", - " ###################################################################\n", - "\n", - " # Generate the uniformly random x, y steps for the walk\n", - " random_x_steps, random_y_steps = ...\n", - "\n", - " # Take steps according to the randomly sampled steps above\n", - " for step in range(num_steps):\n", - "\n", - " # take a random step in x and y. We remove 0.5 to make it centered around 0\n", - " x[step + 1] = x[step] + (random_x_steps[step] - 0.5)*step_size\n", - " y[step + 1] = y[step] + (random_y_steps[step] - 0.5)*step_size\n", - "\n", - " # restrict to be within the 1 x 1 unit box\n", - " x[step + 1]= min(max(x[step + 1], 0), 1)\n", - " y[step + 1]= min(max(y[step + 1], 0), 1)\n", - "\n", - " return x, y\n", - "\n", - "# Set a random seed\n", - "np.random.seed(2)\n", - "\n", - "# Select parameters\n", - "num_steps = 100 # number of steps in random walk\n", - "step_size = 0.5 # size of each step\n", - "\n", - "# Generate the random walk\n", - "x, y = generate_random_walk(num_steps, step_size)\n", - "\n", - "# Visualize\n", - "plot_random_walk(x, y, \"Rat's location throughout random walk\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "def generate_random_walk(num_steps, step_size):\n", - " \"\"\" Generate the points of a random walk within a 1 X 1 box.\n", - "\n", - " Args:\n", - " num_steps (int): number of steps in the random walk\n", - " step_size (float): how much each random step size is weighted\n", - "\n", - " Returns:\n", - " x, y (ndarray, ndarray): the (x, y) locations reached at each time step of the walk\n", - "\n", - " \"\"\"\n", - " x = np.zeros(num_steps + 1)\n", - " y = np.zeros(num_steps + 1)\n", - "\n", - " # Generate the uniformly random x, y steps for the walk\n", - " random_x_steps, random_y_steps = generate_random_sample(num_steps)\n", - "\n", - " # Take steps according to the randomly sampled steps above\n", - " for step in range(num_steps):\n", - "\n", - " # take a random step in x and y. We remove 0.5 to make it centered around 0\n", - " x[step + 1] = x[step] + (random_x_steps[step] - 0.5)*step_size\n", - " y[step + 1] = y[step] + (random_y_steps[step] - 0.5)*step_size\n", - "\n", - " # restrict to be within the 1 x 1 unit box\n", - " x[step + 1]= min(max(x[step + 1], 0), 1)\n", - " y[step + 1]= min(max(y[step + 1], 0), 1)\n", - "\n", - " return x, y\n", - "\n", - "# Set a random seed\n", - "np.random.seed(2)\n", - "\n", - "# Select parameters\n", - "num_steps = 100 # number of steps in random walk\n", - "step_size = 0.5 # size of each step\n", - "\n", - "# Generate the random walk\n", - "x, y = generate_random_walk(num_steps, step_size)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_random_walk(x, y, \"Rat's location throughout random walk\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "We put a little green dot for the starting point and a red point for the ending point." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Modeling_a_random_walk_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 1.2: Varying parameters of a random walk\n", - "\n", - "In the interactive demo below, you can examine random walks with different numbers of steps or step sizes, using the sliders.\n", - "\n", - "\n", - "1. What could an increased step size mean for the actual rat's movement we are simulating?\n", - "2. For a given number of steps, is the rat more likely to visit all general areas of the arena with a big step size or small step size?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "\n", - "@widgets.interact(num_steps = widgets.IntSlider(value=100, min=0, max=500, step=1), step_size = widgets.FloatSlider(value=0.1, min=0.1, max=1, step=0.1))\n", - "def gen_and_plot_random_walk(num_steps, step_size):\n", - " x, y = generate_random_walk(num_steps, step_size)\n", - " plot_random_walk(x, y, \"Rat's location throughout random walk\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) A larger step size could mean that the rat is moving faster, or that we sample\n", - " the rats location less often.\n", - "\n", - "2) The rat tends to visit more of the arena with a large step size.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Varying_parameters_of_a_random_walk_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "In practice a uniform random movement is too simple an assumption. Rats do not move completely randomly; even if you could assume that, you would need to approximate with a more complex probability distribution.\n", - "\n", - "Nevertheless, this example highlights how you can use sampling to approximate behaviour.\n", - "\n", - "**Main course preview:** During [Hidden Dynamics day](https://compneuro.neuromatch.io/tutorials/W3D2_HiddenDynamics/chapter_title.html) we will see how random walk models can be used to also model accumulation of information in decision making." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 2: Discrete distributions" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.1: Binomial distributions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 3: Binomial distribution\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'kOXEQlmzFyw'), ('Bilibili', 'BV1Ev411W7mw')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Binomial_distribution_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "This video covers the Bernoulli and binomial distributions.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "The uniform distribution is very simple, and can only be used in some rare cases. If we only had access to this distribution, our statistical toolbox would be very empty. Thankfully we do have some more advanced distributions!\n", - "\n", - "The uniform distribution that we looked at above is an example of a continuous distribution. The value of $X$ that we draw from this distribution can take **any value** between $a$ and $b$.\n", - "\n", - "However, sometimes we want to be able to look at discrete events. Imagine that the rat from before is now placed in a T-maze, with food placed at the end of both arms. Initially, we would expect the rat to be choosing randomly between the two arms, but after learning it should choose more consistently.\n", - "\n", - "A simple way to model such random behaviour is with a single **Bernoulli trial**, that has two outcomes, {$Left, Right$}, with probability $P(Left)=p$ and $P(Right)=1-p$ as the two mutually exclusive possibilities (whether the rat goes down the left or right arm of the maze).\n", - "
\n", - "\n", - "The binomial distribution simulates $n$ number of binary events, such as the $Left, Right$ choices of the random rat in the T-maze. Imagine that you have done an experiment and found that your rat turned left in 7 out of 10 trials. What is the probability of the rat indeed turning left 7 times ($k = 7$)?\n", - "\n", - "This is given by the binomial probability of $k$, given $n$ trials and probability $p$:\n", - "\n", - "\\begin{align}\n", - "P(k|n,p) &= \\left( \\begin{array} \\\\n \\\\ k\\end{array} \\right) p^k (1-p)^{n-k} \\\\\n", - "\\binom{n}{k} &= {\\frac {n!}{k!(n-k)!}}\n", - "\\end{align}\n", - "\n", - "In this formula, $p$ is the probability of turning left, $n$ is the number of binary events, or trials, and $k$ is the number of times the rat turned left. The term $\\binom {n}{k}$ is the binomial coefficient.\n", - "\n", - "This is an example of a *probability mass function*, which specifies the probability that a discrete random variable is equal to each value. In other words, how large a part of the probability space (mass) is placed at each exact discrete value. We require that all probability adds up to 1, i.e. that\n", - "\n", - "\\begin{equation}\n", - "\\sum_k P(k|n,p)=1.\n", - "\\end{equation}\n", - "\n", - "Essentially, if $k$ can only be one of 10 values, the probabilities of $k$ being equal to each possible value have to sum up to 1 because there is a probability of 1 it will equal one of those 10 values (no other options exist).\n", - "\n", - "If we assume an equal chance of turning left or right, then $p=0.5$. Note that if we only have a single trial $n=1$ this is equivalent to a single Bernoulli trial (feel free to do the math!)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Think! 2.1: Binomial distribution sampling\n", - "\n", - "We will draw a desired number of random samples from a binomial distribution, with $n = 10$ and $p = 0.5$. Each sample returns the number of trials, $k$, a rat turns left out of $n$ trials.\n", - "\n", - "We will draw 1000 samples of this (so it is as if we are observing 10 trials of the rat, 1000 different times). We can do this using numpy: `np.random.binomial(n, p, size = (n_samples,))`\n", - "\n", - "See below to visualize a histogram of the different values of $k$, or the number of times the rat turned left in each of the 1000 samples. In a histogram all the data is placed into bins and the contents of each bin is counted, to give a visualisation of the distribution of data. Discuss the following questions.\n", - "\n", - "1. What are the x-axis limits of the histogram and why?\n", - "2. What is the shape of the histogram?\n", - "3. Looking at the histogram, how would you interpret the outcome of the simulation if you didn't know what p was? Would you have guessed p = 0.5?\n", - "3. What do you think the histogram would look like if the probability of turning left is 0.8 ($p = 0.8$)?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to see visualization\n", - "\n", - "# Select parameters for conducting binomial trials\n", - "n = 10\n", - "p = 0.5\n", - "n_samples = 1000\n", - "\n", - "# Set random seed\n", - "np.random.seed(1)\n", - "\n", - "# Now draw 1000 samples by calling the function again\n", - "left_turn_samples_1000 = np.random.binomial(n, p, size = (n_samples,))\n", - "\n", - "# Visualize\n", - "count, bins = plot_hist(left_turn_samples_1000, 'Number of left turns in sample')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) The limits of the histogram at 0 and 10, as these are the minimum and maximum\n", - "number of trials that a rat can turn left out of 10 trials.\n", - "\n", - "2) The shape seems symmetric and is centered around 5.\n", - "\n", - "3) An average/mean around 5 left turns out of 10 trials indicates that the rat\n", - "chose left and right turns in the maze with equal probability (that p = 0.5)\n", - "\n", - "4) With p = 0.8, the center of the histogram would be at x = 8 and it would not be\n", - " as symmetrical (since it would be cut off at max 10). You can go into the code above\n", - " and run it with p = 0.8 to see this.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Binomial_distribution_Sampling_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "When working with the Bernoulli and binomial distributions, there are only 2 possible outcomes (in this case, turn left or turn right). In the more general case where there are $n$ possible outcomes (our rat is an n-armed maze) each with their own associated probability $p_1, p_2, p_3, p_4, ...$ , we use a **categorical distribution**. Draws from this distribution are a simple extension of the Bernoulli trial: we now have a probability for each outcome and draw based on those probabilities. We have to make sure that the probabilities sum to one:\n", - "\n", - "\\begin{equation}\n", - "\\sum_i P(x=i)=\\sum_i p_i =1\n", - "\\end{equation}\n", - "\n", - "If we sample from this distribution multiple times, we can then describe the distribution of outcomes from each sample as the **multinomial distribution**. Essentially, the categorical distribution is the multiple outcome extension of the Bernoulli, and the multinomial distribution is the multiple outcome extension of the binomial distribution. We'll see a bit more about this in the next tutorial when we look at Markov chains." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 2.2: Poisson distribution" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 4: Poisson distribution\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'E_nvNb596DY'), ('Bilibili', 'BV1wV411x7P6')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Poisson_distribution_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "This video covers the Poisson distribution and how it can be used to describe neural spiking.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "For some phenomena there may not be a natural limit on the maximum number of possible events or outcomes.\n", - "\n", - "The Poisson distribution is a '**point-process**', meaning that it determines the number of discrete 'point', or binary, events that happen within a fixed space or time, allowing for the occurence of a potentially infinite number of events. The Poisson distribution is specified by a single parameter $\\lambda$ that encapsulates the mean number of events that can occur in a single time or space interval (there will be more on this concept of the 'mean' later!).\n", - "\n", - "Relevant to us, we can model the number of times a neuron spikes within a time interval using a Poisson distribution. In fact, neuroscientists often do! As an example, if we are recording from a neuron that tends to fire at an average rate of 4 spikes per second, then the Poisson distribution specifies the distribution of recorded spikes over one second, where $\\lambda=4$.\n", - "\n", - "
\n", - "\n", - "The formula for a Poisson distribution on $x$ is:\n", - "\n", - "\\begin{equation}\n", - "P(x)=\\frac{\\lambda^x e^{-\\lambda}}{x!}\n", - "\\end{equation}\n", - "\n", - "where $\\lambda$ is a parameter corresponding to the average outcome of $x$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Coding Exercise 2.2: Poisson distribution sampling\n", - "\n", - "In the exercise below we will draw some samples from the Poisson distribution and see what the histogram looks.\n", - "\n", - "In the code, fill in the missing line so we draw 5 samples from a Poisson distribution with $\\lambda = 4$. Use `np.random.poisson`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# Set random seed\n", - "np.random.seed(0)\n", - "\n", - "# Draw 5 samples from a Poisson distribution with lambda = 4\n", - "sampled_spike_counts = ...\n", - "\n", - "# Print the counts\n", - "print(\"The samples drawn from the Poisson distribution are \" +\n", - " str(sampled_spike_counts))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random seed\n", - "np.random.seed(0)\n", - "\n", - "# Draw 5 samples from a Poisson distribution with lambda = 4\n", - "sampled_spike_counts = np.random.poisson(4, 5)\n", - "\n", - "# Print the counts\n", - "print(\"The samples drawn from the Poisson distribution are \" +\n", - " str(sampled_spike_counts))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "You should see that the neuron spiked 6 times, 7 times, 1 time, 8 times, and 4 times in 5 different intervals." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Poisson_distribution_sampling_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 2.2: Varying parameters of Poisson distribution\n", - "\n", - "Use the interactive demo below to vary $\\lambda$ and the number of samples, and then visualize the resulting histogram.\n", - "\n", - "1. What effect does increasing the number of samples have? \n", - "2. What effect does changing $\\lambda$ have?\n", - "3. With a small lambda, why is the distribution asymmetric?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "\n", - "@widgets.interact(lambda_value = widgets.FloatSlider(value=4, min=0.1, max=10, step=0.1),\n", - " n_samples = widgets.IntSlider(value=5, min=5, max=500, step=1))\n", - "\n", - "def gen_and_plot_possion_samples(lambda_value, n_samples):\n", - " sampled_spike_counts = np.random.poisson(lambda_value, n_samples)\n", - " count, bins = plot_hist(sampled_spike_counts, 'Recorded spikes per second')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) More samples only means that the shape of the distribution becomes clearer to us.\n", - "E.g. with a decent number of samples you may be able to see whether the\n", - "distribution is symmetrical. With a small number of samples we would not be able\n", - "to distinguish different distributions from each other. The more data samples we\n", - "have, the more we can say about the stochastic process that generated the data (obviously).\n", - "\n", - "2) Increasing lambda moves the distribution along the x-axis, essentially changing\n", - "the mean of the distribution. With lambda = 6 for example, we would expect to see\n", - "6 spike counts per interval on average.\n", - "\n", - "3) For small values of lambda the Poisson distribution becomes asymmetrical as\n", - "it is a distribution over non-negative counts\n", - " (you can’t have negative numbers of spikes e.g.).\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Varying_parameters_of_Poisson_distribution_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Section 3: Continuous distributions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Video 5: Continuous distributions\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'LJ4Zdokb6lc'), ('Bilibili', 'BV1dq4y1L7eC')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Continuous_distributions_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "We do not have to restrict ourselves to only probabilistic models of discrete events. While some events in neuroscience are discrete (e.g. number of spikes by a neuron), many others are continuous (e.g. neuroimaging signals in EEG or fMRI, distance traveled by an animal, human pointing in a direction of a stimulus).\n", - "\n", - "While for discrete outcomes we can ask about the probability of an specific event (\"what is the probability this neuron will fire 4 times in the next second\"), this is not defined for a continuous distribution (\"what is the probability of the BOLD signal being exactly 4.000120141...\"). Hence we need to focus on intervals when calculating probabilities from a continuous distribution.\n", - "\n", - "If we want to make predictions about possible outcomes (\"I believe the BOLD signal from the area will be in the range $x_1$ to $ x_2 $\") we can use the integral $\\int_{x_1}^{x_2} P(x)$.\n", - "$P(x)$ is now a **probability density function**, sometimes written as $f(x)$ to distinguish it from the probability mass functions.\n", - "\n", - "With continuous distributions we have to replace the normalising sum\n", - "\n", - "\\begin{equation}\n", - "\\sum_i P(x=p_i) = 1\n", - "\\end{equation}\n", - "\n", - "over all possible events, with an integral\n", - "\n", - "\\begin{equation}\n", - "\\int_a^b P(x) = 1\n", - "\\end{equation}\n", - "\n", - "where a and b are the limits of the random variable $x$ (often $-\\infty$ and $\\infty$)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "## Section 3.1: Gaussian Distribution\n", - "\n", - "The most widely used continuous distribution is probably the Gaussian (also known as Normal) distribution. It is extremely common across all kinds of statistical analyses. Because of the central limit theorem, many quantities are Gaussian distributed. Gaussians also have some nice mathematical properties that permit simple closed-form solutions to several important problems.\n", - "\n", - "As a working example, imagine that a human participant is asked to point in the direction where they perceived a sound coming from. As an approximation, we can assume that the variability in the direction/orientation they point towards is Gaussian distributed.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Coding Exercise 3.1A: Gaussian Distribution\n", - "\n", - "In this exercise, you will implement a Gaussian by filling in the missing portions of code for the function `my_gaussian` below. Gaussians have two parameters. The **mean** $\\mu$, which sets the location of its center, and its \"scale\" or spread is controlled by its **standard deviation** $\\sigma$, or **variance** $\\sigma^2$ (i.e. the square of standard deviation). **Be careful not to use one when the other is required.**\n", - "\n", - "The equation for a Gaussian probability density function is:\n", - "\n", - "\\begin{equation}\n", - "f(x;\\mu,\\sigma^2) = \\mathcal{N}(\\mu,\\sigma^2) = \\frac{1}{\\sqrt{2\\pi\\sigma^2}}\\exp\\left(\\frac{-(x-\\mu)^2}{2\\sigma^2}\\right)\n", - "\\end{equation}\n", - "\n", - "In Python $\\pi$ and $e$ can be written as `np.pi` and `np.exp` respectively.\n", - "\n", - "As a probability distribution this has an integral of one when integrated from $-\\infty$ to $\\infty$, however in the following your numerical Gaussian will only be computed over a finite number of points (for the cell below we will sample from -8 to 9 in step sizes of 0.1). You therefore need to explicitly normalize it to sum to one yourself.\n", - "\n", - "Test out your implementation with a $\\mu = -1$ and $\\sigma = 1$. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "def my_gaussian(x_points, mu, sigma):\n", - " \"\"\" Returns normalized Gaussian estimated at points `x_points`, with\n", - " parameters: mean `mu` and standard deviation `sigma`\n", - "\n", - " Args:\n", - " x_points (ndarray of floats): points at which the gaussian is evaluated\n", - " mu (scalar): mean of the Gaussian\n", - " sigma (scalar): standard deviation of the gaussian\n", - "\n", - " Returns:\n", - " (numpy array of floats) : normalized Gaussian evaluated at `x`\n", - " \"\"\"\n", - "\n", - " ###################################################################\n", - " ## TODO for students: Implement the formula for a Gaussian\n", - " ## Add code to calculate the gaussian px as a function of mu and sigma,\n", - " ## for every x in x_points\n", - " ## Function Hints: exp -> np.exp()\n", - " ## power -> z**2\n", - " ##\n", - " ## Fill out the following then remove\n", - " raise NotImplementedError(\"Student exercise: need to implement Gaussian\")\n", - " ###################################################################\n", - " px = ...\n", - "\n", - " # as we are doing numerical integration we have to remember to normalise\n", - " # taking into account the stepsize (0.1)\n", - " px = px/(0.1*sum(px))\n", - " return px\n", - "\n", - "x = np.arange(-8, 9, 0.1)\n", - "\n", - "# Generate Gaussian\n", - "px = my_gaussian(x, -1, 1)\n", - "\n", - "# Visualize\n", - "my_plot_single(x, px)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {} - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "def my_gaussian(x_points, mu, sigma):\n", - " \"\"\" Returns normalized Gaussian estimated at points `x_points`, with\n", - " parameters: mean `mu` and standard deviation `sigma`\n", - "\n", - " Args:\n", - " x_points (ndarray of floats): points at which the gaussian is evaluated\n", - " mu (scalar): mean of the Gaussian\n", - " sigma (scalar): standard deviation of the gaussian\n", - "\n", - " Returns:\n", - " (numpy array of floats) : normalized Gaussian evaluated at `x`\n", - " \"\"\"\n", - "\n", - " px = 1/(2*np.pi*sigma**2)**1/2 *np.exp(-(x_points-mu)**2/(2*sigma**2))\n", - "\n", - " # as we are doing numerical integration we have to remember to normalise\n", - " # taking into account the stepsize (0.1)\n", - " px = px/(0.1*sum(px))\n", - " return px\n", - "\n", - "x = np.arange(-8, 9, 0.1)\n", - "\n", - "# Generate Gaussian\n", - "px = my_gaussian(x, -1, 1)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " my_plot_single(x, px)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Gaussian_Distribution_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "### Interactive Demo 3.1: Sampling from a Gaussian distribution\n", - "\n", - "Now that we have gained a bit of intuition about the shape of the Gaussian, let's imagine that a human participant is asked to point in the direction of a sound source, which we then measure in horizontal degrees. To simulate that we draw samples from a Normal distribution:\n", - "\n", - "\\begin{equation}\n", - "x \\sim \\mathcal{N}(\\mu,\\sigma)\n", - "\\end{equation}\n", - "\n", - "We can sample from a Gaussian with mean $\\mu$ and standard deviation $\\sigma$ using `np.random.normal(mu, sigma, size = (n_samples,))`.\n", - "\n", - "In the demo below, you can change the mean and standard deviation of the Gaussian, and the number of samples, we can compare the histogram of the samples to the true analytical distribution (in red).\n", - "\n", - "1. With what number of samples would you say that the full distribution (in red) is well approximated by the histogram?\n", - "2. What if you just wanted to approximate the variables that defined the distribution, i.e., mean and variance?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "#@markdown Make sure you execute this cell to enable the widget!\n", - "\n", - "\n", - "@widgets.interact(mean = widgets.FloatSlider(value=0, min=-5, max=5, step=0.5),\n", - " standard_dev = widgets.FloatSlider(value=0.5, min=0, max=10, step=0.1),\n", - " n_samples = widgets.IntSlider(value=5, min=1, max=300, step=1))\n", - "def gen_and_plot_normal_samples(mean, standard_dev, n_samples):\n", - " x = np.random.normal(mean, standard_dev, size = (n_samples,))\n", - " xspace = np.linspace(-20, 20, 100)\n", - " plot_gaussian_samples_true(x, xspace, mean, standard_dev,\n", - " 'orientation (degrees)', 'probability')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "**Main course preview:** Gaussian distriutions are everywhere and are critical for filtering, [linear systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html), [optimal control](https://compneuro.neuromatch.io/tutorials/W3D3_OptimalControl/chapter_title.html) and almost any statistical model of continuous data." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {} - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Sampling_from_a_Gaussian_distribution_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {} - }, - "source": [ - "---\n", - "# Summary\n", - "\n", - "Across the different exercises you should now:\n", - "* have gotten some intuition about how stochastic randomly generated data can be\n", - "* understand how to model data using simple distributions\n", - "* understand the difference between discrete and continuous distributions\n", - "* be able to plot a Gaussian distribution\n", - "\n", - "For more reading on these topics see just about any statistics textbook, or take a look at the [online resources](https://github.com/NeuromatchAcademy/precourse/blob/main/resources.md)." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "include_colab_link": true, - "name": "W0D5_Tutorial1", - "provenance": [], - "toc_visible": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - }, - "toc-autonumbering": true - }, - "nbformat": 4, - "nbformat_minor": 0 -} + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "zBqSZwuYpmC2" + }, + "source": [ + "# Tutorial 1: Probability Distributions\n", + "\n", + "**Week 0, Day 5: Probability & Statistics**\n", + "\n", + "**By Neuromatch Academy**\n", + "\n", + "__Content creators:__ Ulrik Beierholm\n", + "\n", + "__Content reviewers:__ Natalie Schaworonkow, Keith van Antwerp, Anoop Kulkarni, Pooya Pakarian, Hyosub Kim\n", + "\n", + "__Production editors:__ Ethan Cheng, Ella Batty" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "3fx1fKeNpmC4" + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "DF_7FF_QpmC4" + }, + "source": [ + "---\n", + "# Tutorial Objectives\n", + "\n", + "We will cover the basic ideas from probability and statistics, as a reminder of what you have hopefully previously learned. These ideas will be important for almost every one of the following topics covered in the course.\n", + "\n", + "There are many additional topics within probability and statistics that we will not cover as they are not central to the main course. We also do not have time to get into a lot of details, but this should help you recall material you have previously encountered.\n", + "\n", + "\n", + "By completing the exercises in this tutorial, you should:\n", + "* get some intuition about how stochastic randomly generated data can be\n", + "* understand how to model data using simple probability distributions\n", + "* understand the difference between discrete and continuous probability distributions\n", + "* be able to plot a Gaussian distribution" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "KOrfzq_zpmC5" + }, + "source": [ + "---\n", + "# Setup\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "_H1OF1OOpmC5" + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D5_T1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "both", + "execution": {}, + "id": "OcGqwrrDpmC6" + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import scipy as sp\n", + "from scipy.stats import norm # the normal probability distribution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "S1cSW3-4pmC7" + }, + "outputs": [], + "source": [ + "# @title Figure settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import ipywidgets as widgets # interactive display\n", + "from ipywidgets import interact, fixed, HBox, Layout, VBox, interactive, Label, interact_manual\n", + "%config InlineBackend.figure_format = 'retina'\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "nuc_0WFEpmC7" + }, + "outputs": [], + "source": [ + "# @title Plotting Functions\n", + "\n", + "def plot_random_sample(x, y, figtitle = None):\n", + " \"\"\" Plot the random sample between 0 and 1 for both the x and y axes.\n", + "\n", + " Args:\n", + " x (ndarray): array of x coordinate values across the random sample\n", + " y (ndarray): array of y coordinate values across the random sample\n", + " figtitle (str): title of histogram plot (default is no title)\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + " fig, ax = plt.subplots()\n", + " ax.set_xlabel('x')\n", + " ax.set_ylabel('y')\n", + " plt.xlim([-0.25, 1.25]) # set x and y axis range to be a bit less than 0 and greater than 1\n", + " plt.ylim([-0.25, 1.25])\n", + " plt.scatter(dataX, dataY)\n", + " if figtitle is not None:\n", + " fig.suptitle(figtitle, size=16)\n", + " plt.show()\n", + "\n", + "\n", + "def plot_random_walk(x, y, figtitle = None):\n", + " \"\"\" Plots the random walk within the range 0 to 1 for both the x and y axes.\n", + "\n", + " Args:\n", + " x (ndarray): array of steps in x direction\n", + " y (ndarray): array of steps in y direction\n", + " figtitle (str): title of histogram plot (default is no title)\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + " fig, ax = plt.subplots()\n", + " plt.plot(x,y,'b-o', alpha = 0.5)\n", + " plt.xlim(-0.1,1.1)\n", + " plt.ylim(-0.1,1.1)\n", + " ax.set_xlabel('x location')\n", + " ax.set_ylabel('y location')\n", + " plt.plot(x[0], y[0], 'go')\n", + " plt.plot(x[-1], y[-1], 'ro')\n", + "\n", + " if figtitle is not None:\n", + " fig.suptitle(figtitle, size=16)\n", + " plt.show()\n", + "\n", + "\n", + "def plot_hist(data, xlabel, figtitle = None, num_bins = None):\n", + " \"\"\" Plot the given data as a histogram.\n", + "\n", + " Args:\n", + " data (ndarray): array with data to plot as histogram\n", + " xlabel (str): label of x-axis\n", + " figtitle (str): title of histogram plot (default is no title)\n", + " num_bins (int): number of bins for histogram (default is 10)\n", + "\n", + " Returns:\n", + " count (ndarray): number of samples in each histogram bin\n", + " bins (ndarray): center of each histogram bin\n", + " \"\"\"\n", + " fig, ax = plt.subplots()\n", + " ax.set_xlabel(xlabel)\n", + " ax.set_ylabel('Count')\n", + " if num_bins is not None:\n", + " count, bins, _ = plt.hist(data, bins = num_bins)\n", + " else:\n", + " count, bins, _ = plt.hist(data, bins = np.arange(np.min(data)-.5, np.max(data)+.6)) # 10 bins default\n", + " if figtitle is not None:\n", + " fig.suptitle(figtitle, size=16)\n", + " plt.show()\n", + " return count, bins\n", + "\n", + "\n", + "def my_plot_single(x, px):\n", + " \"\"\"\n", + " Plots normalized Gaussian distribution\n", + "\n", + " Args:\n", + " x (numpy array of floats): points at which the likelihood has been evaluated\n", + " px (numpy array of floats): normalized probabilities for prior evaluated at each `x`\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + " if px is None:\n", + " px = np.zeros_like(x)\n", + "\n", + " fig, ax = plt.subplots()\n", + " ax.plot(x, px, '-', color='C2', linewidth=2, label='Prior')\n", + " ax.legend()\n", + " ax.set_ylabel('Probability')\n", + " ax.set_xlabel('Orientation (Degrees)')\n", + " plt.show()\n", + "\n", + "\n", + "def plot_gaussian_samples_true(samples, xspace, mu, sigma, xlabel, ylabel):\n", + " \"\"\" Plot a histogram of the data samples on the same plot as the gaussian\n", + " distribution specified by the give mu and sigma values.\n", + "\n", + " Args:\n", + " samples (ndarray): data samples for gaussian distribution\n", + " xspace (ndarray): x values to sample from normal distribution\n", + " mu (scalar): mean parameter of normal distribution\n", + " sigma (scalar): variance parameter of normal distribution\n", + " xlabel (str): the label of the x-axis of the histogram\n", + " ylabel (str): the label of the y-axis of the histogram\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + " fig, ax = plt.subplots()\n", + " ax.set_xlabel(xlabel)\n", + " ax.set_ylabel(ylabel)\n", + " # num_samples = samples.shape[0]\n", + "\n", + " count, bins, _ = plt.hist(samples, density=True)\n", + " plt.plot(xspace, norm.pdf(xspace, mu, sigma),'r-')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "q9o8V-mLpmC7" + }, + "source": [ + "---\n", + "\n", + "# Section 1: Stochasticity and randomness" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "45v8eNKjpmC7" + }, + "source": [ + "## Section 1.1: Intro to Randomness\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "SaDZWnv_pmC8" + }, + "outputs": [], + "source": [ + "# @title Video 1: Stochastic World\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '-QwTPDp7-a8'), ('Bilibili', 'BV1sU4y1G7Qt')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "Fj033QTNpmC8" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Stochastic_World_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "DXi2V88SpmC8" + }, + "source": [ + "\n", + "Before trying out different probability distributions, let's start with the simple uniform distribution, U(a,b), which assigns equal probability to any value between a and b.\n", + "\n", + "To show that we are drawing a random number $x$ from a uniform distribution with lower and upper bounds $a$ and $b$ we will use this notation:\n", + "$x \\sim \\mathcal{U}(a,b)$. Alternatively, we can say that all the potential values of $x$ are distributed as a uniform distribution between $a$ and $b$. $x$ here is a random variable: a variable whose value depends on the outcome of a random process." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "rj7c7QQbpmC8" + }, + "source": [ + "### Coding Exercise 1.1: Create randomness\n", + "\n", + "Numpy has many functions and capabilities related to randomness. We can draw random numbers from various probability distributions. For example, to draw 5 uniform numbers between 0 and 100, you would use `np.random.uniform(0, 100, size = (5,))`.\n", + "\n", + " We will use `np.random.seed` to set a specific seed for the random number generator. For example, `np.random.seed(0)` sets the seed as 0. By including this, we are actually making the random numbers reproducible, which may seem odd at first. Basically if we do the below code without that 0, we would get different random numbers every time we run it. By setting the seed to 0, we ensure we will get the same random numbers. There are lots of reasons we may want randomness to be reproducible. In NMA-world, it's so your plots will match the solution plots exactly!\n", + "\n", + "```python\n", + "np.random.seed(0)\n", + "random_nums = np.random.uniform(0, 100, size = (5,))\n", + "```\n", + "\n", + "Below, you will complete a function `generate_random_sample` that randomly generates `num_points` $x$ and $y$ coordinate values, all within the range 0 to 1. You will then generate 10 points and visualize." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "bxLLiyzGpmC8" + }, + "outputs": [], + "source": [ + "def generate_random_sample(num_points):\n", + " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", + " in the range [0, 1] using a random number generator object.\n", + "\n", + " Args:\n", + " num_points (int): number of points desired in random sample\n", + "\n", + " Returns:\n", + " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", + " and y coordinates of sampled points\n", + "\n", + " \"\"\"\n", + "\n", + " ###################################################################\n", + " ## TODO for students: Draw the uniform numbers\n", + " ## Fill out the following then remove\n", + " raise NotImplementedError(\"Student exercise: need to complete generate_random_sample\")\n", + " ###################################################################\n", + "\n", + " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", + " # both x and y\n", + " dataX = ...\n", + " dataY = ...\n", + "\n", + " return dataX, dataY\n", + "\n", + "# Set a seed\n", + "np.random.seed(0)\n", + "\n", + "# Set number of points to draw\n", + "num_points = 10\n", + "\n", + "# Draw random points\n", + "dataX, dataY = generate_random_sample(num_points)\n", + "\n", + "# Visualize\n", + "plot_random_sample(dataX, dataY, \"Random sample of 10 points\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "uHcRMx_upmC8" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "def generate_random_sample(num_points):\n", + " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", + " in the range [0, 1] using a random number generator object.\n", + "\n", + " Args:\n", + " num_points (int): number of points desired in random sample\n", + "\n", + " Returns:\n", + " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", + " and y coordinates of sampled points\n", + "\n", + " \"\"\"\n", + "\n", + " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", + " # both x and y\n", + " dataX = np.random.uniform(0, 1, size = (num_points,))\n", + " dataY = np.random.uniform(0, 1, size = (num_points,))\n", + "\n", + " return dataX, dataY\n", + "\n", + "# Set a seed\n", + "np.random.seed(0)\n", + "\n", + "# Set number of points to draw\n", + "num_points = 10\n", + "\n", + "# Draw random points\n", + "dataX, dataY = generate_random_sample(num_points)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_random_sample(dataX, dataY, \"Random sample of 10 points\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "xSI3mnB_pmC9" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Create_Randomness_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "_j2Z_UtppmC9" + }, + "source": [ + "### Interactive Demo 1.1: Random Sample Generation from Uniform Distribution\n", + "In practice this may not look very uniform, although that is of course part of the randomness! Uniform randomness does not mean smoothly uniform. When we have very little data it can be hard to see the distribution.\n", + "\n", + "Below, you can adjust the number of points sampled with a slider. Does it look more uniform now? Try increasingly large numbers of sampled points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "LIPA1z0XpmC9" + }, + "outputs": [], + "source": [ + "#@markdown Make sure you execute this cell to enable the widget!\n", + "\n", + "def generate_random_sample(num_points):\n", + " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", + " in the range [0, 1] using a random number generator object.\n", + "\n", + " Args:\n", + " num_points (int): number of points desired in random sample\n", + "\n", + " Returns:\n", + " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", + " and y coordinates of sampled points\n", + "\n", + " \"\"\"\n", + "\n", + " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", + " # both x and y\n", + " dataX = np.random.uniform(0, 1, size = (num_points,))\n", + " dataY = np.random.uniform(0, 1, size = (num_points,))\n", + "\n", + " return dataX, dataY\n", + "\n", + "@widgets.interact\n", + "def gen_and_plot_random_sample(num_points = widgets.SelectionSlider(options=[(\"%g\"%i,i) for i in np.arange(0, 500, 10)])):\n", + "\n", + " dataX, dataY = generate_random_sample(num_points)\n", + " fig, ax = plt.subplots()\n", + " ax.set_xlabel('x')\n", + " ax.set_ylabel('y')\n", + " plt.xlim([-0.25, 1.25])\n", + " plt.ylim([-0.25, 1.25])\n", + " plt.scatter(dataX, dataY)\n", + " fig.suptitle(\"Random sample of \" + str(num_points) + \" points\", size=16)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "yroC0BhYpmC9" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Random_Sample_Generation_from_Uniform_Distribution_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "qd47OGFdpmC9" + }, + "source": [ + "## Section 1.2: Random walk\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "EHtLHcPKpmC9" + }, + "outputs": [], + "source": [ + "# @title Video 2: Random walk\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'Tz9gjHcqj5k'), ('Bilibili', 'BV11U4y1G7Bu')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "xCALBgVJpmC9" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Random_Walk_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "mIrkdc8KpmC-" + }, + "source": [ + "Stochastic models can be used to create models of behaviour. As an example, imagine that a rat is placed inside a novel environment, a box. We could try and model its exploration behaviour by assuming that for each time step it takes a random uniformly sampled step in any direction (simultaneous random step in x direction and random step in y direction)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "jHOiAusIpmC-" + }, + "source": [ + "### Coding Exercise 1.2: Modeling a random walk\n", + "\n", + "\n", + "Use the `generate_random_sample` function from above to obtain the random steps the rat takes at each time step and complete the generate_random_walk function below. For plotting, the box will be represented graphically as the unit square enclosed by the points $(0, 0)$ and $(1, 1)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "l9J0EAiDpmC-" + }, + "outputs": [], + "source": [ + "def generate_random_walk(num_steps, step_size):\n", + " \"\"\" Generate the points of a random walk within a 1 X 1 box.\n", + "\n", + " Args:\n", + " num_steps (int): number of steps in the random walk\n", + " step_size (float): how much each random step size is weighted\n", + "\n", + " Returns:\n", + " x, y (ndarray, ndarray): the (x, y) locations reached at each time step of the walk\n", + "\n", + " \"\"\"\n", + " x = np.zeros(num_steps + 1)\n", + " y = np.zeros(num_steps + 1)\n", + "\n", + " ###################################################################\n", + " ## TODO for students: Collect random step values with function from before\n", + " ## Fill out the following then remove\n", + " raise NotImplementedError(\"Student exercise: need to complete generate_random_walk\")\n", + " ###################################################################\n", + "\n", + " # Generate the uniformly random x, y steps for the walk\n", + " random_x_steps, random_y_steps = ...\n", + "\n", + " # Take steps according to the randomly sampled steps above\n", + " for step in range(num_steps):\n", + "\n", + " # take a random step in x and y. We remove 0.5 to make it centered around 0\n", + " x[step + 1] = x[step] + (random_x_steps[step] - 0.5)*step_size\n", + " y[step + 1] = y[step] + (random_y_steps[step] - 0.5)*step_size\n", + "\n", + " # restrict to be within the 1 x 1 unit box\n", + " x[step + 1]= min(max(x[step + 1], 0), 1)\n", + " y[step + 1]= min(max(y[step + 1], 0), 1)\n", + "\n", + " return x, y\n", + "\n", + "# Set a random seed\n", + "np.random.seed(2)\n", + "\n", + "# Select parameters\n", + "num_steps = 100 # number of steps in random walk\n", + "step_size = 0.5 # size of each step\n", + "\n", + "# Generate the random walk\n", + "x, y = generate_random_walk(num_steps, step_size)\n", + "\n", + "# Visualize\n", + "plot_random_walk(x, y, \"Rat's location throughout random walk\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "9xLgLb3vpmC-" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "def generate_random_walk(num_steps, step_size):\n", + " \"\"\" Generate the points of a random walk within a 1 X 1 box.\n", + "\n", + " Args:\n", + " num_steps (int): number of steps in the random walk\n", + " step_size (float): how much each random step size is weighted\n", + "\n", + " Returns:\n", + " x, y (ndarray, ndarray): the (x, y) locations reached at each time step of the walk\n", + "\n", + " \"\"\"\n", + " x = np.zeros(num_steps + 1)\n", + " y = np.zeros(num_steps + 1)\n", + "\n", + " # Generate the uniformly random x, y steps for the walk\n", + " random_x_steps, random_y_steps = generate_random_sample(num_steps)\n", + "\n", + " # Take steps according to the randomly sampled steps above\n", + " for step in range(num_steps):\n", + "\n", + " # take a random step in x and y. We remove 0.5 to make it centered around 0\n", + " x[step + 1] = x[step] + (random_x_steps[step] - 0.5)*step_size\n", + " y[step + 1] = y[step] + (random_y_steps[step] - 0.5)*step_size\n", + "\n", + " # restrict to be within the 1 x 1 unit box\n", + " x[step + 1]= min(max(x[step + 1], 0), 1)\n", + " y[step + 1]= min(max(y[step + 1], 0), 1)\n", + "\n", + " return x, y\n", + "\n", + "# Set a random seed\n", + "np.random.seed(2)\n", + "\n", + "# Select parameters\n", + "num_steps = 100 # number of steps in random walk\n", + "step_size = 0.5 # size of each step\n", + "\n", + "# Generate the random walk\n", + "x, y = generate_random_walk(num_steps, step_size)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_random_walk(x, y, \"Rat's location throughout random walk\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "RcjP6GfupmC-" + }, + "source": [ + "We put a little green dot for the starting point and a red point for the ending point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "t55kLeRupmC-" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Modeling_a_random_walk_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "LBQcyDdjpmC-" + }, + "source": [ + "### Interactive Demo 1.2: Varying parameters of a random walk\n", + "\n", + "In the interactive demo below, you can examine random walks with different numbers of steps or step sizes, using the sliders.\n", + "\n", + "\n", + "1. What could an increased step size mean for the actual rat's movement we are simulating?\n", + "2. For a given number of steps, is the rat more likely to visit all general areas of the arena with a big step size or small step size?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "5e5nCvG0pmC-" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "\n", + "@widgets.interact(num_steps = widgets.IntSlider(value=100, min=0, max=500, step=1), step_size = widgets.FloatSlider(value=0.1, min=0.1, max=1, step=0.1))\n", + "def gen_and_plot_random_walk(num_steps, step_size):\n", + " x, y = generate_random_walk(num_steps, step_size)\n", + " plot_random_walk(x, y, \"Rat's location throughout random walk\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "L7ElkIzCpmC-" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) A larger step size could mean that the rat is moving faster, or that we sample\n", + " the rats location less often.\n", + "\n", + "2) The rat tends to visit more of the arena with a large step size.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "FZ80yLn2pmC_" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Varying_parameters_of_a_random_walk_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "hDAATt06pmC_" + }, + "source": [ + "In practice a uniform random movement is too simple an assumption. Rats do not move completely randomly; even if you could assume that, you would need to approximate with a more complex probability distribution.\n", + "\n", + "Nevertheless, this example highlights how you can use sampling to approximate behaviour.\n", + "\n", + "**Main course preview:** During [Hidden Dynamics day](https://compneuro.neuromatch.io/tutorials/W3D2_HiddenDynamics/chapter_title.html) we will see how random walk models can be used to also model accumulation of information in decision making." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "ROETCK5GpmC_" + }, + "source": [ + "---\n", + "# Section 2: Discrete distributions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "n1B1jTSPpmC_" + }, + "source": [ + "## Section 2.1: Binomial distributions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ZT1dYLospmC_" + }, + "outputs": [], + "source": [ + "# @title Video 3: Binomial distribution\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'kOXEQlmzFyw'), ('Bilibili', 'BV1Ev411W7mw')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ZgsgdLKvpmC_" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Binomial_distribution_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "b61_9ZqIpmC_" + }, + "source": [ + "This video covers the Bernoulli and binomial distributions.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "The uniform distribution is very simple, and can only be used in some rare cases. If we only had access to this distribution, our statistical toolbox would be very empty. Thankfully we do have some more advanced distributions!\n", + "\n", + "The uniform distribution that we looked at above is an example of a continuous distribution. The value of $X$ that we draw from this distribution can take **any value** between $a$ and $b$.\n", + "\n", + "However, sometimes we want to be able to look at discrete events. Imagine that the rat from before is now placed in a T-maze, with food placed at the end of both arms. Initially, we would expect the rat to be choosing randomly between the two arms, but after learning it should choose more consistently.\n", + "\n", + "A simple way to model such random behaviour is with a single **Bernoulli trial**, that has two outcomes, {$Left, Right$}, with probability $P(Left)=p$ and $P(Right)=1-p$ as the two mutually exclusive possibilities (whether the rat goes down the left or right arm of the maze).\n", + "
\n", + "\n", + "The binomial distribution simulates $n$ number of binary events, such as the $Left, Right$ choices of the random rat in the T-maze. Imagine that you have done an experiment and found that your rat turned left in 7 out of 10 trials. What is the probability of the rat indeed turning left 7 times ($k = 7$)?\n", + "\n", + "This is given by the binomial probability of $k$, given $n$ trials and probability $p$:\n", + "\n", + "\\begin{align}\n", + "P(k|n,p) &= \\left( \\begin{array} \\\\n \\\\ k\\end{array} \\right) p^k (1-p)^{n-k} \\\\\n", + "\\binom{n}{k} &= {\\frac {n!}{k!(n-k)!}}\n", + "\\end{align}\n", + "\n", + "In this formula, $p$ is the probability of turning left, $n$ is the number of binary events, or trials, and $k$ is the number of times the rat turned left. The term $\\binom {n}{k}$ is the binomial coefficient.\n", + "\n", + "This is an example of a *probability mass function*, which specifies the probability that a discrete random variable is equal to each value. In other words, how large a part of the probability space (mass) is placed at each exact discrete value. We require that all probability adds up to 1, i.e. that\n", + "\n", + "\\begin{equation}\n", + "\\sum_k P(k|n,p)=1.\n", + "\\end{equation}\n", + "\n", + "Essentially, if $k$ can only be one of 10 values, the probabilities of $k$ being equal to each possible value have to sum up to 1 because there is a probability of 1 it will equal one of those 10 values (no other options exist).\n", + "\n", + "If we assume an equal chance of turning left or right, then $p=0.5$. Note that if we only have a single trial $n=1$ this is equivalent to a single Bernoulli trial (feel free to do the math!)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "ChYeI0WCpmDI" + }, + "source": [ + "### Think! 2.1: Binomial distribution sampling\n", + "\n", + "We will draw a desired number of random samples from a binomial distribution, with $n = 10$ and $p = 0.5$. Each sample returns the number of trials, $k$, a rat turns left out of $n$ trials.\n", + "\n", + "We will draw 1000 samples of this (so it is as if we are observing 10 trials of the rat, 1000 different times). We can do this using numpy: `np.random.binomial(n, p, size = (n_samples,))`\n", + "\n", + "See below to visualize a histogram of the different values of $k$, or the number of times the rat turned left in each of the 1000 samples. In a histogram all the data is placed into bins and the contents of each bin is counted, to give a visualisation of the distribution of data. Discuss the following questions.\n", + "\n", + "1. What are the x-axis limits of the histogram and why?\n", + "2. What is the shape of the histogram?\n", + "3. Looking at the histogram, how would you interpret the outcome of the simulation if you didn't know what p was? Would you have guessed p = 0.5?\n", + "3. What do you think the histogram would look like if the probability of turning left is 0.8 ($p = 0.8$)?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "jb9SpLg5pmDI" + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to see visualization\n", + "\n", + "# Select parameters for conducting binomial trials\n", + "n = 10\n", + "p = 0.5\n", + "n_samples = 1000\n", + "\n", + "# Set random seed\n", + "np.random.seed(1)\n", + "\n", + "# Now draw 1000 samples by calling the function again\n", + "left_turn_samples_1000 = np.random.binomial(n, p, size = (n_samples,))\n", + "\n", + "# Visualize\n", + "count, bins = plot_hist(left_turn_samples_1000, 'Number of left turns in sample')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "M5XsvbkzpmDJ" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) The limits of the histogram at 0 and 10, as these are the minimum and maximum\n", + "number of trials that a rat can turn left out of 10 trials.\n", + "\n", + "2) The shape seems symmetric and is centered around 5.\n", + "\n", + "3) An average/mean around 5 left turns out of 10 trials indicates that the rat\n", + "chose left and right turns in the maze with equal probability (that p = 0.5)\n", + "\n", + "4) With p = 0.8, the center of the histogram would be at x = 8 and it would not be\n", + " as symmetrical (since it would be cut off at max 10). You can go into the code above\n", + " and run it with p = 0.8 to see this.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "1vMuXxA8pmDJ" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Binomial_distribution_Sampling_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "6D_MBuYapmDJ" + }, + "source": [ + "When working with the Bernoulli and binomial distributions, there are only 2 possible outcomes (in this case, turn left or turn right). In the more general case where there are $n$ possible outcomes (our rat is an n-armed maze) each with their own associated probability $p_1, p_2, p_3, p_4, ...$ , we use a **categorical distribution**. Draws from this distribution are a simple extension of the Bernoulli trial: we now have a probability for each outcome and draw based on those probabilities. We have to make sure that the probabilities sum to one:\n", + "\n", + "\\begin{equation}\n", + "\\sum_i P(x=i)=\\sum_i p_i =1\n", + "\\end{equation}\n", + "\n", + "If we sample from this distribution multiple times, we can then describe the distribution of outcomes from each sample as the **multinomial distribution**. Essentially, the categorical distribution is the multiple outcome extension of the Bernoulli, and the multinomial distribution is the multiple outcome extension of the binomial distribution. We'll see a bit more about this in the next tutorial when we look at Markov chains." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "KRhVPtWgpmDJ" + }, + "source": [ + "## Section 2.2: Poisson distribution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "FObKt46jpmDJ" + }, + "outputs": [], + "source": [ + "# @title Video 4: Poisson distribution\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'E_nvNb596DY'), ('Bilibili', 'BV1wV411x7P6')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "bm8dFIpopmDJ" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Poisson_distribution_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "C0e3pxG-pmDK" + }, + "source": [ + "This video covers the Poisson distribution and how it can be used to describe neural spiking.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "For some phenomena there may not be a natural limit on the maximum number of possible events or outcomes.\n", + "\n", + "The Poisson distribution is a '**point-process**', meaning that it determines the number of discrete 'point', or binary, events that happen within a fixed space or time, allowing for the occurence of a potentially infinite number of events. The Poisson distribution is specified by a single parameter $\\lambda$ that encapsulates the mean number of events that can occur in a single time or space interval (there will be more on this concept of the 'mean' later!).\n", + "\n", + "Relevant to us, we can model the number of times a neuron spikes within a time interval using a Poisson distribution. In fact, neuroscientists often do! As an example, if we are recording from a neuron that tends to fire at an average rate of 4 spikes per second, then the Poisson distribution specifies the distribution of recorded spikes over one second, where $\\lambda=4$.\n", + "\n", + "
\n", + "\n", + "The formula for a Poisson distribution on $x$ is:\n", + "\n", + "\\begin{equation}\n", + "P(x)=\\frac{\\lambda^x e^{-\\lambda}}{x!}\n", + "\\end{equation}\n", + "\n", + "where $\\lambda$ is a parameter corresponding to the average outcome of $x$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "OUZs7IbZpmDK" + }, + "source": [ + "### Coding Exercise 2.2: Poisson distribution sampling\n", + "\n", + "In the exercise below we will draw some samples from the Poisson distribution and see what the histogram looks.\n", + "\n", + "In the code, fill in the missing line so we draw 5 samples from a Poisson distribution with $\\lambda = 4$. Use `np.random.poisson`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "rDZZJvF-pmDK" + }, + "outputs": [], + "source": [ + "# Set random seed\n", + "np.random.seed(0)\n", + "\n", + "# Draw 5 samples from a Poisson distribution with lambda = 4\n", + "sampled_spike_counts = ...\n", + "\n", + "# Print the counts\n", + "print(\"The samples drawn from the Poisson distribution are \" +\n", + " str(sampled_spike_counts))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "FwaAnh5-pmDK" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random seed\n", + "np.random.seed(0)\n", + "\n", + "# Draw 5 samples from a Poisson distribution with lambda = 4\n", + "sampled_spike_counts = np.random.poisson(4, 5)\n", + "\n", + "# Print the counts\n", + "print(\"The samples drawn from the Poisson distribution are \" +\n", + " str(sampled_spike_counts))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "-qWygwpOpmDK" + }, + "source": [ + "You should see that the neuron spiked 6 times, 7 times, 1 time, 8 times, and 4 times in 5 different intervals." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "wItnCjK7pmDK" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Poisson_distribution_sampling_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "ts9l9INxpmDK" + }, + "source": [ + "### Interactive Demo 2.2: Varying parameters of Poisson distribution\n", + "\n", + "Use the interactive demo below to vary $\\lambda$ and the number of samples, and then visualize the resulting histogram.\n", + "\n", + "1. What effect does increasing the number of samples have? \n", + "2. What effect does changing $\\lambda$ have?\n", + "3. With a small lambda, why is the distribution asymmetric?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "TQXqODTbpmDL" + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "\n", + "@widgets.interact(lambda_value = widgets.FloatSlider(value=4, min=0.1, max=10, step=0.1),\n", + " n_samples = widgets.IntSlider(value=5, min=5, max=500, step=1))\n", + "\n", + "def gen_and_plot_possion_samples(lambda_value, n_samples):\n", + " sampled_spike_counts = np.random.poisson(lambda_value, n_samples)\n", + " count, bins = plot_hist(sampled_spike_counts, 'Recorded spikes per second')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "xFvdOSmupmDL" + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) More samples only means that the shape of the distribution becomes clearer to us.\n", + "E.g. with a decent number of samples you may be able to see whether the\n", + "distribution is symmetrical. With a small number of samples we would not be able\n", + "to distinguish different distributions from each other. The more data samples we\n", + "have, the more we can say about the stochastic process that generated the data (obviously).\n", + "\n", + "2) Increasing lambda moves the distribution along the x-axis, essentially changing\n", + "the mean of the distribution. With lambda = 6 for example, we would expect to see\n", + "6 spike counts per interval on average.\n", + "\n", + "3) For small values of lambda the Poisson distribution becomes asymmetrical as\n", + "it is a distribution over non-negative counts\n", + " (you can’t have negative numbers of spikes e.g.).\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "ANTEVjNkpmDL" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Varying_parameters_of_Poisson_distribution_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "d2QWTQJYpmDL" + }, + "source": [ + "---\n", + "# Section 3: Continuous distributions" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "0bs1ye2ZpmDM", + "outputId": "aae039b1-875a-4a8f-8c84-6a3aefda39f5", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 582, + "referenced_widgets": [ + "34bf5b356d7b4fbda255665049f8df36", + "44229b29b6e34fd99b9ca14fab650079", + "7f80f5b065704815976c3be30b3ae8cc", + "3f2f18285890479c94aa234de7375ea5", + "0c68ceb7ce1e4840b4ca4061fd958df6", + "083577639f014471b24bf14434020868" + ] + } + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "34bf5b356d7b4fbda255665049f8df36" + } + }, + "metadata": {} + } + ], + "source": [ + "# @title Video 5: Continuous distributions\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'LJ4Zdokb6lc'), ('Bilibili', 'BV1dq4y1L7eC')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "**Note:** There is a typo in the vido ~3.40, where the product of Gaussian distributions should be $\\mathcal{N}(\\mu_1, \\sigma_1^2) \\cdot \\mathcal{N}(\\mu_2, \\sigma_2^2)$." + ], + "metadata": { + "id": "yJ8OFJ5wqFB3" + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "PVQDpWh_pmDM" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Continuous_distributions_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "mzpIhIqVpmDM" + }, + "source": [ + "We do not have to restrict ourselves to only probabilistic models of discrete events. While some events in neuroscience are discrete (e.g., number of spikes by a neuron), many others are continuous (e.g., neuroimaging signals in EEG or fMRI, distance traveled by an animal, human pointing in the direction of a stimulus).\n", + "\n", + "While for discrete outcomes, we can ask about the probability of a specific event (\"What is the probability this neuron will fire 4 times in the next second\"), this is not defined for a continuous distribution (\"What is the probability of the BOLD signal being exactly 4.000120141...\"). Hence we need to focus on intervals when calculating probabilities from a continuous distribution.\n", + "\n", + "If we want to make predictions about possible outcomes (\"I believe the BOLD signal from the area will be in the range $x_1$ to $ x_2 $\"), we can use the integral $\\int_{x_1}^{x_2} P(x)$.\n", + "$P(x)$ is now a **probability density function**, sometimes written as $f(x)$ to distinguish it from the probability mass functions.\n", + "\n", + "With continuous distributions, we have to replace the normalizing sum\n", + "\n", + "\\begin{equation}\n", + "\\sum_i P(x=p_i) = 1\n", + "\\end{equation}\n", + "\n", + "over all possible events, with an integral\n", + "\n", + "\\begin{equation}\n", + "\\int_a^b P(x) = 1\n", + "\\end{equation}\n", + "\n", + "where a and b are the limits of the random variable $x$ (often $-\\infty$ and $\\infty$)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "3pEWyExfpmDM" + }, + "source": [ + "## Section 3.1: Gaussian Distribution\n", + "\n", + "The most widely used continuous distribution is probably the Gaussian (also known as Normal) distribution. It is extremely common across all kinds of statistical analyses. Because of the central limit theorem, many quantities are Gaussian distributed. Gaussians also have some nice mathematical properties that permit simple closed-form solutions to several important problems.\n", + "\n", + "As a working example, imagine that a human participant is asked to point in the direction where they perceived a sound coming from. As an approximation, we can assume that the variability in the direction/orientation they point towards is Gaussian distributed." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "79evZ33spmDM" + }, + "source": [ + "### Coding Exercise 3.1A: Gaussian Distribution\n", + "\n", + "In this exercise, you will implement a Gaussian by filling in the missing portions of code for the function `my_gaussian` below. Gaussians have two parameters. The **mean** $\\mu$, which sets the location of its center, and its \"scale\" or spread is controlled by its **standard deviation** $\\sigma$, or **variance** $\\sigma^2$ (i.e. the square of standard deviation). **Be careful not to use one when the other is required.**\n", + "\n", + "The equation for a Gaussian probability density function is:\n", + "\n", + "\\begin{equation}\n", + "f(x;\\mu,\\sigma^2) = \\mathcal{N}(\\mu,\\sigma^2) = \\frac{1}{\\sqrt{2\\pi\\sigma^2}}\\exp\\left(\\frac{-(x-\\mu)^2}{2\\sigma^2}\\right)\n", + "\\end{equation}\n", + "\n", + "In Python $\\pi$ and $e$ can be written as `np.pi` and `np.exp` respectively.\n", + "\n", + "As a probability distribution this has an integral of one when integrated from $-\\infty$ to $\\infty$, however in the following your numerical Gaussian will only be computed over a finite number of points (for the cell below we will sample from -8 to 9 in step sizes of 0.1). You therefore need to explicitly normalize it to sum to one yourself.\n", + "\n", + "Test out your implementation with a $\\mu = -1$ and $\\sigma = 1$. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "Ba4HT5PZpmDM" + }, + "outputs": [], + "source": [ + "def my_gaussian(x_points, mu, sigma):\n", + " \"\"\" Returns normalized Gaussian estimated at points `x_points`, with\n", + " parameters: mean `mu` and standard deviation `sigma`\n", + "\n", + " Args:\n", + " x_points (ndarray of floats): points at which the gaussian is evaluated\n", + " mu (scalar): mean of the Gaussian\n", + " sigma (scalar): standard deviation of the gaussian\n", + "\n", + " Returns:\n", + " (numpy array of floats) : normalized Gaussian evaluated at `x`\n", + " \"\"\"\n", + "\n", + " ###################################################################\n", + " ## TODO for students: Implement the formula for a Gaussian\n", + " ## Add code to calculate the gaussian px as a function of mu and sigma,\n", + " ## for every x in x_points\n", + " ## Function Hints: exp -> np.exp()\n", + " ## power -> z**2\n", + " ##\n", + " ## Fill out the following then remove\n", + " raise NotImplementedError(\"Student exercise: need to implement Gaussian\")\n", + " ###################################################################\n", + " px = ...\n", + "\n", + " # as we are doing numerical integration we have to remember to normalise\n", + " # taking into account the stepsize (0.1)\n", + " px = px/(0.1*sum(px))\n", + " return px\n", + "\n", + "x = np.arange(-8, 9, 0.1)\n", + "\n", + "# Generate Gaussian\n", + "px = my_gaussian(x, -1, 1)\n", + "\n", + "# Visualize\n", + "my_plot_single(x, px)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {}, + "id": "Ij82CxIKpmDM" + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "def my_gaussian(x_points, mu, sigma):\n", + " \"\"\" Returns normalized Gaussian estimated at points `x_points`, with\n", + " parameters: mean `mu` and standard deviation `sigma`\n", + "\n", + " Args:\n", + " x_points (ndarray of floats): points at which the gaussian is evaluated\n", + " mu (scalar): mean of the Gaussian\n", + " sigma (scalar): standard deviation of the gaussian\n", + "\n", + " Returns:\n", + " (numpy array of floats) : normalized Gaussian evaluated at `x`\n", + " \"\"\"\n", + "\n", + " px = 1/(2*np.pi*sigma**2)**1/2 *np.exp(-(x_points-mu)**2/(2*sigma**2))\n", + "\n", + " # as we are doing numerical integration we have to remember to normalise\n", + " # taking into account the stepsize (0.1)\n", + " px = px/(0.1*sum(px))\n", + " return px\n", + "\n", + "x = np.arange(-8, 9, 0.1)\n", + "\n", + "# Generate Gaussian\n", + "px = my_gaussian(x, -1, 1)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " my_plot_single(x, px)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "-08eK3E2pmDN" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Gaussian_Distribution_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "iAcfqBW7pmDN" + }, + "source": [ + "### Interactive Demo 3.1: Sampling from a Gaussian distribution\n", + "\n", + "Now that we have gained a bit of intuition about the shape of the Gaussian, let's imagine that a human participant is asked to point in the direction of a sound source, which we then measure in horizontal degrees. To simulate that we draw samples from a Normal distribution:\n", + "\n", + "\\begin{equation}\n", + "x \\sim \\mathcal{N}(\\mu,\\sigma)\n", + "\\end{equation}\n", + "\n", + "We can sample from a Gaussian with mean $\\mu$ and standard deviation $\\sigma$ using `np.random.normal(mu, sigma, size = (n_samples,))`.\n", + "\n", + "In the demo below, you can change the mean and standard deviation of the Gaussian, and the number of samples, we can compare the histogram of the samples to the true analytical distribution (in red).\n", + "\n", + "1. With what number of samples would you say that the full distribution (in red) is well approximated by the histogram?\n", + "2. What if you just wanted to approximate the variables that defined the distribution, i.e., mean and variance?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "GRy0DebTpmDN" + }, + "outputs": [], + "source": [ + "#@markdown Make sure you execute this cell to enable the widget!\n", + "\n", + "\n", + "@widgets.interact(mean = widgets.FloatSlider(value=0, min=-5, max=5, step=0.5),\n", + " standard_dev = widgets.FloatSlider(value=0.5, min=0, max=10, step=0.1),\n", + " n_samples = widgets.IntSlider(value=5, min=1, max=300, step=1))\n", + "def gen_and_plot_normal_samples(mean, standard_dev, n_samples):\n", + " x = np.random.normal(mean, standard_dev, size = (n_samples,))\n", + " xspace = np.linspace(-20, 20, 100)\n", + " plot_gaussian_samples_true(x, xspace, mean, standard_dev,\n", + " 'orientation (degrees)', 'probability')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "4_vdStvipmDN" + }, + "source": [ + "**Main course preview:** Gaussian distriutions are everywhere and are critical for filtering, [linear systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html), [optimal control](https://compneuro.neuromatch.io/tutorials/W3D3_OptimalControl/chapter_title.html) and almost any statistical model of continuous data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {}, + "id": "uq07GLimpmDN" + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Sampling_from_a_Gaussian_distribution_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {}, + "id": "mWduI9RqpmDN" + }, + "source": [ + "---\n", + "# Summary\n", + "\n", + "Across the different exercises you should now:\n", + "* have gotten some intuition about how stochastic randomly generated data can be\n", + "* understand how to model data using simple distributions\n", + "* understand the difference between discrete and continuous distributions\n", + "* be able to plot a Gaussian distribution\n", + "\n", + "For more reading on these topics see just about any statistics textbook, or take a look at the [online resources](https://github.com/NeuromatchAcademy/precourse/blob/main/resources.md)." + ] + } + ], + "metadata": { + "colab": { + "name": "W0D5_Tutorial1", + "provenance": [], + "toc_visible": true, + "include_colab_link": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + }, + "toc-autonumbering": true, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "34bf5b356d7b4fbda255665049f8df36": { + "model_module": "@jupyter-widgets/controls", + "model_name": "TabModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "TabModel", + "_titles": { + "0": "Youtube", + "1": "Bilibili" + }, + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "TabView", + "box_style": "", + "children": [ + "IPY_MODEL_44229b29b6e34fd99b9ca14fab650079", + "IPY_MODEL_7f80f5b065704815976c3be30b3ae8cc" + ], + "layout": "IPY_MODEL_3f2f18285890479c94aa234de7375ea5", + "selected_index": 0 + } + }, + "44229b29b6e34fd99b9ca14fab650079": { + "model_module": "@jupyter-widgets/output", + "model_name": "OutputModel", + "model_module_version": "1.0.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_0c68ceb7ce1e4840b4ca4061fd958df6", + "msg_id": "", + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Video available at https://youtube.com/watch?v=LJ4Zdokb6lc\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": "", + "text/html": "\n \n ", + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAUDBAoIDQ0IDQsLCgoKCgoKCgoKCgoKCgoKCgoKCgoKCwsKChALCgoOCgoKDRUNDhERExMTCgsWGBYSGBASExIBBQUFCAcIDwkJDxIQEg8TEhIXFRUVFRUWFRIVFRUSEhUSFRUVFRUVFRUVFRISFRUVFRIVFRIVFRUVFRIVEhISFf/AABEIAWgB4AMBIgACEQEDEQH/xAAdAAEAAgMBAQEBAAAAAAAAAAAABQYEBwgDAgEJ/8QAXBAAAgEDAgMDBQgMCwUGBQQDAQIDAAQRBRIGITEHE0EWIlFSYQgUMnGBkZLSFRgjM0JTVZOhsdTiCSQ1YnN1lKSytNE0coPB8DaCorPh8TdEdrW2Q3SjwmOEhf/EABkBAQADAQEAAAAAAAAAAAAAAAABAwQCBf/EACkRAQEAAgICAgEEAgIDAAAAAAABAhEDIRIxBFFBEyJhgXHwscEykfH/2gAMAwEAAhEDEQA/AOMqUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKVP8AkpP60X0n+pTyUn9aL6T/AFKCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/wCSk/rRfSf6lPJSf1ovpP8AUoIClT/kpP60X0n+pTyUn9aL6T/UoIClT/kpP60X0n+pTyUn9aL6T/UoIClT/kpP60X0n+pTyUn9aL6T/UoIClT/AJKT+tF9J/qU8lJ/Wi+k/wBSggKVP+Sk/rRfSf6lPJSf1ovpP9SggKVP+Sk/rRfSf6lPJSf1ovpP9SggKVP+Sk/rRfSf6lPJSf1ovpP9SggKVP8AkpP60X0n+pTyUn9aL6T/AFKCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/wCSk/rRfSf6lPJSf1ovpP8AUoIClT/kpP60X0n+pTyUn9aL6T/UoIClT/kpP60X0n+pTyUn9aL6T/UoIClT/kpP60X0n+pTyUn9aL6T/UoIClT/AJKT+tF9J/qU8lJ/Wi+k/wBSggKVP+Sk/rRfSf6lPJSf1ovpP9SggKVP+Sk/rRfSf6lPJSf1ovpP9SggKVP+Sk/rRfSf6lPJSf1ovpP9SggKVP8AkpP60X0n+pTyUn9aL6T/AFKCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/5KT+tF9J/qU8lJ/Wi+k/1KCApU/wCSk/rRfSf6lPJSf1ovpP8AUoIClT/kpP60X0n+pTyUn9aL6T/UoLnSlK6clKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKwda1D3snebd3nBcZ29QTnOD6KhfK4fiT+c/cqErRSqv5XD8Sfzn7lPK4fiT+c/coaWilVfyuH4k/nP3KeVw/En85+5Q0tFKq/lcPxJ/OfuU8rh+JP5z9yhpaKVV/K4fiT+c/cp5XD8Sfzn7lDS0Uqr+Vw/En85+5TyuH4k/nP3KGlopVX8rh+JP5z9ynlcPxJ/OfuUNLRSqv5XD8Sfzn7lPK4fiT+c/coaWilVfyuH4k/nP3KeVw/En85+5Q0tFKq/lcPxJ/OfuU8rh+JP5z9yhpaKVV/K4fiT+c/cp5XD8Sfzn7lDS0Uqr+Vw/En85+5TyuH4k/nP3KGlopVX8rh+JP5z9ynlcPxJ/OfuUNLRSqv5XD8Sfzn7lPK4fiT+c/coaWilVfyuH4k/nP3KeVw/En85+5Q0tFKq/lcPxJ/OfuU8rh+JP5z9yhpaKVV/K4fiT+c/cp5XD8Sfzn7lDS0Uqr+Vw/En85+5Vjs5u8RZMY3qrYznG4A4z49aD1pSlSgpSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlBB8bfef+Iv6mqk1duNvvP/ABF/U1UmoropSlQFKUoFK6+7KezLhOPhm34q1WymmYd8LmSC4uw7Z1Waxh2xR3KR8h3QOMcgTzPXK4I4G7OOK5PsbYm8sr50dokaa5SZxGN7mP30ZreRgoLFM7tocgYUkBxvSrt23dns3C+oTaRI4lEWx4ZwuwTwSrujk25O1uqsuThkcAkYJpNApSlApSujv4PW1jl1qZXRJF+xNydrqrjPvqy54YEZ5nn7TQc40q49uCBdY1ZQAqrrGpgKAAABezgAAcgAPCqdQKUpQKUrsbtmsol4G0uURxiQjTcuEUOcpLnLAZOaDjmlKUClbg7T04SGj6edOYnXT7z+ygP2RwP4nJ77x74UWvK77sfcj/u+bmtt/wAHdZxSxa7vjSTbFp23eivtymp5xuBx0HzCg5EpVh7NhYG+tRqBxp3viP36R33+z7vun+zjvunqed6KsPb+ugC+xohLad73i5n35n3xl+9/20Cbps/m+jxoNe0q/wB/2Q6pBpa8TukS6dJs7tu+Uyt3kphUiNckeeD1I5VQKBSlKBSlKBSlKBWx9H+9Rf0Uf+EVritj6P8Aeov6KP8AwipiKy6UpUoKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQQfG33n/iL+pqpNXbjb7z/AMRf1NVJqK6KUpUBSlKDtC1tZJuzlYo0eR2J2pGrO7Y4lYnCqCTgAn5KonuL+yTVpNWttXltJ7Syse9laa4jeDvnaGSKOKFZFDSks+WZRtCo2SCVDba7NOM7nh7gSDV7YRNcWxl7sTqzxHvuIJYH3KrqT5krYwRzxXOnHvumOJdYja2e7W1hdSskdjELcyA9QZstOFIyCquAQSCDQX/tg1my4h40tLcLHdWkV1YabMCA8M5jkJuFI6MokleI+B7s9Riti9tWicF8G3f2QutPiu7i5hjSy0eCKMwRRx7xLeTxSMIvukh7sNIGz3Y2KSshXlf3N/8ALmk/1naf+atbJ/hB/wCXR/V1p/jnoIHQeGLfj/iFobC1GladOsU80MUcKLZ21tbQRXDJHEBFukuBhcL8O4VmHwq2h2i8fcH8IXDaFa8NWervaExXdzeNC7CcYLostxbXEkjglg33tUYFVGByjP4NyaIalfRnHfNpu6Plz7tLmESgHHLznh5Z548cctBdscTpqupq+e8Gqahvz1Le+5cn5evy0HRHaL2aaFxXo0vF2h250+4s++a+08YEZFuoe4j7tWMcUqQlZkaLCujYZAzeZX/4Or+W5v6puf8ANWVbC9wSe50bWbiU4tRJJuLckHdWJe4JJ5YEbR59mK17/B1fy3N/VNz/AJqyoLnxxxTwXwtfXyzWC8Ratdaje3N67xQy2tmZ7qWUWaC5LRCWFHCPsRmLrIGZCBGjtf7N9A4o0N+LtFtksJrSOSW4to0SFGjt+d1DLBETDFPFHmZZI/hLgHIZSnNfbp/LOr/1zqn+enrqD3I8nvbhLXLiVSYN2quFIwJAulQK4XdybcRs9GQR4UHOPYXq2gWM011rFpLfxRwA2lrEWG+571Ob4ljQxiPcSHJH81jgV0D2Q9p3C3El9FoMnCWnWcd33kcNyiW0sgdY2dQ5Syilj3qrDekhKtt6gll1n7mDsXs9ehu9d1KeS30jTNxlEPKSZoYhczhm2syRJDtLBF3t3gClSM1sHsM7QuF5dYstP03hsW/eTssGo3V7I93Ge7kYuYm70BsAjb3xHneGMUGi/dJ8DRcOavd6XDu97IYpbbedzCKeJJQhPVhGzNGC3MhATkmuvTpOl3XCOknVLj3tptvFY3NztyJLgRRyBLWLb5++WRkB2DdtD42nzl5293n/ANoJ/wD9rZf+QK2n21f9hNL+LTf8E1B+9n3aRwHrdzHoHk5DaR3bC2trqW1tBI0jjbEks0Le+YZJGwiusjne65I5sNDe6i7L14U1NrKMs1nPEt1Zs5yywuzo0TtjDPHLG6+kr3bHm1U3svieTUdPjTPePqNiseOu9rmILj27iK6Q/hKbuM3unQAjvUs55HHiI5ZwsZPsLQy/MaDA90RwTptlwtoepQWdvBeXQ0v3xcxxhZZu90qeWTew5tulUOfaBU9/Bx/ete/otN/wapX37qP/ALG8O/Fo/wD9muK+P4OP71r39Fpv+DVKDnbsC02G81jTbWaNJoJr+COWKQbkkRnAZWU8iCPCtk+600XTdB4ht0isoUsYobC4ms4UWOOdRM7TIRjbmRE2En01QPc0/wAu6V/WNt/jFbF/hBv5d/8A+daf4p6DpDiPjbRouF4NXfR0k0pzFs0kmPZHuunRSCU2HEmXxjxrRnY3xNw1r3ECW32DtLWwvbH3tFbSxxSKl/C0s6zAgBV72LdCRjJYReirPx1/8PrT/wD1f/uD1yBoOqS2M0N7E2ya2miuIW67ZYXWSNsex1BoL37pbgbye1i709V2W7Se+bMAYX3rcfdI0XnkrES8OfEwmtsdnvBWm6Lwpd8TX9nBc3t+zR6YLmMO0e4tbWzRpJyz3nf3RI+HHGh54FbF90zwJ5c22ha/YpiS9e2tJ2Ch2gtb37pvmKtzWzmE6soPWV8emtfe764niilseE7bCWuk20TyRKQQsrxLHaxMMZDRWgDD0i7PsoOW6UpQKUpQK2Po/wB6i/oo/wDCK1xWx9H+9Rf0Uf8AhFTEVl0pSpQUpQCgUqS4d0Sa/kFvEFLYLM0kiRRRqBktJLIwRAB6SK/ZNIdCQcHaSpIO5SQcZBTIK+gg8658u9OvG62jKVKppsnUJG+OoBYcv+8eo+Svb7BvtD7CSR8EMqkHrjnnNLlr2mYW+kJSpS90WaMZ7qTHj8FipGQQdvjkHpX1JoEq8ztTHrsin5t2f0UmUqLjZ7RNKkZdEnA3hO8XqWiZZAB7QhJHyio4jHKukFKUogpSlApSlApSlApSlApSlBB8bfef+Iv6mqk1duNvvP8AxF/U1UmoropSlQFKUoOjm7W9K8jBwr3kv2T/ABfcv3X8tm+++/A/2fn8fLrXONKUFt7Gtdg0zVLDUJyVt7W9t55mVS7COOQMxCjmxwOgq5+6449seI9UGo2bvJb+84Id0kbRN3kbSlhtbnjDrzrT9KC1dlHHN1w5fQavb4MkDEPGxIjnhcFZYZMfgspPPntYKw5qK6J4z4k7O+LpBq97NqOj38ir75jgidu+dFCAsY7S5hfAUAOvdswALAHkOTKUHS/bB266Vb6YeEuH7eWCxcMl1eTBleaNzulWMOxlYzE7Xkl2kKGRU2lStT9xtx9p/Dmpy399K0Nu+nz26usUkxMrz2rqu2JWYDbE5zjHL21pSlB1PxFxF2ecUTzX16L3Rbs3Exklskdob9e8fu7hlW0mCSyptkfMcbb3bLP8IxPbz236Y2mpwhoUMkOlpgXFxIrI1wofvtkYcmXa83nvJJtZiCu3aTnm6lB0X7krtq07Q4LvQNUjZtM1BncypG0oRpYBb3Mc8anvHglhSNQYwWUqeRD5Scg4+4E4TmS/0i0udWvu8BSa5M6QWcTMBL3QuEjZpu7LBCY2I8ZBzDcsUoOifdh8V8M8QNFrWn3dxLqchghubZ4JYoFto4pj3jGWAfxgOYY/MkZSoPLlk3mw7d+GE0HTdBvIH1MLFb22o2qxSxvbIkMpNxBK4RHmSYRKNkinEjkMMYPHtKDrTgrXOzjhmb7N2s+paheRbntbSaKUi3kYMv3PdawQkhXK7pZJduAwywBrnrtg4+ueJr6bV5wEaUhYoVYslvBGNsUKk4ztXmWwNzM7YG7FVClB0V259rGl6tw5o+hW8kjXunjTvfKNC6IvvfTpbaXbIfNfErqBjqOdfPuOO1fS+GY9VS9kkjN8lmtv3cLy7jCt8JN2webgzx9euT6K53pQW/sW16DS9UsNRnJW3tbyGaZlUuwjRssQo5sceAq4e6449seI9U+yNm7yW/vO3h3SRtE2+NpSw2tzx5w51qClB1L2I9sGgXOhtwdrnfwW6b+5uoklkyjXBuoyDCryxzxTsdvmMhRVByMqdGdsFlolvdCPR7i5u7EQoTNeLtlM5Z+8AHveE7Avd4yg8etU2lB3d7gvjJ4NDvmugyWGkzzyx3B2lBEYffVzAgB3FomzKcjn77UAnmBxbx7xLNrF5c6pL99vLiSdlyWCB2JSJSeqRptQexBW3+IO2yxj4bh4RsLe6hmfuvshcTCJUlJc3F13bRymQ77gIg3AfcgVPorQlApSlApSlArY+j/eov6KP/CK1xWx9H+9Rf0Uf+EVMRWXSletpbtKwQDJP6h1PxUt0SbflvCXO0DJP/XyD21K6fEuGXAIHms/QsfFVPVVz49T7OefK4mWAiEAE/Cbl12gnmfRn8EU0xvgr4dT16k8+lcY5b7d5Y+PSb061B5KoA8MDA/9asVhopbA+Fn2YAyfTXjpAT4XMKMY5enHTHPNZevcVpBHsGfZtBXJH870Z9HzjxtmeOPtHhlfTIu44bYHOCR+ApHh8tas4n16aZ8KAo3YJHgo5c8Yyfjr6vuJpZSX6ZOAOfTrn2D5a/Pe6N90XBzncfhbT185c4Px1m5OTyy3WjDHrUWjhxN+DIQfNHjyUcsEgYGenzA1Py28HQYBI6HAz8lVTh+dgQGbkDlcAAfJnl08au+o2gnj3KVyAOfMkn2HxPxCqZyWel8xlnbB1LhKVYRerGWgJ2iZRkK3qlhzRvYcZqsXid55rgSY9flJ8ay/Dz7H3Cpyx1e4tw1szv3bHzkDMFbBBG5FOG57eueg9lQHE8UsbCdVzETzOemeowelX8XP5dZM/Nwa7iv3sIQkDJA8GGGHsI6fKORrwqwSRpOobHnry9GQ3Ig/F1HtFQEi7SQeoJHzVdvtmsflKUqUFKUoFKUoFKUoFKUoIPjb7z/xF/U1Umrtxt95/wCIv6mqk1FdFKUqApSlApUpwrw7d6rMllaW8t1cSZ2RQoXYgdWOOSoBzLNgAcyRUtx/2datoBQX1jPaCX727qGicjOVWWMtEXAGSgbcBgkcxQVWszRNKnvpUtIIpLieVtsUMKNJI7YJwqqCTyBPsAJ8Kw67B/g39KhY6rfBUkvoY7WGAOMGOOUXDthvBZZIowcdO5HpoNTR+5b4tKd59jAPQhvdPEmMA5wbrA+IkHl0rWXGvCd9os5sby3e1uFVXMcm0ko2drqyEqynB5qSORq6cT9rvFcF7LJPqeo215HMwltu/mhihdTgxe9NwgVB6mzB6885MHxPxLqfF99bm4kS4v5/e2nxSbI4BIzSlId+xVjU75cFgAMUFMpXbGue5XH2BihgsIRxGxh98zNeSFPNmcyFS8xt1zFtHmKM1yv2jdm+o6BdppN1Gou5UikjjhkWbcJnaOMAry3F0Ix8VBT6Vv7QPci8UXSCR4rSzJ5iO5uh3mPDItklVc+gkEeIFa37WuyzVeF5UgvoBGJgzQTRussE4TaH7t1/CUsuUYKw3KcYYEhSaV1n7jHsDttWhfWNTs47qxuEB08++ZUIkguJ4LjvI4JUJG6LGJMgge2tT9rPYFrXD0Emp3MMEdoJxGvd3EcjDvWIjG1TnGBQU1Oz3VDYHXxaSfYxTg3e6Puwe/FtjG/f9/YJ8HqfRVWreluvFHkq5Bt/JneN4+4e+d32Tjxjl33+27fH4OfCtScGcL3us3CafZwPc3MudkSbRyAyzMzkJGgHV3IUeJoIaldEN7jriYJvzp5bGe6F2+8ezJg7vP8A3sVp7hDgW91W/XQoEQ3zSXEQR5FRN9tHLLKO8J28lhfB6HA9NBWKk+FtAutUnjsLWJp7mcsIoUKhnKo0jYLELyRGPM+FdidvHuVhLBYx6LYQwzL3x1BpLyQlmKQCMBrmZ8gOJuSYAz7a527OuHNb0nXotMtBCmtW080UQdo3hEgtpTJln+5sO5L4J8ceNBR+LeHLvSZ5NPuoWt7qHZ3sLlSyd5GkqZKMV5xyI3I/hVE1ffdBDVRq119lTGdT/i3vkw7O7/2O37nb3Y2f7P3WceOa9+yjsX1viYGSztc26tse7ndYLZWHVQ7+dKw5ZWJXK7lzjNBrulb1419yrxDpNtNqEnvKWG1gkuJu4uWLrFEhkkYLLCm4qqk4BJPhmtX9nPAmo8Q3AsLG3a4m273wVSOKMEAySyOQkaAkDJOSSAASQCFapXQmq+5B4lt42nzYSbEZ2SO6beAoJI+6QKmcD1q57oFKUoFKUoFbH0f71F/RR/4RWuK2Po/3qL+ij/wipiKzFGeVWe1txZxFzyldefLmM9FHoOf1eysPhKzDuJCMhCCBy+fn0A6166/egqz8yMls/FlVA6+Az8orPy596jRxYdeSqapKHY+cd2CBt6lyMYPyH9dfOn3dzGBGdvX4eAzEY5jIOBj0isaMZG8DmxPieg6k59OcfPVh4c0YyMRks+3mSMKB6Ap/5/8AvFy1DHDyvSW0O4kYAl3K+gbsfMvIDFWdtOM6gFO89Hmgnw+XFTfCXCisVZh+CAFwCAB8Y6nrWzdJ0GNMKBgZB6Cs2XLjK9Pj+LlZ9OftR4Qzz7sjr5vIbcdOvx5qCGkT2h5LlTyK4ypBHQ+I6V1je6BHOR5o64yB19P6KRcCwMwYrlRzwccz4cq6nNK6vw+3Mx01sb43IA+EhXzlPX4yOnOvWz1CWI4zuHt5H5+QFdG6n2fwkZVMEeIHPH+uKpnEPZ8hU7R5w9mPD0Go/Uxjm/Fy01BqesFjlfN8CEUlx4c2J5D2E451KxZKiNvO3AblPMYPMMPQT7PZWDf6D3cpR8eY3nEh+QOBk+dg/Oa/NX1yOPIQkgryIHUjpzOT6a7s2yXeN1UbrOnPbb0GdrMNh9AJ6AA5yP8Aka8+I9OIxcKMxsq7sfgsFAOfHB65qNv9Va4JJZsqpwdxwPaBjGfj64qxcIahBIpDkkbQrc+TAqo5r0z7RVt5LJtTcJldKpSs3WrQQyMoOUzlD6VPSsKtMu5uMtmropSlSgpSlApSlApSlBB8bfef+Iv6mqk1duNvvP8AxF/U1UmoropSlQFKUoN+e4t1+3trq+05rhbC+1bTZbDTNQb/AOXu5D9zTd1UvJ3bDmNzQIo85lq19sIvtB4Z+wWsXJutXvtWNxawy3Pvua0soCqmfvGZiscjxSbQD0uz0IkVYT3LmnDTdN1TjCKz+yWp2LxWmm2/dtKLaSYL3l6yJlmCrKp80AgRSjcN5Zd0dl+mNxBY2l5xba2yXEOoJFo89+ws7rUGl3slrc2/dqGhMgG1CPuyoCYyo3zBwrqOnTW2wSxSQmWKOeISxvH3kMo3RTJvA3xOvNXGQR0Jq1djvaVfcLXY1G0Zcle7ngkyYbiEkExyAEEcwCHUgqR6CQZ33UGqa1c6rONVj7m4i+5wQJk28VruYw+9mxiSFubd51Yls4IKiJ4B7JNX162m1Cxt/faWswhmijkQTgtH3gdY3K94uMDCFmyfg450HXWna7wh2pItvcRCw1ru9qAskd8pVSxFtcbQl/Cv3QiJ1JADt3afCrk/tw7L77g29W1kkLqQLiyvYQ0QmRXwHXmTDPG4G6PcShKEEhlY/vBHZLxJcXcEEGnaha3ImjZLia1ubWO1dGDrPJNJEBCEK7s9cgBQWIB3z/CTa9byS6bpysr3Vul3POB8KGO497rEpxyBkMLttPMBEPRhkJzj/XLtOBbK8W5uFuWNqWuFnlE7brqUNmUNvORy5muN59fu5JkvHuJZriJkaOaaRpnUxNvj5ylshW57Ty68q7IutGn1zgK2gso3up4VjYwRKXmY219IsypGPOZ1XLhQMsByByM6I9zHwt3XEem2eo2skAMksywXkLwl3S1uJLVikwUlTcRpt5EMygc80Gba9mnHfEhXUmh1Gfe3exzXl4lqV3HIeGO7uI2SPnle6ULtxt5YrdXu0rO7i4X0hb3LahFeWEV07OsrmcabeictIpIcs6AlgTkjNVn3XfDPF+r6vJZw2+oXOmOsK2KWwf3hsMKd4ZnQiGOXv+93NckN0wdmyrj7pvgi+XhLS9OSFri402bTxdpbAz933FldWsxHdglgk8ioSBy5+igr38G1qM0kuo27SyNDDb2vcxNI7RRb5p2fu0J2puYknaBkmuXOLuJr65eW3lvLqaETv9ymuZpI8q7bTsdyuR4cuVdJfwa97Gt5qNuWAkktLeRE8WSKZlkI/wB0zR/SFaA7UOz7VNInuffNldQxR3Ukfvl7eUWz7pH7tkn2906uBlcNz+Sg6Nsf/hzN/Sj/APIIapn8H/xXY6bqs0Fy8cMl/ai2tJpCFXvRMkhtt55IZtoxkgM0SKMsyg3Ox/8AhzN/Sj/8ghrmjgDs71PXlnaxtmuzZrG88cbxiUJLvCsiOwMvNCNqZbmOR54Ddvbl2I8V6NdXGtW9xd6hE8kk3v2zuJvf0UZfconhRhMNu484d6KqkkoOQ5ztdVuIpffaTTJcbnbv0ldZt0gYSN3itv3MGYE557jnrXXnuNNT4xt7+PTLqDUTpAjm98fZO3nRbTZEe497zXKrIr98I4+4Qsu2SQ7BtLroT3VEdouvamtrsEAuhyjxsE/cxe+wMcgRd9+CB0OaDor3emu3dnZ6I8FzcW7SJc940E8sRfENkRvMbDdgk9c9T6a0X7ka7kn4l06aR3lkea5Z5JGZ3dveVzzZmJZj7Sa377tHhi71zSdG1CxglvooY9zC1jed+6u7a3aKUJGC5j+5YLActy5xmtCe5KsZbbibT7eWOSGaOe5WSKVGjkjYWVzlXRwGVh6CKDL92JZNc8U3tsuN80umRJnON0mnWKLnAJxkjoK377sriyThDS9O4d0xms0uEki72E7JUtbNYQyiRcOss0k6s0gO5tsuT55zz97su6eDie/mQ7XifTZEOAcOmm2LKcHkcECuhfdI8LN2iaPYa9pQFxcWwd/egdFkKXCxC7t/OIX31BLCnmEjcFk27iyBg4mg4q1CNZI1vbtUnR450W5mVZkkBWRJVD4kRlJBVsggnNdNfweXEdnE+paQ8y2t7qEcBtJdwV5BEtwrpFu83vozKsipzLAucYjNaGi7IOIDHPcNpN9DDaQS3NxLdW0loiQwRtLKwa6EYcqiMdibmPIAEkCvDgvsz1fWLeXUbG0ku4rWURzC3IedHK94pWEHvZMjp3YY5HhyoNj9qvY7xZwq02oCe6uLYl2l1KwurjcU87z7pQ4uIjsGWZg0Yzje3WtDV3Z7ibV+K5Jp7HU4r1tLitiY5dUhlSaO5MihIYpbhRNOjRmXcjb1QRx4KZCvxr2nC2Go34te795jUb33p3ODF7298y9x3ZHIx91s248MUFdpSlApSlArZGij7lF/RR/4RWt62VoP3uL+jj/wigu+iRd3bu/icj29MDH6arPE9wqwY/Cbr8jcvkIAqzpOBavnkVDHPh0LCtY6rcmRWTJ6rj4h1rLjjvJqyusf6SHBlq1y6gHzg2AG+B0znA+Fg55fF1rd3CfDohBA5ljzY9T4+j4+XtrV/ZJADJu5+avjW/8AhmHIBPjmsvyeSy6b/gcUuO6l9AsCu0k9PQMD2+k1cEjG3l1qF00YOMfEaslrECPafRWSZbr1pNM20jAVT8nQ9fTWdZpkczXgseEC451k2z7cA4yPjzVkmnN3plww+H6f+jXlqekRygjxPj41mW0vsFeofPhV8mOmbLLKVzN228OTWR98KuVOY3YY5qeeTnGD/pWjyGb1m5nB7rB+dRg13vxBpEN3G0MiB0cEMp8f9DXPXG/Yh3ZeWCVjHz81ssR/NznmKt4856rJz8Vy/dHP10uPNHU9ck5ppUhiDEfB6ch6eRFXLUeFjab16sUPn5B83qQBnkc48M9Piql2PPvIw2cgADn6f/ar52wZSyrvdaV39vvA89V3jA6+sP8AnVOraPAF0rWwU4LB5EbPx8vnBxWt9Vi2SOvgHYfpqfj33HHPPWTGpSlaWYpSlApSlApSlBB8bfef+Iv6mqk1duNvvI/pF/U1UmoropSlQFKUoLNwBx9qmgO81heS2jyqqyiMqUlCklO8jkVo3K7mwWUkbmxjcc+fHfHOpa7ILi+u5ryRARH3reZGGxuEcagRxBtozsUZwM5qu0oJbiniW91WRbi7uZruVIYoEknkaRlhhXbHGCx5Ac2PizM7HLMxM92cdqes8Oh1sL17VZmDSII4Jo3YDaGKXETpuxyzjNUulBvHVvdWcVzp3QvYoM8i8FpbLIRggjc8bbeucoFIIGCK0vql/NdSPcTSyTzSsXkmmdpZZHPVndyWdj6SSaxqUF37M+1nWuGw6WF69vHKweSEpDPCzgAb+7uI3RHKhVLoFYhVBPIYjuN+PtS1q6XVbu5aW9RY1jnRIrd4xCxaLYLZEVGViSGABzzzVZpQbbu/dJcVywm0OrShChQukNrHcbSAOVzHAJ1cYz3iuHySd3TEVwR24cQ6LA9ja6jJHBI8kpSSK3uSJJSDKyPcwu6bzliFIG53bG5ia1zSgk+F9futLnjvrWeS2uYW3RzRNtZcjBHoZGUlWRgVYEgggkVc+0Ttv1/iCAWF7e++LYOkhjFrZw7pI921y0ECPkbjyBx05VrmlBbU7SNVXTzw6LpvsWxy1p3cG0nv1us953XfD7uofk/hjpyrH7P+PdT0CRriwu5bSSRQsmzaySKpyokjkVo5MEnG5TjLY6mq1Sg25rvuk+K7yI2z6o6o67XMFvaW0rA9SJbeBJUOOXmMtajpSg2RwB258RaFB7xs9QeK2UsUhkht7lYi2Se698wuYl3EtsUhdxJIJJzXrfj7Uo788QLcldTMskxuhHDnvZUZHbu+77kZR2GAmBnkKrFKCY4y4mu9YuJNRu5TcXU/d97MVjQv3USQp5sSqgxHGi8gOnpzUr2ddpOr8PM0lhey2veffEXZJA55YZ4JleFnAGA5UsASARk1UqUG1OLfdDcT6pDJZz6kxt543iliit7SASRyLskRmhgWQqykgjdg5PhVb7Ou07WOHS/vC9ltRKQ0iBY5YnZRgM0U6PEWxy3bc4qn0oNpcY+6D4m1aFrKfUpO4lXZLHBDbWveKRhld7aFJGRhkMm7aQSCMHFatpSgUpSgUpSgVsnQvvcX9HH/AIRWtq2VoIzHF/Rx/wCEUFlvATBJ6CAPl2/qqiy2+0+knLfF41soKGt2Uj8PAHpwOf66pGpwBOXiRg+nkOdZcL3WrLH9sWrsmtjh5PDkoP8A17K6A4Si3qvh/wA6032fxiO3Rugdi3ydB+qtrcN3rhcrGxI6dOfy5wB+msHLPPOvX+N+zCNh2tmABjx51nQnZgemqUvEN7EdzwEKPHA2n5QeXzVN6RxJHc45YOeanrkVzeOT214Z7W8AY9uMn5a9bWLnmozSbwSM464OP0dKzZNTWM8+g68vAVbMZXd2mIY/+v8AlXoRiqVP2hwRNtKOc81OBg8+vXpWXpnGMVyfNBA+XPzGu7466Y7LvtY7iTlUbPh1I9IIr6e8VxjIyfQR/wBCsGKTr7P+dZrdVZMdzTQnaRbd1KynmOY5+G7qCK0fYRBLoL4M3Tr6SP0j9Fb190VJ3DFx+GilSPBtx5H2NjHy1oH34DKjA4YP+sEV6GF3i8jnx1npbOB7wpJInPG3fj4gCfiqvXcm5mb0sT85q08O2yqJpc8+5VOQ6AKpOfQcjw51V7tMH2HpireHW7ftm551HjSlK0sxSlKBSlKBSlKD8ZQeRAPxjNfHcr6q/RH+lelKhLz7lfVX6I/0p3K+qv0R/pXpSgkuC9NhuLyzt5I1eKe/soZU+Dviluoo5FyuGXKMwyCCM8iK7e+114X/ACYv9rvv2quLOzr+UNP/AK007/OwV/SmlI1T9rrwv+TF/td9+1U+114X/Ji/2u+/aq2XrGpw2cT3U0iQwQqXllkYLHGg6szHkqj0mvrSr+K6jS5ikWWGZFliljYMkkbgMjqw5MpUggj01CWsvtdeF/yYv9rvv2qn2uvC/wCTF/td9+1VsTR9ftbx5oYZ4ppLSXublI3VmglGcxyAHzH5HkaiuzPjqy4ithqVmzvbtJJEGkjaJt0ZAbzW54yetBUPtdeF/wAmL/a779qp9rrwv+TF/td9+1VtaozS+ILS6lntIriKW4szGt1DG6tJbtKGMYlUHKFgjEZ67TQa8+114X/Ji/2u+/aqfa68L/kxf7XfftVXHgTjez1r3z72Z294Xs1hcb4ym25gx3irn4SjcPOHI1ZaDVP2uvC/5MX+1337VT7XXhf8mL/a779qq38WdoWkaTILa81C0s5mjEqxXE8cTtEzOgkCuQSpaNxn0qfRWfwrxVYaqrS2d5bXqIQrtazxThGIyFfu2OxiOeGwcUFB+114X/Ji/wBrvv2qn2uvC/5MX+1337VW1q87u4SJWldgiRqzu7HCqigszE+AABOfZQat+114X/Ji/wBrvv2qn2uvC/5MX+1337VWxuHdbttRhS8tpo7m3l3d3PC4kjfY7RvtZTg4dGU+1SKkKDVP2uvC/wCTF/td9+1U+114X/Ji/wBrvv2qtrVG8Ua3DptvPqExIgtYJLiYqpZhHEhdyFHNjtB5UGu/tdeF/wAmL/a779qp9rrwv+TF/td9+1VsPhTXIdTt4NQhLGC6hjnhLKUYxyKGQlTzU4I5VJ0GqftdeF/yYv8Aa779qp9rrwv+TF/td9+1Vtaq52kca2fD9q+p3bOltE0aO0cZkYGVwieavM+cRQUz7XXhf8mL/a779qp9rrwv+TF/td9+1VtSNwwDDoQCPiPOvqg1T9rrwv8Akxf7XfftVPtdeF/yYv8Aa779qra1KDQ3al2EcOWWm6hew6csc9tpt9cQv75vG2Sw2sskb7XuCjYdQcMCDjmDXF3cr6q/RH+lf0b7bv5H1X+p9T/yU9fzoqYivPuV9Vfoj/Sncr6q/RH+lelKDz7lfVX6I/0rM02LLAY5Dw/UBWPU1wxbFm3/AIKkH4z4CuOXLxxtd8ePllInZk2Rqvq5ZvjPh+r5qo2tSmWTAAyMDxzuzkj9AzVy1C854GCwPmj+eeQb/u9aqaQkSnnnaQMgjmx5np1rFx/dbeSeo2foOnZSKLkAqDPhzxk/pqzy6jcRslvDyBwZHHhzxgY6YHM459KxuD7cSlSegA6eI/0rYV5osZCyDajj1RjPsI6H9dZZ7exx4dK/YR3CSFTIZY8MTlJJC33NigA7zPNyAeeR151HcQNJZSBgQHKh2RX3gEgEhXwCcE4KnmOoyKtXeyw8xtBHQgEHPhiq9r0kknJiW3HkCOpPpPy1o5OTC46kOPgzmW92/wAWemwuza5NyvfYxuAz7T4/JUzrdvHhm54XmfHPs9tYHBFt72t1UDHmjP8AvHrUmbhCpDdTn0881mxumvPCzuNdHWJ2Z2hiwkeC5Ch2Vc9WZztB5/B9lSmncVSFVZ4vhd5gGOMkCE7XZ+6cyRoeRVtuCOYr3k4eEbEoq7GJOF5deZ+Op3TLGCJcGEL7Snp8ByHiM1r47hJ3tl5ZfKeNmvrXf/Lw06+jvAZY9qspwyKd2Rjkc55jl1FZwc8h0JHoxz50tdJVWMi+aW5fBG0+OMYBBHtryuMq2TjPTp4Vjzydau+mnPdQQ744sfCfzV9AKknB+PNc4xaNOqi5YBED4BY9TnoPTiumu367jMccJGWJ5H8JSCDuHxVoLjm7ZgiH4CK21RyAJ8MD9Zrdw39sjy/k4Tztv4TGk6wqLtKMySKcsgDMMgqeXjg+3wrEvdIkVDL8JM8iOuD4kdRUfw0WKn+bt9m3d7T7f+dX7T7hincyDqMKcY5noGB9PTI5fFVP6l48tRxlxTkx21zSs3WrUROyjpnI+I1hV6eOW5uPMymrqlKUrpyUpSgUpSgUpSgUpSgnezr+UNP/AK007/OwV/Smv5rdnX8oaf8A1pp3+dgr+lNRUxrr3TP8har/AFfcf4azPc+fyJpP9U2H+Wjr17dNGm1DSNSsoVMk82n3KwxrzaSTumZI1/nOwCj2sKoXube1fRn0SySTULS1lsbOG1uobq5hglhe2UQ7mSRwdj7AysOR3Y6ggQl++5r/AJQ4n/r5v8DVX/cXaibPheS7Ch2t5NTnVSSAxiBkCkjmASuM1Ne5Gm9/fZrXFDC11TXrqWydkZO+tovMSZVcA4JYqfQyOOqmqv7k3/shdf7msf8AlPQWnse7W9c4oW1u7fSYYNPLhL+8uLhl3N3jCVNPiwHmEUezdM/mmTvUA+5nNh7J+JIbvV9fsksba1lsZtPWa7hUCe/M0VyyNcEKCTGEIXJPw26V4+48/wCzumf0M/8AnLioHsC/7Q8Wf/udH/y95QVzsL42s+HrTiXVbt9kMPFGqYVcGWaU933cEKkjfK5GAMgDmzFVVmG8OyrWdR1G0jvr60TT5rj7pHZq7ySwwMAYxcF0XbORzMe0FcgHDblXkjh7gS/vfsnr1j/GrvROMNUuk0ucd7a3igws5SLH+2KF81gdxwAuGC56w7H+0Wz4ntE1G2O0/AuLdyO+tbgDz4ZB6R1DYAZSCPQA0h2x6jptrxjYy6g1slmOHWDm8EbQbzc6iEBEgK7ielZPYFoNrdcQXvEOkQC14f8AePvISRxm3tb6+DxGRrWAhQIIxGQWVVXehxneTX52parY2fGdjPey28FsOHGVpLt40h3Nc6iFBaUhMk8gPGongfVbA8YRpoDK2nz2Era6tkCNOMqRzmGVQo7kSCU2q74ht3SuAdzzUGybrtO1jVru8stEsbOeDSpjbXd7qNxNDFNeLnvbW1SGMsWjxgyOQuT4DazzXAPaSmvaVdag1r3U9mL611DT5m3rHdWsRM1uz7QJI2Vl546OR1Brnzsz4b0yz1DVtG1bUb/R7wanPeWjLqsmm2l9Z3LYiljKuIpJjtBOTuIkVQCY3Cbs4F0DRNP0zVk0q89/RSreTXUwvFvv429n5+ZlJBcpsY8ycnnQfPZZ2mWNtw1HxK9pDptnFFdye8rJVEaMl/cW6xQLhF7yecDGdo3zcyBk1C6l2wcQaXbwcQahpNpBotw9v3yQXc0upWEF0yrHPcI8KxSY3oDGgDZYA7TnGvdC4ZudW7Pora3RpZ0FzcLEgJeVbfWrmWVFUc2fu1dgoyWKgDmRUhw5ovBet6fHPPr94sMsMRubK+4hdGhkXBMUltcSZJSVDtO0htishYYNBt/t+7V24XSwnW399x31/FayCMs0qxONzPAiA99LtB2py3EqM1A8Zavrl7omuTalYW+mxtpFy1lBFcm5uAGtrrvlunGIw6gQY2ADznz0wIz3TiRq/CqxndEOJNKEbZzujDRhDnxyuDWyfdCfyJq39U3/APlpKDSPZz2n69DodtdadpEU2naTpcQubu+uDA921pB/HBYwJ5zJCY3XvWOHZWCglSDuCXtgsotDTi6VHjt3tY5u4BDy99IwiFshwA7d+dm/AGAWO0A4g+Df+yEX/wBMN/8AbmrUXFPD1xqPANmIEaR7aOK7eNASzQw3M4lIA6hEcyH2RtQbIv8AtL4rs7TyguNEsRpyxC5ms4r6Y6pb2pG9pX3wCBmSLz2QYYAHIXBxH+6+1+DVeFX1KBt9vdHTp4mIw217mI4YZ811OVK+BBFT3aV236BJol1epf2kputOnjhsxPG1y888BRLd7cEzIweVA+5fMUljyrVXaRw5PpPAMFjOrJOvvaWSN1KvH761JrpYnVuauiTKhU4IKkYHSg3b289qLcKWtleCAXCXN/bWUoy26OKSGWR5Y1QEySARck5ZzVN4t7cda0M2upalo0dnot7dLbDFyZdTtg6u8UlxCgMYdoo3k7hcsu1kJDAA+vuv/wDZ9B/+ptJ/8u4r192x/smkf/VGl/8Ak3tB4cR9tOu6U9lf3ujQ2mj6jfQ2SK90TqkHf7jFLcRKDGjmJHlMABK7SjMrc66Crnz3df8AsGmf/Umnf5e+roOgqHbd/I+q/wBT6n/kp6/nRX9F+27+R9V/qfU/8lPX86KmIpSlKlD7hjLkKOpNXDT4O5QKOuCc/H41GcG2AkYyHOByHtPj+sfPVh14bFOBj0nwAHh8ded8rO3Lxn4b/i4anl9qqZO53MTzGefXmTn/ANahDOVYMepwT6BnnUtqsWCg678t83/vVa4glCsCDyBAJz89d8frRyX8ujezFu8VW9i/qGa21Z23IZ6VpTsXvg0cZ+T5iR/yrdyzqkeSQAB6f0e2smOPeq9/hynhEFxrqMdlGz4ycHA8c1WdFV7iVS2DlQ23rgkdPbivjjUd8GZjnPwefIeNRHAvEkUc4WXzegDY5Z6Dn4VGcjTLJpvCxtysYFelu64ORnHsr1j1q2MS4YE9eWD+qsCHUEfIXPP8I8gB7M9fmrq4+tdonJ5b312ybSa3m80YVgeYzWVLpIPPJx7D+moDXNGwomh82ROeAeUijmVPtPpqX4Q19ZQAfDkVPVT4gjwNdzHvWSvk3rePbOjh2Dbjl7aiNcIHPxq03UysOQAqocVSbR8XP9POuObGTqKZnbN3po3tiuNkqx9QQckk9Sc/8q0bxWMkRkjO7OefIDx+blW4O0m/Wd5MjJB80DPLqB+vNae1bEshUAEA8wfSMY+PxrTx9R5XyL5b/wAsng+TLgHo2VYew5wcezlzq9aW24ANyAbIOSfH5xVH0C3MTlyD8IBeXUZA5+wAj9HhW19OslYD0MAcf7w3fozWX5E/dtZ8e/t0qPHmmnImUeaQQSMYGPi9POqhW5ry1UgxnG0gLjpnl6a1ZxJphtZCnVTzU+z0fGK2fE5dzwv4Yfl8Wr5z1UZSlK2sRSlKBSlKBSlKBSlKCd7Ov5Q0/wDrTTv87BX9Ka/mfwXeJb3lncSNsigv7KaV8FtsUV1FJI2FBZsIrHABJxyBrt77YThn8o/3PUP2WoqY2lVR1rsw0O9la7n0rTp53bdJNLZW7ySN60jNHmQ+1s1W/thOGfyj/c9Q/ZafbCcM/lH+56h+y1CWzbS2SFViRFjjRQiIihERVGFVVUAKoHIAVg6Tw7ZWcJsoLW2t7Vt4a2gt4ordhIMSZijQRneORyOfjVA+2E4Z/KP9z1D9lp9sJwz+Uf7nqH7LQbH0fTILKNbaCGK3gjBEcMEaQxRgksQkcahFBYk8h1Jrz0/RbW3kmuYreCGe6KG5mihjjluDGCIzPIih5ioZgC5ONxx1rXn2wnDP5R/ueofstPthOGfyj/c9Q/ZaDYej6La2fedxbwW/fytPP3EMcPfTvjfNL3ajvJWwMu2ScDJry0nhyytJJbmC0tree6bfczQW8UUtw2WbdNJGgeVtzMcuScsx8aoP2wnDP5R/ueofstPthOGfyj/c9Q/ZaC58R8E6XqTie606xvJVQRrLdWdvcSLGrMwjDzRswQM7ttBxl2Piaz9C0O0sF7m2toLWPr3dtDHAmfTtiULWvfthOGfyj/c9Q/ZafbCcM/lH+56h+y0F54o4T0/VQq3lla3ojJKC6tobjYT1Kd6h2E+zFeujcOWVlEbOC0tra2bdut4LeKGBt4w+6KNAjbhyORz8aoP2wnDP5R/ueofstPthOGfyj/c9Q/ZaDYuiaTb2MS2tvBDa28e7u4LeJIYU3sztsjjUIu52ZjgcyxPU1B3vZvok8pvJNK06W5Zt7TyWFq8rOCCHZ2iLM+QPOJzyHOqt9sJwz+Uf7nqH7LT7YThn8o/3PUP2Wg2Fqmh2l13RmtoJzbSpNbGaGOU280fwJYd6nupVxydMEeBrJ1GyiuUe3ljSaGVGjlilRZI5I3G1kdHBV0YEgqQQQa1p9sJwz+Uf7nqH7LT7YThn8o/3PUP2Wg2NBpVukIslghW1EXcC2WJFtxDt2dyIgvdiLZ5uzGMcsV9aTpsFnGttBFFbwRjbHDBGkUUa5JwkcYCKMknAHia1v9sJwz+Uf7nqH7LT7YThn8o/3PUP2Wgtdn2d6NDN7+j0vT47oOZBcJY2yzCRiS0gkWLeJCScsDk5PPnUzrujW1/Gba5t4LqBipaG5hjniYqQykxyqUJDAEEjkQK139sJwz+Uf7nqH7LT7YThn8o/3PUP2Wg2FrGh2t4I1ntoLhYJUnhWeGOYQzRgiOaMSKRHKoJw64IycGv3W9Etb4IlxbwXKxSpPEtxDHMsc6BgkyCRSElUMwDjBG44POtefbCcM/lH+56h+y0+2E4Z/KP9z1D9loNha5olrfqsdzbwXSRyLNGlxDHOqTICElRZVIWRQzAOOY3HnzqQrVv2wnDP5R/ueofstPthOGfyj/c9Q/ZaCx9t38j6r/U+p/5Kev50V2h2pduHD97puoWcN93k9zpt9bwp71vV3yzWsscabntgi5dgMsQBnmRXF9TEUpSlShsDs8QMrADkkK5P85iWPy5H6BXvxN50AI6k/oPh82Kw+z2fZDKfEuq59m0/q61naqrSQrgeaMu3Pw/0ry+ezzr0uCfsjXmu3JYhc4CLt5deZyf+VQGrxhgoHT9eP/XNTvEMahN45tk7vjz1/Tj5KreoNt2j0KevxDFaOL0q5fbbvYzeEQK2cGNnVh06OeX6RW3+INTZREozgx7iR6WyBj0Een/WtBdiuoAmS3PU4cA+O4bT+kCt+6BMtxGkTbQ8Z2DdjmD069TWTkmuTT1ODPfDEJrUnfAqDjAPU45dMfLy/wDevLQdHhbBZQ58Mkhc4OMkEE+nA6/pr4430DvVbu2Ctz6jK7h4kAg9QKsXZvb2LR9xMu2ULABuJAMhkZT3ZJ6EcjjoCvSpsnuLc8Mpjvuz7j6tZZFBiQbBgcgOZJ9vgMV96LLOjYw2Bz5g4PUHHsFbW0ngjTt+/YdjIPNEr4VssMjDZ5jHX21GDR7O3KSyEd2m4yiRzmQMxC4UsM49AHhU5ccv5V4/Jwvrd/pFx6nIAOXMjp6Rjr8dV2/vJLOb3yoOGI7xR1dcA78dNwHPPj81evFML38qRWS+9IYye9n3OZJcblG0E+ahBDc+fhjxpfaL3QEZd5mH4bncTnAzgDp15fpplj4/ldvPW7Nfxfa82WpCQBhzDAEejBFQnGlzshkk6EK3Xn0HiK9tOjEahF6KAB8QGOdVztPvBHbyZP4B5Hln01XvysTlWhdUulk707girPBHLJIdoQTCYocZLsoELsdoOAOeOho/Btl3uyRskszE5xk88nOKj9dvmnLDON77iFzjxx8uCfpH01fOH7EW0AJ5MRgZ/nHzv0AVr5JrHX28fDLzy/wx7HTPuzegcuQ+M/JyIrYfDyFgnhtGG+Yj9Y/VVV0GZQ5bAK8wSf5wPhjkcEH2cqu/CkQYZzj4XXllc4XHgTmsmc8smrG6j8nt9xCeJ5c89PE/FiqV2iQI4cKOVvsAbnlgeTciem4jBGa3RoWmpdjYw9inkD7MHxOfAmnEnZOkytGs5WR1I89QQNpBC8j0NaOHhu/KKOfkllxcsUq7cU9mGo2LNiIzqpP3kEyADxMRG9h/Oj3r/OqlOCpKkEEciCMEH0EHmDXovMsflKUogpSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSsrS7F7hxEvUkc+eAM4ycVFsk3UybWzhn7naM5OA0pA9OdtTM8nfW5QHby54BJx6PZXlxVEsKQ2UfwIlJJx1fBBY/OawrGVlUhcs204yWCL/POPhEDoBj4xXkZZTPO2fl62OHhjIo+uRYXuueSwGPEn2+ioXUYQGK9dpKNjmARyb5c/qqw3APfKCSfOGGORuJbHIeA/wDWoy6tym9uoDMefsY9fZWvj9M3L7Y+hXvvOaOdegIVyCfgnrkfMfkrfWl6qCFmU55ZGOfy8jXNU9xtJA6dSfT/AKdavXZ3xR3QW3kbzT8A9Mew+z0Vzz8ds8os+LzTG+N/P/LfbzbxnGQfH29SOXL9dSmi2Ecg6jb6GwcezHWqXwfrgnzGcnYxA3YK8z+AN2AcdeXhV20y1AOVIywz6Dkc8A+Hx1ml1XtcPNnJvFPx6YOQWZ18fNdh+o8+VTdlo9ogBO6R8fCY5I+U5Pz1D2tpIdpBwBj4/R49RyFWSxsgg5nmT0+X9VWfqfS39Xk+tf4fUcaxAgACq/f5eUciVUczj0nr8n+lWNsZ9n4Xx1Wp79TNtGcEZOAMY9vLI8fmqrLtTyZfaWVliXP660R7oPivCGzTm782PgigePxg1f8AjviTu1MUeDJjqTlUHrNzz6eVcv8AFl80kjyMxZm3AFup9uB1J5Y8BiruHDvbB8nl1jqMDhW2Eki7hlVbe59KgghefixwPiJq6z3TXDkLyCBkUeG7luPzn/wiqrw1A+xpFByASvLoegOD1wMnHpAqZ4duBvEfPo2fTk9T+jFd82Xu/TLw46k/lPramMbjnafg+lhyGT6MnnV04UdsCIKW80YxnIJyQR6Tiqe12ZHHLzFITHhy55Hs5/oNX7hMFDyGQwXOcZGBnI8ayz/yafUXbhXdCveq2/BIeJh54U+IB+GDzq06DqYuZch+XMFJcjA64BxvQZxyIIFVR3DBWWQCTJRuhYKCANy5Gc5GDyPjzqf07ZhnljJIIjjkXkdwJwwfG5cn1sg8udetxTU1GLk77bHk0eG7QI67jgMQwwynwdGByPYyn/mKqmv8AxvkXFrHqdsfw2RDexenDEZmxjwIb0dKsvD0klvtDuJYSVAl5LJC7clVhjaQScB1OD0IOc1ZpZ0QFnZFUY3MzBUbcQFILHAyxAxnkfkzqvTD+XMPHXue7W4ia40uUmRSxWF33QyY5mIOxLRSj0E+wjxHOV9p00Eps5IpEuA4jMDKRLvbAVQnVi2RjGc5GM5Ff0ZSPu3aVYwO8xvOB54XO07h1fDHzueeXSsLiLT4USS8WGJnaFj3ndIZfNQ7CH27zj0Z5VFw2jb+dNKUqpJSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBVx4AbHQY55ZvWGOnPwB5/GKp1TWjaosKhOYJbLN6PR8nsrP8AJxtwsi7gsmctXTXomZRjrlgfTy5/N0qJSVk+5HHNfEej5M1coI1kjEvUbQG6ZXA5Pz9lU++tGEmeR5cm655ivM4pp6mfaJ4g2EI4UBon5ZwMq2GA+cGksKNuU488H/xAEY/R89fOsWbMSPDzQ3xKMnl6f/SscXPeYUjBQ+H4SHp4/g8x83o57JWaztT7zQ+8zjwJUcuQPo/0qOEBXwO5TtPs9HzVNe/WjYuOeXYOp6Mvo+MdQfCvh1DtvGefXwJGMj2HpV8t0zWSrbwHqpgAkz5xIRsjOxSQd2egz5wPxL6a3twjq6uqyFgQwz4c+o5/N09lc88Ow4kx+C4K+w/9GreGltvgswTeHwPDp5uOm3ryFZeSS3T0ODPLCbnbo6HUF28iPj/X7az11pdpPIhcYPXqP1mucLfja5XAbki7geoPMAKQcnPItz+KpV+Ip3VTzClcnb1Ab8FufwhyPjXP6djVfm2/hsXizjQIrAHBJXmPAnPLGefh0zVZ02/upssjAb+bPz80Z5/97GeXs51gcMaHLcYkkyAcnzvhdfMIBGMgeJzVm15o9PhKooUnOF5DLHxPy880kk6VXLLLuqDx/qPcRtGpJdgQSSST6ST1/wChWmdVU7sZAzjrnd8Z9H/vWwOKZHlPm+cxJyx6Dl4eJ9n/AKVTb+0KMWOCQvM9RnkD16+POr8Lpj5u/Sx9n8kaJKh8QQpPUttPzDl+kVHcPR4m6HkzezI55Ptx/rX3w9ZyNkg+YoLt4gZwQD4DGOntqW0K0A3zn4QBAHjhiF+Tlnl8tU8mu59rOOXUv1UnHZlCScd2zbkYcxg45YHtq6aDcFSMcxhS2SPNyP0VX9M89DEQNoIZCc/BGcirPBAqIZD5sadT1JI6D2t7PCqMdrrraWuLgyJIwHOGPfuHLkrA/H0zV84TuffJjQE5ErbSPBAhIA9mSKovD0qyQ3D4xuhkVfSBtOPlq+dk9k2yCUgnO9s+g93y5/Fzr0+KXxx/li5rJuth8IWrCKSN8us0s5CEABIVcx5Xl1ZgGHh09BJ97pvuZtHUXETlkV9okVoxjcZVwcMCMHltOM+kD6i1AKuQNzPgIgOMoOSgHBIyCW+N6q1nw99hXGoxyTCJ5VjuLaSTdHCshzuHPDDmWLnLElfOILCt8n4jB/NWmxs4tPdIk+529wNiKPvSXI5oEHSLvULDAwC0a45tzkkOwmM/BfIweiseWcehjyI9PPoaTQRyqbRwGjdTsPg6dRgjo6cuY5jzSPZgWjP51pKS0sS7klPW4t/giQ4//VQ4R8eJVuW4AczsfzkpSlUpKVPcD8Opqbzwm5S2eGxu7yJWieVrp7SNpmto1Qgh2iSVt3nECJsK3QY+g8MX+oL3ltZXl1Hz+6W1rPPHkciN8UZTIPhnNBE0rI1GxmtnMM0UsEoAJinieGUA5AJjkUOAcHqPA1j0ClKUClKUClKUClKUClKUClK/M0H7SlZFrYTTLJIkUsiQKHneON3SFCdoeVlUrEhPIMxAzQY9KUoFKUoFKUoFKUoFKUoFKUoFKUoFBSpbhTSmvJVhUE55nGMAeJOfCot1Eybq48K6qVgKEEsc4x4jHh6TnrXxpDbixxnHT2ePL/rrV0n0SKxtTIzDzeRcLjPUBQOZPPkPkqnxK6xtLt2CTIhB5M3Lm+Phf+9ePl3n09fHrHt4zRbwABlu8yfVB6cz89QvE9h73xgc2GAR0OBkgDwUempGa4YmNBkKiKTk/CL8yx+I5+esTj69VgmQwbYVUnxB6Nj0E55+01dhNXSvO/lS2tgWV+WCc48OfMn217XKIjJt87chzj0942Mf9eIr8vrWQIh6KF5DxJPXPsr80FFLoGBZ3yBz5Ko3MPjJb9ArVGX0tej6YyQxXR+C0sifEV7s/wD9sfPVvMAZcHGehr2e2WbTCVXDRTys3j5xjDgD0Z2fORUTHqS4h6sZVC4GeRXzT4eyq+bj1JfuNfByfhkWcIUlSAR6D/yzV44YtYmwQoHxADn/AK1CjT+QYDly8cn5aktPgCechIPiD0NVTemjx3el5Bit13MRnBx4k49AzWo+NtZNw5fnjoik5xj0D4+fzVYdWvZCuzPIjHjyz+mtfa5cLCdpwzcyc9AD4+0/oGPGovSdSRhsxbJzzGRuPJVz159CSfAeysK301ZCZCQ4VgNufNBzgZ6bj7Byr1lujcgoo+BG0rEYAVFwOWeXPIAHtrEij5+auAqhifjyP1Cu5LJtmtlulqsljjhkYkBF85j6SPwQPSeQxWJwxb96CwHw3UAegDe5J9JGV+cVB6s5CRxnO0uSRz5+byz7edWnhM7YeXJiQSfQp3D9Q/RVNn7d/brfek1ptqHd/CNQFyvUgbRge0n/AK5VK6ojSqIVGFUHAU8uuSc/pJ9NfmkoCh25OGX5SfE56DBz8oq3afpQUZb1WyT4ZUnl8pFWcHH5XpznlMVatl2DYOWRyPUecuBkfhE+it12pW1t40bzAUYsx5BIwpMjHJHVRtz/ADxWnJ4DIHWPpDbu4J5HcgLHJGfR051uDW9QVcTFN8PdRKcgFShj7xjt/CBaRAcdNvsr0/j4d7+oxc93qLFwlukbvDht/wAAj8DzQe7bJPPHMEdfZjnaNSs0lQxuu6N12PjqADlXHtVuefDr4VS+z23NqWgbzUm/jMJyW2I3MJk/inwMehgPCr7FLn2EHaw9B/0I5j2Gu8+r0ovtTNGkeyk+xc7eY3nWc/hnwA8B1xt8Oa9GTM1q0bOgmGBPbMXAzgEhcSRE/i5YyevTKHqtOK9GF1EYhydPulu+cbXXmUz+CpxyPhyP4ArF0d3vIvu0bRt3QSVHAHfAg7ZCuSVUjJ2nnzIPQV3Lvv8A9/7/AC49P5zUpSqHTffZBx77yhttV1CSzurezCaRplrBaWsusW7YQGZX2pLHaral1bLsZCUG3c2Xwu33itHnvrWQavY6ha3McFlAl+G0o2sb/wC0CBUja3M1tiVQu/JePzhhhVM4Fto73TtTsVt4ZL+IWupW0vdBrxre3k2X8UMnwgEiaOURrzYNPyPLFR7y41CZcvLdXVzJFErSyNLNNK5WKFTJKxLEnYg3HkNo5AUSu3BXFjai0eh6nK91ZXLrDBczsZbvTLmVtkNzbzvmXuRIyLJC7FCmeQwQ1F1qwks5ZrWQAS200sEoGcCSB2jcAkc13KcHxGK2hw52X35t51XTLtNc0/UrJ0M5VLN7SQPyUyfxa4Ec8O52ViNkqEMQGWqj2x3CT39zOt5b6iZikj3dpA1tbSStEocRRmSTKqRjeHYPzbJJNBbte7KdP0o27X+tJax3lpBcRCOwnuJ90iky70ikIit4yUxMx+6EuAq7CTHXnY/eJqh0ETQttgF618cpbrYYy1267iygN9z2Anz8edtO8e3um76K5uLFopEmUcPachaJ1kAffeMUJUkB9rIdvXDL6a2lqXGtja60A9zAlvf8MW1h78+5XEFtcO8ssTTA7ojFtxuVwV8+Pd5pJohp++4EsZ7e5u9O1T7Itp0QuLyCSxmsXNtuKvdW5lkYSxp1ZDhlXBJyVDRPaFwcdIjsJjMJvslpsGobRH3fc98N3c53t3m0Eef5uefmitk8RRa1ZW109xqWgW0M1ncQYso9MNxqKSphrW2962SzbZRy3krt81jjGV+eMuH14ksdJura+06FLDSorDUBeXa272ktuqKzujKWKHDkdCQEK7g+QELbdjLSalbaJ78G670lNT7/AN7Z2FxMO4Effjf58X3zcvJvg8ueLp3ZXDeTpp1tqsFzdxrLLqjLbTpY6dBAq99KLtyEvAsrCMd2F3FgTsAYjbltrdkvElhOl3A9svDMcS3BljVCR78dQxLYSQxFX2E7gGHKtM+551u1t2u9OupltIdW0mfT/fb8ktppExG0hPJIyGcFiQAQmcDJAZEnZtZXkNxNpmrDU5rGFri4tXsJ7KWSBOUk9sZXbvlX1AM8155ZA31D2ZWcNlZazeastna36StsWymurgSI+EjijhcmVTGGd5SFCEIuHLjFm7PdC8jzdave3dg7tp1xa2VpZ3a3Ut5PO0RDKFUFbde7XLsOQfJ27edZ7UryJ9I4dhSVJHhtdSWVEdWeM99aqBIoOUJKNjdjO1vRQZ2vdkljpcie/dchgtbtUfT5orKe4kuYnVGM8sSPttIEMijezsG5nzcHEevY5d/ZOfQ2nijSzhN1cag6kW8dlsVxcFN2Qx3he7LDDLJ521S1fvuir2KeLSBHIkpThyyRxG6uUfYwKMFJ2vkHzTz5VtTi/iXT5tX1TTXu4IodZ0OCxivu8R7eG47mQxiV1baqMsxOSQMhB+GKDXPD/ZRYasZzYa0tylpazTzLLp89tOGjXMWyOWQCS3kYMDKDmMhAVbeCJ204W0V+H1dtUSNG1hWbUfsPdPKkxswGsO6V+/dFGW70N3Zx0zWX2NcIjh+e9lvb/Tkmk0e9igt4L2OYujd3JLM5wojQd0gQN5z7pDtHdmq52f2qaxoUmhRXFrDqEOrLfLBd3CW/f25tUhJiZ+TMrl8jw2jONy5JQumdndpDaw6nqOpjToL1pPeEaWU15c3MUZA98mKNwYYcFW55yHj5gsoN47JuGYI4eIbCLUbS5t5dKsnGojfFbxxyG9LtcI2XgeJVZnjyxA2+JwMLXtGHFFjpiWt1ZR3ukWh029s7q6jgbEPdxpcwNkpNC/dFt6kqQ688qRWPwbY2+k2vEVib6yupH0i0w9tMGhacvdrJbwu+33w0bPGpZBjMijkcgEK5q3Z5ZyWM+rafqg1JbBoVvoXsJ7GSNZ2CpNGJnLPHknkQOSSc8qVrXVbP7J76KLSuIY3kjR5bXTliR3VWlInugRGpOXILoCFz8JfTWsKBSlKBSlKBSlKBSlKBSlKBSlDQZ2g6VLeypbRjLuT15KqgZZ2PgqqCSa3foHCkOmwP3TGWbzRPKeX3RULtEmMbAo5kcz0B5nl99iHArwL3zjbNKo75h8OGJwGW3jJGEmYFXd/wFKgcytbC1OzSCARqoRI9wwBgMreYQmebMSQST/O5knFV543KaaOKSXbWfGsh7lYuT7VWRsk4BY8sADmc5z6ADVU4nDd3HIG+6bRyXJCLjBL+jJ6/88Zq/vpLBzCSU8wAheeUB3BckcwG649tVzU085oGU7drYwp54B83/eJxWGSS69fht3+YpDXSuElAA2CNSOufQTnp5wYfNUVxYpuZDK/LGAMeKKOQHXB5Y+X01n8Kr57W7jCscrkcsg9D8TKD8pr5miAZllRineEZT4cZzyK55HB8K6xxu9uMruMK7uhsVPEqMgAFkj9J9HLw64xSz09IMXPPAKouOQHeDdluWcY/Tisq50Q26GRZFlibJDqPumD17xTzBHiRkdfiqU0rHdxop3PIxDDCujIoCAc+WMDPj8VX4TtTlem2eyHTEmsRI6lleUO3oCkMgJHo2MhJ9JrGl4Xt7WXbtAAZgDnkCxzn0DPLNXH3PlixspYmzjdcd2MYwFEZUdOm5fR0FSutaUs7AnBEqZwcDmp2kqfHzSh9mRW7l4/Lhln40q4eTXLZVGe07k7SPNPTlWLLAVPIemrqOH2RSrHcB8H0qPR45+WoW6sinmnof+uteZcft6uOWlD1e2ZvTn2Ejl7MeNU7U9Eyx8F2H0nJJ5Z9NbR1OEA5/wCuVVPiB8A46jJ6eGOY+aq7HWU2qfDw7kT7urQNGoOOfnBt+fADaDX7bKIpQhBPmR5BxyLLnA65XP66+7o7oogp+6zHzMfgqCd7P/NIBGPEE+FYmo326Z58fCMe/AIxhdpOCAQCwNXTfgxXrLpncY6arpFMngcMp8Dt5f8AMVnmAxhIR+GUJx6B1/SD85qMa8zGwPUY5fKQOXpAqw2brIyofhxrHIvtV0G5fjGc1iyyt/ra+TX9rXwsufufIMysV/7px6fRV1s1Kp3pclQpI5AZyvIc/CqDw8hV4yPBse3PP9Px1fmQybbXGdu4tjlgjOM4PpxXofFxmmbnvb07O9IEkiB/g3MbZHI+bIjjHs6Hl7Km9H0+5nlGnuUa3jt4pGZeUilAI8ektlWY+wAZOSBncE24XuX5fckBOCfwGYH5cMa+YoTZqNagBaQyulxDkn7g8mdwHRQu7d6OYOcAg+njjrH/AH/NYMst5bXO9hadO7UhLiE5hP4PeYyUwOsUqfrPoFZ2haiJUE5ypUFZVPVQpwwb0tE+efqk+kVibFTbe7sq6hsjp3bEMCP9xiGH80mvG1g3zvOpO2cBliHwO+jOyWZyOiFdmAeTFc+ipslxcb7e3HKyXFvLsZoxHskGOXeICdzZ6gAbjgYPm8+RxUhpV0JhFP07+1Vse1cH9HeGvbWrYyRSQD4T20yr6CdoH62HzmoDgS77y0gbl9xl7v0YjfKxj6Dxmqp3j/ab7fzwpSlUumXo2pz2Usd3BI8FxA4khljIDo4BGRkEEEEqVYFWVmUggkG5S8U6Pet391pctvckh5JtHvBaxTSZyX96XEEsVuxOCTCwySTgVQqUHQHZT2uOJjpFssGmWtxFfMt5qt/cX0sd972dra4knuXWBIzLHGGgWMKS7EEsfO09xbxGt+sCe8rC0kt1mWSWwto7UXZkdSHlSICPKBMLtAHnueQIUQFKJfgFAK/aUQ/AoHhQqOuK/aUH5iv2lKD8CgeGKYr9pQTWlcJX11EbqK2kkh+6EMuwGTuV3TGKNmEtxsHwjErbcHOMV5rwxee9zqAt396hDIZfN+9B+7acJu70wCTzTMF2A5BbkavXZ3xNptmLKSSRIntxdJdB9Pa8uWed7hY5ba5dilparDLCWSAJLujn82Qy7qhNSv8AT5oopjdXSzQaRb6WbS3jaJpmtoRaFjcMDF7wniBleNhvJkdNvPdQQmqcHX9ogmltJI42eNOis6ySgtEkkSMZYZHAO1JFVmxyBqQ1Ts/vbaD3xNDIkrX1tYRWwRZXmluIrqQqGikbbOjWyRm3K78zpnbyDXry40y1eaeJ45FOo6XqFvbpprQytDYXxnMF1eys891emJx90ldot0TsHzJtEXw1xNpujlGjuJr7GsxX75tmiK23vDU7Nm+7Ph71GvUdhyRiqAOfOKhTbjgnUFkjtTaSNNMszwomyXvRbqXnEbRMyO8aqSyAll5AgEgH1v8AgTU4Ead7R1iiiadpN0LqYUXdJLGUkPfxovN2i3BBndtq0ji60tlS1jkhZFh1hi1jp32Pt1nvdJl0+2Cxtmd5Wcr3kh2oqiIDfsLVGaNxNbRGxLFsWmi6tYy4QnbcXn2Z7kL6VIvbfLDkMtn4NBV7rQLqJpo3hdXtI1luQdv3GOQxKjsc4w5nhAwTnvFxmo6tj8baj3em2iOkkV/qEFsl4silGey0d7iDT5dr4fbcK8J3Eed9jlIJBGdcUClKUClKUClKUClKUClKUCr52NcKtfzrMV3JG4CKV3K8uA25x4xxKQ7DxJjXq9UzSrJ7mRLdPhyuEX0DJ5k+wDJPsBrsTsy4NTS4UTq+xeYHQE5wPWZmO4n8JiB8FUpJt1PtYdJ05LSMRr4BmLMc5PwnkcjqSSScYyWwMZGIQW/exrdsd5LZtougABO2Vsc2dl88eCqQevWYu/4yzREj3vFg3LA4DtjctuD6gXznPoPTz+XnO4t4muWB755ClvCACWJOI0CDoPglgPE457VB0+Gojy7VLV7ELkuxCkTOjKoLDa7rtA6k/B+f2VRl02d7yzjycy5mkT4WxFzgk+0jHx8q2Xc6XqUqASQRIELgyFwzecd7bUXIB5jqT4VJ8J8ORQhbv4UjqkjO3M4DZAHoUADl7Kyz4/6mU6X/AK0wndaI1rgiRFFygZmMjkKMljl3K7fSCoqt2l4pf3tLGxLyKM7Ty8PPXqcenqK6pi0Nmit+aKd0BBKFsFgBkjeM829POqvwhwZEby+aXbLLC0YQ7AoUTPIGZRuYqehHPliu8viby3HGPypMdVoPiDQ4lCpDuBfL4EhKgDJ3EP8AA5H088keyvDs+0KczPIPvUMUku0AkAhQoAA+CS8g5n0iul7/ALOdPkkYmBUdm2+Y0iofNIHmh8ZzjpVS4q0/3nBcTRIIH70RTdzlVMaGKVABk7R3JycdWXJ51bx/Cvl5fhxl8nGzUSHZFq/vKe30yRCvvkTsZGJUrIhGI8FeYaME5/3APHF5giCHuuRGCCB4OGK8x4HCc/kql8UaKUOn3SEq8bs+70YCDP6anLjiSMTGOQCO5ctLGp3CKTuY0aRu8APcgs8fwiPHma2TDU/ixm8u9pWdPCoHWLAMDj/o1OQzGVVuMbQ4yVV0kUEnkQ6cmUjmGHUEVi3xGPTmvG5uO45ar3OHPzxljU2uW7bmAOAgG5mZuZPM8umB/wAqqWtTLImQRs2k7uqn0AdCxPT0VfuOowyOowNwIPLkR7ceFVXQ7ZbmRd4CrEAzg8xuyAOQ+Eqglh4Z2/FVGPHvLS3ky1jtXBbwWcAZx93cZXqxw3SMeCD1j8Qqlm6bf3uOW3ay4+F53UfE36q2DxdYieUSBSqFTtQnOAhAz7ScZPx1WtV0oqFIGQrke0ZJOPR41fzcfjNMmGXk8tJtSy7slQ/wSc5wOYOOvLp8tSyQOFjLYEgDBWHR1DZUhgcHk1eN82xYwBjC9ennZORipPQZgE2NjbuDbTjKE+Kejny+KvOuLVv8LBw4c4ZvwWU+OeRx4c+uPnrbHZ7ZmRzKwPieefWIxz65HP560/PL3XQ4z5/h05Z/QK6F7PrfbDu9IVxkdUkXcvzHcPkr0fgY76Yvl5eM39sXSF7t7iA4VYjLhvQJULZ+fnWaZEZ1tOkEcaLckHkYSCFjb2eO4cxnrgmo+fHeXLE+a1sc56Zjzu/QR+mtbcF8Ub91srFJJbkGSYsWG+ZmWFRkkhVjReRPifRXrdTHv7ef7rY1vr0tus9i0LXEEIaSKdHXY0DY+5Ejmp2tzOPHA8CbzwjpsdnGiAl3MbBpGOWYxsoA9AUKwCqOgHxmqdwvbLEJLBmMhaEJuOASGUocYGABKmQP/wDJ7c1O8E6kZoYXJ5hgj59JjeJvl71Kjkn7f7RKt2/mjehyD8TqwH/iC1SuDfuZv7PoYpTKnoAXcqD/APhU/wDeqyX2pRQI0juqBcPzOPgMGwPby6VXrOCY3stzGq+9LuElZXb4eEhB2RjmR8I5bbndyzWeTqz/AHr/AOu7+H8+qVTPKuf1Yvov9enlXP6sX0X+vWd3pc6VTPKuf1Yvov8AXp5Vz+rF9F/r0NLnSqZ5Vz+rF9F/r08q5/Vi+i/16GlzpVM8q5/Vi+i/16eVc/qxfRf69DS50qmeVc/qxfRf69PKuf1Yvov9ehpc6VTPKuf1Yvov9enlXP6sX0X+vQ0udKpnlXP6sX0X+vTyrn9WL6L/AF6GlzpVM8q5/Vi+i/16eVc/qxfRf69DS50qmeVc/qxfRf69PKuf1Yvov9ehpc6VTPKuf1Yvov8AXp5Vz+rF9F/r0NLoxJ5k5PIZPM4AAA+QAD4gK/KpnlXP6sX0X+vTyrn9WL6L/XoaXOlUzyrn9WL6L/Xp5Vz+rF9F/r0NLnSqZ5Vz+rF9F/r08q5/Vi+i/wBehpc6VTPKuf1Yvov9enlXP6sX0X+vQ0udKpnlXP6sX0X+vTyrn9WL6L/XoaXOlUzyrn9WL6L/AF6eVc/qxfRf69DToX3P/DrXE3vor5qusYY9Au5O9IyMEtujj+J5PRXU/EU7I5jTBlkZY4gfghyDvdsdVRNxOD+DJ4gVwRwj26ajpcfcRQWRXYEzJHcFs72kL5W6XzyXPP0AchVom91Zrbyd+bbTNwjESgW91tRAcnaPfvInCg9eSj25s48pPZl9R2hptmi4iziGEF5JGx58nJ3kbwzuO8j1ig/ANOFV98zPqTJhecOnxtnAVQRJcMOvM559cKepIrim991LrUsRtTb6cqMRvKw3QdwDuIZjenkzZJxg8zUkPde66NuLTSlCKERVt7wKqD8ED3/0OF+iKtvNi48a7kuLTajLnJ3FyxxliQCT6BnHQch0qsTnurWUDrHb3OPZsWUr+gCuR5fdia+3L3rpXoP8XvOf9/qMl91VrbJJEbbTdsySI33C7yBIhRiv8d5HDHGc866w58Y4y47Xc6LiKH2Na/44xUDw6MX2pD0ran5iDj/xVx+vut9dCLF720zCd1g9xd5PdFSuf49jmVGcD09Kx7X3VetxyzXQttM33KqsgMF3tATbjbi9yD5o6k9TT9fFH6dd26nDg94PwXQ/NKmf0E1rPjxh3F+hHjGRy8Wtmjz/AOH9Fc2z+7B15wQbXSuZB5W954MGH/z/AKRVd1j3SOr3QlDQaeBcBA+2G5GNm7G3N2ccmPXPhVmHycZNVP6d27P4nfbYrL+FHYXUq/7yw7l/SoqHfSvfbC+8feEoA9Dzw2zD5fub1ylfe6f1maAWbW+nd2IGt8iG63lGG0kk3mN2PHHj0r2sPdT61CCi22mlW28jBd8goIAGL0csGpx+Vh4yVzeLLe461W5ay/i5CGGPK7XIj80nOUmPmxsCT5snmNleaEZMXqmqAbgCw28irja68vguueTD5R4gkEGuU9c90xrF4HV7fT1EgKtshuRyOM4zeH0Viar7onVrkYaCwXHJdkVyCq8/MBN2fM58gc48MZOc/Plx549e2v42d4736rfGv6lv5Dn4fKfCrXHw371g7pgO8Bi7xurB5GDMoPQBUkQfGDz5CuR9J7a9Qt5Vue4spGRw4SWKcxlhzBIW5BODg4z4CrJe+6d1mYMpt9OG9lY4husjasagDN4cDEY9vNvkq+N4Y23P+l3y+e56mHpubX9MKCLA+EZE+XbkfJWuI7e8ndmYHuImPeE8tuSFB2jl8LHyCqdc+6E1SQKpgsPMbeMRXGc4xz/jXSsS57dNRdSncWKhgA22GfLAHcASbk+P6hTmuOc1FXHn43teNYudiMhyeY2E8vOHPl8oNS3DMWR3xyww2NwyAf8AQNy59eVaP1DtHvJ+TpAQDlV2SAL4nbiXPPNSkPbDfoghENoFBB5RzZ5DA/8AmPRWG8Naf18dt26Faz6jcdyi5ypBz0wq7i2ceqOnpI+OuneELgSWcEw6933Djph0+CceALLy9korgjhjt71LTnEsVvYlgCBviuCMHqPNugcc6sVh7qvW4VkiW203ZLJ3hBguzsbl8D+O8h5o5HPStnxdcc79/wDTL8nK8l69Oq+NXKWV5OPhBJUBHLG+NlHPw5sPmrTfZzfwS2bWkZxKyF8tyb3zH56OSfQwI+I4rUvEfukdXvrafTngsEiusb3ihuRKuGB8wtdso6Y5qeVQHDfbHfWCLElvYsoBDb4JSZAQQe8KTrvzuJ5+NasufHU191VMe+3Zei8QSze9bpIpC06bealQW2LIy7mwORhz8TZq7cJ8Mzp3iSShYzcSOkcWdygzd+oLsPWY8gOh61xJH7pvWFWJBb6diBw8Z7m6znY0eD/HMbSrEcsdBU3B7sHX0ORbaX1zzt7zrtVM/wC3+hRXN5pr2nWvTtjjDT4xbXIVQGWOGbPwmwjhjzPP/wDTNfXC0h97WRPXujEf95YsH9MZriS691/r0iyIbXSsTRGFsW95yU7hkfx/k3nH0146f7rbXYY44BbaYVhdnUtBdlstvyCRfAEeeR08BXM5JrX8/wDWnNxtrnylKVnWFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoP//Z\n" + }, + "metadata": {} + } + ] + } + }, + "7f80f5b065704815976c3be30b3ae8cc": { + "model_module": "@jupyter-widgets/output", + "model_name": "OutputModel", + "model_module_version": "1.0.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_083577639f014471b24bf14434020868", + "msg_id": "", + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Video available at https://www.bilibili.com/video/BV1dq4y1L7eC\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": "<__main__.PlayVideo at 0x7fdbf01c7040>", + "text/html": "\n \n " + }, + "metadata": {} + } + ] + } + }, + "3f2f18285890479c94aa234de7375ea5": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0c68ceb7ce1e4840b4ca4061fd958df6": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "083577639f014471b24bf14434020868": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file From a00e48fb4ac2ccac7c7eade4b7dca0a680d5efbb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 3 Jul 2023 12:24:50 +0000 Subject: [PATCH 7/7] Process tutorial notebooks --- .../W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb | 5424 ++++++++--------- .../instructor/W0D2_Tutorial1.ipynb | 46 +- ...py => W0D2_Tutorial1_Solution_950b2963.py} | 2 +- .../W0D2_Tutorial1_Solution_bf9f75ab.py | 94 + ...py => W0D2_Tutorial1_Solution_c368c1ef.py} | 0 .../W0D2_Tutorial1_Solution_bf9f75ab_0.png | Bin 0 -> 26666 bytes .../W0D2_Tutorial1_Solution_bf9f75ab_1.png | Bin 0 -> 810637 bytes ...=> W0D2_Tutorial1_Solution_c368c1ef_0.png} | Bin ...=> W0D2_Tutorial1_Solution_c368c1ef_1.png} | Bin .../student/W0D2_Tutorial1.ipynb | 134 +- tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb | 4535 +++++++------- tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb | 3714 ++++++----- tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb | 5277 ++++++++-------- .../instructor/W0D4_Tutorial1.ipynb | 108 +- .../instructor/W0D4_Tutorial2.ipynb | 71 +- .../instructor/W0D4_Tutorial3.ipynb | 71 +- ...py => W0D4_Tutorial1_Solution_90e4db73.py} | 2 +- .../student/W0D4_Tutorial1.ipynb | 108 +- .../student/W0D4_Tutorial2.ipynb | 71 +- .../student/W0D4_Tutorial3.ipynb | 71 +- .../W0D5_Statistics/W0D5_Tutorial1.ipynb | 3802 ++++++------ .../instructor/W0D5_Tutorial1.ipynb | 19 +- .../student/W0D5_Tutorial1.ipynb | 19 +- 23 files changed, 11460 insertions(+), 12108 deletions(-) rename tutorials/W0D2_PythonWorkshop2/solutions/{W0D2_Tutorial1_Solution_9aaee1d8.py => W0D2_Tutorial1_Solution_950b2963.py} (99%) create mode 100644 tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_bf9f75ab.py rename tutorials/W0D2_PythonWorkshop2/solutions/{W0D2_Tutorial1_Solution_5061d76b.py => W0D2_Tutorial1_Solution_c368c1ef.py} (100%) create mode 100644 tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_bf9f75ab_0.png create mode 100644 tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_bf9f75ab_1.png rename tutorials/W0D2_PythonWorkshop2/static/{W0D2_Tutorial1_Solution_5061d76b_0.png => W0D2_Tutorial1_Solution_c368c1ef_0.png} (100%) rename tutorials/W0D2_PythonWorkshop2/static/{W0D2_Tutorial1_Solution_5061d76b_1.png => W0D2_Tutorial1_Solution_c368c1ef_1.png} (100%) rename tutorials/W0D4_Calculus/solutions/{W0D4_Tutorial1_Solution_a0e42694.py => W0D4_Tutorial1_Solution_90e4db73.py} (98%) diff --git a/tutorials/W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb b/tutorials/W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb index 04c378a..25fce25 100644 --- a/tutorials/W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb +++ b/tutorials/W0D2_PythonWorkshop2/W0D2_Tutorial1.ipynb @@ -1,2751 +1,2675 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "wFv9HHo6dxsV" - }, - "source": [ - "# Tutorial 1: LIF Neuron Part II\n", - "\n", - "**Week 0, Day 2: Python Workshop 2**\n", - "\n", - "**By Neuromatch Academy**\n", - "\n", - "**Content creators:** Marco Brigham and the [CCNSS](https://www.ccnss.org/) team\n", - "\n", - "**Content reviewers:** Michael Waskom, Karolina Stosio, Spiros Chavlis\n", - "\n", - "**Production editors:** Ella Batty, Spiros Chavlis" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "YrNzsl6VdxsW" - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "nEVfwEutdxsW" - }, - "source": [ - "---\n", - "## Tutorial objectives\n", - "\n", - "We learned basic Python and NumPy concepts in the previous tutorial. These new and efficient coding techniques can be applied repeatedly in tutorials from the NMA course, and elsewhere.\n", - "\n", - "In this tutorial, we'll introduce spikes in our LIF neuron and evaluate the refractory period's effect in spiking dynamics!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "XAg7NafOdxsW" - }, - "source": [ - "---\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "u0mObsJHdxsW" - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D2_T1\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "both", - "execution": {}, - "id": "ny3G5i-ndxsX" - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "UmxphSK9dxsX" - }, - "outputs": [], - "source": [ - "# @title Figure settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import ipywidgets as widgets # interactive display\n", - "%config InlineBackend.figure_format = 'retina'\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "eqAqy7_8dxsX" - }, - "outputs": [], - "source": [ - "# @title Helper functions\n", - "\n", - "t_max = 150e-3 # second\n", - "dt = 1e-3 # second\n", - "tau = 20e-3 # second\n", - "el = -60e-3 # milivolt\n", - "vr = -70e-3 # milivolt\n", - "vth = -50e-3 # milivolt\n", - "r = 100e6 # ohm\n", - "i_mean = 25e-11 # ampere\n", - "\n", - "\n", - "def plot_all(t_range, v, raster=None, spikes=None, spikes_mean=None):\n", - " \"\"\"\n", - " Plots Time evolution for\n", - " (1) multiple realizations of membrane potential\n", - " (2) spikes\n", - " (3) mean spike rate (optional)\n", - "\n", - " Args:\n", - " t_range (numpy array of floats)\n", - " range of time steps for the plots of shape (time steps)\n", - "\n", - " v (numpy array of floats)\n", - " membrane potential values of shape (neurons, time steps)\n", - "\n", - " raster (numpy array of floats)\n", - " spike raster of shape (neurons, time steps)\n", - "\n", - " spikes (dictionary of lists)\n", - " list with spike times indexed by neuron number\n", - "\n", - " spikes_mean (numpy array of floats)\n", - " Mean spike rate for spikes as dictionary\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - "\n", - " v_mean = np.mean(v, axis=0)\n", - " fig_w, fig_h = plt.rcParams['figure.figsize']\n", - " plt.figure(figsize=(fig_w, 1.5 * fig_h))\n", - "\n", - " ax1 = plt.subplot(3, 1, 1)\n", - " for j in range(n):\n", - " plt.scatter(t_range, v[j], color=\"k\", marker=\".\", alpha=0.01)\n", - " plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", - " plt.xticks([])\n", - " plt.ylabel(r'$V_m$ (V)')\n", - "\n", - " if raster is not None:\n", - " plt.subplot(3, 1, 2)\n", - " spikes_mean = np.mean(raster, axis=0)\n", - " plt.imshow(raster, cmap='Greys', origin='lower', aspect='auto')\n", - "\n", - " else:\n", - " plt.subplot(3, 1, 2, sharex=ax1)\n", - " for j in range(n):\n", - " times = np.array(spikes[j])\n", - " plt.scatter(times, j * np.ones_like(times), color=\"C0\", marker=\".\", alpha=0.2)\n", - "\n", - " plt.xticks([])\n", - " plt.ylabel('neuron')\n", - "\n", - " if spikes_mean is not None:\n", - " plt.subplot(3, 1, 3, sharex=ax1)\n", - " plt.plot(t_range, spikes_mean)\n", - " plt.xlabel('time (s)')\n", - " plt.ylabel('rate (Hz)')\n", - "\n", - " plt.tight_layout()\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "NJ9dKyBodxsY" - }, - "source": [ - "---\n", - "# Section 1: Histograms\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ycTJR187dxsY" - }, - "outputs": [], - "source": [ - "# @title Video 1: Histograms\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'J24tne-IwvY'), ('Bilibili', 'BV1GC4y1h7Ex')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "nz-ISqLTdxsY" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Histograms_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "x1tiEDGvdxsY" - }, - "source": [ - "
\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "Another important statistic is the sample [histogram](https://en.wikipedia.org/wiki/Histogram). For our LIF neuron, it provides an approximate representation of the distribution of membrane potential $V_m(t)$ at time $t=t_k\\in[0,t_{max}]$. For $N$ realizations $V\\left(t_k\\right)$ and $J$ bins is given by:\n", - "\n", - "
\n", - "\\begin{equation}\n", - "N = \\sum_{j=1}^{J} m_j\n", - "\\end{equation}\n", - "
\n", - "\n", - "where $m_j$ is a function that counts the number of samples $V\\left(t_k\\right)$ that fall into bin $j$.\n", - "\n", - "The function `plt.hist(data, nbins)` plots an histogram of `data` in `nbins` bins. The argument `label` defines a label for `data` and `plt.legend()` adds all labels to the plot.\n", - "\n", - "```python\n", - "plt.hist(data, bins, label='my data')\n", - "plt.legend()\n", - "plt.show()\n", - "```\n", - "\n", - "The parameters `histtype='stepfilled'` and `linewidth=0` may improve histogram appearance (depending on taste). You can read more about [different histtype settings](https://matplotlib.org/gallery/statistics/histogram_histtypes.html).\n", - "\n", - "The function `plt.hist` returns the `pdf`, `bins`, and `patches` with the histogram bins, the edges of the bins, and the individual patches used to create the histogram.\n", - "\n", - "```python\n", - "pdf, bins, patches = plt.hist(data, bins)\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "5oz5kUTZdxsZ" - }, - "outputs": [], - "source": [ - "# @title Video 2: Nano recap of histograms\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '71f1J98zj80'), ('Bilibili', 'BV1Zv411B7mD')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "9jQQkT_0dxsZ" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_histograms_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "EIIDPXaNdxsZ" - }, - "source": [ - "## Coding Exercise 1: Plotting a histogram\n", - "\n", - "Plot an histogram of $J=50$ bins of $N=10000$ realizations of $V(t)$ for $t=t_{max}/10$ and $t=t_{max}$.\n", - "\n", - "We'll make a small correction in the definition of `t_range` to ensure increments of `dt` by using `np.arange` instead of `np.linspace`.\n", - "\n", - "```python\n", - "numpy.arange(start, stop, step)\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "fItxVGbjdxsZ" - }, - "outputs": [], - "source": [ - "#################################################\n", - "## TODO for students: fill out code to plot histogram ##\n", - "# Fill out code and comment or remove the next line\n", - "raise NotImplementedError(\"Student exercise: You need to plot histogram\")\n", - "#################################################\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize t_range, step_end, n, v_n, i and nbins\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 10000\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "nbins = 32\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r * i[:, step])\n", - "\n", - "# Initialize the figure\n", - "plt.figure()\n", - "plt.ylabel('Frequency')\n", - "plt.xlabel('$V_m$ (V)')\n", - "\n", - "# Plot a histogram at t_max/10 (add labels and parameters histtype='stepfilled' and linewidth=0)\n", - "plt.hist(...)\n", - "\n", - "# Plot a histogram at t_max (add labels and parameters histtype='stepfilled' and linewidth=0)\n", - "plt.hist(...)\n", - "\n", - "# Add legend\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "x4UQtJIJdxsZ" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize t_range, step_end, n, v_n, i and nbins\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 10000\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "nbins = 32\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r * i[:, step])\n", - "\n", - "# Initialize the figure\n", - "with plt.xkcd():\n", - " plt.figure()\n", - " plt.ylabel('Frequency')\n", - " plt.xlabel('$V_m$ (V)')\n", - "\n", - " # Plot a histogram at t_max/10 (add labels and parameters histtype='stepfilled' and linewidth=0)\n", - " plt.hist(v_n[:,int(step_end / 10)], nbins,\n", - " histtype='stepfilled', linewidth=0,\n", - " label = 't='+ str(t_max / 10) + 's')\n", - "\n", - " # Plot a histogram at t_max (add labels and parameters histtype='stepfilled' and linewidth=0)\n", - " plt.hist(v_n[:, -1], nbins,\n", - " histtype='stepfilled', linewidth=0,\n", - " label = 't='+ str(t_max) + 's')\n", - " # Add legend\n", - " plt.legend()\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "Y5OLvElNdxsZ" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Plotting_a_histogram_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "n_IkDWiBdxsa" - }, - "source": [ - "---\n", - "# Section 2: Dictionaries & introducing spikes\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "oiVm4Jopdxsa" - }, - "outputs": [], - "source": [ - "# @title Video 3: Dictionaries & introducing spikes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'ioKkiukDkNg'), ('Bilibili', 'BV1H54y1q7oS')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "-Kw4Yxtkdxsa" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Dictionaries_&_introducing_spikes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "_-YxgwqOdxsa" - }, - "source": [ - "A spike occures whenever $V(t)$ crosses $V_{th}$. In that case, a spike is recorded, and $V(t)$ resets to $V_{reset}$ value. This is summarized in the *reset condition*:\n", - "\n", - "\\begin{equation}\n", - "V(t) = V_{reset}\\quad \\text{ if } V(t)\\geq V_{th}\n", - "\\end{equation}\n", - "\n", - "For more information about spikes or action potentials see [here](https://en.wikipedia.org/wiki/Action_potential) and [here](https://www.khanacademy.org/test-prep/mcat/organ-systems/neuron-membrane-potentials/a/neuron-action-potentials-the-creation-of-a-brain-signal).\n", - "\n", - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "q4KoInt7dxsa" - }, - "outputs": [], - "source": [ - "# @title Video 4: Nano recap of dictionaries\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'nvpHtzuZggg'), ('Bilibili', 'BV1GC4y1h7hi')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ApfRaFCBdxsa" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_dictionaries_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "74mzXQFxdxsa" - }, - "source": [ - "## Coding Exercise 2: Adding spiking to the LIF neuron\n", - "\n", - "Insert the reset condition, and collect the spike times of each realization in a dictionary variable `spikes`, with $N=500$.\n", - "\n", - "We've used `plt.plot` for plotting lines and also for plotting dots at `(x,y)` coordinates, which is a [scatter plot](https://en.wikipedia.org/wiki/Scatter_plot). From here on, we'll use use `plt.plot` for plotting lines and for scatter plots: `plt.scatter`.\n", - "\n", - "```python\n", - "plt.scatter(x, y, color=\"k\", marker=\".\")\n", - "```\n", - "\n", - "A *raster plot* represents spikes from multiple neurons by plotting dots at spike times from neuron `j` at plot height `j`, i.e.\n", - "\n", - "```python\n", - "plt.scatter(spike_times, j*np.ones_like(spike_times))\n", - "```\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "In this exercise, we use `plt.subplot` for multiple plots in the same figure. These plots can share the same `x` or `y` axis by specifying the parameter `sharex` or `sharey`. Add `plt.tight_layout()` at the end to automatically adjust subplot parameters to fit the figure area better. Please see the example below for a row of two plots sharing axis `y`.\n", - "\n", - "```python\n", - "# initialize the figure\n", - "plt.figure()\n", - "\n", - "# collect axis of 1st figure in ax1\n", - "ax1 = plt.subplot(1, 2, 1)\n", - "plt.plot(t_range, my_data_left)\n", - "plt.ylabel('ylabel')\n", - "\n", - "# share axis x with 1st figure\n", - "plt.subplot(1, 2, 2, sharey=ax1)\n", - "plt.plot(t_range, my_data_right)\n", - "\n", - "# automatically adjust subplot parameters to figure\n", - "plt.tight_layout()\n", - "plt.show()\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "tsbGEihmdxsb" - }, - "outputs": [], - "source": [ - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize spikes and spikes_n\n", - "spikes = {j: [] for j in range(n)}\n", - "spikes_n = np.zeros([step_end])\n", - "\n", - "#################################################\n", - "## TODO for students: add spikes to LIF neuron ##\n", - "# Fill out function and remove\n", - "raise NotImplementedError(\"Student exercise: add spikes to LIF neuron\")\n", - "#################################################\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Loop over simulations\n", - " for j in range(n):\n", - "\n", - " # Check if voltage above threshold\n", - " if v_n[j, step] >= vth:\n", - "\n", - " # Reset to reset voltage\n", - " v_n[j, step] = ...\n", - "\n", - " # Add this spike time\n", - " spikes[j] += ...\n", - "\n", - " # Add spike count to this step\n", - " spikes_n[step] += ...\n", - "\n", - "# Collect mean Vm and mean spiking rate\n", - "v_mean = np.mean(v_n, axis=0)\n", - "spikes_mean = spikes_n / n\n", - "\n", - "# Initialize the figure\n", - "plt.figure()\n", - "\n", - "# Plot simulations and sample mean\n", - "ax1 = plt.subplot(3, 1, 1)\n", - "for j in range(n):\n", - " plt.scatter(t_range, v_n[j], color=\"k\", marker=\".\", alpha=0.01)\n", - "plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", - "plt.ylabel('$V_m$ (V)')\n", - "\n", - "# Plot spikes\n", - "plt.subplot(3, 1, 2, sharex=ax1)\n", - "# for each neuron j: collect spike times and plot them at height j\n", - "for j in range(n):\n", - " times = ...\n", - " plt.scatter(...)\n", - "\n", - "plt.ylabel('neuron')\n", - "\n", - "# Plot firing rate\n", - "plt.subplot(3, 1, 3, sharex=ax1)\n", - "plt.plot(t_range, spikes_mean)\n", - "plt.xlabel('time (s)')\n", - "plt.ylabel('rate (Hz)')\n", - "\n", - "plt.tight_layout()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "lsjGq9uadxsb" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize spikes and spikes_n\n", - "spikes = {j: [] for j in range(n)}\n", - "spikes_n = np.zeros([step_end])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Loop over simulations\n", - " for j in range(n):\n", - "\n", - " # Check if voltage above threshold\n", - " if v_n[j, step] >= vth:\n", - "\n", - " # Reset to reset voltage\n", - " v_n[j, step] = vr\n", - "\n", - " # Add this spike time\n", - " spikes[j] += [t]\n", - "\n", - " # Add spike count to this step\n", - " spikes_n[step] += 1\n", - "\n", - "# Collect mean Vm and mean spiking rate\n", - "v_mean = np.mean(v_n, axis=0)\n", - "spikes_mean = spikes_n / n\n", - "\n", - "with plt.xkcd():\n", - " # Initialize the figure\n", - " plt.figure()\n", - "\n", - " # Plot simulations and sample mean\n", - " ax1 = plt.subplot(3, 1, 1)\n", - " for j in range(n):\n", - " plt.scatter(t_range, v_n[j], color=\"k\", marker=\".\", alpha=0.01)\n", - " plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", - " plt.ylabel('$V_m$ (V)')\n", - "\n", - " # Plot spikes\n", - " plt.subplot(3, 1, 2, sharex=ax1)\n", - " # for each neuron j: collect spike times and plot them at height j\n", - " for j in range(n):\n", - " times = np.array(spikes[j])\n", - " plt.scatter(times, j * np.ones_like(times), color=\"C0\", marker=\".\", alpha=0.2)\n", - "\n", - " plt.ylabel('neuron')\n", - "\n", - " # Plot firing rate\n", - " plt.subplot(3, 1, 3, sharex=ax1)\n", - " plt.plot(t_range, spikes_mean)\n", - " plt.xlabel('time (s)')\n", - " plt.ylabel('rate (Hz)')\n", - "\n", - " plt.tight_layout()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "IzlkaSZDdxsb" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Adding_spiking_to_the_LIF_neuron_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "CTiz2Z-Tdxsb" - }, - "source": [ - "---\n", - "# Section 3: Boolean indexes\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "pmW04_j2dxsb" - }, - "outputs": [], - "source": [ - "# @title Video 5: Boolean indexes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'G0C1v848I9Y'), ('Bilibili', 'BV1W54y1q7eh')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "8Fr72otvdxsb" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Boolean_indexes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "cU0QZwjEdxsb" - }, - "source": [ - "Boolean arrays can index NumPy arrays to select a subset of elements (also works with lists of booleans).\n", - "\n", - "The boolean array itself can be initiated by a condition, as shown in the example below.\n", - "\n", - "```python\n", - "a = np.array([1, 2, 3])\n", - "b = a>=2\n", - "print(b)\n", - "--> [False True True]\n", - "\n", - "print(a[b])\n", - "--> [2 3]\n", - "\n", - "print(a[a>=2])\n", - "--> [2 3]\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "5Lb7tI8zdxsb" - }, - "outputs": [], - "source": [ - "# @title Video 6: Nano recap of Boolean indexes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'dFPgO5wnyLc'), ('Bilibili', 'BV1W54y1q7gi')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "2NoklIbudxsc" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_Boolean_indexes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "x62OGFk8dxsc" - }, - "source": [ - "## Coding Exercise 3: Using Boolean indexing\n", - "\n", - "We can avoid looping all neurons in each time step by identifying with boolean arrays the indexes of neurons that spiked in the previous step.\n", - "\n", - "In the example below, `v_rest` is a boolean numpy array with `True` in each index of `v_n` with value `vr` at time index `step`:\n", - "\n", - "```python\n", - "v_rest = (v_n[:,step] == vr)\n", - "print(v_n[v_rest,step])\n", - " --> [vr, ..., vr]\n", - "```\n", - "\n", - "The function `np.where` returns indexes of boolean arrays with `True` values.\n", - "\n", - "You may use the helper function `plot_all` that implements the figure from the previous exercise." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "Jyl4b7Bddxsc" - }, - "outputs": [], - "source": [ - "help(plot_all)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "1ioRRAfYdxsc" - }, - "outputs": [], - "source": [ - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize spikes and spikes_n\n", - "spikes = {j: [] for j in range(n)}\n", - "spikes_n = np.zeros([step_end])\n", - "\n", - "#################################################\n", - "## TODO for students: use Boolean indexing ##\n", - "# Fill out function and remove\n", - "raise NotImplementedError(\"Student exercise: using Boolean indexing\")\n", - "#################################################\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = ...\n", - "\n", - " # Set relevant values of v_n to resting potential using spiked\n", - " v_n[spiked,step] = ...\n", - "\n", - " # Collect spike times\n", - " for j in np.where(spiked)[0]:\n", - " spikes[j] += [t]\n", - " spikes_n[step] += 1\n", - "\n", - "# Collect mean spiking rate\n", - "spikes_mean = spikes_n / n\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, spikes=spikes, spikes_mean=spikes_mean)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "rERQivhWdxsc" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize spikes and spikes_n\n", - "spikes = {j: [] for j in range(n)}\n", - "spikes_n = np.zeros([step_end])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to resting potential using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Collect spike times\n", - " for j in np.where(spiked)[0]:\n", - " spikes[j] += [t]\n", - " spikes_n[step] += 1\n", - "\n", - "# Collect mean spiking rate\n", - "spikes_mean = spikes_n / n\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, spikes=spikes, spikes_mean=spikes_mean)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "NGCU9IP6dxsc" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Using_Boolean_indexing_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "2FdA8HvKdxsk" - }, - "source": [ - "## Coding Exercise 4: Making a binary raster plot\n", - "\n", - "A *binary raster plot* represents spike times as `1`s in a binary grid initialized with `0`s. We start with a numpy array `raster` of zeros with shape `(neurons, time steps)`, and represent a spike from neuron `5` at time step `20` as `raster(5,20)=1`, for example.\n", - "\n", - "The *binary raster plot* is much more efficient than the previous method by plotting the numpy array `raster` as an image:\n", - "\n", - "```python\n", - "plt.imshow(raster, cmap='Greys', origin='lower', aspect='auto')\n", - "```\n", - "\n", - "**Suggestions**\n", - "* At each time step:\n", - " * Initialize boolean numpy array `spiked` with $V_n(t)\\geq V_{th}$\n", - " * Set to `vr` indexes of `v_n` using `spiked`\n", - " * Set to `1` indexes of numpy array `raster` using `spiked`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "WEbW_Toldxsk" - }, - "outputs": [], - "source": [ - "#################################################\n", - "## TODO for students: make a raster ##\n", - "# Fill out function and remove\n", - "raise NotImplementedError(\"Student exercise: make a raster \")\n", - "#################################################\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to v_reset using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Set relevant elements in raster to 1 using spiked\n", - " raster[spiked,step] = ...\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "l8kCiXlgdxsk" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to v_reset using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Set relevant elements in raster to 1 using spiked\n", - " raster[spiked,step] = 1.\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "HK372z5pdxsk" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Making_a_binary_raster_plot_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "LeBTKG9Cdxsk" - }, - "source": [ - "---\n", - "# Section 4: Refractory period\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "o4bGbMLMdxsk" - }, - "outputs": [], - "source": [ - "# @title Video 7: Refractory period\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'KVNdbRY5-nY'), ('Bilibili', 'BV1MT4y1E79j')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "_XKDSAxKdxsl" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Refractory_period_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "SZgjb2mvdxsl" - }, - "source": [ - "The absolute refractory period is a time interval in the order of a few milliseconds during which synaptic input will not lead to a 2nd spike, no matter how strong. This effect is due to the biophysics of the neuron membrane channels, and you can read more about absolute and relative refractory period [here](https://content.byui.edu/file/a236934c-3c60-4fe9-90aa-d343b3e3a640/1/module5/readings/refractory_periods.html) and [here](https://en.wikipedia.org/wiki/Refractory_period_(physiology)).\n", - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "mgivzSyUdxsl" - }, - "outputs": [], - "source": [ - "# @title Video 8: Nano recap of refractory period\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'DOoftC0JU2k'), ('Bilibili', 'BV1pA411e7je')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "QAWP1By_dxsl" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_refractory_period_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "wzbvqMUfdxsl" - }, - "source": [ - "## Coding Exercise 5: Investigating refactory periods\n", - "\n", - "Investigate the effect of (absolute) refractory period $t_{ref}$ on the evolution of output rate $\\lambda(t)$. Add refractory period $t_{ref}=10$ ms after each spike, during which $V(t)$ is clamped to $V_{reset}$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "_Bwf0hdmdxsl" - }, - "outputs": [], - "source": [ - "#################################################\n", - "## TODO for students: add refactory period ##\n", - "# Fill out function and remove\n", - "raise NotImplementedError(\"Student exercise: add refactory period \")\n", - "#################################################\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "t_ref = 0.01\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to v_reset using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Set relevant elements in raster to 1 using spiked\n", - " raster[spiked,step] = 1.\n", - "\n", - " # Initialize boolean numpy array clamped using last_spike, t and t_ref\n", - " clamped = ...\n", - "\n", - " # Reset clamped neurons to vr using clamped\n", - " v_n[clamped,step] = ...\n", - "\n", - " # Update numpy array last_spike with time t for spiking neurons\n", - " last_spike[spiked] = ...\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "uMf4A9Wadxsl" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "t_ref = 0.01\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step == 0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", - "\n", - " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", - " spiked = (v_n[:,step] >= vth)\n", - "\n", - " # Set relevant values of v_n to v_reset using spiked\n", - " v_n[spiked,step] = vr\n", - "\n", - " # Set relevant elements in raster to 1 using spiked\n", - " raster[spiked,step] = 1.\n", - "\n", - " # Initialize boolean numpy array clamped using last_spike, t and t_ref\n", - " clamped = (last_spike + t_ref > t)\n", - "\n", - " # Reset clamped neurons to vr using clamped\n", - " v_n[clamped,step] = vr\n", - "\n", - " # Update numpy array last_spike with time t for spiking neurons\n", - " last_spike[spiked] = t\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "oeQlc83Xdxsm" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Investigating_refractory_periods_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "OmBgWmGedxsm" - }, - "source": [ - "## Interactive Demo 1: Random refractory period\n", - "\n", - "In the following interactive demo, we will investigate the effect of random refractory periods. We will use random refactory periods $t_{ref}$ with\n", - "$t_{ref} = \\mu + \\sigma\\,\\mathcal{N}$, where $\\mathcal{N}$ is the [normal distribution](https://en.wikipedia.org/wiki/Normal_distribution), $\\mu=0.01$ and $\\sigma=0.007$.\n", - "\n", - "Refractory period samples `t_ref` of size `n` is initialized with `np.random.normal`. We clip negative values to `0` with boolean indexes. (Why?) You can double-click the cell to see the hidden code.\n", - "\n", - "You can play with the parameters mu and sigma and visualize the resulting simulation. What is the effect of different $\\sigma$ values?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "Ay9Tk2zPdxsm" - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to enable the demo\n", - "\n", - "def random_ref_period(mu, sigma):\n", - " # set random number generator\n", - " np.random.seed(2020)\n", - "\n", - " # initialize step_end, t_range, n, v_n, syn and raster\n", - " t_range = np.arange(0, t_max, dt)\n", - " step_end = len(t_range)\n", - " n = 500\n", - " v_n = el * np.ones([n,step_end])\n", - " syn = i_mean * (1 + 0.1*(t_max/dt)**(0.5)*(2*np.random.random([n,step_end])-1))\n", - " raster = np.zeros([n,step_end])\n", - "\n", - " # initialize t_ref and last_spike\n", - " t_ref = mu + sigma*np.random.normal(size=n)\n", - " t_ref[t_ref<0] = 0\n", - " last_spike = -t_ref * np.ones([n])\n", - "\n", - " # loop time steps\n", - " for step, t in enumerate(t_range):\n", - " if step==0:\n", - " continue\n", - "\n", - " v_n[:,step] = v_n[:,step-1] + dt/tau * (el - v_n[:,step-1] + r*syn[:,step])\n", - "\n", - " # boolean array spiked indexes neurons with v>=vth\n", - " spiked = (v_n[:,step] >= vth)\n", - " v_n[spiked,step] = vr\n", - " raster[spiked,step] = 1.\n", - "\n", - " # boolean array clamped indexes refractory neurons\n", - " clamped = (last_spike + t_ref > t)\n", - " v_n[clamped,step] = vr\n", - " last_spike[spiked] = t\n", - "\n", - " # plot multiple realizations of Vm, spikes and mean spike rate\n", - " plot_all(t_range, v_n, raster)\n", - "\n", - " # plot histogram of t_ref\n", - " plt.figure(figsize=(8,4))\n", - " plt.hist(t_ref, bins=32, histtype='stepfilled', linewidth=0, color='C1')\n", - " plt.xlabel(r'$t_{ref}$ (s)')\n", - " plt.ylabel('count')\n", - " plt.tight_layout()\n", - "\n", - "_ = widgets.interact(random_ref_period, mu = (0.01, 0.05, 0.01), \\\n", - " sigma = (0.001, 0.02, 0.001))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "USvwwwetdxsm" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Random_refractory_period_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "wbrisIUbdxsm" - }, - "source": [ - "---\n", - "# Section 5: Using functions\n", - "Running key parts of your code inside functions improves your coding narrative by making it clearer and more flexible to future changes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "Wbkh6UFGdxsn" - }, - "outputs": [], - "source": [ - "# @title Video 9: Functions\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'mkf8riqCjS4'), ('Bilibili', 'BV1sa4y1a7pq')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "KaF6pniedxsn" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Functions_Video\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "1nZFx72zdxsn" - }, - "outputs": [], - "source": [ - "# @title Video 10: Nano recap of functions\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '0An_NnVWY_Q'), ('Bilibili', 'BV1pz411v74H')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "YSoWNoG5dxso" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_functions_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "_oVXyOVDdxso" - }, - "source": [ - "## Coding Exercise 6: Rewriting code with functions\n", - "\n", - "We will now re-organize parts of the code from the previous exercise with functions. You need to complete the function `spike_clamp()` to update $V(t)$ and deal with spiking and refractoriness" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "H6vTyYwFdxso" - }, - "outputs": [], - "source": [ - "def ode_step(v, i, dt):\n", - " \"\"\"\n", - " Evolves membrane potential by one step of discrete time integration\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential at previous time step of shape (neurons)\n", - "\n", - " i (numpy array of floats)\n", - " synaptic input at current time step of shape (neurons)\n", - "\n", - " dt (float)\n", - " time step increment\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential at current time step of shape (neurons)\n", - " \"\"\"\n", - " v = v + dt/tau * (el - v + r*i)\n", - "\n", - " return v\n", - "\n", - "\n", - "def spike_clamp(v, delta_spike):\n", - " \"\"\"\n", - " Resets membrane potential of neurons if v>= vth\n", - " and clamps to vr if interval of time since last spike < t_ref\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - "\n", - " delta_spike (numpy array of floats)\n", - " interval of time since last spike of shape (neurons)\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - " spiked (numpy array of floats)\n", - " boolean array of neurons that spiked of shape (neurons)\n", - " \"\"\"\n", - "\n", - " ####################################################\n", - " ## TODO for students: complete spike_clamp\n", - " # Fill out function and remove\n", - " raise NotImplementedError(\"Student exercise: complete spike_clamp\")\n", - " ####################################################\n", - " # Boolean array spiked indexes neurons with v>=vth\n", - " spiked = ...\n", - " v[spiked] = ...\n", - "\n", - " # Boolean array clamped indexes refractory neurons\n", - " clamped = ...\n", - " v[clamped] = ...\n", - "\n", - " return v, spiked\n", - "\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "mu = 0.01\n", - "sigma = 0.007\n", - "t_ref = mu + sigma*np.random.normal(size=n)\n", - "t_ref[t_ref<0] = 0\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", - "\n", - " # Reset membrane potential and clamp\n", - " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", - "\n", - " # Update raster and last_spike\n", - " raster[spiked,step] = 1.\n", - " last_spike[spiked] = t\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "vPlBUtUpdxso" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "def ode_step(v, i, dt):\n", - " \"\"\"\n", - " Evolves membrane potential by one step of discrete time integration\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential at previous time step of shape (neurons)\n", - "\n", - " i (numpy array of floats)\n", - " synaptic input at current time step of shape (neurons)\n", - "\n", - " dt (float)\n", - " time step increment\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential at current time step of shape (neurons)\n", - " \"\"\"\n", - " v = v + dt/tau * (el - v + r*i)\n", - "\n", - " return v\n", - "\n", - "\n", - "def spike_clamp(v, delta_spike):\n", - " \"\"\"\n", - " Resets membrane potential of neurons if v>= vth\n", - " and clamps to vr if interval of time since last spike < t_ref\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - "\n", - " delta_spike (numpy array of floats)\n", - " interval of time since last spike of shape (neurons)\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - " spiked (numpy array of floats)\n", - " boolean array of neurons that spiked of shape (neurons)\n", - " \"\"\"\n", - "\n", - " # Boolean array spiked indexes neurons with v>=vth\n", - " spiked = (v >= vth)\n", - " v[spiked] = vr\n", - "\n", - " # Boolean array clamped indexes refractory neurons\n", - " clamped = (t_ref > delta_spike)\n", - " v[clamped] = vr\n", - "\n", - " return v, spiked\n", - "\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "mu = 0.01\n", - "sigma = 0.007\n", - "t_ref = mu + sigma*np.random.normal(size=n)\n", - "t_ref[t_ref<0] = 0\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", - "\n", - " # Reset membrane potential and clamp\n", - " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", - "\n", - " # Update raster and last_spike\n", - " raster[spiked,step] = 1.\n", - " last_spike[spiked] = t\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "JEBg55MZdxso" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Rewriting_code_with_functions_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "O3GugPeCdxsp" - }, - "source": [ - "---\n", - "# Section 6: Using classes\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "8BWkoeTddxsp" - }, - "outputs": [], - "source": [ - "# @title Video 11: Classes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'dGRESMoNPh0'), ('Bilibili', 'BV1hz411v7ne')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "CNAcLcLOdxsr" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Classes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "bY9swpA0dxss" - }, - "source": [ - "Using classes helps with code reuse and reliability. Well-designed classes are like black boxes, receiving inputs and providing expected outputs. The details of how the class processes inputs and produces outputs are unimportant.\n", - "\n", - "See additional details here: [A Beginner's Python Tutorial/Classes](https://en.wikibooks.org/wiki/A_Beginner%27s_Python_Tutorial/Classes)\n", - "\n", - "*Attributes* are variables internal to the class, and *methods* are functions internal to the class." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "2PoK8siydxss" - }, - "outputs": [], - "source": [ - "# @title Video 12: Nano recap of classes\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '4YNpMpVW2qs'), ('Bilibili', 'BV12V41167yu')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "U-ywaGZ_dxss" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Nano_recap_of_classes_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "gr_s8q0zdxss" - }, - "source": [ - "## Coding Exercise 7: Making a LIF class\n", - "In this exercise we'll practice with Python classes by implementing `LIFNeurons`, a class that evolves and keeps state of multiple realizations of LIF neurons.\n", - "\n", - "Several attributes are used to keep state of our neurons:\n", - "\n", - "```python\n", - "self.v current membrane potential\n", - "self.spiked neurons that spiked\n", - "self.last_spike last spike time of each neuron\n", - "self.t running time of the simulation\n", - "self.steps simulation step\n", - "```\n", - "\n", - "There is a single method:\n", - "\n", - "```python\n", - "self.ode_step() performs single step discrete time integration\n", - " for provided synaptic current and dt\n", - "```\n", - "\n", - "Complete the spike and clamp part of method `self.ode_step` (should be similar to function `spike_and_clamp` seen before)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "Fjz_HLIKdxss" - }, - "outputs": [], - "source": [ - "# Simulation class\n", - "class LIFNeurons:\n", - " \"\"\"\n", - " Keeps track of membrane potential for multiple realizations of LIF neuron,\n", - " and performs single step discrete time integration.\n", - " \"\"\"\n", - " def __init__(self, n, t_ref_mu=0.01, t_ref_sigma=0.002,\n", - " tau=20e-3, el=-60e-3, vr=-70e-3, vth=-50e-3, r=100e6):\n", - "\n", - " # Neuron count\n", - " self.n = n\n", - "\n", - " # Neuron parameters\n", - " self.tau = tau # second\n", - " self.el = el # milivolt\n", - " self.vr = vr # milivolt\n", - " self.vth = vth # milivolt\n", - " self.r = r # ohm\n", - "\n", - " # Initializes refractory period distribution\n", - " self.t_ref_mu = t_ref_mu\n", - " self.t_ref_sigma = t_ref_sigma\n", - " self.t_ref = self.t_ref_mu + self.t_ref_sigma * np.random.normal(size=self.n)\n", - " self.t_ref[self.t_ref<0] = 0\n", - "\n", - " # State variables\n", - " self.v = self.el * np.ones(self.n)\n", - " self.spiked = self.v >= self.vth\n", - " self.last_spike = -self.t_ref * np.ones([self.n])\n", - " self.t = 0.\n", - " self.steps = 0\n", - "\n", - "\n", - " def ode_step(self, dt, i):\n", - "\n", - " # Update running time and steps\n", - " self.t += dt\n", - " self.steps += 1\n", - "\n", - " # One step of discrete time integration of dt\n", - " self.v = self.v + dt / self.tau * (self.el - self.v + self.r * i)\n", - "\n", - " ####################################################\n", - " ## TODO for students: complete the `ode_step` method\n", - " # Fill out function and remove\n", - " raise NotImplementedError(\"Student exercise: complete the ode_step method\")\n", - " ####################################################\n", - "\n", - " # Spike and clamp\n", - " self.spiked = ...\n", - " self.v[self.spiked] = ...\n", - " self.last_spike[self.spiked] = ...\n", - " clamped = ...\n", - " self.v[clamped] = ...\n", - "\n", - " self.last_spike[self.spiked] = ...\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize neurons\n", - "neurons = LIFNeurons(n)\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Call ode_step method\n", - " neurons.ode_step(dt, i[:,step])\n", - "\n", - " # Log v_n and spike history\n", - " v_n[:,step] = neurons.v\n", - " raster[neurons.spiked, step] = 1.\n", - "\n", - "# Report running time and steps\n", - "print(f'Ran for {neurons.t:.3}s in {neurons.steps} steps.')\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "y9UTOvgWdxst" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Simulation class\n", - "class LIFNeurons:\n", - " \"\"\"\n", - " Keeps track of membrane potential for multiple realizations of LIF neuron,\n", - " and performs single step discrete time integration.\n", - " \"\"\"\n", - " def __init__(self, n, t_ref_mu=0.01, t_ref_sigma=0.002,\n", - " tau=20e-3, el=-60e-3, vr=-70e-3, vth=-50e-3, r=100e6):\n", - "\n", - " # Neuron count\n", - " self.n = n\n", - "\n", - " # Neuron parameters\n", - " self.tau = tau # second\n", - " self.el = el # milivolt\n", - " self.vr = vr # milivolt\n", - " self.vth = vth # milivolt\n", - " self.r = r # ohm\n", - "\n", - " # Initializes refractory period distribution\n", - " self.t_ref_mu = t_ref_mu\n", - " self.t_ref_sigma = t_ref_sigma\n", - " self.t_ref = self.t_ref_mu + self.t_ref_sigma * np.random.normal(size=self.n)\n", - " self.t_ref[self.t_ref<0] = 0\n", - "\n", - " # State variables\n", - " self.v = self.el * np.ones(self.n)\n", - " self.spiked = self.v >= self.vth\n", - " self.last_spike = -self.t_ref * np.ones([self.n])\n", - " self.t = 0.\n", - " self.steps = 0\n", - "\n", - "\n", - " def ode_step(self, dt, i):\n", - "\n", - " # Update running time and steps\n", - " self.t += dt\n", - " self.steps += 1\n", - "\n", - " # One step of discrete time integration of dt\n", - " self.v = self.v + dt / self.tau * (self.el - self.v + self.r * i)\n", - "\n", - " # Spike and clamp\n", - " self.spiked = (self.v >= self.vth)\n", - " self.v[self.spiked] = self.vr\n", - " self.last_spike[self.spiked] = self.t\n", - " clamped = (self.t_ref > self.t-self.last_spike)\n", - " self.v[clamped] = self.vr\n", - "\n", - " self.last_spike[self.spiked] = self.t\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize neurons\n", - "neurons = LIFNeurons(n)\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Call ode_step method\n", - " neurons.ode_step(dt, i[:,step])\n", - "\n", - " # Log v_n and spike history\n", - " v_n[:,step] = neurons.v\n", - " raster[neurons.spiked, step] = 1.\n", - "\n", - "# Report running time and steps\n", - "print(f'Ran for {neurons.t:.3}s in {neurons.steps} steps.')\n", - "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "WsGDqK1Gdxst" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Making_a_LIF_class_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "HxigVxS5dxst" - }, - "source": [ - "---\n", - "# Summary\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "NefLjZPLdxst" - }, - "outputs": [], - "source": [ - "# @title Video 12: Last concepts & recap\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'h4mSJBPocPo'), ('Bilibili', 'BV1MC4y1h7eA')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "4lJhZj9idxst" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Last_concepts_&_recap_Video\")" - ] - } - ], - "metadata": { - "colab": { - "name": "W0D2_Tutorial1", - "provenance": [], - "toc_visible": true, - "include_colab_link": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "execution": {}, + "id": "view-in-github" + }, + "source": [ + "\"Open   \"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "# Tutorial 1: LIF Neuron Part II\n", + "\n", + "**Week 0, Day 2: Python Workshop 2**\n", + "\n", + "**By Neuromatch Academy**\n", + "\n", + "**Content creators:** Marco Brigham and the [CCNSS](https://www.ccnss.org/) team\n", + "\n", + "**Content reviewers:** Michael Waskom, Karolina Stosio, Spiros Chavlis\n", + "\n", + "**Production editors:** Ella Batty, Spiros Chavlis" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "## Tutorial objectives\n", + "\n", + "We learned basic Python and NumPy concepts in the previous tutorial. These new and efficient coding techniques can be applied repeatedly in tutorials from the NMA course, and elsewhere.\n", + "\n", + "In this tutorial, we'll introduce spikes in our LIF neuron and evaluate the refractory period's effect in spiking dynamics!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D2_T1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "both", + "execution": {} + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Figure settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import ipywidgets as widgets # interactive display\n", + "%config InlineBackend.figure_format = 'retina'\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Helper functions\n", + "\n", + "t_max = 150e-3 # second\n", + "dt = 1e-3 # second\n", + "tau = 20e-3 # second\n", + "el = -60e-3 # milivolt\n", + "vr = -70e-3 # milivolt\n", + "vth = -50e-3 # milivolt\n", + "r = 100e6 # ohm\n", + "i_mean = 25e-11 # ampere\n", + "\n", + "\n", + "def plot_all(t_range, v, raster=None, spikes=None, spikes_mean=None):\n", + " \"\"\"\n", + " Plots Time evolution for\n", + " (1) multiple realizations of membrane potential\n", + " (2) spikes\n", + " (3) mean spike rate (optional)\n", + "\n", + " Args:\n", + " t_range (numpy array of floats)\n", + " range of time steps for the plots of shape (time steps)\n", + "\n", + " v (numpy array of floats)\n", + " membrane potential values of shape (neurons, time steps)\n", + "\n", + " raster (numpy array of floats)\n", + " spike raster of shape (neurons, time steps)\n", + "\n", + " spikes (dictionary of lists)\n", + " list with spike times indexed by neuron number\n", + "\n", + " spikes_mean (numpy array of floats)\n", + " Mean spike rate for spikes as dictionary\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + "\n", + " v_mean = np.mean(v, axis=0)\n", + " fig_w, fig_h = plt.rcParams['figure.figsize']\n", + " plt.figure(figsize=(fig_w, 1.5 * fig_h))\n", + "\n", + " ax1 = plt.subplot(3, 1, 1)\n", + " for j in range(n):\n", + " plt.scatter(t_range, v[j], color=\"k\", marker=\".\", alpha=0.01)\n", + " plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", + " plt.xticks([])\n", + " plt.ylabel(r'$V_m$ (V)')\n", + "\n", + " if raster is not None:\n", + " plt.subplot(3, 1, 2)\n", + " spikes_mean = np.mean(raster, axis=0)\n", + " plt.imshow(raster, cmap='Greys', origin='lower', aspect='auto')\n", + "\n", + " else:\n", + " plt.subplot(3, 1, 2, sharex=ax1)\n", + " for j in range(n):\n", + " times = np.array(spikes[j])\n", + " plt.scatter(times, j * np.ones_like(times), color=\"C0\", marker=\".\", alpha=0.2)\n", + "\n", + " plt.xticks([])\n", + " plt.ylabel('neuron')\n", + "\n", + " if spikes_mean is not None:\n", + " plt.subplot(3, 1, 3, sharex=ax1)\n", + " plt.plot(t_range, spikes_mean)\n", + " plt.xlabel('time (s)')\n", + " plt.ylabel('rate (Hz)')\n", + "\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 1: Histograms\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 1: Histograms\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'J24tne-IwvY'), ('Bilibili', 'BV1GC4y1h7Ex')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Histograms_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "
\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "Another important statistic is the sample [histogram](https://en.wikipedia.org/wiki/Histogram). For our LIF neuron, it provides an approximate representation of the distribution of membrane potential $V_m(t)$ at time $t=t_k\\in[0,t_{max}]$. For $N$ realizations $V\\left(t_k\\right)$ and $J$ bins is given by:\n", + "\n", + "
\n", + "\\begin{equation}\n", + "N = \\sum_{j=1}^{J} m_j\n", + "\\end{equation}\n", + "
\n", + "\n", + "where $m_j$ is a function that counts the number of samples $V\\left(t_k\\right)$ that fall into bin $j$.\n", + "\n", + "The function `plt.hist(data, nbins)` plots an histogram of `data` in `nbins` bins. The argument `label` defines a label for `data` and `plt.legend()` adds all labels to the plot.\n", + "\n", + "```python\n", + "plt.hist(data, bins, label='my data')\n", + "plt.legend()\n", + "plt.show()\n", + "```\n", + "\n", + "The parameters `histtype='stepfilled'` and `linewidth=0` may improve histogram appearance (depending on taste). You can read more about [different histtype settings](https://matplotlib.org/gallery/statistics/histogram_histtypes.html).\n", + "\n", + "The function `plt.hist` returns the `pdf`, `bins`, and `patches` with the histogram bins, the edges of the bins, and the individual patches used to create the histogram.\n", + "\n", + "```python\n", + "pdf, bins, patches = plt.hist(data, bins)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 2: Nano recap of histograms\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '71f1J98zj80'), ('Bilibili', 'BV1Zv411B7mD')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_histograms_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Coding Exercise 1: Plotting a histogram\n", + "\n", + "Plot an histogram of $J=50$ bins of $N=10000$ realizations of $V(t)$ for $t=t_{max}/10$ and $t=t_{max}$.\n", + "\n", + "We'll make a small correction in the definition of `t_range` to ensure increments of `dt` by using `np.arange` instead of `np.linspace`.\n", + "\n", + "```python\n", + "numpy.arange(start, stop, step)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "#################################################\n", + "## TODO for students: fill out code to plot histogram ##\n", + "# Fill out code and comment or remove the next line\n", + "raise NotImplementedError(\"Student exercise: You need to plot histogram\")\n", + "#################################################\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize t_range, step_end, n, v_n, i and nbins\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 10000\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "nbins = 32\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r * i[:, step])\n", + "\n", + "# Initialize the figure\n", + "plt.figure()\n", + "plt.ylabel('Frequency')\n", + "plt.xlabel('$V_m$ (V)')\n", + "\n", + "# Plot a histogram at t_max/10 (add labels and parameters histtype='stepfilled' and linewidth=0)\n", + "plt.hist(...)\n", + "\n", + "# Plot a histogram at t_max (add labels and parameters histtype='stepfilled' and linewidth=0)\n", + "plt.hist(...)\n", + "\n", + "# Add legend\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize t_range, step_end, n, v_n, i and nbins\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 10000\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "nbins = 32\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r * i[:, step])\n", + "\n", + "# Initialize the figure\n", + "with plt.xkcd():\n", + " plt.figure()\n", + " plt.ylabel('Frequency')\n", + " plt.xlabel('$V_m$ (V)')\n", + "\n", + " # Plot a histogram at t_max/10 (add labels and parameters histtype='stepfilled' and linewidth=0)\n", + " plt.hist(v_n[:,int(step_end / 10)], nbins,\n", + " histtype='stepfilled', linewidth=0,\n", + " label = 't='+ str(t_max / 10) + 's')\n", + "\n", + " # Plot a histogram at t_max (add labels and parameters histtype='stepfilled' and linewidth=0)\n", + " plt.hist(v_n[:, -1], nbins,\n", + " histtype='stepfilled', linewidth=0,\n", + " label = 't='+ str(t_max) + 's')\n", + " # Add legend\n", + " plt.legend()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Plotting_a_histogram_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 2: Dictionaries & introducing spikes\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 3: Dictionaries & introducing spikes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'ioKkiukDkNg'), ('Bilibili', 'BV1H54y1q7oS')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Dictionaries_&_introducing_spikes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "A spike occures whenever $V(t)$ crosses $V_{th}$. In that case, a spike is recorded, and $V(t)$ resets to $V_{reset}$ value. This is summarized in the *reset condition*:\n", + "\n", + "\\begin{equation}\n", + "V(t) = V_{reset}\\quad \\text{ if } V(t)\\geq V_{th}\n", + "\\end{equation}\n", + "\n", + "For more information about spikes or action potentials see [here](https://en.wikipedia.org/wiki/Action_potential) and [here](https://www.khanacademy.org/test-prep/mcat/organ-systems/neuron-membrane-potentials/a/neuron-action-potentials-the-creation-of-a-brain-signal).\n", + "\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 4: Nano recap of dictionaries\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'nvpHtzuZggg'), ('Bilibili', 'BV1GC4y1h7hi')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_dictionaries_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Coding Exercise 2: Adding spiking to the LIF neuron\n", + "\n", + "Insert the reset condition, and collect the spike times of each realization in a dictionary variable `spikes`, with $N=500$.\n", + "\n", + "We've used `plt.plot` for plotting lines and also for plotting dots at `(x,y)` coordinates, which is a [scatter plot](https://en.wikipedia.org/wiki/Scatter_plot). From here on, we'll use use `plt.plot` for plotting lines and for scatter plots: `plt.scatter`.\n", + "\n", + "```python\n", + "plt.scatter(x, y, color=\"k\", marker=\".\")\n", + "```\n", + "\n", + "A *raster plot* represents spikes from multiple neurons by plotting dots at spike times from neuron `j` at plot height `j`, i.e.\n", + "\n", + "```python\n", + "plt.scatter(spike_times, j*np.ones_like(spike_times))\n", + "```\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "In this exercise, we use `plt.subplot` for multiple plots in the same figure. These plots can share the same `x` or `y` axis by specifying the parameter `sharex` or `sharey`. Add `plt.tight_layout()` at the end to automatically adjust subplot parameters to fit the figure area better. Please see the example below for a row of two plots sharing axis `y`.\n", + "\n", + "```python\n", + "# initialize the figure\n", + "plt.figure()\n", + "\n", + "# collect axis of 1st figure in ax1\n", + "ax1 = plt.subplot(1, 2, 1)\n", + "plt.plot(t_range, my_data_left)\n", + "plt.ylabel('ylabel')\n", + "\n", + "# share axis x with 1st figure\n", + "plt.subplot(1, 2, 2, sharey=ax1)\n", + "plt.plot(t_range, my_data_right)\n", + "\n", + "# automatically adjust subplot parameters to figure\n", + "plt.tight_layout()\n", + "plt.show()\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize spikes and spikes_n\n", + "spikes = {j: [] for j in range(n)}\n", + "spikes_n = np.zeros([step_end])\n", + "\n", + "#################################################\n", + "## TODO for students: add spikes to LIF neuron ##\n", + "# Fill out function and remove\n", + "raise NotImplementedError(\"Student exercise: add spikes to LIF neuron\")\n", + "#################################################\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Loop over simulations\n", + " for j in range(n):\n", + "\n", + " # Check if voltage above threshold\n", + " if v_n[j, step] >= vth:\n", + "\n", + " # Reset to reset voltage\n", + " v_n[j, step] = ...\n", + "\n", + " # Add this spike time\n", + " spikes[j] += ...\n", + "\n", + " # Add spike count to this step\n", + " spikes_n[step] += ...\n", + "\n", + "# Collect mean Vm and mean spiking rate\n", + "v_mean = np.mean(v_n, axis=0)\n", + "spikes_mean = spikes_n / n\n", + "\n", + "# Initialize the figure\n", + "plt.figure()\n", + "\n", + "# Plot simulations and sample mean\n", + "ax1 = plt.subplot(3, 1, 1)\n", + "for j in range(n):\n", + " plt.scatter(t_range, v_n[j], color=\"k\", marker=\".\", alpha=0.01)\n", + "plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", + "plt.ylabel('$V_m$ (V)')\n", + "\n", + "# Plot spikes\n", + "plt.subplot(3, 1, 2, sharex=ax1)\n", + "# for each neuron j: collect spike times and plot them at height j\n", + "for j in range(n):\n", + " times = ...\n", + " plt.scatter(...)\n", + "\n", + "plt.ylabel('neuron')\n", + "\n", + "# Plot firing rate\n", + "plt.subplot(3, 1, 3, sharex=ax1)\n", + "plt.plot(t_range, spikes_mean)\n", + "plt.xlabel('time (s)')\n", + "plt.ylabel('rate (Hz)')\n", + "\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize spikes and spikes_n\n", + "spikes = {j: [] for j in range(n)}\n", + "spikes_n = np.zeros([step_end])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Loop over simulations\n", + " for j in range(n):\n", + "\n", + " # Check if voltage above threshold\n", + " if v_n[j, step] >= vth:\n", + "\n", + " # Reset to reset voltage\n", + " v_n[j, step] = vr\n", + "\n", + " # Add this spike time\n", + " spikes[j] += [t]\n", + "\n", + " # Add spike count to this step\n", + " spikes_n[step] += 1\n", + "\n", + "# Collect mean Vm and mean spiking rate\n", + "v_mean = np.mean(v_n, axis=0)\n", + "spikes_mean = spikes_n / n\n", + "\n", + "with plt.xkcd():\n", + " # Initialize the figure\n", + " plt.figure()\n", + "\n", + " # Plot simulations and sample mean\n", + " ax1 = plt.subplot(3, 1, 1)\n", + " for j in range(n):\n", + " plt.scatter(t_range, v_n[j], color=\"k\", marker=\".\", alpha=0.01)\n", + " plt.plot(t_range, v_mean, 'C1', alpha=0.8, linewidth=3)\n", + " plt.ylabel('$V_m$ (V)')\n", + "\n", + " # Plot spikes\n", + " plt.subplot(3, 1, 2, sharex=ax1)\n", + " # for each neuron j: collect spike times and plot them at height j\n", + " for j in range(n):\n", + " times = np.array(spikes[j])\n", + " plt.scatter(times, j * np.ones_like(times), color=\"C0\", marker=\".\", alpha=0.2)\n", + "\n", + " plt.ylabel('neuron')\n", + "\n", + " # Plot firing rate\n", + " plt.subplot(3, 1, 3, sharex=ax1)\n", + " plt.plot(t_range, spikes_mean)\n", + " plt.xlabel('time (s)')\n", + " plt.ylabel('rate (Hz)')\n", + "\n", + " plt.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Adding_spiking_to_the_LIF_neuron_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 3: Boolean indexes\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 5: Boolean indexes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'G0C1v848I9Y'), ('Bilibili', 'BV1W54y1q7eh')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Boolean_indexes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "Boolean arrays can index NumPy arrays to select a subset of elements (also works with lists of booleans).\n", + "\n", + "The boolean array itself can be initiated by a condition, as shown in the example below.\n", + "\n", + "```python\n", + "a = np.array([1, 2, 3])\n", + "b = a>=2\n", + "print(b)\n", + "--> [False True True]\n", + "\n", + "print(a[b])\n", + "--> [2 3]\n", + "\n", + "print(a[a>=2])\n", + "--> [2 3]\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 6: Nano recap of Boolean indexes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'dFPgO5wnyLc'), ('Bilibili', 'BV1W54y1q7gi')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_Boolean_indexes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Coding Exercise 3: Using Boolean indexing\n", + "\n", + "We can avoid looping all neurons in each time step by identifying with boolean arrays the indexes of neurons that spiked in the previous step.\n", + "\n", + "In the example below, `v_rest` is a boolean numpy array with `True` in each index of `v_n` with value `vr` at time index `step`:\n", + "\n", + "```python\n", + "v_rest = (v_n[:,step] == vr)\n", + "print(v_n[v_rest,step])\n", + " --> [vr, ..., vr]\n", + "```\n", + "\n", + "The function `np.where` returns indexes of boolean arrays with `True` values.\n", + "\n", + "You may use the helper function `plot_all` that implements the figure from the previous exercise." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "help(plot_all)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize spikes and spikes_n\n", + "spikes = {j: [] for j in range(n)}\n", + "spikes_n = np.zeros([step_end])\n", + "\n", + "#################################################\n", + "## TODO for students: use Boolean indexing ##\n", + "# Fill out function and remove\n", + "raise NotImplementedError(\"Student exercise: using Boolean indexing\")\n", + "#################################################\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = ...\n", + "\n", + " # Set relevant values of v_n to resting potential using spiked\n", + " v_n[spiked,step] = ...\n", + "\n", + " # Collect spike times\n", + " for j in np.where(spiked)[0]:\n", + " spikes[j] += [t]\n", + " spikes_n[step] += 1\n", + "\n", + "# Collect mean spiking rate\n", + "spikes_mean = spikes_n / n\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, spikes=spikes, spikes_mean=spikes_mean)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize spikes and spikes_n\n", + "spikes = {j: [] for j in range(n)}\n", + "spikes_n = np.zeros([step_end])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to resting potential using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Collect spike times\n", + " for j in np.where(spiked)[0]:\n", + " spikes[j] += [t]\n", + " spikes_n[step] += 1\n", + "\n", + "# Collect mean spiking rate\n", + "spikes_mean = spikes_n / n\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, spikes=spikes, spikes_mean=spikes_mean)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Using_Boolean_indexing_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Coding Exercise 4: Making a binary raster plot\n", + "\n", + "A *binary raster plot* represents spike times as `1`s in a binary grid initialized with `0`s. We start with a numpy array `raster` of zeros with shape `(neurons, time steps)`, and represent a spike from neuron `5` at time step `20` as `raster(5,20)=1`, for example.\n", + "\n", + "The *binary raster plot* is much more efficient than the previous method by plotting the numpy array `raster` as an image:\n", + "\n", + "```python\n", + "plt.imshow(raster, cmap='Greys', origin='lower', aspect='auto')\n", + "```\n", + "\n", + "**Suggestions**\n", + "* At each time step:\n", + " * Initialize boolean numpy array `spiked` with $V_n(t)\\geq V_{th}$\n", + " * Set to `vr` indexes of `v_n` using `spiked`\n", + " * Set to `1` indexes of numpy array `raster` using `spiked`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "#################################################\n", + "## TODO for students: make a raster ##\n", + "# Fill out function and remove\n", + "raise NotImplementedError(\"Student exercise: make a raster \")\n", + "#################################################\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to v_reset using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Set relevant elements in raster to 1 using spiked\n", + " raster[spiked,step] = ...\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to v_reset using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Set relevant elements in raster to 1 using spiked\n", + " raster[spiked,step] = 1.\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Making_a_binary_raster_plot_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 4: Refractory period\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 7: Refractory period\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'KVNdbRY5-nY'), ('Bilibili', 'BV1MT4y1E79j')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Refractory_period_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "The absolute refractory period is a time interval in the order of a few milliseconds during which synaptic input will not lead to a 2nd spike, no matter how strong. This effect is due to the biophysics of the neuron membrane channels, and you can read more about absolute and relative refractory period [here](https://content.byui.edu/file/a236934c-3c60-4fe9-90aa-d343b3e3a640/1/module5/readings/refractory_periods.html) and [here](https://en.wikipedia.org/wiki/Refractory_period_(physiology)).\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 8: Nano recap of refractory period\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'DOoftC0JU2k'), ('Bilibili', 'BV1pA411e7je')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_refractory_period_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Coding Exercise 5: Investigating refactory periods\n", + "\n", + "Investigate the effect of (absolute) refractory period $t_{ref}$ on the evolution of output rate $\\lambda(t)$. Add refractory period $t_{ref}=10$ ms after each spike, during which $V(t)$ is clamped to $V_{reset}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "#################################################\n", + "## TODO for students: add refactory period ##\n", + "# Fill out function and remove\n", + "raise NotImplementedError(\"Student exercise: add refactory period \")\n", + "#################################################\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize t_ref and last_spike\n", + "t_ref = 0.01\n", + "last_spike = -t_ref * np.ones([n])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to v_reset using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Set relevant elements in raster to 1 using spiked\n", + " raster[spiked,step] = 1.\n", + "\n", + " # Initialize boolean numpy array clamped using last_spike, t and t_ref\n", + " clamped = ...\n", + "\n", + " # Reset clamped neurons to vr using clamped\n", + " v_n[clamped,step] = ...\n", + "\n", + " # Update numpy array last_spike with time t for spiking neurons\n", + " last_spike[spiked] = ...\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize t_ref and last_spike\n", + "t_ref = 0.01\n", + "last_spike = -t_ref * np.ones([n])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step == 0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:, step] = v_n[:, step - 1] + (dt / tau) * (el - v_n[:, step - 1] + r*i[:, step])\n", + "\n", + " # Initialize boolean numpy array `spiked` with v_n > v_thr\n", + " spiked = (v_n[:,step] >= vth)\n", + "\n", + " # Set relevant values of v_n to v_reset using spiked\n", + " v_n[spiked,step] = vr\n", + "\n", + " # Set relevant elements in raster to 1 using spiked\n", + " raster[spiked,step] = 1.\n", + "\n", + " # Initialize boolean numpy array clamped using last_spike, t and t_ref\n", + " clamped = (last_spike + t_ref > t)\n", + "\n", + " # Reset clamped neurons to vr using clamped\n", + " v_n[clamped,step] = vr\n", + "\n", + " # Update numpy array last_spike with time t for spiking neurons\n", + " last_spike[spiked] = t\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Investigating_refractory_periods_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Interactive Demo 1: Random refractory period\n", + "\n", + "In the following interactive demo, we will investigate the effect of random refractory periods. We will use random refactory periods $t_{ref}$ with\n", + "$t_{ref} = \\mu + \\sigma\\,\\mathcal{N}$, where $\\mathcal{N}$ is the [normal distribution](https://en.wikipedia.org/wiki/Normal_distribution), $\\mu=0.01$ and $\\sigma=0.007$.\n", + "\n", + "Refractory period samples `t_ref` of size `n` is initialized with `np.random.normal`. We clip negative values to `0` with boolean indexes. (Why?) You can double-click the cell to see the hidden code.\n", + "\n", + "You can play with the parameters mu and sigma and visualize the resulting simulation. What is the effect of different $\\sigma$ values?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to enable the demo\n", + "\n", + "def random_ref_period(mu, sigma):\n", + " # set random number generator\n", + " np.random.seed(2020)\n", + "\n", + " # initialize step_end, t_range, n, v_n, syn and raster\n", + " t_range = np.arange(0, t_max, dt)\n", + " step_end = len(t_range)\n", + " n = 500\n", + " v_n = el * np.ones([n,step_end])\n", + " syn = i_mean * (1 + 0.1*(t_max/dt)**(0.5)*(2*np.random.random([n,step_end])-1))\n", + " raster = np.zeros([n,step_end])\n", + "\n", + " # initialize t_ref and last_spike\n", + " t_ref = mu + sigma*np.random.normal(size=n)\n", + " t_ref[t_ref<0] = 0\n", + " last_spike = -t_ref * np.ones([n])\n", + "\n", + " # loop time steps\n", + " for step, t in enumerate(t_range):\n", + " if step==0:\n", + " continue\n", + "\n", + " v_n[:,step] = v_n[:,step-1] + dt/tau * (el - v_n[:,step-1] + r*syn[:,step])\n", + "\n", + " # boolean array spiked indexes neurons with v>=vth\n", + " spiked = (v_n[:,step] >= vth)\n", + " v_n[spiked,step] = vr\n", + " raster[spiked,step] = 1.\n", + "\n", + " # boolean array clamped indexes refractory neurons\n", + " clamped = (last_spike + t_ref > t)\n", + " v_n[clamped,step] = vr\n", + " last_spike[spiked] = t\n", + "\n", + " # plot multiple realizations of Vm, spikes and mean spike rate\n", + " plot_all(t_range, v_n, raster)\n", + "\n", + " # plot histogram of t_ref\n", + " plt.figure(figsize=(8,4))\n", + " plt.hist(t_ref, bins=32, histtype='stepfilled', linewidth=0, color='C1')\n", + " plt.xlabel(r'$t_{ref}$ (s)')\n", + " plt.ylabel('count')\n", + " plt.tight_layout()\n", + "\n", + "_ = widgets.interact(random_ref_period, mu = (0.01, 0.05, 0.01), \\\n", + " sigma = (0.001, 0.02, 0.001))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Random_refractory_period_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 5: Using functions\n", + "Running key parts of your code inside functions improves your coding narrative by making it clearer and more flexible to future changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 9: Functions\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'mkf8riqCjS4'), ('Bilibili', 'BV1sa4y1a7pq')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Functions_Video\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 10: Nano recap of functions\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '0An_NnVWY_Q'), ('Bilibili', 'BV1pz411v74H')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_functions_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Coding Exercise 6: Rewriting code with functions\n", + "\n", + "We will now re-organize parts of the code from the previous exercise with functions. You need to complete the function `spike_clamp()` to update $V(t)$ and deal with spiking and refractoriness" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "def ode_step(v, i, dt):\n", + " \"\"\"\n", + " Evolves membrane potential by one step of discrete time integration\n", + "\n", + " Args:\n", + " v (numpy array of floats)\n", + " membrane potential at previous time step of shape (neurons)\n", + "\n", + " i (numpy array of floats)\n", + " synaptic input at current time step of shape (neurons)\n", + "\n", + " dt (float)\n", + " time step increment\n", + "\n", + " Returns:\n", + " v (numpy array of floats)\n", + " membrane potential at current time step of shape (neurons)\n", + " \"\"\"\n", + " v = v + dt/tau * (el - v + r*i)\n", + "\n", + " return v\n", + "\n", + "\n", + "def spike_clamp(v, delta_spike):\n", + " \"\"\"\n", + " Resets membrane potential of neurons if v>= vth\n", + " and clamps to vr if interval of time since last spike < t_ref\n", + "\n", + " Args:\n", + " v (numpy array of floats)\n", + " membrane potential of shape (neurons)\n", + "\n", + " delta_spike (numpy array of floats)\n", + " interval of time since last spike of shape (neurons)\n", + "\n", + " Returns:\n", + " v (numpy array of floats)\n", + " membrane potential of shape (neurons)\n", + " spiked (numpy array of floats)\n", + " boolean array of neurons that spiked of shape (neurons)\n", + " \"\"\"\n", + "\n", + " ####################################################\n", + " ## TODO for students: complete spike_clamp\n", + " # Fill out function and remove\n", + " raise NotImplementedError(\"Student exercise: complete spike_clamp\")\n", + " ####################################################\n", + " # Boolean array spiked indexes neurons with v>=vth\n", + " spiked = ...\n", + " v[spiked] = ...\n", + "\n", + " # Boolean array clamped indexes refractory neurons\n", + " clamped = ...\n", + " v[clamped] = ...\n", + "\n", + " return v, spiked\n", + "\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize t_ref and last_spike\n", + "mu = 0.01\n", + "sigma = 0.007\n", + "t_ref = mu + sigma*np.random.normal(size=n)\n", + "t_ref[t_ref<0] = 0\n", + "last_spike = -t_ref * np.ones([n])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", + "\n", + " # Reset membrane potential and clamp\n", + " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", + "\n", + " # Update raster and last_spike\n", + " raster[spiked,step] = 1.\n", + " last_spike[spiked] = t\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "def ode_step(v, i, dt):\n", + " \"\"\"\n", + " Evolves membrane potential by one step of discrete time integration\n", + "\n", + " Args:\n", + " v (numpy array of floats)\n", + " membrane potential at previous time step of shape (neurons)\n", + "\n", + " i (numpy array of floats)\n", + " synaptic input at current time step of shape (neurons)\n", + "\n", + " dt (float)\n", + " time step increment\n", + "\n", + " Returns:\n", + " v (numpy array of floats)\n", + " membrane potential at current time step of shape (neurons)\n", + " \"\"\"\n", + " v = v + dt/tau * (el - v + r*i)\n", + "\n", + " return v\n", + "\n", + "\n", + "def spike_clamp(v, delta_spike):\n", + " \"\"\"\n", + " Resets membrane potential of neurons if v>= vth\n", + " and clamps to vr if interval of time since last spike < t_ref\n", + "\n", + " Args:\n", + " v (numpy array of floats)\n", + " membrane potential of shape (neurons)\n", + "\n", + " delta_spike (numpy array of floats)\n", + " interval of time since last spike of shape (neurons)\n", + "\n", + " Returns:\n", + " v (numpy array of floats)\n", + " membrane potential of shape (neurons)\n", + " spiked (numpy array of floats)\n", + " boolean array of neurons that spiked of shape (neurons)\n", + " \"\"\"\n", + "\n", + " # Boolean array spiked indexes neurons with v>=vth\n", + " spiked = (v >= vth)\n", + " v[spiked] = vr\n", + "\n", + " # Boolean array clamped indexes refractory neurons\n", + " clamped = (t_ref > delta_spike)\n", + " v[clamped] = vr\n", + "\n", + " return v, spiked\n", + "\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize t_ref and last_spike\n", + "mu = 0.01\n", + "sigma = 0.007\n", + "t_ref = mu + sigma*np.random.normal(size=n)\n", + "t_ref[t_ref<0] = 0\n", + "last_spike = -t_ref * np.ones([n])\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Skip first iteration\n", + " if step==0:\n", + " continue\n", + "\n", + " # Compute v_n\n", + " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", + "\n", + " # Reset membrane potential and clamp\n", + " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", + "\n", + " # Update raster and last_spike\n", + " raster[spiked,step] = 1.\n", + " last_spike[spiked] = t\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Rewriting_code_with_functions_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 6: Using classes\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 11: Classes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'dGRESMoNPh0'), ('Bilibili', 'BV1hz411v7ne')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Classes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "Using classes helps with code reuse and reliability. Well-designed classes are like black boxes, receiving inputs and providing expected outputs. The details of how the class processes inputs and produces outputs are unimportant.\n", + "\n", + "See additional details here: [A Beginner's Python Tutorial/Classes](https://en.wikibooks.org/wiki/A_Beginner%27s_Python_Tutorial/Classes)\n", + "\n", + "*Attributes* are variables internal to the class, and *methods* are functions internal to the class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 12: Nano recap of classes\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '4YNpMpVW2qs'), ('Bilibili', 'BV12V41167yu')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Nano_recap_of_classes_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Coding Exercise 7: Making a LIF class\n", + "In this exercise we'll practice with Python classes by implementing `LIFNeurons`, a class that evolves and keeps state of multiple realizations of LIF neurons.\n", + "\n", + "Several attributes are used to keep state of our neurons:\n", + "\n", + "```python\n", + "self.v current membrane potential\n", + "self.spiked neurons that spiked\n", + "self.last_spike last spike time of each neuron\n", + "self.t running time of the simulation\n", + "self.steps simulation step\n", + "```\n", + "\n", + "There is a single method:\n", + "\n", + "```python\n", + "self.ode_step() performs single step discrete time integration\n", + " for provided synaptic current and dt\n", + "```\n", + "\n", + "Complete the spike and clamp part of method `self.ode_step` (should be similar to function `spike_and_clamp` seen before)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# Simulation class\n", + "class LIFNeurons:\n", + " \"\"\"\n", + " Keeps track of membrane potential for multiple realizations of LIF neuron,\n", + " and performs single step discrete time integration.\n", + " \"\"\"\n", + " def __init__(self, n, t_ref_mu=0.01, t_ref_sigma=0.002,\n", + " tau=20e-3, el=-60e-3, vr=-70e-3, vth=-50e-3, r=100e6):\n", + "\n", + " # Neuron count\n", + " self.n = n\n", + "\n", + " # Neuron parameters\n", + " self.tau = tau # second\n", + " self.el = el # milivolt\n", + " self.vr = vr # milivolt\n", + " self.vth = vth # milivolt\n", + " self.r = r # ohm\n", + "\n", + " # Initializes refractory period distribution\n", + " self.t_ref_mu = t_ref_mu\n", + " self.t_ref_sigma = t_ref_sigma\n", + " self.t_ref = self.t_ref_mu + self.t_ref_sigma * np.random.normal(size=self.n)\n", + " self.t_ref[self.t_ref<0] = 0\n", + "\n", + " # State variables\n", + " self.v = self.el * np.ones(self.n)\n", + " self.spiked = self.v >= self.vth\n", + " self.last_spike = -self.t_ref * np.ones([self.n])\n", + " self.t = 0.\n", + " self.steps = 0\n", + "\n", + "\n", + " def ode_step(self, dt, i):\n", + "\n", + " # Update running time and steps\n", + " self.t += dt\n", + " self.steps += 1\n", + "\n", + " # One step of discrete time integration of dt\n", + " self.v = self.v + dt / self.tau * (self.el - self.v + self.r * i)\n", + "\n", + " ####################################################\n", + " ## TODO for students: complete the `ode_step` method\n", + " # Fill out function and remove\n", + " raise NotImplementedError(\"Student exercise: complete the ode_step method\")\n", + " ####################################################\n", + "\n", + " # Spike and clamp\n", + " self.spiked = ...\n", + " self.v[self.spiked] = ...\n", + " self.last_spike[self.spiked] = ...\n", + " clamped = ...\n", + " self.v[clamped] = ...\n", + "\n", + " self.last_spike[self.spiked] = ...\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize neurons\n", + "neurons = LIFNeurons(n)\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Call ode_step method\n", + " neurons.ode_step(dt, i[:,step])\n", + "\n", + " # Log v_n and spike history\n", + " v_n[:,step] = neurons.v\n", + " raster[neurons.spiked, step] = 1.\n", + "\n", + "# Report running time and steps\n", + "print(f'Ran for {neurons.t:.3}s in {neurons.steps} steps.')\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Simulation class\n", + "class LIFNeurons:\n", + " \"\"\"\n", + " Keeps track of membrane potential for multiple realizations of LIF neuron,\n", + " and performs single step discrete time integration.\n", + " \"\"\"\n", + " def __init__(self, n, t_ref_mu=0.01, t_ref_sigma=0.002,\n", + " tau=20e-3, el=-60e-3, vr=-70e-3, vth=-50e-3, r=100e6):\n", + "\n", + " # Neuron count\n", + " self.n = n\n", + "\n", + " # Neuron parameters\n", + " self.tau = tau # second\n", + " self.el = el # milivolt\n", + " self.vr = vr # milivolt\n", + " self.vth = vth # milivolt\n", + " self.r = r # ohm\n", + "\n", + " # Initializes refractory period distribution\n", + " self.t_ref_mu = t_ref_mu\n", + " self.t_ref_sigma = t_ref_sigma\n", + " self.t_ref = self.t_ref_mu + self.t_ref_sigma * np.random.normal(size=self.n)\n", + " self.t_ref[self.t_ref<0] = 0\n", + "\n", + " # State variables\n", + " self.v = self.el * np.ones(self.n)\n", + " self.spiked = self.v >= self.vth\n", + " self.last_spike = -self.t_ref * np.ones([self.n])\n", + " self.t = 0.\n", + " self.steps = 0\n", + "\n", + "\n", + " def ode_step(self, dt, i):\n", + "\n", + " # Update running time and steps\n", + " self.t += dt\n", + " self.steps += 1\n", + "\n", + " # One step of discrete time integration of dt\n", + " self.v = self.v + dt / self.tau * (self.el - self.v + self.r * i)\n", + "\n", + " # Spike and clamp\n", + " self.spiked = (self.v >= self.vth)\n", + " self.v[self.spiked] = self.vr\n", + " self.last_spike[self.spiked] = self.t\n", + " clamped = (self.t_ref > self.t-self.last_spike)\n", + " self.v[clamped] = self.vr\n", + "\n", + " self.last_spike[self.spiked] = self.t\n", + "\n", + "# Set random number generator\n", + "np.random.seed(2020)\n", + "\n", + "# Initialize step_end, t_range, n, v_n and i\n", + "t_range = np.arange(0, t_max, dt)\n", + "step_end = len(t_range)\n", + "n = 500\n", + "v_n = el * np.ones([n, step_end])\n", + "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", + "\n", + "# Initialize binary numpy array for raster plot\n", + "raster = np.zeros([n,step_end])\n", + "\n", + "# Initialize neurons\n", + "neurons = LIFNeurons(n)\n", + "\n", + "# Loop over time steps\n", + "for step, t in enumerate(t_range):\n", + "\n", + " # Call ode_step method\n", + " neurons.ode_step(dt, i[:,step])\n", + "\n", + " # Log v_n and spike history\n", + " v_n[:,step] = neurons.v\n", + " raster[neurons.spiked, step] = 1.\n", + "\n", + "# Report running time and steps\n", + "print(f'Ran for {neurons.t:.3}s in {neurons.steps} steps.')\n", + "\n", + "# Plot multiple realizations of Vm, spikes and mean spike rate\n", + "with plt.xkcd():\n", + " plot_all(t_range, v_n, raster)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Making_a_LIF_class_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Summary\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 12: Last concepts & recap\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'h4mSJBPocPo'), ('Bilibili', 'BV1MC4y1h7eA')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Last_concepts_&_recap_Video\")" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "W0D2_Tutorial1", + "provenance": [], + "toc_visible": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/tutorials/W0D2_PythonWorkshop2/instructor/W0D2_Tutorial1.ipynb b/tutorials/W0D2_PythonWorkshop2/instructor/W0D2_Tutorial1.ipynb index 59f621d..db23b72 100644 --- a/tutorials/W0D2_PythonWorkshop2/instructor/W0D2_Tutorial1.ipynb +++ b/tutorials/W0D2_PythonWorkshop2/instructor/W0D2_Tutorial1.ipynb @@ -47,9 +47,10 @@ "source": [ "---\n", "## Tutorial objectives\n", + "\n", "We learned basic Python and NumPy concepts in the previous tutorial. These new and efficient coding techniques can be applied repeatedly in tutorials from the NMA course, and elsewhere.\n", "\n", - "In this tutorial, we'll introduce spikes in our LIF neuron and evaluate the refractory period's effect in spiking dynamics!\n" + "In this tutorial, we'll introduce spikes in our LIF neuron and evaluate the refractory period's effect in spiking dynamics!" ] }, { @@ -296,7 +297,7 @@ "\n", "
\n", "\n", - "Another important statistic is the sample [histogram](https://en.wikipedia.org/wiki/Histogram). For our LIF neuron it provides an approximate representation of the distribution of membrane potential $V_m(t)$ at time $t=t_k\\in[0,t_{max}]$. For $N$ realizations $V\\left(t_k\\right)$ and $J$ bins is given by:\n", + "Another important statistic is the sample [histogram](https://en.wikipedia.org/wiki/Histogram). For our LIF neuron, it provides an approximate representation of the distribution of membrane potential $V_m(t)$ at time $t=t_k\\in[0,t_{max}]$. For $N$ realizations $V\\left(t_k\\right)$ and $J$ bins is given by:\n", "\n", "
\n", "\\begin{equation}\n", @@ -611,7 +612,7 @@ "execution": {} }, "source": [ - "A spike takes place whenever $V(t)$ crosses $V_{th}$. In that case, a spike is recorded and $V(t)$ resets to $V_{reset}$ value. This is summarized in the *reset condition*:\n", + "A spike occures whenever $V(t)$ crosses $V_{th}$. In that case, a spike is recorded, and $V(t)$ resets to $V_{reset}$ value. This is summarized in the *reset condition*:\n", "\n", "\\begin{equation}\n", "V(t) = V_{reset}\\quad \\text{ if } V(t)\\geq V_{th}\n", @@ -773,7 +774,7 @@ "for step, t in enumerate(t_range):\n", "\n", " # Skip first iteration\n", - " if step==0:\n", + " if step == 0:\n", " continue\n", "\n", " # Compute v_n\n", @@ -856,7 +857,7 @@ "for step, t in enumerate(t_range):\n", "\n", " # Skip first iteration\n", - " if step==0:\n", + " if step == 0:\n", " continue\n", "\n", " # Compute v_n\n", @@ -1008,7 +1009,7 @@ "execution": {} }, "source": [ - "Numpy arrays can be indexed by boolean arrays to select a subset of elements (also works with lists of booleans).\n", + "Boolean arrays can index NumPy arrays to select a subset of elements (also works with lists of booleans).\n", "\n", "The boolean array itself can be initiated by a condition, as shown in the example below.\n", "\n", @@ -1195,7 +1196,7 @@ }, "outputs": [], "source": [ - "# to_remove solutions\n", + "# to_remove solution\n", "\n", "# Set random number generator\n", "np.random.seed(2020)\n", @@ -1557,9 +1558,8 @@ }, "source": [ "## Coding Exercise 5: Investigating refactory periods\n", - "Investigate the effect of (absolute) refractory period $t_{ref}$ on the evolution of output rate $\\lambda(t)$. Add refractory period $t_{ref}=10$ ms after each spike, during which $V(t)$ is clamped to $V_{reset}$.\n", "\n", - "\n" + "Investigate the effect of (absolute) refractory period $t_{ref}$ on the evolution of output rate $\\lambda(t)$. Add refractory period $t_{ref}=10$ ms after each spike, during which $V(t)$ is clamped to $V_{reset}$." ] }, { @@ -1707,13 +1707,13 @@ }, "source": [ "## Interactive Demo 1: Random refractory period\n", + "\n", "In the following interactive demo, we will investigate the effect of random refractory periods. We will use random refactory periods $t_{ref}$ with\n", "$t_{ref} = \\mu + \\sigma\\,\\mathcal{N}$, where $\\mathcal{N}$ is the [normal distribution](https://en.wikipedia.org/wiki/Normal_distribution), $\\mu=0.01$ and $\\sigma=0.007$.\n", "\n", - "Refractory period samples `t_ref` of size `n` is initialized with `np.random.normal`. We clip negative values to `0` with boolean indexes. (Why?) You can double click the cell to see the hidden code.\n", + "Refractory period samples `t_ref` of size `n` is initialized with `np.random.normal`. We clip negative values to `0` with boolean indexes. (Why?) You can double-click the cell to see the hidden code.\n", "\n", - "You can play with the parameters mu and sigma and visualize the resulting simulation.\n", - "What is the effect of different $\\sigma$ values?\n" + "You can play with the parameters mu and sigma and visualize the resulting simulation. What is the effect of different $\\sigma$ values?" ] }, { @@ -1944,17 +1944,18 @@ }, "source": [ "## Coding Exercise 6: Rewriting code with functions\n", + "\n", "We will now re-organize parts of the code from the previous exercise with functions. You need to complete the function `spike_clamp()` to update $V(t)$ and deal with spiking and refractoriness" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": { + "colab_type": "text", "execution": {} }, - "outputs": [], "source": [ + "```python\n", "def ode_step(v, i, dt):\n", " \"\"\"\n", " Evolves membrane potential by one step of discrete time integration\n", @@ -1963,7 +1964,7 @@ " v (numpy array of floats)\n", " membrane potential at previous time step of shape (neurons)\n", "\n", - " v (numpy array of floats)\n", + " i (numpy array of floats)\n", " synaptic input at current time step of shape (neurons)\n", "\n", " dt (float)\n", @@ -1977,6 +1978,7 @@ "\n", " return v\n", "\n", + "\n", "def spike_clamp(v, delta_spike):\n", " \"\"\"\n", " Resets membrane potential of neurons if v>= vth\n", @@ -2050,7 +2052,9 @@ " last_spike[spiked] = t\n", "\n", "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "plot_all(t_range, v_n, raster)" + "plot_all(t_range, v_n, raster)\n", + "\n", + "```" ] }, { @@ -2061,6 +2065,8 @@ }, "outputs": [], "source": [ + "# to_remove solution\n", + "\n", "def ode_step(v, i, dt):\n", " \"\"\"\n", " Evolves membrane potential by one step of discrete time integration\n", @@ -2069,7 +2075,7 @@ " v (numpy array of floats)\n", " membrane potential at previous time step of shape (neurons)\n", "\n", - " v (numpy array of floats)\n", + " i (numpy array of floats)\n", " synaptic input at current time step of shape (neurons)\n", "\n", " dt (float)\n", @@ -2083,7 +2089,7 @@ "\n", " return v\n", "\n", - "# to_remove solution\n", + "\n", "def spike_clamp(v, delta_spike):\n", " \"\"\"\n", " Resets membrane potential of neurons if v>= vth\n", @@ -2254,7 +2260,7 @@ "execution": {} }, "source": [ - "Using classes helps with code reuse and reliability. Well-designed classes are like black boxes in that they receive inputs and provide expected outputs. The details of how the class processes inputs and produces outputs are unimportant.\n", + "Using classes helps with code reuse and reliability. Well-designed classes are like black boxes, receiving inputs and providing expected outputs. The details of how the class processes inputs and produces outputs are unimportant.\n", "\n", "See additional details here: [A Beginner's Python Tutorial/Classes](https://en.wikibooks.org/wiki/A_Beginner%27s_Python_Tutorial/Classes)\n", "\n", diff --git a/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_9aaee1d8.py b/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_950b2963.py similarity index 99% rename from tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_9aaee1d8.py rename to tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_950b2963.py index dcce5f8..25a9424 100644 --- a/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_9aaee1d8.py +++ b/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_950b2963.py @@ -17,7 +17,7 @@ for step, t in enumerate(t_range): # Skip first iteration - if step==0: + if step == 0: continue # Compute v_n diff --git a/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_bf9f75ab.py b/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_bf9f75ab.py new file mode 100644 index 0000000..1174616 --- /dev/null +++ b/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_bf9f75ab.py @@ -0,0 +1,94 @@ + +def ode_step(v, i, dt): + """ + Evolves membrane potential by one step of discrete time integration + + Args: + v (numpy array of floats) + membrane potential at previous time step of shape (neurons) + + i (numpy array of floats) + synaptic input at current time step of shape (neurons) + + dt (float) + time step increment + + Returns: + v (numpy array of floats) + membrane potential at current time step of shape (neurons) + """ + v = v + dt/tau * (el - v + r*i) + + return v + + +def spike_clamp(v, delta_spike): + """ + Resets membrane potential of neurons if v>= vth + and clamps to vr if interval of time since last spike < t_ref + + Args: + v (numpy array of floats) + membrane potential of shape (neurons) + + delta_spike (numpy array of floats) + interval of time since last spike of shape (neurons) + + Returns: + v (numpy array of floats) + membrane potential of shape (neurons) + spiked (numpy array of floats) + boolean array of neurons that spiked of shape (neurons) + """ + + # Boolean array spiked indexes neurons with v>=vth + spiked = (v >= vth) + v[spiked] = vr + + # Boolean array clamped indexes refractory neurons + clamped = (t_ref > delta_spike) + v[clamped] = vr + + return v, spiked + + +# Set random number generator +np.random.seed(2020) + +# Initialize step_end, t_range, n, v_n and i +t_range = np.arange(0, t_max, dt) +step_end = len(t_range) +n = 500 +v_n = el * np.ones([n, step_end]) +i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1)) + +# Initialize binary numpy array for raster plot +raster = np.zeros([n,step_end]) + +# Initialize t_ref and last_spike +mu = 0.01 +sigma = 0.007 +t_ref = mu + sigma*np.random.normal(size=n) +t_ref[t_ref<0] = 0 +last_spike = -t_ref * np.ones([n]) + +# Loop over time steps +for step, t in enumerate(t_range): + + # Skip first iteration + if step==0: + continue + + # Compute v_n + v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt) + + # Reset membrane potential and clamp + v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike) + + # Update raster and last_spike + raster[spiked,step] = 1. + last_spike[spiked] = t + +# Plot multiple realizations of Vm, spikes and mean spike rate +with plt.xkcd(): + plot_all(t_range, v_n, raster) \ No newline at end of file diff --git a/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_5061d76b.py b/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_c368c1ef.py similarity index 100% rename from tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_5061d76b.py rename to tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_c368c1ef.py diff --git a/tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_bf9f75ab_0.png b/tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_bf9f75ab_0.png new file mode 100644 index 0000000000000000000000000000000000000000..b2dce3039b3ed78c642c94269d970fc6b791a757 GIT binary patch literal 26666 zcmdVDd0dZq+dkfm8-u~5NrWPVqG&;pLS#uoiL^*6ElQ<*HMU4)OGOJBl~zyLRwpXIvV*ZX~*$9WvbdCnD86{VTe z`KNPnan0PlOYtBV*AyNuu5bIln}UCtvMD!^i_55Yx8hbc`@qgRhrlwMbotLi^DnvR z{^&dZBscet89!!w<4u&SzAE9R z-{2CsxSlLFR0j2WR%o~-2&;au?8zwP?|H-6E>aWg(H zu3vQ4KREqdbk}-4?dPIcmfs|`0{*W(#{Z4W^q*{R+I3u9s(XbKifkvp7LG2koy^6h zFf&Dhb6R#SQ|7GXoM|}6_~Wja^%v%c#W$v|t@!26nq!R?Zez#BM@rnhtKyCFoO;sv z_TE{2#ipY?XRxa#6#MyY%(0}6gO&PC9lC7Cz%yCX%Ift;``dbT*#o-0qaAU1&kPD4 z9thoeKvngJmzQRG?iEW}HagIuF;$6z1{YWAo=2r^XP6s5%g~=K^4$DB4VQ+nevHc)HF5s@>(rW}wX^c6eo_Z(I8R=T zZ`n0L`7L+Ebp_%wS&nHVAMYK|(pu=J<7oVG;n_guLvnj z5Kn2odFtKIugxnu-#pkJ=+vFUTZ~PKy;m))Tsh6$ynNyepyenYJ#A%eKJ!v2!Y zeT91}Hg`nFOLS@Ix=61#uT8$J&ct+uoC~oN8Pe z_BPXvy{BSLYB4^Qo|w~-Rm@7MDf_Elw(YFsZkXt{)9)yc;pToDr4cu8-HB|mrjJGU z{EYeL&fSUI6V*h^=~IV;YT!t>e^{nA{PY+UT{R z-owM=r=NZruIq8t&{KVa=c=4)S`n*Wy+T}E{KeU}IXiaj;O2gM>g_|53jFn*iI;AM zvqRz#b4|w^Rw!3)$7b7`cJuzxc*9=bIOpZAqopCaTc<8k``|xw!TOWcIBzju>J36N zRc*Z@MOLql9$d)0nO`7DCQ?5<#MM#D{=>!8nRA4M)}OAvZM?y>JUZWWgGGH>mNfIi z`|DFA^k%oTwB*b;3cS&VU&yd?N9F?mx`Ho zI)`mjRCIMMWBcA-l!{GlQlX=zr4`d#;3w{LZqn4lPJ+yb51!7tAa~o(kFP|YJuVg} zuA8yhwbxTOn>`}WUh&+#j=wZ)pXVVJndPi6ewnvZA1)SpxWjX9z~G7y=YuWj1>UD| z3=>VoSFVis^KC7~wmM#GRj{Uatbx6SU)tnHZf=Xr3ESGUrTbE^oi2-14@<{M6zMr| z@ZiA@{LGc8-lJ`CO|sa-!ntWd04vUQAPR>{DP`TFO}2#ze&V`1Er}Y@M-Key^-jMc z*hqHGmLFfs>h*EE?Uaf0A24`x`QFHck74`no#?7g|45|->t9S)S8g<(i83j8C ze=H2R)uxO6k$7k|vr1|T{;K=-Z|+spva7Rs>NwE8C(jiD$+FrwJm=k`y^?VgpOoYy z&vas4LM<9HuytL=1~>Mk^@%v!W~DjwH6CjiJsc+QJp7pwK$O!w&e|J{REZtejdz_G zP43NI&~)dR`4wDO_Oum&Kj8gd-itQvj5jPgF)=!TWgcIu<2usfFKc6BR`)_$a>%J+ zOefH>{V{LsJ#3_*Iy>F(7s*N?aOu~lIlPV2&78N%*6jOvYZo*7DSW@r?pwkJ!N0zF zs-Ne-(XvrT#+~h zP&^8rM~@yowQ9+qx4~TNqCU>x0$P>+&Z_vG1I(FMP){j3ARTn}-R^Y=8=uOeG#=?6 zdu}#gKlj>|4Gqqc=II0FyB;0%EuJsucy_$Y!p-x+_REJiW+3k&rg-?vIoWSAWgqe{n6x9Ij*qv>`*?WJo--^7UWEA2`{A-+E)vW`JovZxkd2KVOCMtKY1^3e zST^5K$}JGjTu86<%N$>hSI+g~m%fUyv$5^xTLdd3x?VD&(YG|D({JuO;@36Zy*;w0m6wa_c+$UwuT{4Tx{X_ociUxp_PF#oSvFo-v9q-}C=}=M z?F*ZZPvtSkoZI&5UPXdmfrNeJ(SVLai!TmI87n~E&2^C=Cl$}s#&fN#C=8L8kIfiu z_g{Zn6M?|SZzu`*V!;OUYdM~Ld58AP@m6SA@2j<`(6&Tm>zX*hiGFjxu|Rkx3fjP&M+yRH*^@#*2b^=3yBj7x?4bYz3PDTro_)%D~8 z(xQY5yJvnC*)P|Fd#Q+as|?|5HH#nCS2lT;T42dM4?KK0%!o-Q?a^^qU;Zy7mx zZK2FFt?NH<-`Ml$MDwc~y{+oQaqzyhS;?y588^5n959(R{rF02zPAF|wJ)Ed|$-$?y9Fp-JvF!r5>@cU;}dtsYN zY~AJ~AlEpQ$##kP%QWtg`n#VtWRC3no?mLEkkFnzdro|5er?&6SaH~CjUKZKTemOfXUaml=>q+0aLgt!O`7TUzI@OZzYuWxdeCL4!2Na!06`FB? zbKfcnYKM>AxqbU&HP$MkG@W_RdP4;#R-#bx zE;OAO8)`6J!YPDj9%3S)a^S{wRQef?;+8%01e{h2;Q5FY0aEuhA;5?aY3|;=yVjvK z$g)D)E{9On_DkPi$#5Cl8Lkra5c`hGF2uMECjc2yEd~Zu07uyOzg1f5yQi%9SLRj~ zCSRCVrUt+q%f_nb%n(mgZ5aU~s_t^yWZUU6U(zu9+I&e7?QnqF-b#I+9X<8wQE3~P z8-9PGW?|W+%ZqG}T;rD(sZO%cZL0(V%{iS^H&@cA_!@v;*Tr_uv-@;A82{ud#cKaj&p`I^aG3g;OLjvi^mZP9hUVY8Oqp`7Z<;sstu9r^nd+n zvL%9oaBMnK=R6s!lW9nu0TslADJ`^AG^={H{9|#@f_ob*ywA!mva7XJiq=V&6#zq3 zEzjYXR=zy#ZL$(^lyJa)xdF9k?bP)kMg)n;N*k;;WdaX^`a`7lfZh#tXAh0DhBEhS z_2tR4EK?TijSu%ebsqUb`LU^JL&G)PU~>Pvy^=lh{cpBgmdFjgXZ;?X=9^oq?b|Ou zadsRD*rT^8PpKkSnz#pD>1rGXx-BMU|1~pE^F>~v`aH)zu6A<2vuMQX_D;R~r4_^>;*$GgXMDAh*Q#Ubl2?y`%Ox!hkY2jd#Sdo;5MFjo6Vq*F^R=iZ!N zytezTW>3bLdCEZhdG4hX?`&qwm{CwS-PkT^Kc_5}^9||x*5pvtv55q; zDhW?tXA{sumVmlOqeg+2I}UH!;&f9x;%bB3`KzgEdVzlms4sd4a5lP4F-xVkMF>Z%vYg4-OTU z`qIn=hfA%7Dv?|p7c>m-40FSh<*iAoeOA*8v|W_Y*B8Z#`?<4n&9Ucd@%a=3I4t4! z3pYNg9%C-|iA9a5UY6TLh1>EiKRC3WzZJgk-r>*zyW)lvC6GTS3jNnfYntva3YOcn z^V&Raa2q%*p-Mi(6FqLa-FmM>%5WZ($8PQ4^7xC6L0)2nPUd9z_hIRC=y|CRJEAHRkaIj4yV@SiNF!mn zFE1P%9NwZrJB8x_y6Jv?54Hf&%GCJ|CrcZxyA%6p>sQetA&p)fVsRsg-C1f^^94s>zg?)^x_zfMo8 z8-_oB=>2%lVrO=EiMc%(&8E)ylGxt6eA;IZgvr^ozelqBXr8yFuTV1JhowS|Su@0= zdo!4oQbqkJ?R)1=?yDP1n-VK@JUu+rM0hh<1945Jx?|M>F`G~>e8K@xR5Sw}SG=hw z?MR062t%{;apLNSmzVRN<_V-KIdOL2&Gau^AGIVb8?%QIS^e8Uor)0#w(oqJ7byi8 zT;kJLBJbu`(FbszG1g67+fHL+7ev_p^rt96vpnCuyjB8~2^>E2>|cY{^U7?rD@GJa6NTN_Aapw;+K$?#W&Dt{j z+iG>DqF2~{b5BLH2FtVxY;b{l?q>V3q4^?Nzt zl$9BMX&M0v0wmVzS+iX~pHBw1h(F57S)b=SRMk4;AaM7#S##4Z$^#?BALS4@s^{=- zdRmH#fL0rk+exCVT0NDy+6aZiFE5~$Rj=ec|8^g|n(COabd$2k)rUDmVy6mT_3Qbm zN(Fb;EQKc0nBz%+<8M9$K|KeRcabt1=eXzuhwWF`@r++sQBm1{?GjSQkOIt8yPgk2 zwFvII$y|WD1Wp{$Nx?3IRiA28tZ(1GolV#ylReQBuRWJ}(PwsxQL6j$y?2PoZ>0Jj z5=H|t2e}akLQgRM#;dAhr7FY#uN$bCGXK_dhy# zLnRz5UGK4>-aa(9BkbVgO60-Qx4&avJX5p$<%OVkZD)nmDn~ML&G(iw1tQl0pR5bt zec=53UJ0Klh7(}s$GnMa*(*nXB!<~1Q#sLT@m3v%oyBk4H+*$ltn5gV`8I;@Z?tDF zE~(?OfI#%;A4TT_iCko{L4e5{7ksi}?Mm$f@G zzk8!|D-3g{~kscZl~eE+krg#nUHT}chaX~8PtU@vl{E3|H<)Ny1B zRcmZYVP*d(O1fZ09~}_!&DZi>3o(i~LDMKj=8jLj85}VZzCDv0H^3Bqd4g`6zkC@S z+0KL;cgH0i8p4X>J!O-nafOXBx%jT9s z&dMBb5r2B}_03@@eJ3E^6PJFfA;V=Mu*f`-?3MhKX*u7m%JZ1xH;f`8z1@UT*ao!gVn=-+5#aVV%0-;dgV8AGasp7{u)mSnJbSlkg~`1 zkPt!niMIVr>07#QLk_Ue!|j)6wR{BGxdcdjE+V2n)^)07&FQ2akozUiWi7BsUHjlDOW^q|5AC6N# z^5U#9HvI~rE$QiR%QSSgLl-X*Y9t|>YRUXU+~IbzL}~x)Lkd3 zv7%FpqvJ=UATM^5PxMU8lpnj^a#tj^7`V#RU>~zvPQ5~!>l|;C$HN2aH!&yzgfglo z;@?-Ac@X!P6d=_PKHo`Jp@$D2%4x>o34;W`?O>P{T)+OY1X8Of@webS_^9Y3WQG&^ zvDH^Y=kXU!GMc2Ms85hkRJUj`^R+M_tluEd8g7!*9amDUwXHeLFb4MNHRh>ngjhr8 zigC2Z1uBl*fa0CYm+|@Hk*6ni1Vztg-aW7zNgd^x_>@@-=UJrn?!-n-1|VxfF!J&g zdl6A>DmVW5rDae0kYUypLXWOvT^a?vZYIz^Z1|S})UUqu{rrQEp2lb7B@W6+dfXVoDlYSD{zZC72U4=p@L;L zZQR_JZ?;a&asOsgqrblQ6_mQ!%l)*T<(b-c*UALDu|_l_IM8NL3yB&6R}Kj5x#Aha zCxlgpp;B=VHMqF}5z1J8dA%au4*zyuA0JPG;Kt4;FP~|zXD*{T+L2GV^F0c@Pl(OVkPnJ&S2@*T zqb&qV@kaPqN>w44kGIe-hHG25@KJ?``b`*x-L2n|@2SP@r~ThthfmJj@A58-+T?Yl z2NAnPQH6CszUu|_)rSi}rYU36A8vEnH56|nl^QhB%q^5vpa zG73Pjkj6vu=tdNHp%fwO8wC*aOk11G}8B9dtm{TUlhvs}%f^q!bl5PxP0PxcaobJ=*mA)Y;y|fF<=L z0kC9DbXnwm{_s-&^KZYBN6N#4PEg}RPqC}TL7Oz3O8z+)QsC&n7UG{W*JsjfXUQ~Q z=Z~?b>!c6>aKyTP)J}IgPh=#rwJRKk+4r}e7ZQ5-^UuqK_XjV8?0Pz}LIjqht}o0Z zf3+3$MH+q^wm(Dw$CPkGm#J<|=m4ulm$kgqwe67~5xY*BC(_TNAk78mCOPr5D93kn ze7}LTvcI(HVN`~OvC11;ZXX&vmHw=6!|CcD;aM6Kh|vi7T{!FH<(#o%#LCaWng-SM zgaEOOeae9)C-UXe96%;JCFr=={&sGvVf7}#R~#&2vjl-04aSXEmEUH_L7_mTVP?ok{7>k`U%eRIvkzJL!}G2xqxZNmaJAcmXS}n6JiXC zEhWTH506l9SrZ@A+69D(G_-BhiMX+>MLFN3BtLop%*UdDiqWASL8A~CTWvq4j!~F& zC#0wdRAVR!R&Zh};*;)~zgSfa-Hc&^Dm0VKwQHUGiUN)|5)iLXcdFQ079xucNi0E& zPR;xiw-?>=)7TO>f3y7=<$LR6YE4o7Teb$->7d zsDw%@5G;zoPlh^|XDwLb&ydh{Y8ze8n6Sf;4_LLWP+eU&_r>hgL&<9WXe%=qn3?{m8fQAUlLPbTmDkP}X zP6mr2slkU03|&BlzA!Z?sc-(|P3M7Qc}dcMU}<7;TYCj){Q2Z z#TJkE>G5BGA|?{1za?0qMT=z5dg!D(=ImY(^y8!{-+PRYJKAxKcGo4_yOKUr9mu1y zaS6GW&HR{{!ZlMf-NBIaqpJKrTD$+pmA2nWf&Y0)@4xUxx1l~zD2hD2t;S5_x*5ku zmYDC$sh|H>G>iNT>hV8$$&;z%1|(sQa7FRPrfU??!Gjf+h{mNIY)S-<5%ANp+P%=W zQje)CA*%Iv)ojT1UbFy0T6Fq(CPusOX$Ar1@|KG48M?#UP~SZ)uc!xz)v~QJ=zX=+ zr}GQr@Ts$F@M4dbu&?3cCCCDK3Pfm31@j?X2C!fpC-6LHnOV*AHDqTJ&m6b7qp*{&fp(G`-L z-;6nwV90`8q3b4BkI24KOpI|`^|7Ec{0P#aM4*fFRDCTCw#_65Jb6Cmtv#-Q(&w9i z3O-=nCTUMkPyNR1D*>$G-lit#hbuRPfkL)ya2d*hGx=u9%y-1b@afp=B>{?1a* zfiFkFPfQ#3PVSbUF33rPuU@%kjhBZ9jJvt05a;A|e?Obx;q1+y6=KRb`5R;Y z#7yO}n|sBsAcu6?no%^y#m1ZwV{ZHXdcAFOYyU>TlWj$*nV+BwVXG3zEa*L_{LZ@I zczm$4D!>*}N2nW{j-NR&7FPU8z+wdnlpg`#eT!GEUcK5*^yiVj z){5O6hV43zg~yRJd&{u8MAHh9qSybRyN#hX`Fq`M8c+MVzZ%oi@MzVNcdGNwoqO1c zQ?<ykqY2)NlC4?0?OZJ){}Lyhmy?&H9y|f0iTEe)<2E z^|#3ZtH!~X8aY@(c3W)@HkMKT+E$tmSB@I$@jp3>7in;6rR@k`;p8?V?fH2Dv(>nu zk1#*n{Fg9V`(uK`JADKeA+35MZ7Si6g*pv&uY(Yg3`DID{h>;XVaMI42*J3e`V^%@ z4)HSGiNJ}upmiJD!b7}~19DKd%tM-s=@t0Hh+ARLaWoEUBeB3TZT|MrUO$qkiHs$4 z5NHQakNqz%fl%H|Itiq_*terl6cdkff|$%6e^sAmbQh7JPo;ez@-4M7SoIL^K@EKK zdnCA(jA#G+c@IRR3SYBzLw9g;GB zw+wa<9^yuo104b)u5yQj2iiK{g%4xEpTS~G(KiQZgKk$4bHtXm`cRA76X zJY#M)+1Fqu?j<8S^_hbEddg;v<$)%BsN^GXG?tM{-7hKntIi(itdc|$fzHf)^vb>bg@^{{$suL-(O&TjiX$< z)>2b8*Vo>b*Z-s*VIBw9|D|4g`tnXm=E1-BWn7c*P5-5Zj0h*@?>{M%3T+*jJCj-r zOy&M%y;b3BBvl239Px_Mq8s-PtY!B4_D+X9=sH;FOwbYak@q(9;6wXpIlqf2)!7Fnw;Fe@v| zDrwtY2uELwBzj67Bz9M8r`pE63xr1Y2{v;mnmZJ;kr5-)d+^sN=?vtUQXQrtw_sX_ z-ANMZK-p}Q&J?YQa$b>W)ZUj_ll~hsxM5t*=Uc-45D_K1@GuWM=%B_C(F%=(+7~5f z6b>g>xjmB;-)~|c=v!+>hTEQFgU@%U_hNmsfjlVlkxig1|Ixa!tJniZvAM(_HNuinXthJZrCIA6 zwOog9Bc``bzb_If>K-YJ>oze6y3}|2j9mF&?SE;{sEzSxmy4;DaRF3ScIj*i8P$X!}je??W&9pPOq3 z6wp8**#%dNnsG6B-M?$5Rh^BhWJM`>E*sxi6%gp<=70WN67As9e%)_W=zpLs@R>dw z6qE;S4stq?uuISVM`pJbHcSdsxUlv?ee$P=o)TTuY65PU}qiO}AENWKu@E|ueWM7RB@jS#z{;g%LqMgYl_dmP_@^cnJZxM{V8S`Y%o+4`) zve-j#sN@zW&Vd?SDYn2=dKEpvN{WgoM~Qhj8q0KXyKCW_l?a>6mg`p~5sA7=;O$rk zAMNx!Y_s*jaFu+jif`44okN?`{ZcM9K)~F|NZ$0lNY&c)7N~qkk*1zfh%Ck1F4C*H zq@*6B8Kc;iTP+%TEHzkM$~Wg+it)Ug zhyi&yYtw3?IjPwNN?a}&D}6L7k2SN%6rmEtuRGDML8Lwe4Nn3p{T&tJu_+(#+M{vA zbY$h?p_b~D6EJ!A)-k>=R)O*V@QY_0Tt+Ux2$gOiM;X?IWr3Qh?^Q4LevJnXIVt@~Q19Qk_f7<8(u|z{Xh0 z{j??o7n#u>8TAgwu*zsq7n1cT)c##sNx=FcN)<({_L|5hOo>pyIpx#SKoAfQ_+2lqs zxu3NAiR8r<&DAMFufZI?LknE9Scg1(U5VCf71CEI9}l50)DPQ4{zP6RFjyM}VHlp2 z!g2m}y3-*=bn=YFp}Eop%jS2l21lzO5}-c|Qm7wxt!2s(NIKo9yvZrTd?mcZg!!_v z#FgD6Unllu!`YKjFmfNnNgLUR=mz?c^!$)R2gLO0Bx%ZVZXBkIHVh7UR2v8;{%0TP zNBszefm>j=ECr}f4HR`8>JHMu>ct2wXuhenSz}oWW@U(ur+TdYhJTjcw__IsVWdiR zew+cLZ{Zl^`Q5sXZI4=V$8idxhsMx-BrBOgYmRSCt+kkrWExIhS@SyFQ+c)J*4WoM z);p@!K0v+I7U$d|+7>d=X~g^|>n{=Kv^MSU{GR9T28-Z~-+r3$4jFx~AIyCVNW1CB z_>b%6;93ofz+^lEpL5id7Nnw>7kk_VBRs;yF^!|&jXmTR4n|S)gFlG5@XRm1akq~R zK`(!t*z}XuEzj1gbq?6{rA2q8_bO$JefXJkYXkK#g}%enSu-S1lp_OLSopwt4^cCEF#M;859>wCShm&Fv{CAThR)Cv5_9{vD@@7C+s& z!V1zYjSh`yFE@ao5~mWUlm6n}gi+C5Uvms-Fsu{DPzUKPb6**x*vCf1Vh`LNv(805 zW@@1QYIzkay1Q$_PUoAqs89M~a$ceAK4p);Lb_d!G8sg|)p)C+|s=mYhbX_u6kF;~oU95V0zft~gn4hMF|S zrT4p(Wi=sXW<4P)ZjKkvR>>QYMxFRftof#y+u@;)EV4!zcJFunatmP(w>8O^PqjpD zr1@6BAUqaxkWs3-DoNVG5S3#eYDW{;;CttgSQW-b5C5j8mq7Yu|Mu&7QEfi{XV_F+ ziJ~X+|<>toQjrD7;a5|zi&e)HZ%XD66!58^5lK~i|c!zO&qN|HWsIuC_)he zEgTT;w5*Gyr6cCP;?4R?dm^579#&6s0tI0FI~k)Oo5NufSod5&t&Tbtd10`V-rLhg zJS;1~e-jJBmAHbrkjz8ij`Ig9Fc%@ubeq`gK_ls!F~3ASY2{Sxh{R{Y%~v^V!5*|= z&m}{chlegAKRq3j!!w%a=zTj-jX4LBT}aB*AuAeWP1f>iD)qt6m#0n?#MY!c4UtQmAz|jHzpd`WxhQc&v6mmyfEdmlrw#zPmTQ$3?tU5QaJ5jED=D zXs@I{Ws@ytQ^SagqGAGk<=s#T`F~#r&8mjr4r)622nJ&Z4x+>aAaYsigz2+x%wYKu zHjgVE+qU~g0aOj19J9XLHL+BPBp38@?Fy5J;qp~MU&S{ou}8|xx^y))`DwDw>su?7 zGF>N5Kx5|gELpYA{O!=p4_cNmm>cBQI(EdUzX<*I`q&py!EdYL^|%;Du}Vk?5lDbM4PF)JwS2ou+Uu*?=Vxyr z0daY(9r;bsYSQR1P4%3Xyv8{CL3{|@D`vxfzkt%B=6R8GTFn|aA}VZI7dHq!1eK*8 zTmc+2oi4;^4C&(Jn51vl#JPL}W&tcPO2dAz%1Cm8auxN2@W)L}5n+y{Aho$O9d%k5 zI`Y$BD!s53JFM7*s8FZyQ&$X8yO?aD_4>P$$r9%rPlM44u&p{at`^3$z`ReLhWY4Y0b-UE;Os; z6I}nKHQ+E-Z9zV&VksLqx!t#-t#8T(%e9V%DIrC1bFVx8i@U#s!Jfp5bC(`I2o#-# z_8Dq}wsUZy{yXvs(*+Q;0T3a%t2NlE*g|;w{1nGn;;uvB6N7hV(mj|dNBJwhYZi9n zzprBoV+Ci(O$k_~$7j4opSeBvnPIi>x%{E3dcg>-=knoIm29$ELXI|Y?fVQrt_OYe z$dox!+15Lb(y%M!S*F9u0pQ4(X_$|jma-C$>P0o1PK04$_7$FqfNv@=tv4#QeiL_ugNzS2SixPf=B2FIDLAA z^l2|(8R$BpSSI_GZ)|GcH@mpGsbvGFOI>y<|JA=QSl!YJcx(4RAg6`ih;??gp;>S(~ewm z?LS=-(VbqkN$_U#)r)XIvZ?%A3Ej>JUI zNLkE0icFmIx0uQx!oH227FCxqwRtW*1)N6x$|rgOtlO&&i3IiiF?`5xOb{I~Evyr| zRWC@!g+q`1y1xXj_dOl%mzI6Kl!VwJT~wEWR{5)_9k0-pQmb>fbx4$6&YQI@qbUz% z^Id!G$$$!C*W~o+%HN-h8yr`jC<4_+!lQNJY)*BV{b~*JL^_??P!G#eA!>;49f)&FpYFT;<;}05TT_F{$SN`| zx9QY*c%Zb)>v3a`(IBItWW!S)Rs&iG7J2fCdn0}ujbb1`oUhFizmsF}i2CQIh|CodVxQH#|uzI`DDHdMIe zbJfQbJ7!!FU`!MIPs~gW8bBxX(5NJshM8346RIw=#=ry|ck*SzgoV!)(}(FUrGkPg zxs3)tzgle93_wa<#D!>TEbT#&$P7S$HS6%>u&CG5j zo91^8Ot;+I5xrKQhGiG(3LaVK5F#}R5u|E?hOg-axt)BoxaA#=!??q@{K6$DylBYA zFe=))aAVfBBv~vKqE;si{_-F@FCghqzyt+=Jo5HqibUM!32cX4|8@F}sHz4RY*n57 zM|h?TaTn3W)S68;GfKV0gq>}`4$h-qbi!&__wr=dQ!7y_exn-jyY3PZ3-|Ug++K6Y zu#H^317dq1reIm(%yyQJl>}|tz(R|697FOpu#n9ja6h57NtO&%B)Jln8pt%7>9OOkQVB)MVzs5GW)>?Y>TO z=nco0Q%9ZQDB!RY%GaI&$deQ{F4yBU(a>HP>L;%@peCQeA`2MzIR{_K47DYP%<_2)|a72eZGs zZj)&um?y@7H$lU5P}ki~bmZcy93pAc-NXUV2f7ys7ige?IVSCwiNx;3+yjHrhLK{s zXGe;{Ge*nec4BPPQXw+f(sUDpq{WzqWX#+`K4M1`z*9j#jZRp7_>db-N-4#GJKvO0 zs#0)5I>ZD5Rv;QA0$4O~lUlw{pl_-Z3L6H6`4(Zb;uzm)o53T5a%CaXm06e@FyRgz zor&S7a-1EBE+B|JB)exvz=4fD8|_`8g@IThs)KWtxU(*Jcqj?FNP*3}(G~*7{n6;K zG<|OwVUUp$6Er`_vkW~=;su}rF(oo%yZ@6YdVOg(=w)gXlh!dsL(e*#HhVZH8)*m% z91*Y-?I7YX(TYYDbpN|_d9@c~TMr1G6N z!w%|Ow{1PO&R}YdwYIOi_-dWBjnu88?1ua;t=Wp)=0pP%*P)r0yugi>=;9=|brT`K zg{Z}SW94Yhpg%UBe*62!mQ@CRWf3aqQxRiMovkTVRDe~XJFR_W+aok%!&nXjzVySUgBXuVi7c=MN$uXK_g_hv-oz@Go z)?7!I+JRWN!gYCSrYz7EO&B^_)iMCYL}NJ~BFE6QRq{oW85rtFqA3;oG;9G%rD!yK zoRQmzuk1WbV?40Op(KLZ%|=5>fJ4tuGsHZItgdL;F-s0jcK;#(J5y;@SHISE|s zKIZ$&(Ky0%0Vpb_L9%SA-!#@4FWDolh(7#Y@_CQK;dTb>VM2Z%QBZVGVe+_r(s}GQ zWgvGmtsGd#g-NwmdvU)B`A&;q+3UrgFdW4hoGVO+F*pFXtMzW>-&B- zl|1M-J~>Jq{dd__XToG>?=XM&^KNn@J3D?F|77LQO#N zNA?YNrJa7GLNhlLKSEkb(aJ>1bkZvAc7Fc6q2jx%kj0>jIPJKH$h@Ho}mlQcg!C*JMnh`;LN6q}7h}V7m z1cOEK<}<_3Ys@_{JGmzDS+|i}4I3R0j|cfFPf{~6jWW%s?XXB6IGi?6eyle>WMVaS z8UP!6#uv+qQky(YevC{nF$yUm-?d2`#=gA#NZR%Ng9b0jn7@OD9v#^?K<$;>+&9qe zMnk7ePuJmhz8w~oPaag8Qr}WAhPl%CCp)LqM<|D3Ub{-7B>C70TsMIrh$!CzP`bK; zhSemRPUgC=$3qYK3gYElnzb61DX#7N12;3TPDlvov!txu?-K}{mQffmDVm03*j^6*s$7= z19b%ecJa_h4klupRyG!c2j!?2Ec;484b=635MdXP3Zj@9y^A?+*WgZ6#9pC(Z|ajw zMy|0N@fr(?O&_f0 zI6lI~shu)ljrVl4gEC30s#np9ymfpJ~xrmj_M8$kkj(%@+7p&>g_6CRW` zH4ycput}>ue=!f;dNg{Pu8|z{1T<*#ValsTHtqF!Tb%6`o%z*hE*R3}0rL{t2Vhrt z(5U+=ZIF61anK~>iIPe54iz#u?@MeD3X>uGqBYtzVVpwK5EDg;)+~r6l8!D& zGiD~2d#xX1-gY02>9k1PfTE*%wr()yFooe^TjUX^OWGO=4kI0!1TE1;;*n)v(FP^O zY>bZNqe%r&WLEaEC{qEZ?IBr*7(q8i!xwq{EBQ~#BGV;E zA|stYfQ1939$s>rTr{9xqmLeH3q-&2oz%&ta2u+1Ky$o4#*EQiriosfusU#DR=S-U zJE}x^K$8eSXo`b^Z@>%59@G3L0KwWXK*x7Y8q>BZQxCAQ9a`HM00WM?+@LW`?c>mX!Bb-Tj8^z6b+w9-@Xds>U(C1~n*Mv(HW^;0?Yu88i@1 zOqXpZ;REP4-jw;tpImVoc!_54;6!7iY1cP^j-Ucd$ks(*`)L^U6L5oP zTT-6a6_@Ow2GKsDlnqAP_N)X9of5@cIN2%3>VL02x z(2+Ro=QiS}>;y)Xrj{Hwt|tPBay+$9+GX?wF{1}@j!}*SBsUT0aR*(HR__=wfiD9S0CH(+G|_s98Mf4>r=7b9K#68GVb*A8 z-yGX5QVw<~yXGRbOGiG1NST^;cyMr!&3s{kp|r0G z#N!P4exiZh7&V_!GlXp-!0r`fbCcXHqTK;;s5k-S^BPQ7xSz{B;73^I9xC=4GuRdN zi!N05=U8&FS<-v%<}BWJUU&w#bWYZ?RSWK3Yb> zz0ilt7nau5P1L2e&7AP|_BN|cetINSCvL^=>gwue2XyLIc9b^Wjo z*e-@0P`b)a!#+hdJ7nW_;Q_ywGWb#HemMMo|{tqURt16 zwzji&#T+g`?!UDRkn=VCJgiDhtk5O~QaY@&-k#}uYZ~S&nNOL{!^6|o-Cc|nBqqdz-V0yjgv?D~!H8)MWntNFBO{~8$b6hazOyI= z$6aX8|C08py*(diL(?Q<^P$kUuRy#=N=e-YpY=#5&d9_>q^ql|t+TTbSBX^&?jIO1 zfUiN`nfm0vV9Zhf;9%I{sEODRpg8vG-{+*L{%piJjkr|5+ODtCm*J1XS5{-?*SQfp ve*fQd-2eK2vib3=bNK7P#((>Vd~*B2ZTHa4hIhB=l6UV=QHj=GK{8o&0hxoqWuuvLz=i#f%kY&f3a-l3_j1pDv@8Sj2K4C92Y| zHxt=>z->?%6dj{c7zG>oReCTBZ&7|!@mDzs*0n`!-4$>5T0abaR5Ph~{@!1A|GZst z_3i)rjZzV5M#|`4zhWG`mU!c9_Raq|_x!!4H&0bR{QHH0vxCq5bT#?!OW*#h=lQ=4 z>GvRil-y0?_?Pki-xFN>^Q7Saz5SO;}1pZ{w<&O2WH|KkJy=={Bk z)~A}tZ3l94A14Ur6%$F(?b1m}N$~Ky|2~ny@IOhwOjvFYdcxFUOQx_>q(*!}L+JX0 z7J?(W32XWvQ`l_9n4;H@)p9hB$U716C#U}X`rF+f7&C48pDP($z%ZZFx0KEVH{Ub= zkM)!*d3!$5%F0Uocp_fe=HG|(d$}KL=JtI5ehc??d3m{6Pj;rBfpO?;Y#|9wi=;!=KZTf+}^^8b7H zZ-f4Qh{+5oJmhiY*`R;l#!;33_gMebsekyd9eK`m!Iy77?(L%*yOy0Vekp8HVKOj^ zprI;^mT5@4)p~R=cYG9ffSr@#;^Lxw6lp!Xe|S_JaTtHCxw)Bg$8hIPKtzOi;a+&5 zh;{h4JEYxo&Wjf>=0~nCEiL_jEe-sP{;)>w(4voDq&;H4G$x{DyHxhW1WGuTS)<7t#j9dM-E#D96wSaDuP}!Nth-y+}9BTmw$^1W|5Q z%2(%uuM`&N4HnAY@zGpbad9y=8o#=FwvEu-cAwB;wWnQ@6%~e^;tc#mVf1HeF-089zvnx4Gp9B;4$?(hiUtt(_T5# zdz_Q`jyL#XY$uM}$OmmxB#pfuJa{?pb!$DHJhtfP#)6>Zd{X?(}~DiJ*^yW=_R|+1tWGXw$rnYPt0xKm=hB4G>@NXS-C_h z*lWS3Rvcbv-@G7l^2E{X+(9IT(0)XqcpeiAQxX#D+Yi@)Ay6dosrv7&O4B~4^9xX7 zjz*53(2rw*b?Y?gM)@4ZVqEsCHY=1?Vs6b<9ck8IrBZt#r6V0>2?X=u>G3BVWYdDMOB?}U@ zub2`B)$*NA0H)S}(ze&C>^t2cK#rU{aMcj+aaT(m8ynNs(K%=Ss-&dE*2yXL=g*&* zsi{}M3-wh6^;JXQ@>MglvM*mQl^cqukg^Tml6Z$6)oellN4OcJP?&T5%re`rorq<; z6^U|XHFWQI%qZ#12DOW+X+__*!%&4h%ot=u_i1!DPvyxd~UVa2hq z^g)%hJ*>H{P0DZsV#qJO7a%PnAi!|3_@n8<$Sa)GNx&aaQsFJ~@3OLRX-BJRi~ZKo zfrN&aT@TNlH8VrZOhh>%0Qa-!Q*r!dZvPAA_%P;pPBINIx6k;OniNA(zT9o6x}p`0 z)A|;9yy<*25cczLr=0}yt+lETlinv46cki*t}iYveQIfIXb^36Qa&jnBENXu`j4?E zZuo)DPLtr~nblz3s`hqi#<5aq0|x{Gp$a(G%gd|e>@q$>QCS(ixabPJWL4=yOSJ%p zzPkFEzPqEBZv}j)W%RX++tl&vNl8jNP%@= zp@5|vWQ@wQIFpwSvevj>%nneuuY{@OTj8kG2Kvi)@7{eoY>XbKS(OR`ljPye2j}rc z$2nDqjIC{VMgrs1HZW-Y;@FfBKW!*CJX1KgO7w7>pUUbd%rKh(&y$yz=g{=-CKgI> zRlML3KjL_b3sbHcrE;wNAl^d$T=*$n$$-eHX7AK82yWxxC~DHvmM~BrkTa~hYw=f2 z+NqA1PoZQhAA-CjPX0SBL0zZq%3P(r!1{h|ZS6nVo>(Z^owq)-WWf7ERi8X%tbU9( z;C-S_+R3Dj14!|C`S{!bm$9($kTwW73}Cpnc6N;#e>4e3H6*ju*RNa%z zV@$wPbFW%|BQzW%zhek@%~ih>_z!-_>Xzz2U%(`{VX`_6HqhUCSInkKq3i{iGgx{Wa zQ;H=yp~i&_1ds#yi2JCD-Yg{**cww~1@}2Qd~m<^4k+*HTTa6kplhK7c09UW7;y1Gsn@x*s+9{w-k{!ZSLUmSe+RagSBb{t^*m;Q9+M!?l_ z-W(L><#lgv1`a-|S#(k6npX-WZOm88Tv%K*+68iiA4cy`onTh|_t(#ymW_}T5|Z*w z@59S2cIxam(>KIK+i+ebs?l+U6F6D_1Ki2o*t%+x;3uNH^zMl z+F4YmUPxR=L2=0b($Xm)dHxn+ZEamVLAYcz3%J_NBVgw#Dk+_8TcRi{DhfF^a=Umq zq0wk_zQ$f)vlV9>Wqc=Rwrm{&kG6RP*b_JxdNVS=CWH9n2c> zl`kshxN;zD9(sN#l;1NLf zAOvTWy^l`|+3NAo1V@>k9P}iryA4o2;^6|)VsYV*@}I}Q%hEJlZ)q7i##q!+ihktZ zxp+puciZ`x5OZnfr#`mMLGscb~idu+y5ZF|6z0QZ|B2b@lif;E7noJ?l4ly zamlm~DbdvU7%GtMdTb|q>f>Z%N>dPTzVGix;0)k@*)-_|XWR+uynOo#C=`K!4h#&W zxCo)wkJHl9+<-*Icc$qclL6}3zTVfobSx1F|BF<>vke>+e?P0Ksd-gZUe49euR^=@ z%aH>pa5 zDONcdXT;0KKx@dJj$Nofc9$p-hJv$i7dchffKm?+4}(BT2*(ExUI9=*uDAf#(P3Ox zUj9xsLQ?(na9H52o_#cx%3HV|tlP5!m6UwDckZZTfIFE?CTHafpTgOV=Y`qF@LW;V zT1n3|-pV{M&bLzF<>eh-3$iNuGQr1oHo)YccQ;WIOHeV-FR8#hJ$Z5tc#$B>&Xx6q z=__&Q?d4dNs*0z$`uKdo1f!<%RC39UC}dQK?0HuwW@D%7FGy*tGVF7rJtXdl4he{3=Xg%~XvPwxfUWGUscclk+7}B}62$3iYoBdi_{5+)z;P3d1!Nv#m38g?09&2J78Jk z@&}I*%{~v5+9nNc?;B{MBlytqvr0;#(XYvc@TbeR11xN=|2TYdPX*@r%aEkT4U`~4 z?I@m*5wkL5wZVja$JGODef+CM5n5rAx4LGgq|I)Cle~zPcfEr*(mCDO8DAOBlkJup z6Ni4+*kV;MVlGp+^YNn1iUBD%Wc&u$N3)A&W1olQ{(9+BLpJGh9soF3s?^}vi`Wkd zdKWIMTe}w3O}hfDMc%r*VxRUzZ<`19(LrQ}V+Xp9wgylm+Q6*QH}%hCT6Tm~8W$9! z@2(AhJ}D^Ww$I216SVJC`_X&-T58ASGc$KJBj#h-7VBdU>VdS90mX5XBhkC2;<FX8 zo^ik8<6W`r=03UPBB7ER;EBG`2@eai)lH#7w_J!K!h(5cv)NB@R7;T_=$&m|jbgL* z=|5M2|I|~IXUyDY>W?3O3pJwXjC|$6$Zs?!q-Cwwf@@VpJLnuFdRu=FN3`B|X0sr* zrpn4uP__I=p*3!q_YHO6(Bntz+;_uLfw2Yz2MggS4OKz$Wb*pDaN{%v^B8FyJS-(8 z)eD3t2DDijV`T<{Yhg1hPQGJ+(AM7WjO2y4*X#ltq^Gd_%a^yj{^P+hG3|YlZnb$g z&%PgeRHDN4VFb83{tGuU$Ac=FWLgRamYvPAdyjR8^aLZ}XuAd16ChC|P6(JvRn+G`|oIvPRx*qOm~ z-bXWz{8czbZrD{7Rg`yDqS74jjJ94_NAE8ld@wvFOm!>K0BTb-f z8BWBHl<|2b7Ow9^^ccGHGmR?7m2Rv8p#kJdnrCi_KL+x$jyNDg5=}#&9=CUN^y$ku zgw#C@3Hb`HW~u|R)`7*kL5N-%tt|^yPz+R1SL#WP7?syu?I0}pjH{ITaxd#j)U7da zbGWsYl`C)>ReK2(`N)~e?o8_ijP8*3ldv*m)qnk@^bz9&lR0qYUj!w3=XLpAJU6C5-Oy6Yx z;w+!S-1nl9Qu0Uaxp`a&C04uxQ~sjf?BusYoxcVohxnTow0AZ(Dg^{jbZyv?6?>dg zE*Hy<X7bg^IB?2C*Oy z^WS=bJMAo;Q|(jP7UsKzS&^lk2#8eb4X}1Kfb_n5((5`uLzA5INwVuN=|4TisYX9D z8x*iSKVQ@beUXtTfqWtZ&l$no4(v{P1s1vF>~cr>!sVKA2YHd#edeuPxd9(>g9c=x z`8d?8Ztaxnz^`AwbPNr|Q?3@^&*%h8&g!6R6?N;&SzhQBKe!&ozMa%gO`<>m$jV?d zw48RUvu18RoWJ20amOrA(s6t2DAif5Wl%_v0`64;^;-q~8#WrYNThxOQj*s~h7N%pD72Y* zw|rY@NxU`h?$qlz91bU%ayf;0PLdOV>>i~cjz=U6W@aX(wY9Z+0L0Hxx;Y=mcM=^j zVz8(2dSoqFoKJtlXu2Ul-pbkp|4uOA_?% zC-CN(%{DznAT{7jW51#%%?^rab@ICX%nb&Rd_X?b8wo@fmVXGrx+O*5FN;hT>Q!Cy z;dqs==cc*gV&`wdDJ{dM#`{^;NdQFDqU#F4X%>qc&6iEAXqNQC%2JDc?IeZAU>{ z1LMm70lR)O3NE+y>8XaeFu!L|(3XR1Kb8GskcC{KF{`GQ*2tF~AYEyh1w0>A44~hk zaUDg>KWr+Te3BXGx-bBeNwLS&ZhYg)Gj7Qb0s*`l^5O3K!0ye^A9}Q<Xg7WoBoc&gK~4{}Y2NCNAi*UCE0T zOS^f3S2NiT`Dfcr8ZMW+_DQsoZFHE+?@@sD-WyiGv(ZR!J^6dqiVVX9ax1y}9 zw!GM_mx!}t8-GO|)pc*YWYp)E))VDsRK-Ha?KVo^q5O(8dUzs3>yVZSIV5GE^A${?b%ROw3 z_62+AP_mFlFe|8Gf{>r2?oj5UPh}cln(KJ9q$-|WEBS3gABfh`=g~tO{HW=pbLa*i zkOt*1c=^ncHAJP@H=2lHpQ^b=pCO##vo8Yeb{UxoIiAP!se%(<25hHDLCyDRj*HST`$_Lj%h-HiZ^-_wc^88CLNSx=C`0n2!66YPTT!mg-J6vowEU3Q zUQ7CyT%)H@0E!i|3ol&hNLpfU#vL@Ld%=R%2QAt>9fR^l*Fz;({=Qz;b#xT1^DQU9 zMg9K$`vCoPc6@w%xM!$lvP|PHf0_sO<+NIsKHCA`(ronmuz1R{&eR{ZDFQlG(RZ8j zo~kbJuHmCe_(c}wFk=7!^rX@I(z*eb?5}a3H~;`qX66oODD}s?bEye5S`_?;k0HQu z5!y(EMFy!XCg91Z(w1C?ga;n}W>4YAq&a|v<~yk5OG=|NWMfO@y$}8hWOh(Elo{VPtQ_j zh2UTvcI@QjIo_vUUIb`*47qi$7vOh*ER(_5%Clz!z(kb$)z};cqYXctnVGS5a7cdp z_HEF-1BoR2xo?Iy__e!KXA+jhh?S4*-a6&fo^LvAY|85%RXbpwKT4AlBdUMS2%mpu zVy_w(+3@#iHXNzQ3#abZnqbWg4~|}R1`PwXi{d*zbOTHe`9n`FMG+7CqGgSZpALw094Pj687AT5wT0i?Y}q zIduwoiS-ISQbqKQ62YMsT;w4&QQ_=YCMd)3Ce6HoffA~0+Sn5o?kPx3g30W8GD&O+ zI`6NUD?RW~h20M1()QUFl|6OhG3jj*hma5W0NZpV3rY0h(CAP1cst@Yw`LU>XkKl) zh`Ikpb`5lW2QJ37XGB)K#;O`vA7b`~=5}ZP!-VMkGb_IV-HB7--N%?1aH`rK9k$QR zPHL3>n`+&g?vcI&f1Ts=Y*}3}6jU^@fB6SG3~ZS_0=O_r1^_;)T3a21pLIt7Xc-zc zHlAUf@dFk7&XZFKGv_ThTIySG&-wga2|(`vIT274;8J;#gP+2*B4T<6Rbe{!!NjhY zbv`rFX=sl&jMYIYP+{4AA0}HNkC+17HzpYfUy+fK9#Drd)dVgvk@YqsWh?7k^yY^< zedn9ru5-G~b#`_(o+*JE*3z!5Jm$Xyxxc|aKlFkHc>=UFEjb`M&&Cq`7$AlQzk?JJ z!#&Je@FEf9G_uAwEbw99K0d@9&WVb)pZJ68)3}d!K74N3&8FdHY@>H1#aSdo0Fv4y z>MO+^-*tlPHb%8EH~*_If-v=Y(S_s?5$Uwy6O21*ct`yD_+qF}IQi4Tn#%dm17 z`-5*VNndU8`drq#cMJ&QaRd9Ra`2qoH(%ID?4=>vHP+4y8r?e&TI%ELTVU@QYUyf? z-Bv|^8y;%8)?a0ai$?=|71b84hJh}9{vyU{zupNSek%%>n1t0nJn4J3rnOS>!^Gcz z|1DB3PnZq;M~Wh}Nz?&I?rMUB^qYx(nJBEUy!c%J<(-?g8}A2GI%uM{D+vWldC2J# z;l9{qH6R63to{1+-b$ zZU67~CW;flP};OC3R4c}jt@nmkH^2*Q?!nH=MsQ&-AF=x>$(rT7B_FT%1HC*<9>(t z&CFVU-K%J-JE}nDRu^E@o&Z9r+@bNuD(azA zpVZ7^;{{K^q3K(9yWwI`e`RLYupM|5JsVj#rMy^EQ3xs0+?aA#$#VH;Zg1a1Wu`+0 zw;f5hJ~612s%a;q(vLl(d|tDb5-XzsX+TQopj?PPTWtw^S+TxKw&AUoi}76#6%`fp z2rVCMQB*AImXY+hOk52~oaOGO?x+}N!a_M;82Xktlx-o1wzs!8AeJSI`_@*B~;sHdRxMO@~|0m+dSR0Yk8>hzW@>xUdE-1A1Sh%}uh+pC~6S(#I4%}hIHwa$Kw5zfL z@Ya_Dw!gi-fZtBc=M$@(|GF0Z0CXX)Yx1aT&H?E*xSOf;*Oo4F|FOn-$7lySjrw>=C>OZo?>@xb_Arf`)&g3txk~~Y3mQ# z#sIX#RoUbnIj7}>Pu#u?zT02hyU&TW<$Gjr=7=>s`Bl^-2T);m{c4??lkOxqDNsPD z6@Q@PTPJO*TwV74($(0D=vKf&!9VI_Bg%|nsjp_Fk|BBp#uq^fsb$})-KB%STgfba z`tpRgdDVo~-9Axq#mbNTXuw6ZijVRFE?s#aR0(C93{^bcqb^y#v;62<1&0 zY6avuxJjM96#%_e#Z1OW&dhr`#Ul{89L|N!jbCcS{(jQj25-Nm;je#AXO@E1P$>!tY0tpOh4-5G~? z`mpLtc*3YlM^h|(1J;zAn?J3ez~^;Ak&*ls3pXkSXa(YD+A++FTqDYOTc?q$^`#H4 zH<<2|$VVjVuZp8z*n$toKw()KkXp1Y)S>SE0c0A~*UGfl3EDxtww>H*}K1=L=<8KRGZgTdT6`pWM@bJ&oVlOQ?f_+Wql6xEIx6{(W!&2o6_ z1=wDY;ohxtXX(HVpCTsUQ8qwPYN|BqfzHWaR_6?#)26<91sv}0P}BKn>4E<+4_|O~ za+KzcUxu3KB{M=p3SlPZ_>j-@!CNWwrYl)tQNVsl&yaIqD8?P9~_c zjdbw?i7>A1fONr==LGekA3&0D4CmE?W%$G9!({XHjZN*rZ)7A@roehd%=@_1<%BcZ zBqe^fu~SSpk6?su+i9^bJP*pz%sH`q=N;_Zh6^C@ldNHz^q25(^l!i zFMeXJu;cvSnTBoXDVic&8f~`0rhT=V6d-_nP)AR9DbIzdr%9u{0A~P5d30{ZvAF_K z5-8~FYO(50+Ge&~76_V8lF@)y$`v~d?7Jcb?6fc@v1+co5kYy+d=dnY5NQeF8^h{S zft@C|@#TRl2`Q-5Wux33WQ7k>(5Tv2bt~~1#~$-EP}A?3-8neKp4`njHaFt#|A5BJ zrPt@$r%f*>J81u@dUkM_ll)AUrX~6YtQtmSO7#$<0iul(Y*I)`I&!^s=F+@XeECEV zhvwcRC7)`fLmjxgt*tq>%AD2e9RDWF;#CJ5-JO*uhpSmLAF8GR0*#-82!oD%8u>y?s*Sne$`qdj8ryq-*f$BRe_6tlm}m+4EE3us-Qn@hab6@qQwU7{okqmoU` z#(WpLxYcYu7r@oI;qz5W7cXAg28*HEs0or-cFt?T7AN?amc)`pdE-k~dHZnrMty0t zE3#pLLNE2RX`8M5doJ6cSqT7^?MdiAw?Ni_(vi2PB_gj4;^j)YPSba*WHG|h;H6^Y z^gG^SdgawV_D5OEh6N!fmmqi>oB2tIu=tBwGgy9&VYCTRJ1ahtx+K^u=L!Xh#pse`T_5pWb zD8&$iIXoMyGcoDTIT7_kA?kMc6cf&AEkN}z?WGeb$x01%e|NE<2F=VIm41A3hXaE)Tm`Ue1GW;7jY*IEpok_#Heh%uVAnK2}Q@Bm-z&&CvZNA#yM0@Qe|sSB8(9 znGg*Xx)D_nhniM8uOF`PW=B)R|LkSk=mlj*++b!ESa;0am;WDC)~44LDI+E}*zMSm z<;c2L=&kMpz-Cf#r?*-FpNb>x4dgGdDUw5%dUAaZra2-chY^-=0ZcQwjEG01+-i)PRI>cOBjl*TOgiX zxP)p2SPQ1jNz!-vLh-Xv`||j8A1|ejb_%R&&Rf$^?gi$1{y|+5=F(P@s%wR)y*<=L z42$G`;LWa3uY>M})nB?yjP zWsm^nXf^bw@n*yu!Mf?H*N3ca@4c!c&!c(ka8i+GZ{+0n-{B1C>TPmbE2^Q8u3csN z+}a>~-_EZxe^v1Y5Pr}~B0alhVl8#j@iwVneAEX=>R3gh6N>e15K=TT9+t^+|LYfY zOkmH3(UuIs;j2jv>{P|1Ccc3O8rP`mN#NM3A_=Ccl{)A{YV4exl=MahJvTL|9{Ht6 z-}b4OmYBjN>W3SC=N$n_G9cI)E7+|K1H6MEIeM=dm*R(TL-g1n%yEZ%fRK-3?!0YY zlD@H-)vkZk(@qt^mv2cQb;TsaZ15c!o0--YCYahv0lBX_4o*?Sn8b*tNwljz-72K|(Mg7Rz zrDNxi(3WpT@tJpZ-3LA6$^>rs|D$*3%2PEJ%SwwtQw0%Yi6UmTs-~s~qyYld*^KFZ z=1C?~CXMmlc_|S1NFLJADA;4kq!+I(-ZQU{(&UheRh@7Ks^}<9qnBxS{2y{5r}6`s z|04bM0TVl%iE2WjZ)q@U7Fj>m)}E}{22)Z|6P9TlGdt?4+>cg1v4+c1kjKbqfK_67 zEE@~R;4W^~S61@p=H{j_bJBLF#BB$Q(vo6V|6JGF?F#BCmk_Px-NN$81!swP^&kS7 z*5yS&Dj6YRed<@|;dMGN`#S7;T_`TV&xl)LiS?!}+mfu@)d@wl93N{lA$xW#<~n*1?SrOtsrB%>Wf6IeC@CR5N!zGdSv;U z=j@_hV<9{pkm9SsRyv!e6g~#o+uO5n$6B!R0Vtf(NJ912)e2vq7nV${%f(2DuyY5> zXN@OnZW6-1+PRF8x1|KPjQ2p}-1=&KBa@+mV(1vVi4P9cK_jg|B#*fWsC-%{FV>jz zYBwkH`M3p%BZ15+G+gv3c{MNgGcb-t1u(aI!u8~CNP)Wc)rBz^oh2q1#Zk#Y7QOlv z=U_MrMe0KOlH)r8;*n9+Z2p?)HnTo-DN(tSgS84AEg~W^V5PNUOc{?>!kS~NG;i{T z!yif+c3rA8PrbmSc%#&FM6qi((nf{sIX@A=xw+72cb}Wv6U3!Aa?n?s7^$u`hgBQ) zBeG4{KX`B+UtIH2N?xyVRB$t$Tg_goYNihov?!`U*jihgr1%Gm>W!$54n&0Gndo@h zUC=TRT+2NxF{&^7htAVi4rWO7N#=Lv<6uXl=!6fn!t59oQK8EBJ5|2&&N^18l>mpg z%QKJkX_4U z#(555ZWyWRKqx}Sj9JPk3zz)vi1kMZ1O;@c4Sa;(Thh;>b&TZAZA`Pc>ljM&M|R$7 zi$2L3pik)p3Qu9Dnw}po+TL7QwC&SV$_}dIaH{`}B*T)|3h9|KCjdw0EF>V{Ond7r z76T|5L>Y=FX&`lpre($GM6bq2pOuq?ZJQcuw9yCJ_1h>C?Nyg5sVja*E1P>{`Y^dW z$(g=L^@>@q=uwE-R6sLy6y42pha~FY2z~Ik68;CzuEE@v0R<540^}I%c+1eRZO6X- z((hJ?ha;yUu_E6YJqXm?m@U6vdjP633|k+sGm<#bw)?fJu~7_wfxl_AWigJzqqRb* zil}uj9K#bz?>0!?Xaab5Tug!Z@fEoK-H6pCmkV%($$6l5c%o0Y1HUTOG zO`8y~R5TU1O5#B>_nCvAWdY1wuNaBdW+4%6)=6BNh%FCa?u>)j4V>o4TYdpL2K2bl zhhq#WK#DLfJR2$_(Wgjmc1roj6wA}ECkL&B}oi<_1`4UiRm&qN9 zgb&ydspuH7k$Z4L8kGhJ(5r)DX&aUI;Kz?D*>plHeZ$?Y`fAt9M>HQrZJN4SeoL2( zbKQ7V{*^AHP5csjTH#)w9*f?1Z_r>9u~T--^*W|EuNz-(5O;^}XBP}VAKk6jh>6R!KZe zqo`ebJh&tSywt0^>}#}@Nwy!?@zfsptK;N=B!@0vvqu@`$8=9B!hRgAQ~6=AXX_KS z29R6>>~>GxVriKvrk2}wp~BJDXndZWHe7c{(h%P;;FZNSyK1HM(gP_{xG2fF zdKxLVJJZw~m4oxv)SD1;L1F{+s^j>+-k?y43N2m(GOh4^{*Jgk(*>?OGh%+~X&$KXYxqtjzUybb1-x9EN`iP8b38YAy* z0Tl*J2A#LczDZon$2BCJRXw8_$$nReKIGJZL$nLCt#N`y> z3EuQR?83`=3O$g--`eo2xt7|M5l)cbMLGz#dGJHUWOjiVf3Q(D(d@KAkDFBaAYJ1>!HbL;fMQh%0 z6Sc3d)vKLz{ae86ZlUofuP)B`GVs-Swp({g_VYZsAf+~S*!WCP_hs#I{5~clev>#3 z^fPW`kBJ~UM7o0tw(_X7!ebi1*3SanB|(Ki{g9STec?=f@te28I*|cowEW_|iimrF zeqs$CjfeN*V~+4>Kv+)ikfs|mI#GT$lIe_&5?pTETCUW>bt&<~t{5sG?st{5gH4)2 z#MerF*)9QaDY5SJHZ?t;4-7KX5-?Wpm+XJg-4gc;kc$D# zy+nQ?=!7>H+dqBt3Mh9- zGfHYU;n~-_BA1?+aNkK(HbYJP%!`T)s*Rq%9Hc~+QPV>;O! z_S~5@*}n=RwSShU*XNk!N-t!jO15Uw4lE>wE)cC9L<(vSs>+ZBwZ0l;#+$J1PBsyF zt;qcIQ{6#>@p5U+*%{{L3p&)ZDj?hK={A zq0-3j$-Y1R5mLC--M1xmw%YXPgm2R1cV)a)X{l$kAV9w^U9{$;*qxzLY(H_-xiM<_p z{ox(;4eRLsOQQiRiBb3%d}JLD_2G8@@kK6J??C93>hrY5?%}CY?fYtdk^=ZpHGb)b z$_eP|{CF~T%s~bI;UAW8boA1hRGpd|z3r2aZhnp}?4I>~Gh1&=?MPw>+ zd-e^Mr~@vEgTwDF*g7Wrj|vNOQ(FQpt~pyPSb=w{gT>NTBHM4HXDe9EOJ$hsp;F_+O|NaW zPJf+wm-Mi}Kh5Ea9EwG#em_$M$s3H>oYdex8Wj5%bwoe-bQkBW=f57zEFn+ z{4zLilED{(o8o#aa7oAK%s3z7|} zf$7{bKN%!D;nokz&r~4%vHYf6A?_<{_^8Vu)s2R5Wn81}i2 zv&A`$Yb7)`taVMO72!$2A{!J7q+{I>Q11-LJA?kipJ$*ZiFbE`&Y$FAu$5{*{=Ek@ z&J%62qI|?7{esy1`PsTjMK&ZNp>^f+S?LimRkGCb>S=WHtVdFrsS}&)%a%BnT)&%h zUf0V&b98Q2NXj(C-fQE8|M-Y?AIez*#nYHnU|XQ0q7<4~3+@B`oFUnLm;i|eY4-7D zuZ6KUs?8U04fVGQF5zFZsMce~~X5x*NN!F{i`1vqq8JGx{VtW}!UB>an z3ra>2^|ApkxSTN;4lV@$6Z*o$Gc_a-dTrstfpjksdNRrcCjXIGKHS6T=OvclLc&b) z215`IX@?xmCn~=$xgs9@E+|DWd`Oz?I}_xcwEo?KY*aPKU;3RIJ0s_`<@@vN*RMf@ z`Qo9<7iIElpplGldiF9}?_G}k=8ELRq^4f`pZ{}B5I`{5lRLZp2elVKX}_Dv09ZCB z$RO3B7129?;}{xI@~SvPIJL-V7$!sl!uJ}S4ImO-WS~17w*3%`iy}ul(~D!a(-xPN zW5UDzr{qiKj@IW=M`(E?=Z=*D|Ho*Tz1F^k2b^6RLn(2G{zLijHUbZi_M|gD#M!qk z+b_PYKzJ|+bf9mh_=fgVksI2Cwja1_qR|j$ZBHW$qmzDiaA9drOObR4Gw2TdB8f;8 zLbD}wnvy_udC7r7uZ?w{u`ykG)|Yov@!b$0HGN7VE~4^bvl~+$f^W*}iI_+83gr zz$(x4SnO^na@i>7n4@#hbHG!ABF3~2U83lM9cG)>n4ar3TCOhq*`Y32(ae>rS}e^R z%i1Lt7mEOK^vTHRK8~uBO8?}plJGpYs>v-e@<8EZIB&bLfxEEF%c`=nvTlEE#YAB& z*^&Q45~lxy2RJ_=+fX_7dD?n&S*ai&+mGa|`DuNB*G#?6^qv1nO7-r%7fK$CNX8!-qa)Gs=sJ#~4Y1M!zT#ACg{Fxv7+9yv*Z=a4cj$NrcV$xAQuGExebN`Sdt4_u z_;YvCr7%e(Jd3NYv*+o+GoM6bb9dopql`rI!}dz8X>5_iQT)VIFB;Q&&^1$ne7Z4g zp*d-#ri4{LLPvYdK&XJ~&cnu5g38(FTlxJ3Yn&ePj#Jk3w$XJY5d#A{4TKFmrDne_ z%JINj$JR8&e=sD)+TnqLKf)fL4Lyk(m>1epDGaLO66-?>hi2oRgC2h014K00`EWW) z;k{7HJ6gRlst)?j*FQQtEs06!BedrGAgH2BxhyHkvU`@QEq&>^;zPki4$(N=<5!C` zF)?H@&10?)QkL`2`PT$Q%({)kU=%qLZ;UigX|EOpiBdqx z!&2ehdxfiWL!8HZID7SG%!2Ieu2@*y`ihRUe@NnO6=E%_BY)8$%LWrykW;H*UD%Lf zWd)`r6Q)Qr$YR^qB-!RRAosZ4pD@9-P4&g+kUu7FW0tIy$98S({aHYw5v{ol=MPpN^&9bTdk6 z%lss3J)yaVKp64IIXvjA=o7VKU78-2I;~9@BvP|;dDUv%H3+R)RhU>2y%4_#QO)q) zHLLuZZKtg;v0&KLCfPvnP`M3J5{WnZgZ^}e`?|30!bq26?^2Ob2mUSF1aKK@Uuh{c zzDzL3vJK&Gb>zSBp*q+q?2#x|!emJWdZ^3L+Q9b7ics6{^ETKS7STWj$sHMEcQl+G zVmI-3$d7s0_e&EYA5&$Aa9 z(MS~Erh-fywB98u2VmLgNj9>wwD-#zLvS`NHS6xgqn$EoM2t)Be_`%6!w2kzn7V#HdekQ{2EF8^)W5LIBOBpkjlO$ z`s%T+RPXWp*4D@MZj!{`{^OuE3ZhyKe?eyXJ~6=ov(eRnKfmPz1keb}V&uM?zlOByTJGcQeIL+`03P|DCC=EKVc$}DM zTK)^;@OT|N`vqYm%E~F8V^dIj2*YrNt*fak2trEhWS^0TDFSl3F*N(1n*9Ztx0{CG#La#4 z=W!@>AC8mRWF$A_+yu?F+ni59qH&em0G7r52ttxbMOh~k2a8FWkHfx>q#NrQj(+fI zCO|lj)F#X`{$?z9jEy;2$66mkBdt@1%eeUPP<mXZr1o_}&A2cdi^{ zvD@SUQb)>^FZ_-BCTIEiFlEp*c`DSZb%<$t;-$7BeCKz(Y#ScXg~P?Q23>=?(gYLObjUbFlxjC3qE2JA5K&(UEl1iht zXW2(7?u$zJzosriX5>7hC%v=57kaopmHiMTcrNQ4YrGtcA7A8>8{a?N`5oCn=#E zdm&|WQ>XLr*+z&iw(CXGyJcri!RhbmZYk4ml;<3lf}_Qp^|DVm@T8 zr7xN~fp2E!;1`2We;VDCc!DFxw)F*Vdh2F9NfXW93ghagc%R6Ngv2zDB1LfG z()IGz;EyEl8itRAcX|p=9|mojZQ(D6aRzjAu5MzimwG>W4~aRZ0fr9OO?tvz#x*8? zy<22Y_@YcPR55cT{k2f3%CqVRGwchjqsG{ihv9mB=Z_Ijf|gf&b9d0gy}(Ak3RTczsgZxgZ`y+;nV6KMFpoj1SB;1lwj=(+|WRBPBMSt?*|$8WU$ zUx{m-WX-P5B z)e3DF;Xgi?>3X!4lY>;K&s2&x!l^5}%=1d5kp5o)98vrD*CS|W()#w!&Q*PzvUI3z z*n-GE?P`PLELMzAx3f7QDyicsyoU&1I~s6Bq{LlEIuC~i z*&aIEB0|z-zZ%zDvGI5 zYSb(QI36%26+Y^7^&hBGmJe=Rki1Qo3e{K-4;~H7OrgI}0WR?Og5Zp=pZcRgWEao` zGyli40TeIv<@KC;MyvQNXlVr~1!>dP zHy+;}-dM3=sV^CEgsWOTVY|Wx5xr4^GStpy7EsH9w0c%+c(S2&!{i8YR$^A%f$9|Y z)XbzrSq_yJHS{2(P;*T@z_USCqzbl9)Kz0QVk&PuXvLt0HdkVZUf2kn$ufDprU5Go z3%u3>>TUeWgAq9~zJZ0QfGpcK{Kfh-#rqBpSben=U*p9yi#$XZ$TQ@RXxMw{Q|~$u zxWycSq}yZ(t_eBI6{a_uIjsA3Ds0DGE&=aY;uyU_N|9utXX>DJNBIARelygHHX*+R z-6v0gsgFEc{UtTgthJr9@YB*;!RD#rSooq>Lu&8FtC+;Y-1YiQaVaJ@;1zrBv*}P+ z2wqYyh7~-!BS7hUf@dN!%A_?@M-zguPbv0`s7;K+uBw1{8*Vwv5o7u#&#T4qS-_xA zP*4>fj)5O|MkbVe|B1gh(Ajx7Yz$$5!1Os|Z6s+GCa6%SW%XdM{fWq3)82}o3zx{T zP(Y(h<$GhG z6?KV-K%FMrK`er+Xbe@pOG;bn)R*;Dqb~&@H@E*>&<@(#JhAl?K2Q9Y765&1cX38M zA1KL>24B$Q!baMaw?-;sN!yP<2VN>)7Sj5bKVvso5kB)>H`KV7s4Tfj-sZctRJ(j` zj|rWXFTX9WLg@EuwtYB3A^Cy^J+NUI`+I?N?5M`D;g3O+(yD372-dwJ=9=!fMUx|@ zvq79b+)ln(YPd+ix*+g8pZ7D9YTZ%<-D{IwN`@*Q`X-xzFe99LUZdywKchRYfd;N4=1%75P zTw|X%+CI7W^K>e50$z6^Sr)fDI}uyCE`VXp9Puzv31|m5Zte6DVSNUDJ8!b$>Zgv`zq2^9O8E=${n(v=~@mtdn z7MGc8uA~U`Wi%&yZ-&R+Pi34$C`%r~tPYf}7{PE`>)~#BzJY^kaV>e0sN5cx)3k3I z``6*+Dqv%K2IDu0b>3ZO&0s3CpVN46agUquQb4^~>KPN_x0pj{eZ6hVFAxzslYYZx z!i+$WwDT|F40P-N=G`Ikd7F-4D{4jYaqg0}Am^*0b#Z5gi@LIR!X-uhYt+6BppHXh z5>GOs^){9N7OZ#ZR!7v4W;+%+vzz^-Fl;w~arugR@~Bd^YU$bP=QBg>2?D{-Vzul{ z%hy=oX|lx*gR)TZ&{)AqVd}=Y@+g<>YBY(WPC}+G zHUIa7J(m>H&(hMf&yooQV}3j$l&(zl>iOodK(>Oe*~%&mFT}c@U1S3GVlP8}t4kqniv5XI80N#jo?5t*J?pQx~Ckqa+v@Mk3-G<2a>;fZHaw9ETL{Kh#6t$c^ki7F4gph4CSd86bvtLC!HHJ z;2ZGz$Ez#=B!?4wbzf3!FgFzQ)GWokZ4rkotMmDZ zg2-LQHv55z6yfOusZthwTxz@6b@|M}#6QMCMB(X_>2cH1$DpXN=K5PVsXS`V2tKnr zkf7ky8%qBbtGSveaBej0#pz@9@>iEWac8e$1P@nTdjN#%l-n^IHV0Ivg`IML;&9mEg4a8{UfNG;Z)IHepVXt?Z5z2n($OxWuuEZJk|{9*yE?jC zP)3){570fi+=%$>G@YXlY8n>C15Sr5$&EESN_dTjmOs7WaH;%%v#S>>Zvj_L^wc6f z?aMPAP;%PkoM4}pZtV(iUnm~FZa)9=Y4q3y%MFT_yc$38o=6=4Q{oYEz1!Ap|%MKDS(=bB}Nrc3DtHW^VMZToE?`vcegvJE5Q(@)|M~ zQnqZCgCz8tHs8m+{HFYKT2`t|LZ|4`dbCtSr_qFY;pG0xgj(1cogeVu>vR9rc!;4v z18oSJwdrvpOgG0$st`@z-&qmb;#3@$q>U9sWsFjY{I*PMAh>&WShioL7EpYZC>|?J zTDJ3-ag%Kc(p?uGv!+k4$Q`qqAt(OOF<~+$rz3}WCS>~G6%mwBKi7WnZr#_mz2O!k z!>!i3DA$iwz zeB&h!8`B^pieNVk0_R8B{4Q3VM}eiW%bFEpiY|`PO}|^)@9oT|zG}4`+VBo`cDUps z*ItjF->7-P=5F>EW4~3QmX+^50A%9=prgIfah{;&0*((m)|u_Oo?Mn0=7re^0(HeJ zC~!#zmfvDybY2K#fS(r^4b=hk4tS^JD4?}n3h;WvZ@&u_9K7d+HSgMV^nxyACD_z2 z-LdNt$ld0LiF&y&S3GR=wf6w`WG~6GjIs}D-Z?nB?UfJc1t8=-PM9{Z-4R*0r*7Y{$YjwLR=xglEN!F9;{ z$StvMcb~-GFzS0Ai7DaKqSz-p)V0zBwA7#Yl)C1Em+z9!{*_j)#U_Bn()q^RU}YTB z8$@e}8o|1vBXDO7(7Q)%X|VOT*L%bS}q-O$p8n?@@CjrO&1v^%d+LpP_8T9;=qP=Kx89eKoK=fiZTk#D@x1O5$;4kMd zdUqN%qADXcAz+gx$-#F^&!QF$VU2l@l9kV_3VIr_(k2z1X8lH=zxp2Z~}*2g^6Dmo6`!4W*uTdltW?+0`?yDC#DjA6m;#fhEu zB@z{>B;J2Ml$TTEc+j@|>;%YoG$6pm>j=NFs`!cQp6AW9l|&%2n8bl{qmXn2riQ~U zBV!*H%1zVHV)eAXA(J@p zbYHz6npQF0WG5d-w>7RN@qb(+xt{6dxHc_ZUS!6o1LiQukg&-8`V21HH*gWorFufE zz6RT5ozA*TDdadfTolvsJM?2+n3sX^IMkrBDGW_WSt&BtmMzWzN32Dp$8+w<=ZeNx ztf}8I?|oFFM09CIVhuYEseR_c)U$R%qet@ryVUEv4z2re|DW8xae{|8T*fU*4SCYw zI>kk^jyWo??El!!?X|}eQ{=ldBV@{7`mGiPNW_jN5nXsRc#{vCWd{Y+I_f{!Dsoq~U|gG~$TXPLu zRC~SwTg_E@bqt&HhI%<%Cuoyx2WF&DCa3*?Yn@BwWq^V3Uy9d?WZ_@Ava5$BU!6%8 z2y+W}0x?A71y?ifB|lnWy0-9LjdTt}i4%*w3zEqm~~$Wgr)qNAK38`sM6gOsQ_FGI?@jucyL(RFBo;TJA1hEy|bFnK+j$M;)6 z)Yn5=Q0bjZ`1Ch3#hiUwCN@EoxLCaeSQnZH>g?d4M1}4P0?UgCD7wt&HZ~1pde3|i z6i**N?Qm`ObWEpMXzKz)z2=e(-0Ikbfi|ayA@MqOZY|w)t9yJl=;gg>{)Oo6`O>PB z&=|hGAkMB`+Z)2K^F1S$fq_T$-{C9Q|F>v`AH<|PPC`6eMy2mnG9Z5w_kItn&*2&i zcuL$0^<}lvxlbx=PMYeMU;~2&tzHM}d8PtQc_uhNbfvDcl0S|z;UKJOEIa+?+p4BG z^luR0!}|KZTt&U7KD-@(n@X(;^(+twi-n*9P)-sC?N6_%bVK96ii{9_swT!7ZC;!8l{zNg_RJ|g={?DlXA$M%9!_nK$~fm*GxEh`S?EA zyRm;%yiXCw&G2IVe0_z`{v+JwcP9KFU_p3GpZO`b1nZ7m$swS^ixo*&N|<*yC}&*l zFl)vZNsZM9rGLp+Y>`EDR*=cJo^ivT$BL|z7b2uS0I^2*32-3%Znef{F8%8QA z8-<@_ADY)lkh(}OTsr~Nb{*xqCG57Rn$8Yfaw!&tNY}XIG#NtzjA1fkT=Sb=m3oR)WyH%Tj^~^re zLyA=GpE3tlOX$^bg8`Pfv*A&E1M~!!o9bN>fe+Sx@D%u=j(Zy3V{^l}7#5bFrnx)( zD&WoIMmidPYD%zkFW))n$a%D@?&=LH2=_I@7ii$~9FLHh8@d`b+p0y2`n1s0r>T;o z54a5c4zqE`GO`P~sG{ZZbX^E~Dw6GFirZ0pKdgj*aY{DR>xX~9zd^)oVceSWpyeX8 zenFC%)Z^vbMWx<4ybQ3-iK%mC*g0Y<#vXcZugR4KPda)^(Vp{=8jx!@pmUi;nX+n3 zyYwhsCE*llL7ZIbid?^gSp?1$`UemfbJk%$90jLxxJc^cpwukS+u6c{hMR-yxvo*d zXAi}5Y7jC7rlb$8)mM*I!p}`pN;V^fuji*VN6g5_z3+WM&wqhS0B?e00jmn%E5fG4f(5B*2%N@kZs!)ZYMHFYAj26#zBX{p3uV z1E;4`6ON=X^0swXH)aAPu+O7kev(soFpAF;)V1<`_OY>5A=x>#Ya4Maj+i{SM4r^t zb)oI?)iISAt8wEm=$kYRI8yKI@}1G%&Y+P)=o|5km>Su+KDR&>6aA#fsULzydAQ%| z4gR-{DQQF95~#nYOMBQ}25xv;0%@x+BqtoDOpWr#bBu!xKoMg9dPA(3JQR|3dL_KR zeYPm_(&)D?GP{}Z1~C6fz#44cX2*XG8Z!|J_ND-H;q+@F@B9G&$limm%*i$j?2Q`| zoP*FGQ`y$O`TouyZEWzGR=wlG(7l?Amvn5qDJntd2nuU%q;RRI{61awt-%$5a;bI} zh#!c7-LQVveNs*0Ipc(vkK=T@=+GQKebb&R0QSK#03T^gF><|88c47+Y(3%PMFZ0W2cn z3xF#MS^?zb&xsv9$V~Xr#(kNKZPwv6>B}sQ6WaB0Jv(*ZcHI~{Q)XK~P-7xhh3o+I zkM0uDmp6=iLKbC9$`yD`S-YpfZ*a7 z;{s^h_~f4Gn)u!vimt$5#$GA;uIp((d2ZSZbp?i+puf^x({9c%j?kXE8PU|qmf1NR^t{4Ur62Ph@<>#+ns6C2`hjOHCj4;;%VwOj zsQ+cvwHH3uIY_J-uw(%VW3zxx$ALEaV2JA*#dISeap#5i9*taTIdMoCjqVjQ|A1#Jm*rzV~+Bk|MuJZ&(E zTc#wk^ZzTx-T(a| zz4fh!ZA>)D2Y0-Xo%-c7hZrJr`Bh5Z3*Q_6TXR-?2$=(Z#)eDJcziV`JUoqJyVOl} zoMSEcrSxoNikTm*jEHi=XKh(xp-kLSn%oi!+^FKc3Zw*r$6baPJVEv3y7uha9-YhY z9k@nFEk)mZlFm=R;YdX$VNOS^A0FET2U5j@0xEoY2|FsXguAg&n&Y8pYEpJwi=1{A z-*o7FYzkX9pIXeKrl0T~jeg1^b1(O$T2HgQ1Fnpl8Czm&6`Xw8y4e~=|E;{p>;EQd zUD7zty89D2TN9=Du}o*u^KOnHX|i-}f8&ZJ65otSfu?gVKXOpXzT9=Q-j_meD&j3Gg=iO(Cdu`B&*ff$+RlY6B7Wzt}$v-wR7Grq`O zeX8{R(CQ9{YH6Ru#W-J#ERkUgOE#fZmj~eS+k{Er7I}4xvwEaHzrXV-Km7^P|0`hIGl2ikVP`shB`*NueE++5sTwYVAUzD_YwqzBiG*F87Q`ag&-S zvh%(eH6(R>*RxAC@DrIFpc2|Vha7^C+F&a0aeDc;^_gll2#n>>J_EXBxFjdsovOiQ z+}3qT&!Hjo!$|A+xv@mnw)=aOdX;K>n|1LejR1*}-|wn@aa5wL`Ui{UkANrYbVtqU zN%8E*F_=#}v~;P)78%k7UuJ58iqv81zMIYioCD z{=(iP^U$k_d%gKfH`XXVWKx=cT+Q}*03LkW@|P=a=G*!w%5A*-y1@?ZxO?6C%eWt* z4VY$UMWNur`c%gh#nPRb9vi~ zA(NEK|Md4kR~Hcc2M3BSj=d7)0o@Z_f9;x%Ev%`o7HiNJ5)yLJ zk$4%Q>vvhm1q&-oS`J0u`5#99!f#ieG70rhdxVq$cL(NpRv zCAppbjy3E1)+Dg!<)tHsj)_$96&DyAd}4{4Q0=BrT*p;VG`NYR_{Zi_#GZLFN#HKj zax{~^eGyG*sK_Ir(zn7Q4Ir@&ALfsU;r)*)h^zcJ_0s*xGR1q4)@v$HW}1n0;oMSL zFiAJ$7al)Vrh!TGlAzE($T0Ul{oM(+6k#&iR(%1>*opZ6Si5UDuy=4jEa!8Tut+WZ`=&KWM zB(P0Q=FEnIIpct6)~|#EKE(zYXL`IDEWgLosZ|qKvVqpX&whOYoY}L|zH;GUwc5Kk z!Vyy|vU-g=QV)TGsCgUe3t5M7=OZh9!C{+C^_xjX2#+fb4PJgGIe1}_0gC8$v8A`O z@g~fUIiP@ZFNJqn$ycIxMcKdqJcN>IBKu$CN|hO<;433e;jsr4P2W7$EBqrDbPha!Mojed(lTpF+R$Dg?&CTzkd2p8? zi#}^A@@8gl*u3j1l`6UELX1m;LkaZiDTI-z-oyNN4MRK`pOmH>CY86L8rhx^9t8Fi zqH@;*(4N(CAM*De5CpP!TIhn~q=nMT5Y}1ZrnUmdTwPHm;*Td!nyju?ke0HBk z&Dp{oUB>31JI2)W{&nYx7V^O{GD4wAzV>(#^f#>p6Q#Z62G;@YrHJw&Z~ZPFNw13q zYCM5(JQfXW1aj}Gx;4kcGl`hCRH-=f#rt>XqpCR;B}q(GQbl5N28#qG1p0 zm}V5JHLMx|{BQ8A{p_gTF0(eK0{xPu+1sV}@EUX2H$Xp*Qk^G^WlZRY=^1#h$rgkd zOC6c|ZSwiy*7=vyu1&#Oks>oQ}%V)fv|<8iP2W9`52>wk*R_K5ugO-6{V4JrFAqBBFe=5_pBl z7g7-|P(C$d9f`QN%=|X~KQBRPllb5~t#N99RH2Q~O0_}Hrkfy_d z1Y}|W@6MX`1j{CYJkfEqGmZeJpgdD4zNgKZx`COnx!a&fk|gp%C{#9yHCyK>rC0^(haD2I5b|8n7R-D)!6EBtz)-Ak)G=2Pd zcP495jM??`Xn1lr@M#FH^-g~(gBDPP!`f;mu14*iJ6s>o-wwd1)y?g!XRI+S&zqUS zo}Y$H!Q#Nei@yW#v@r6+2aGBH<&cmQCW7<#I^G!HTUKv1zZvBW5oS$-tV7)s@_`ScAryf^dy-R-Wbzrh{y`omyh@xH0a-zjwtk5kR)Q={-#>E(OJZcv z0LiB1v4~aWo(rcNM|;nt!kmi|Pk;v(UM9KyFLYo39cTR;@lwe2L819|dlsktXQFy7%u z=tjNI#iqQv{m4y;;Rj9&wP`*cQl#0=TaAo=)QjBn|9A`jq!J*`<&;cg3%w@v>E=(t zYLMF>zh*O+2lxnInG_HGzMc{Q53c;5BmM4Q&lQCQWO`%ss)sHbgvJZt60e$FdM3|* zM(^=nDpbj@Q%*Y2XoXv~*;pZinTOe!xipnR>Xw|+xnz_9SbP>mU|b^(f?ME0WG`l4 zPz+8N1ZGWP?$8|ZP6+gD3s=wh{42Vnql4d7;oK$M2&Q8QQDWDnv^rp z)D(5kz@Z8l(~3ENY@^7GyeOG7n^oneD8|!a1)rR4n0GmjX7hxtI*>5qrh>EQ^&j9% zP*PD+x>Vq2gz+iry&V{>n2jL)K>NB!GkKS_)V}Vb^7nK*X&JwM z7@fnMp@ufM<^==B5*T{VI-y^#tgWe4S63%XP)yG#!1oP!I+$#|d6^hi(&w=2_N&wdNfbkAnqwcdIYkon*sN~o{O;4<3Gb_yw%7X|7e{!4EWsIPh<4WmIk z`(R_%xd&Rj?kJnsulNjO##y57{K{S2*I^ZP&!u7tPN7h45K>hwN_atU%TwDltV}Vy zTnO%cs2%!{sM&eF{~aBMm>m1K$uZ0O!@>iC)% zUenfvkcGe!Ld10w5TvvT@g1m$2CeyrOqP#i+Hj%tJ>8dT#4MaHh%E~h5+YkVZa%L% z%uQQ(eso#SZB&CcIeNSMdsEwl@`*suT(QKyIhLpo2}jt;zyL5`o@r4jP(J~G3z+Jr{e7?eWwF9O3IKwoFbK&szCPDC zRW`DQaLy0k$9~N_S`|!#g`pr9BRCqQPH^zLBxe3)+^`0gpc@qJmhx-%xJ`q8hwa1* zL+5fyHP9}h%Zu>J9d`#1qEUkhS=}#IpWPuBti^ltUS)LDU2R;hH73;sQKV1csq%^L zpEE*)Mfx>`OF)kBtIf4wh}68~B%E-5j_)d6n;zZlcVIVvv+Y+Kr|y%A&uPTUI0$O0 zF=Z;r9cm~2hdSii%C&(CWuZ-5g-q)=;V|(Tleg5E<(hF^L9A4N>I6M`q@{j%@z>P( z8{t=yBt=j?Pe`n#X{n!v7GH#PJ!QwRkr;UmuJ_WVg)@_eXeqUcN9TvyEgw*2iuK1c zra1OrxXV!+OVg$gh>GoMjNgN^J#LhN1&wokyq)Vxp)`%G^1{gb?-*d=-RYg7V8G|e z-2~QQMYm*xG;FRQSP$?|%GhG{rJi+#>h>DrYMAC_rp*Fum`P~OSG`!vHr3785W;UI?WwA$beyhsoE}i-KOcL)`cF%<;Q*#pylRwYs3Bwk zWp)bk=DA{t$hazu%yC_L3kyDV`Ii_hKYUsYmHg11Nirg+Gq_edLnfu0MFKM#+8jiT zZywrg^;2;D(tN3`f7u?;tw^LW$d8G|Y|1A==rhyY_{r)I2lpNM* zqcb5uy@_EANaC5am~(=qCqeru=e8sglYS8aoo?fnYITl_A4Cl%&-AkE?{u-i;@;4h zwb-VbXo8LKErQPW104yi&@<*{x+La32f(^3G@c((2jP0Bw(TNS0Jm%R5cM*C(tptH ztVeQ8@60bu{ny*fgjAT|3Go<-s|!3f!1w68S;pi-YUP1-s#xZgWijL$6McAR%`Wa= zCm6r)Tx$2GRCOD*`ZX2b>rFYrwx-K&ar^g<<_W3{Tt4)I137Zj0CMZ}m3&JIekDB| z>kOBYJb;#YAgrj$%@W(5U_m`FekA>bjIDhF;{lx7JlGm^dK=T^s@s}vRI=5CV5_8( z3T47?1;QExG+2%W)@fY8Z>zwX%drPZg%ug3%Z>6NaQC$Ae%z@jF{@~9WST78wg`!) zyKaGHuD%#&N6y51DzWR5ao1^|Qr^*7(9`o7W@dJlBU$8%e zMBmmF?!ee%B3%G=Q<>ZYxL}>eCy)-kgp@D%}DY};o} zybEVWn45wem*Dz1auU8%{m<$A6xu`y&UW1(d7N+4E9(LO#IoL%J`Hs$Gq{3j6-r^p-AFkLx9+78Vq|tlD)1Bi?b$ z=S}fI1sDFXHUofh1U}S;2MDDSK^dYffIxXF>fgMHYg#Fm3-oS?lh~Bh)*<)OUjWm` zDVfwnHJ~W%wWbGHM0|Bp$S_%g_o}^ztsjDQVV+MNr~N*iwJ!bDW;piZ(3DfJy+~T& zY@4U<%kLY%{S5rS8LMb*oCNK+$A>iT7HDeBLy7VjA&K;cL5-U;7~nj9o+zRhre*~b znP}sz^Y4$D4!*}Md6%z8({%I`Sbkaop_VQYUxiZ^1 zXkhcy?-hW%z=Eggx0LPf$->LhB^iiK$QW{5MeB#9=${t~y;8XY&0TsUne= zx=tT*u$NBx`uRpywcZR(pbvpk{vN?StzF}ZK~P3pBh$iboz>XdhP=p;G>YkxUiFAp zoCoZ{R2ZxHA5^Js@zri@h(#io;;r5LV0}6$1~+t<^w5DoSsL|#IG3JxT%lz*gidBs z!n$e=Ek`g7RD8ek>Il6@KSmU24M-NV$19KE1eWfv3Cc#+ zhZZkGriwt+h0b7jcMmtd;bYIi;Co%YYyzbcRO)%BWt1wlfJdEVVk}fAW7$$T?@7@) zS=&X9S2LVXb`T#n=RV|(2wN$`;d9fP&xOhrfT!+aLrBF%sT1%j0tHH5 z)r&RCy7D+#3XC=t>g2|!MP)z=TWmks;N#d@9!nQl9M}I4ZTgx*9ibbF+s=E52A*+~ zVoV%n>K2R7xSu{XQq4{j@eb1}ehre-B#1z}N|1Ed&!tiVTEIXbdbI5AMOp}*x8$d2 z+L&Sr=+4x*C7aoTCiKahdutcE*q=HzuD+p;+>BKwOu#~;G;|}v$Q)M|hygEvJj&v7 z_!cKwCGhum-HQ&setz0#o(m=hU0Zgk4C(`so{eiCwDLWVf>f}ElZjcaLqx9$i4Jz+ zUd7cHE2)pvU*CRb_AfzSovpQyavsKhdV%M#>4?UD8ODxXejkm>Kz_F1wDnfY0otx>G9cheyfhK+l9OhJ@T(Zz}K^TCB{L>Mz(r{6F zJO*Yjb+W!YW-h>!u8pm9pNuuFGUu431_+c=7z;}Eh^!t zs!V5w+g7Jra|dHF2Uh9pXsLh|PPaRJw))e|2Rsxlz=yj7K#(u`_OsPaK(OON11f!< zsF-AkxzxBd0-6J)!*{Lkl7Xds8c&qO-ZlZJ@jxfBs(0{UrJ-DKRwpWz4AZ+KSiI}k zd`4=|xD5URGH%!E5dR4gM5Z6dU4AD!+9OMsg}pgO7m0c|*42fVscA`Yi<`rRr_L7# zBB*CTI87g9;2l!bPaTkiO0DLv^*_yiM2q`BgqfrZGsMH(4X@72RR@DT5e!5icurV0zoGuL;{zfBh^=1Xk!2n~i9A zDRKV1pk6_X3dPi=MBj4jALNDYzrO26^a|*=hZ++iMZ-QBQl!=@e8wz!bnoSPs#1c+ zN{HS@JWVPp1(FY2H3j;u$2{`^6#xJ`PDVZ{)=wp_4Ua}N zY+1wC0$h5fiL$3YBp}y&Gr73pWMx1e;Hy}mpskr8;j&DxWW%h;CD=OMTsU_mJAudJ zdJ;64NVN@ThiUTz$G5C?qdr!yN@O=yd^{sxPI4TvcFPzAMtV z>($biP;C*(8EHZlx{+7GmL=#v*Z;V1#U|*F2p~lni+TiZuBC`zqaHawQV=~!tp*h0 zU}kx|5#pYmotnf{ON6e%auM0Ue@5NH#O}j1C#QW(7RtKw5}?^kO{J$`40%8GYE+WZr@0uSfG@TMR=MT?l@YigWw~Okq@$4~43Xe$xpZ7YbGvpM ze|^sKpINTQ!A#a3hPCh`zhr$&xB8MOw^xD?@t>=YY8Z;12m_ zL<7W>_M0oJ(o$%oy?fEhn83|Pq3FQi9as1d2ih5D0>MsN!V)bQUXThYAv-VAe;R%u zPP-M+3CR*oIaM3`ROe*|d@)LeUq5eEQH8QH@U&4X1Yxo%F}2y>gr%u*35}W{lN#Wq z$Bp8A%eD>oISyBfA;u0*7x`zXVpWPQC;mE;I*&~H>v6#4(Xi3F|Gx9w=3k@tdrZNB z&`jKYew8x-X0GO!^^f!=4&k1p-4@*H*;gO$5Ad}{oR-14@CA$fYg@@Du8|Hj7pqII z=#sNS`oyyUJvh=U1tlnp08H(WYT|JPvq! zKtAv5{!iNDpGMnOB{Kd;9mMkwE&N9_-0qsi2)GP0j}Zc2nVk<9oehKi;?(|ql;n3~ zG>?vl_{kdeZ<8vvHMrgCSveO6zmn zgz=@8v`2J*&{4zf2E$$r7rc`Cx-i!D3S;nnVV`1(GxKz!h=Fl+4-2`&Y+d_-wPrWc zPe)99`xfUEWH*nzi7T4R4`Hag37RcTJ9YgMj~coE%$J$NSIO%Hlgn2|5HF||FSONGa)iy43e@?ZkM5_xtzXL1=Y3Tz*Lfm$u4f- zlLqXPKqZmO!v#bT?*?@}F@aizBGF$a-p>Cs@kpsqW@kAxo=up&D>km5nlps90t+ss zu8_-yOce<@tFMTk)4{Gs2i>#6UwJM@6Dit47NnzpMK_;Lgf|7(P`i&IB{VK{I9v)S z`;uUWf06b0!|erZ)1HpXkw zrWBao2+V!#LDVInjF{XU!E*D!ahutv*8fzg=Avo;nQ$rKP*ce7MGiumS(mT&-cVa< zICQSprIgRrVa$6lD*f2_ihoakQ}yb5t_wcM+Ot6&6A-GMa?T^}sGZvQL8b>Kun0O3$lk_chjaoM0Cq-T(%C^!{VyJ#D{3u3)|eD&f57 z@*3(S%NDrBq_afUDi;L9>9cnK@zt!Uhv3MjS+^J8W>mwzpBWhGRSl z@@@a7PXfF9JKt8vjvVeseOR^9t`QN9GJm8_%iEo4czAj4j@fqW7x`M6T-)@g&m6Vi zdSXs0NHuq;Dv4}FS|8K-WGLP?ANoDeg`qnNvX9$C#3z~j(wV&(XM_E!T{D=v+wI?7 zqb@P-E+_fw3RRzmy!Y+qTt4e%5cKd0jzCt)=M%uL1z2dHlCP;kn4xnIr!Rm7Aau%t z!R-SZdaCf0=mq|}-L234)A{aBeF?jG=r%btrD_vi)R$`S9nxzi9vv+67&OmB9Du4C zDdx$VH+<&sd2k#@R7aCxG{taKW7xI6roJ6}@9<_Y>4Md)Nb}M{ac%LlO!$lJSY*T; z&#G?0pj3_M#)=hZ?^$6JkChXa79%lZM!wRSc1DPv+?r7ti$zoit#za5Ct|ENHnI8y zJ#)=E*yIXnVCyqszbuD;SZH@?N78s_JK_NR<^~y(>4*wgR5x99)M(|dMp-y2J>K3a zyz2F}!Jr|Nwe5U^gARmN%=(5&=C}7o9nrTxQaI$aU*j&N>{k~k;}EyktA)~`=9leA z>t3@s)4xNa{wr9#Jtj8gc;nNiyo?~ZA8i<$6VPx`(wh4HI)0=f9#Nh0A{6K+hHt?N z8mLYuB&dIZf--$05gv_qMOmD)zxm6QW(J{|Q~%YVkK{xiPld9OIboKemeQv39KJpw z4Gn2i+L)3Hx<>NjiT4yd9)23J4_}#lC?MD|LH)T#(^QBrYPA-8F%iet3ai?6vee@c zB`RiScL`z?uIU@TpM`KwjU8m9h|0*z<$6{2GY+ChOE~jhoPxcvdtqplkYJ)9wN--W zf(!)tb42&}u@OIm@WM?CXwZcGCrZMEiEskZruo zDJ=@OAgasSW1$GCK4frMvUKU0u@#o!*Jv$?LRk1Gna(8XhB$xP^QC`Mr6GEUO7THT zmsy3O$4j1y8BE$^4R&!_JZ+yf8Xc-JrbQbs&Vpdt_l zR6d8;SckmBqz1(Flskk5w&fdbI?m~EO&Ewd7j9BHA4 zFr{9E$ylhof<7&@QLGYIs4-^EfvZ|$WwUFeU{OJc807?eJgig`Ua*;sQC@CUS|2@6 zOyT$3lVT4~r#0p=r}=2P9vOQ<0==in*M_Zkd-I?HK!w+BPgEjDovY{XVzlDUO^}z$ zB9_V);G2o?3{=ilrA~a%SBY)P?DtA3eCfof%ip(gNUQ4c-9R7J+4eSxioS${+j0$0 zjSEc?;n5)>;vk4HWyGuk454+4uM&=w!@AD+P(v`mIS!cQlwMZ(H0)$=f3Hw?W8K?F z><3iBr+ML*ql8sYHcw?uYwHZ7J6)65_FaH27aiu*b&-br2IHXsa< z3xZk%g^uQy(9pMfhQhr~r53U)W${0=jQ8`AgrIr@h}X<*?51eYW`>txR%gJv`NsAX zcYiBBMV=3!W4pu{+5Be}g9+gEduZQdo9LoyC7R1~|Hb_|*2}ux6hx`m?GW4Gmq-&g&W9<+a1$k`j=wdoVu+1`IRQVH}sE6N^uY}4C~sIob6hsP(@<*LVAc{ z8ZOIr7j|((2n1eQxVnOYPSok=dIPuSM4&HGZ;?io$4-G?E~agk)smDf9t_rWz$0To zkfMzAhRWj)GGyd05_!`}3#M7V4^`QzFupFO@*_Ze279VfuIQMf>p4mBXCzqx38C95vneS&iq$k~~Xta1hc#>(oaEB62 zG4*wq>&I_P!CJ~s<>HZcDNh^BDc+%>XTTsV30}@?%lU(z0C52P5)#6J5<+{Dqt3F3%gJE*7%SzGh>#)r{s1!O)MpxgfK#uuprpAn0cBH3Zl_310!4 zPw201xkoo{BOMGa)`Mg~Sv(k-^ouvBMj+Pwq`@21MvMqkVgDJqy*Q%NS$Tf-;ZiDL zD3WgH-BFp-HYy-_ zE_Y9z=7YOHLsGX@Uo_Gz=K4|lEPid?CcjC~s1?Kao9H3z{NBB(POj@d1!*3{UGam+ zciSNLHVH_IAvtf+J-st~77RA(t~Yab>k|LaPal4RhGcX@dRBwp3`8)bdlF)@>=`h`Csdl*FiPfkdtc6=g+ws=R- zPF{tUnJ~5V;rC&-zE{sYnHwRlNTQ1PCt9lRkQ2RGh62g}|D67F@A z?H%xK>DRvN>v(1W#(Xj)Txe{ujmc5Er-Wi5Ytss~Rg6L~lR5jteOI0u^r_CCD0Kqb zJ>>kl#@XcENo9ajhIJ!TAdwK4{0d%VDT<_Myfx6%hebQoK2jYi! zLDx^lu_ZUozCYt<_**+*_#aKQPst@55G+xAAfEHtzI?B^T$c93`^1HcQ`GdFlkVw` zGBN9QahsAW;AQ%w4dP88`7z9sJMhQnGPiUk9Rku*wyydhFCBU3fHbhb z9LE#M+~gS6-T+jekmJMCrKr}Y_SEyR{lG^T-1~nF;PbuU7d-3sYJ|lj){^BHIj?rq zx=lVA8%~5*dS6X6gjVN=x}$alDH4bKP6K^H4fyNyojnsm^>mF>;Gly|&uBP;BY5dY z3`Ao#^PU;Ly{cfyuh_*QE3JIo&MWHj=JbZ~amZRtOvkMdU*I=Rj<~Z9XqMyM3z&|2 zKfKexE5)guWCX*uzm_CK_#JKz{b$4)KPBA&4S$aVNBsB1%yhZx?6HxH?yzFF5{asB z?T4g>!kuV=nckiN$6acjX4aKOQEWyYq*Tm!P|(?(nxVuV=`Ge-mqB{v2M+k|J(zsg zdoU3c&+pQdpNNtA=dGFIIR-~h>KE@*!tz2at?$#;T~?Ng%P+Et2i&Ny{s(wVy~Uv# zX|t0G^iLd=lqeTm4K&wz|6F`IUEVM6t>KH?x4RBhm3lJYtvr)STU-U} zw6_Em1K9}cl+gCt9z*sG@?GfuxHeyAp0RiD{;p;(@0yd&#_8D#xuCOaJl7n;%AvJB zSz}>_`&UJu`FmI>J(enza+gd%A=bn!-FzCI0Kp4SXid$K7*n2;xnCFU25JgW>sBZWB8)5(Ohf>2@kOYK ztO4cI2iwooKB5>=nf{o3CO+s0>{xM~h?03{5e6|;NyCiyqYBbzdqdf#%NK^ZhIDN# zg!>zyQ6N+OR%Fn3gFSkn>o4%c!Up1?t>tu%|US(d_j9XI+{M@NtBiccXU zLZq;#=G`!|6eQ(Ee*?gRE-!n+Y}P~QVd(6TwJ%@JG`X>^mn)vEs>^vW+L0>f_@1T9 z^BeriCsjljQ+`c}gR?y3Llg=s2o)v_B<_9UC?$n)1bk&A^2D3wdULE#71@nlF&j-> zg_Q*!fQHxCp$c6QBT7sq4CobS)awtE$#$Y~6vaAzn-JiZ>J@I+bnicSmkm~nd~y-GZeiB#QIMYJY_W8bdE3f_IUt;MQw5$-AuUI9X-p`F8PC+)G8gq3 zC2S+hceU^*jM77qZl3)T)=$0|5{J54!B^|&Yz+m$n8zztF3z=?!8L+Z;*j*P`@n!u zL585GoQLoq0xy(j6zp7+<-U(v$Gq7bw$?f7DkFX{>0dr-zivwUsTIDaXqIB2GwjzG zG|(;~P>qY9da`j=xm&FF8WAfVG3h?NdB-m@B@Y%wyD+MDl;UXLyaKn~UN?itepQ(~ zzpVocW^IGL?AD5)Q)TCRB0ygq;Q{m$aCVTt+2*%%A&QF2iIy`v>$am4)aOgQMQnVY zu8!YTnWBK_@RGi7Y7?S4`>cz**uX~^oHwdJ#Av&m#Nzbgk3k2MKV`$hUH8wcukDN8 zShP~9YVP%GOwU^pY}|wvShTn`9~a3$Rd)sLMvSK6yb7JKF*~cM{oq_zwF1i`D-aL) z7iA#JZt~#XPp&vhQS55tUwqUeX|j!UoWf_n(D4;+r{?%YV^+*!Hcx9hi^0n5p|sxHklW;y;RH1P>IjH~raf-5<={hK*73#})9 zmtS$huT%mk0GKu}0p)C=bam~YO+Ve$mT!{9Z%kWSKjtHeMYGjrU83o_EB4>}dQdWL zx*GGcE3&l0G~55dE3RoM8>YPe$`T=|nhP%T92#ckbn$y(uanEmjr_E_O3cB8-TRbr zKOp3+o*S}?V}aL~_g54y%EF)t z&mVGTx)f4EOQl3kge#$k zCL6PDIezD`+kGE@$hs6L+ATA_^#vh%r-U18JD;VpobL4Fhtn(2nk8O6A#`r#$J$n( zknuvaY!m;l{Hn|cZkLF68tdxD3gjI1^DnvK@%6NSj}$f*OSRIuZH8F{g^S)%l@m!I zWDy=6l$CQf+QFyPzQjrB!?lvNAu~T{ij!xIMyCG7p{1OR1N8&0WcNTyb95$2DUCy+ z^F#Z4+i)p<8~5hiv5~%nw2LC!r)ODOzj7M(!sUDfQ4=h8IA4SQwgsigVXG;Pi!#j? zicL<}u`>F|uYA{eYR>wDXO@D(h0V+wsjG_5Vd~5#__?FCoWFlBa1*UcHXq1>ZA`Hr zTr1oiJwxd(EsQlDv3kn?IZ&vIheF-J)dM=%(Vqp4QCNZ8*c-S{Py(HH0nw?FvQ(g_CCw3u&^?`jL)cXP#GW<4+~OX)x+@+`Zu z#bU`n2QTZH>G-(1KCd|iDM*~!Z2Nh`QZJ1EM>D0q3o6cLicmNuNnVtq3Kf&;;%fw) zI=N(T*5&Cp>E?TF@m22W4lu14aCjvlskWoDHQpNc&?+J#fbzcb%;266#6GBXPA~|$ zZGxCoHuIYFjYVMlBj=*_oMyOSa*)$d^XNsPW)&!EGVB(!Ei)77`jS)b$Wx7%b74KP zk&#m12W5~qK~wQA6=8PO3>@W(3R2o?tGt@(1&p(cnbRTRS?vgKXj@1G9+m660S;$_QI|o9RCodY<;|wT|4J5|VppTN z`*G(C>!(P^wGl3B|M0&M#*Rq7AMdYxpEEVG$4$HUDzUETdBXzil}*QoPlZJhmmuep z5svwl#bR2+r-N5VUyE{LN>fwj*Lbw1Y_V6DanAhJ!URYm=C9N|6_%mr4BpKXt6_Xa z=BYc&1Gk^5;MUn$OPCaJWbF(4YL&Ofz?9Rt4O?i~S>MM1Ym2pZ}5 z!3m3yvSs@5WeR|~9yR4bmK;_A)&B$pkr#mAL7R@vg(6%a*VnsFL;)DWuDn zOxA*UgC^IE^bxZODhgkD%OS4CexQ}K6M;4~u^K%nERAW4t(4Bv&bgY8$i>KIP!9kF zt#kI8(zeXc){W##kX`pEmbax|3F?vYsIAN1eVBo*7C159M}kYrheTF#xs*HApxRDP z$i74}r^=SpJv{<~njSmiCfMEsfJ-F%O4Q;^b-7)q=VM{R8oy(1d(n^f{TH)B=kCc? z_Mf?p?0w6KSs#qI5vUf!LCdJ-$j^|xEZC~zPS4ARebipN-U9|u>d4gT&=~ZGL%~o-8l@vE`VYCh2h;dX^2%3xCjol>F$~mx{Qg`| zGT_9`@2m|qnCT=8a7?(Pxtz&<;bh8Ym-P3K@MhGF8nVc>XDbGAw@>c>`pYiu6%rcX>DCSNt=JRRCH3)a`xcbDC_ zK4h-@;@ymgf^d90eS6spXIC|Uq!)K+ZcCs(hYgXFwPR&gp7RmA8COST@=wrT(mEN8 zY+i6Y?SNMb2m`zqd_qA1KeOHupq~Yl#k>pQG%4Yn8{y2KAn#lj@3&r>LjP5fBbQKw z6I-4O?S)rI#g)Zx4}kVm#`w6}7S*ezKmJ>F^qc8m{uSKs^2D{H#)eaUgri7dv(Rmo zUO`$)+KrjF>6a%#SfN?SQR!`=VM%;d?oUwuL~sX3ZW#J*YilSYj1Lv4-YK)2{uAjv zA37JQQR)xgD*(d3xs(fAGm5xC8PsqRU3H&K_k96L6iCb4pamB$W9(OI%@-yPO06UC z$zvHq!uy6A9^{$Z3wg)12BY^iD3 zI_-~BQ>FYHuPZ7Zua|<_9OGncUY)dU#eac0`dAU<4OV(coL!a$cCN;Hpj~vi5P!^4 zuRceNJ^7b$_V(Mxs+?~9j^`^Il0-e5VJkaPXzZX$TplHkgNPUlE8~#k7nI6Nv`27U zKm@=YZMbZcjUJ)lG>g#o^*3Wb9RxT+GK}}L!?~=r&7t26{ zo0MhU={i5DmZkdUmIL#WKI*F+O(bq`#bK@Xb$jL<%$9eLfA(B5)Gm5TAqZyU%6`9< zL0@#jO3HM*es+jhvVuSBHz1c& z#IWvo{EowlDlP~*0g9eH$DZ!Zu6_gOtHDwJWlEA8dJy-DRTRom$`k8TS{MN^y=4Zw zBCC93BYwR-cg+XIX>)FxTW7{lP=z0I7$U z*K_8CWFTC5t~ZrOT~p+m`{VErCZI?#pj@f_A2oX3=~mqvel{T)-!p9G8)%moFUBZ zGAc*$%8YG-2DCN!CfcOd{9zulan4-FR_4N?E2g^TgScDca&(M?ORZza;a+h5u2L)Y!C=tVYO}f~N5>JP$ z-fjju61h)-Olhs9#8d}5A%e_({*zhsBAj-62gI$q1xw4#W-7qB(*ubOZ-@eNb0E(0 z@ig7U=b%h+>*dG2z(0XCI?=zY2G}D(yUN{Xff7RADQ0*uTHzFygf;5Bvs0<^4bt}q6sa+zW4=DFsi@kZey{pY4}m-ONpjUQL}gMwWsXf zhv6WiZPiLYaBv?A8;sD*5dVISVMUPLdf;gF9Z1A|i9XKvMoXzrqR~2CKU>mzjAKTi z;VD%M{tq`9h@Q0~9`^Y8sL87j@w+;^a~y`ouGQ0Bn#*4QlR;=?Ed?nd2XLfh8^`h= zU%Fsukr*E>%0c;kz5)6T!W)q#NcyY!4okH?UwBQVls~ysflnz;WH2}`FaVSpD#1BV za`J;4;E_}g6yt0n=-uoB5V}T3nw6b~JbH@H?xqFf#l~1+&F6o&$;Av!w^xW+HJy<( z=ON>72q)BC&ezc1t!-+O0{+H8iYYO!dEWk9zj_e1EC4LmzwrM$F*0QX17mph(iXKgp?zYDs4 z_QPj8*X2RHt=?qQyX?0FGe3>%Kf)1a*7I3V<&Y;Y5~)*=)UT zCR#FgI8aivci4)6ybm`a189$xXe@Y$!k&ycUf3*}{rIVcn9r*tKw_Qc;^w?|;JxQo%cZ zf;|tY@V3VV@I06fmqh9%jxyXSezz(>zl!7k4&8f0zs9-oEz;xi+z_Y;|35uQL5wh8 zOiS}6;+k6~sY4-(Fu;L}ecl`Qe2z3p?r7nv$V?ISGW@;NHnF4Jm#8@D&Yf6I(A0()L|7F(I_tQ$=?q|Fgn^I`iWlEB1sZ;mGP?wq5x*Im;|tflbgKPg{1k)+!&-aq)GuWY zv}4XWMaAe=QtUY$S4{gSz|@r8ouV3KWzrMn#~U=2dKu5Z-htGf0vmT=xvRa{V=P+z zPUD`o^TB_7lfUSdEPVrvEb_azLQ|8Ie|IM&+x#EDM*(3e0+4>V^L8NuoO9s;+=5PD z*ow=xGSo6-!J|_c?M9@#q-Fl5#Tfi$*8fkc%7ThoyS8@BjKMJ1yo~j6GO=5$wi*vyCjgP(k%YIsfv1t~pK|qdgImH+uK!=6eg-g(L+0->kcj z{K9$fA+8);?MzWE(t+H8uuRNJ80bl0{C^yEi$N6D6K3E6UUp?On9LX=)raCKKqMSD(O>_{yMwBV9V93zSrm-`lx zpeldC18lEC3w+Fjv`OxR=)mu;1F20JEj=z6j?WmH0>y$Y<=S{dj9gI-vFd76l(l*A1t}r@ zJ!zHQ=Fx*q;!6fJ?2L#zv+tz2tA!k?nZv#hDI7lobTjRxRN+L2)dFa(+NKvs{ z_@S{bU!^h3Xsh<~`#oqbIWNWWcKR7)B>4A8m8yN0YNVioWQkvvjluh*aL(^`!Yh!R z_`@4T6-EV{_?C1=;ui9%O8!j{^4j4x7L$lyss=l2ppd1#cDi%tA>io;nFs~@u3D`a z9VM%pS;xH-2J)>nI&j7>Ki&thqIYwu>Wd<+=PjoI4ctSeE2rdOF-XW~2-Fzx%BA+L zm89TrpObU#7DSB_4NEqUh<{T=89lj@(J%Cpg}J~GW&AKmh4c7Zlfyj}xHWfT%nrKb?4C;|? z`D8D69Y|*gFamdRw-@eIoN4Mgb5Evg)N8Wed**qDdQB1?R=dI6ezIP8vi=CsQ<9%$ zEPKkd>G3HVT#fr?5PK+)&yFRdv~`q@=ExfGtOKz`RYxB9ecJ#TB@uKaGHwX>vQtzz zj(I{sihMfWwb35ZxJt$Xg!Mnt{7VLocQ|j!YR;Z9oB+pE&P6lt;g36RMH3Kpl_@*D zgJ^7mgI)u7slOs3d0m3p-0Cll!306tcX)z=$6~r}>F#iGI!D*G0^%ggz32Ma9^t zd3&O#i~dq_63iM27G323bEMDu)eVgcp+-f3OeweJ?E)$Gq80^iVj$K%H2+1>W^Zb- zoM;<=oUXqLuPidSTVuOm%;%%xvayD`IlO?s1;#l()4;@GGrw^i)Kp}By}?k#tcNT& zqZ^)Pda>_Z=M3wBNs428iwIk{mvhtPi%Z)2-`YM@pM=vT7mflh%lW!Hq4nR+FVQI?znl;#!|=A-s>J9!Ks-ng=gTu<6?J zRUh{RneE>)$z(u2eA1P>SBfJ?0819{FMdJTx~Fg?kSHtrfIGkqwti%D{63!GJZJwz z#eA#!yC?kVvJXqyR4~G_l4^X4yLP;U6@M4bl0N)dIGjoNTHs3Hx`Dc^Q zr0=|>(LLj$I>p}xRLsj$Hdn%VKyArn@>WwQ9jh32Y?6ybn}Yak4nqEdC#zZl?`^H( zIG4~a^$y4538C$La5+*qCEWYiPe zR7LZJ2S6d~%CC5vCb@fh{o@fi?BK~R@9dgGkI?}B_JWM2b`s@fjgx=OO_v$l&qe3h zeOOaUcVGhkN9n6wBrn1NOrItLEkUhhN7Bm7(pmY@8tucRGO@%DT;>2 zu?haWk=HER+vUM@4r!l8Du(lSO2+!4BZL{iL+zq6_UcqBb(H#i z&!%>L2NhB$$j)EnX@$B=9K5v0UQ66i=!N?f5w;69%KZ!L==Q7zXh9-&|OHh4$;>3Tjp=uK4<1Tn{5Gc{#uC0 znMlH@oz3*w20iRB5P?@~Wp_SmN%FFOH+$4@k>UAztlMfCzH^<}o@-xRu z_O^#z7i&2RY6!sE`9`q8ZGFmfeqX4H!Tpf&&9(nIhwNfcM19G&mEu zQ|DE()nJ`OS8!Fi9RGIRvQEf_E`Fi7VydI`=e{Gs>=t=du3?O z6iLR}Q9%rh?M9G!4#4F7HDMZ80s7|MlIx50<5qT%H`TtmXTD0!A~b(Sf#6(nWBK2s z+my0661N+2^dpzW@feV$_m{4Lv!kd)wsZg8T3ow+q& z9M-NcgaeOczr@I8{O~+X-5c{VxCV*^33Y=JzU||(n97%`@vRaSu0^k}u)CVYc6|>g z%#21|W@fkwzX*WJstf#2NMXfi0zf<=*xGbX3{;Tz2O2pP)Qs5rp+_OFrB#wTU29HI zPO)Uq|LL=1stv}z6kW;UTTkz;T1S7Sc%@ON5=(k0ISo$4% zvuY2v&^veXK)&4Wr=DE(8}t*vWvZbT!aako+ZFG1&Aqi)9R6XCYhg#w7jm_?gb_@R z#TDv>n1H6jhah{P2|zrlLetZ9vXQ&+d$YF5BDqXlk&0(np{M!ES+%XzaDMZ(LXk#d zCPTLO3H^08Y}X^UXMBrDqhE7Ouw1`qGaqU`mhh5u-(a+O+7i@&fHKsy*ABpYycsS7 zu6$gJL(Qiqc$G&jdT(kRHxyjF!!Qjx$@>yK{TT}%D}qOFJ-f_yhVM95EYHZ^?P}@E z{8le_Zn^R03TrxU_o>4o``7Lzq_l|!p#q28zl!{~W|I82ug-y{lw3Z%s*DODuQG#! zv#Ee}`Puju``XwPEIP=^WgmD1rUDF2k|(im^8!|Grtv&Ec|xH&;j^xothA2S`Rc{4 z46L#OJ+yZo{nN}s!{Tn33@5A+2SZ$-Uce6iTE7$&Wcf-oH7VXyR7dWQ_P5+k0R%%I zSpGmVI`;*g_)JXVk)|lF$51v?+7V-CCy-z{0Nt12tTp4j-e}J(W!RSUAzHkc|HO7a z?RQT^3s=i1d}?8(C@w?&r_S?Ni-=^n&|E}TJ%}_Ou3s>Y;b>Qv%+O$KH!~-y<8{cN zoLM335$bTY@Jz)=EtTm)E}4rMM8JAdx*^{Mkd1k73hoP1kl0D1Ei<&o-Rqw>B3Ijt z|CHQ6KTXKxQZe$0Ju!@6L#{$gRqpqnmy2^mOb+y8g~l2J7p%%{s3z5Nz9bEu1p1bF zm39o)FWkyyP{NTjSYgOZK)9I|hoZ(@^&uWO9ws330tcbB%ljOai(LYfbjIH05AlwnkNnL& zV@Kgi?0S#Et+Xl3eRuk<0#b!wDmn|;FlSA-)sH-gQQ5<=9=7^1qkIAV`KJWS z?#<1>2xZ|Q#FaU&gEQzHo}x8C@S*WxIf>tZ!yiBz?#oQNWxb1qvPyt z^Q^m>U}RzUnp9kYXTDy6iv;mMYTnA0hln9F!%=(M_dJ>tt8{jurOUddl9{+;Ut#%! z*keX#q5P(FY;&6E_mAnl`sjGE7Ar3wY3}xC!>0F(7yLm~1@uV$O(@>}2P`@-s_4ha zGJ?Rv+0qOiCXX5oPHMq*4bG#wrfx#8`y>}I8H|Ynr-%MR& z2RbY{P_-+6BDbJuR&v`GpgZJo5P}8pPdgP?1xt6pJT)HS1(`BNvxEKgw(MXJrO2*^ z#oV`(S6F9CCBw#zkIB3$Yo7uWyh|?3~4Je-DcW} zEe)zmRw4x7QK8)dBdS-=Tpo{0E_5ck8a_9zCp$7Xx<<0(uKd0TEu3%Pi)-m6~t6_ zk%7=KznZ7glw`VSrlBNy1+9Y2{-RDHVy;${<^h$F?rKf{nT@Qf!TtEZy7{YrT`z0V zeGc4Q(jD>(;lZMbIg8^6PBD>dbo`4>s8-sBu+rQC_k4q<-BY59Tg=Qmlr~e4Oo!M2 z{3e|ZCc^i}dr~5@&b@-A9;1)Ch!(-$IcZipHvitF#v*`*^^D4|g>Odw3IoBZT_(g= z2Q%4Yntw@Zx>e8xWLOn7AX-A(f4{b84oMRhn>3~tnOu@C)0zs)mT?|vQ#V0Br<<2t zD(z~~@gFb|L+;d(w(QF)?RFAbmtjWBYTb&DrocX?|0B>wOc{;DOJb#+ru~?Dg?T$1-H1Gr@S1Tp-@(192ZpyYCgaMyXY5$OX;o8EEgdeG5Yif`8zpzRE7t? zYKu3sHELJy<71j&1y?ONY!LPB9Q~TLbJ+3bQ^OZaqA##7p8e!koCdiQ^8Cy5r1gVu zyVXdCUhLEO`*Go-H}MSUW1X;`Y%xLicYtW*1>c-b+3P&CIoU#1Y9{3Q!>5k(6V zbR}S!dFBh)-yI3(T6NJh`>nw9aCsj&n|qB;T_$RXaO}+7(@bt0YARx7Ubwb96asb` z__QmlAx2y&rXWxU>_hPC9wUfOjWW@+7+DKp4w^U!&$)iuwb^*{e1AeGDBGX)jZO}S zWC1l;K>D7e#n4(O0ZjG{_oY1&%juxqminkz!b1}ga`<=#54i9f`5k)A!ypt#rPDz|ly0okm72S9Dc zR!K9GT}di>@4|a|H=$cP;6e_dnuQg!vA=$fd6T~M(G{#UktXU)JJ0hQUj|Wp=ZV)&DM_I<#R`E1y`T^U$R6g?rOAW z2ks(-o70Aq0;YH$T<4r2wzF^kYp_AdkW32*P)lL!P)c4HO6Wzyt7$1#CuCgW?7kKn z>UzdmG#Leazj$#w`ASd%F-Jl49OO))(js<=&92@IIJx^iYJGliIx0Qc+vuuFp1dm4 zpN6`z{P*5knI(KpmlYt=2w_4-Y%giNuy55FE`hrDd~z3`uS-YW)@8j9<+hPYfVv2? zhi^a1{Cw8VaOVT=guIgBi!ACsi`LOG4md67CbCyV*Llj0%FO)lQYC*_7s=6`+H-r>O{NYPYO5($pngeV~&W~M_10a z1Fbdw^sGgrKf}g8gT5q(wwQO>>`m$&s&{-G0S^)iymh0dhs!<=RjFd;qCrT*pVE_j zZR)GJuJ}9cnrq`nHBFCnX>%`T5-+HjR;Yx{V&OqHqTo!9^s5*B(Wxy6Cl(QOm67h% z=Il%mYY?KOt}>J*1C&Y1l5eVT6_QHr*89-D_4O`%Iczu3Tf92}@@ES_CvwvE9|2aVCuk0pb@*4+AD)g_+Z zZrsLHA%@Drg1B~*Omv;%b9b#_@oYpqr zHV!-FL>2;tHW-O34hYNu=G1l|VXcL?KucY;-Dyji1-c@h-}i!_$_ zr>@Eu z-@$WuqR4tcx_G-t2K@sM((~eB1kFg z_&S|v>7J;BC~e;%zN(0l<|bYSS*#_Ry8C^QxA1~Ch%uUABcL}WL-6*RMi4fxUFyc0 zbNW;7klF}ef%lEgk0OF8Cf;jcH~FRz>fcQLHkU7UQN>RD0ZUJRm~I%c0USks0G^9g zCQGpyHWMTNkE!>Lr}}@x$Im%dHp!lqs1(_Iv`8|_CZj0oki9n*GBe&J9HQ*(W1mB0 zCC6ST9D6&q!*TdNeIK9S_@_VW@sQU!&)5CD?(4el>(&>}i@uE|AIaRW?6GF&@H5|` zDW%)wh@k5GRhm$R-PE&f{gJ8>?Cj%k`d3mJqMmM_qsmkMc_HSEe(}(^i4sxvaLks_ z!%-_+PaFJda_#~X*;1#(VTVo~_lV53KkBE5HKGdN{Rc^sC*_#ndu<8?r$8LEc@v!Y z$VmHy)W5H+nmjR>kp4lFEe~#r3Y~ziYWQhh*9>JQEK48G4Gu3!rh~ul7-JtTwGzX#AXI>XzZ|U5tS|mcw)mH8-h{er;N7={m{9ocaZJJPUus*V zw;n9On$A8V-u-uvzmLA-(qcA=Ug8b^$wKq~`nWHHD86d!AKqRQGz+K)`- zFAv`6q~H$Ls4soQ2K}WuLEV01xt1dsbqlO`1XSx)rcFQ+*hfWNZ;_e%4o8%Kw&t|P z0+ZiER@jS0#z(r(G+unu_PXH!ZFgnl^M<3$oiZ*=pj@^0^0CT_t+gPI-8NM)zJw_F zQ|D)$rKq0C;Sr+kOIz0gll)eygKoT-Lt~;4(uL@6!LCW7mMf7AGRc#^^|y7C7SkHp6h%T+oh4_ZCNL8mE|amZxNphU*xE_`Ke zu5D1`WY5>G;j)}4xoHnFz{yGM$~PX)RE#7Va5iAZAwx0nBJ57KdB62l6s(M;PR*%2 zC!SXLKc9mOGV|XLwyMziSS=O%U-i@!1kP;Xc514Zq*e2q0(cixqv-@V8v+@ukGAFlj?bDS6H=6aPD8Zgq3VlK!Xk|ANpiJtA z6>*@=ymnB-uc2Cky~I%xSIlnXwvPK)8*%-p;wa*00^E%*c6tdFO=}f;3oFFZAn(=r z!TS#r{5%B!)UsEHAEKynlv@nuYoSTBO}#BjmTTAL^&ccD-F$g^()66^#iScwYm;Mx zl4#gMuTSGU!W;JFrc7i^+h@_VtMKA$AF_G==;fQ5m|WMO?oX-!fs6if2ei|WDVW$y zVqAzVUng6mD-Z<+9?gEE*@d!P9;y6STv8(a>1x}>v1|CB@Yee`tN)i9tQzd&=SSS6 zn-6;rdEh!L!xu-Jbw7SZuw^M{&?TdWXSSB^z%_uUb6{ZnOP9MM%dKrWF2On%f#^ZE_)Q&}FdlMMhF_3;mu?0%!p3H#Ot-bFzjQFW zxT@JTmac-km&izzFL^i=3dFMziRN*WO1q@bBDSq~H`S5IM}ttk*vn&^(Kt$4S5 zP39wc%6yxK^k@5o6OFdIcd7}5h{|{VVO0=}`wAIH!`f@m0umH?iSCsZv11q46ETHx z5$$Y2G#qtU#_=5&I$$t(VU<1)fQ_E*oycly|E<3rNp%;xz34~o@Xg=&=5u<>m4`Xz z>hJXD3D-HJ|5(Sr5)89JDvQ#k;Y4KmG|<47o%}L1Dn-7FCr14{M`&zfj|4+=NP2VS zDUw7<@t3{bDc;7%M;>6t8@)4O{fB_m0h?(aGJxxGyAJYhOpA<#a^ZRLd?wPb_!XtB ziCw#&v$LM(XRdS%qv_tsBZWv=!e3@-_t#`#sYnJ){f>IDKIF0OZ;04oS76_+`qXFB zKlLrD!F9!M>sz?>w|G?6eGE$aOki~D?>5_9$J9iiwd5`KX!e&=5GqPojpwWwD~@Ja@P$)0!zXD7>{cx|_WcV76$z0%PrA|8$Ik8wn`7%wiy znt`amKBH({HJGQgO{+7bGO?2Ft946~uVm{C%?<^a z*-`%W4$ok{G1{<@OJ@D7_%=xhrk9(YmkxJyor5g$1fzs&pvF?xK4Kn5j>2C9uCZc$*9T`D%ShlTJ^R&#cCCoaETr><^Nb=p|Fj$TyvSa)EzG?Wi*5 zR7WzdK!RA%t-HX7UhA=DWJABfO;=lK85Ol_hmuPXV9aIe;fZYJbb8k-F!Cwlm8xNK~s|pW&~|+DH!gxeeKZeadZ%m46p2ho!DUp#mAKI6xMW zjivUoR$HS$d+fsw&rYepiJj3Ao@I-_q9a!sN5+AL>E&o=T{N*---i4D-L&=(n!;~w zw9kQ4JCdK;r4yI!Q5Z5sO^%`CGeiO_cKse4`0-_a2Oz8o}Q8tl)cvebIMy z81?qW?sh5`T1KiIC|QBBL%ehRL~|Bzx{++*&qB8YGZD$Tw9lNf(Q*jRX}iVBktt&O zTV!Fxjqg|Hc23URV L#TQe#IbAlfp?^lZ$`);B@V9Q`I*0d1CX<1iWW#XB1%WO2aQDp2jGT(f)H3a) z^7Z&rc$nL`nqTx1%e9fA$DdgB#L66));UbQOork zkoVMqt5w3bPutDpM(MW#j}}ts1?VCi_>^~I)M0hQFU07L@@k+IOlC|~lu9M(x4b9p zoOjIbuG&$=b#PSfvkB7&Jz}WO(zYJJL#?JRcL`jll7n7taTMNM@tNBPOXcQ-QhjX zAj{&*yE`Kz+^CL93R)xUO6yFTCuFrf$m-D1<3FuU0)79|1lau9=pl4 zppEBFF!%1V(1Jf3#~ZepYj|Yd+z~4J9beef^>G?Zkj7#Vsu;oj?zN&g2d7o@(Qr3c zr)rly7nWNG!6nwlt6HqlrK)A^CJvD-6l(b1)%!qj&u!Ahka;f2Yg}xXCWDF-W~Gr^K|B z+E=s>|0XX?S$`w`L0U6Jm&>YzsLf{XV8}xeP^iDoj_%tn#2yWNS3V0OnY-;&o<6@= zUlOq%haayM3SFa$Kg+yO6u0nT=?`JIT}xu-Rb%ukkeGDvF@W>TlZq-wUF(jK8XZ%2 z8M3B$sp_ph1(NL|P}_E4^G59evi-G;3;j=_@Me!(exVG9VDpzcvOA>o9N{dJ3^i6s zI=|usEwtbbryjCrK|!x-gWrWX%1#y)|Td(aKU435tP&^M*=}jC?QaksN z^>HIEM07`T$2w->-IqA)qMe~!lToWL9ivSq6@F_3Wj?Ms`MR0RuuO&<^t5KvlanzZ zz?+^Fi=e&5$|1!MJw>v_pRHPviekt$62*@ZgOSaNh8S^Ms`W^J}2oTD69rB$tjbQc`PAtk2iO3pMDDZzFSBYM3@QZ#H?>9 zXQ=mGMIl@4*2_thO8;3I%CV;wy<3kA7H@wNI`SWD@zo{lyf*6_Rg6*J__5t!;(pjR z>y*aXCcWP)se)#^)l*O1@#fH&g%{S!P5m=|CA=%9N7q|ME~R3NK$RmSb?%vLNW zdxaZ79GF~oFyql1|HvIzmI4Ql5C_c*2n*=yF;|TC2Cv`k8xMpEwN7#lxuywz%?0F! za|(XH*$uUI*emT`T6k7GytAjnqM>c=u49V~T3T|P7K*fC7#|;}6wvYcN!gZRz%JH+hD|C@{Mli4<7@(obv1!kuB{M=wO#tqPST z+x7Q|v}sR}HQA)2f$|@X99{EAgMywO^J&+Ap2&<8{PS@7L$nRCSWxw;Ih20!oTQ$) zHQUnCve$rB@9#Qbi#RCCP4V6VKK$Op4Q!2MNZl-}*xYEx{|dx&3kyg8GgpZjbkoxg zX9_D$8^=73DI=$vmz9R_=EzXb=(ia&;_dDaPPBPrkO}?wlfzzGZsd%QK%z@mxsnT` zrEM^rrYtv`@d`gnF5o(0S!?q{P}iQ?cK zu*C6|wRxmUh<9YV6mm<$vBXwmRN)n#}Eqh9LW8_(-$UZ?-j7$Zrzycxe;g{)p3|0#TE%KaKDnTkKIghPSY0L-kMNmk!*d)lEmq1=vN_9OlQho0&>mpY~ zwq1r#SJ>t#hs`SY-rZ5;Q&^f*ZGK>tWmeoxT_po$d)@^L;+)-qcgeeF$rDHJya)X! zMWZ8$1~39Fza*lHUBsD6?pF$Yr^K+Srlw{NT+Ubf8!9U$MfSF!X=LhEl*H=hL>@z< zezZcD0`1m+V&iDmB>2|NuEj&x05Y^t$yc^X_{J=0!O3qdxJ=W_@NJ>$9_9S42w6>?1N!DKIQT$bq-uS8kdWP7f~L^ql_LtDXpPl=Z7Sw4P{YX8pbtAuL!<* zc}8>4SE5n_oB!woj~S`{_wQfNpLhM=>cNVege&hi6OCdg3P1A_)ep8kb&sQ&c4;cW~$3j`OXBP1KlPgYI}lMGb9qpUX!_RUD2rN6w04`{5&lzEvqo>gtH z{rS`McGI?#W3ovVimgfHP2WAa6T7pG--WV*yW9Rpb=x^F^V#&*H&tqe8i9Ye7XM^h zo7QJ5U}*{UUMRZ5JC*VCG6WmEAPP`4F5F3Z7sIu78%^DTVmnkI1Lnv_LNc*4|VI{KPK{yS4MI7;Yr>_n)Nrxk0^|y#bqEl;+b)YU!9Kf zskfb`Q89qM#EUjprKq+2bkSDZlb8#IFMeaJ$H)@Yv5wY%z&{sLQZ37V(a21@B!1F( zJ8t9z7>#|jlHP9-syYxEb*bv<6_*6;E|J}`$dD>3K<`S!NxQh z(dyd}HCedf-0gY~ceu^wC-YWn^R?=kp8{(4=d-zLd>5px(OAW%>V;ug0 z0?I*f?dSV@%f7HD7fThg8au6Wb2_yb-b9NZZ2%X-k7|!> z>Ol?|MKR6So2Y9$87?xOBM}cP)lxCHJ|4&>mkLdfJm~pD*xycg;>{^`b?#4!*#CW6 zRU-b>>#QN@`}he_mD~~B#oWF3pyVM6d6|_fPUnuRjyQfhT?|+aL!FQG^^l(Oa6Q@J zm{YV1soWt>p9LbHO<=Ly=ra<&1EYGT#G4E1(vx7#=JnVQ;_ZZm6ssSt*RiNj2YTG! zB10L9!gdbs-dF**l0xIV?FH96E#ldgx!tAJipd5s?EgsB{Y&hZigO0k;Mi&J_B3bv z{SVWnImz!tKS}}%`{K24)2IL}q*$&gbMo!&J<}q_zomQ2$L9y?bkXT_QBhzaF6d;= zU5@s>YXTNkI6SFRI?+~;4%<%PE?gjQfIao|1&6uio-q02a8Mj?NK)*}&Dkg+D$4Q} z!$pIWjKpal0nBK3?VI&4$!V~y0i08riDW4bN9jHe%<%ow>EISkq2SMNZL)%^FTH<` z5Q%r-7If@0^RXZ6Xllws?S5b4;wW_Wm^Te(42EYt-+HhWDWiP)ZcHD}G%u^hW_v+s zG%NphNo;KFYwWGaEYO>L;SkT0{vzK#_GpX5)!YzT8v2d$K74>Ob$E3Ue3%Z)C*qk2 zO$e0$E^tJ5$zNj2{0UnxD0hn*6R3AgNDpMCpnWt}5?=oPO0UuaY@+&1*Tg&K0!GRM zLhO8|RS+2&j$7_z1f#@M=*r_qRJ*1-66 zii?-pke(rxM=%{Ft#2QAuj-08x3fvjQ29e@O?6q`Q#|i_c-OvOZ=m=f)}N3b+=z-l zl$o*VU3R!l2+)pz{5!|odAbOhJ+7cta8^$?z0hAJ>bh?=O)EZZ)W0$V^2#fx%Y4A@ z2&XE>;nP?9(pwLX{#kn4=~|CiZ7Xl-7tyPn*E*#xG9I&q4`PK1J``?SAKC10@~oU< zv3Xi40bt?F%qY_1QONV-RMHeUf8gT8(j-CZ2;W7lPq(&`a#8j56I)u#sUS0Ldvmhk z$ft44R-bfZ$KcD<-U%z0&Mp$*Rz zyOwty6x2M;XG5&kO#zJ5XFT$2d}mdKtFXN7IWi~=m$+A40qePV3Mzy3?vzpU(7hXh zzkm^OXl{J~Y#otPRpkfY7`JbIeQDQ@BWZjiN8j!C-U_cIp-iFm#*H8wjPf9HLz>gE z=Ydm2i}va^r@`D@zb982eo>)4b;Eu|U*?C~^Odm8#oA+<>c7Kli}l?v9FwM#H$_ZJ zujl2xcQn zN|>8U@&sh%-=&APFQeGp4Xk9p3^j0xIGqfyX75n(5svrQ90<|O$lAI_jtZ%v`b=#$VARzIeNX6>i*da@gnwy%6{dLk(3RNG{gP+r`d; zXzkx_M%LB~cDqI)5BQ)KJemUbMZe^73OgYX+qcnqD*Xu6piH+`^ZD?r1-w*c4?L1r zg&b|yp;@Jd+VhEIW_>54lxB0csdHa z80j`VuIrTxRAcdlbYAN$oVv~*y`2s^i?PwrDwQb2mO&!z;=c!$ca2lHjlB>E1g36~ zalu^aA=(f#bHgv9)GUN%`3m~{usZrpv`5j~?-MsS2R~G+1!AO%!|95Z?6hlYbT_&K zPqsVgeSG}=-yZ4p6&wbEv^$+<`go@xQjG1Tz^n$EcN^d zeg_rB4JE+!c+Dk|tGJo9*ezF!XPV2s8O`QDNgI1xea&)<;n)M*&&OA7{H=y5b9Z)E zBZUxMIxQ~AJ_+0YH+VKMN^!jy zW4Ce(ev5PzlOQoN))3TZ=jbERo{D6R7-RbaI^xP016`G@k|r?|?2Q{~GP z|LP6h(Fh~k>9enN&;fRT(dJfN*!BJQwCKgbJl({OZbj`)Ln|pw6BRS)0$5^WGL;*v>`y${ z$X!}>8~A`txTmbCM>1@BC93_)EWGVHu2Q84Q%MY*T1piAebv!Moe_lPYJX{W+Miir zP#PdzW}J1FFh^9LQlmM8W;EOii?<8XvPv_N>@i;E4*EVX{NQ$J4kO)~{!djKsSjZX z!}xsC%f(E=_KZ2>i(Q(drzHXLmc!b)hd#c0qMxie!}X%KrREJA}@xz-xVIF zgjY|3c((ReHI8p2+9l)hUrG$}S%d^|*3RvQ&s{D1e&gP!*(B1_!Bx$j3sgnyL-=f> zW9mF7!JA&~xj{j`8gW3Pm{CXxv}B^+x?R?lWsXO-Y>Nsr(-OxRuf^fo6Uebh%+V=e@0P6PKXOc9B^_UU(mDS*D!@atzue&gQ{! zidJ|O*A|)GwnGbF$;5KF5I0RM>^T67cZ1A@aNJ62y(`Qcfqk$$dU}LW_nM|ReT|`g z!*{U>Y-&W%v|-qyru;lTWdazxs=FyN{?-)owf~ta*+JD)n*3!OV#BigVkF`4zADX0 zA;a||64%@);3zbq;)W4~AGm1w#ND5xnPoXs1@&yYgy`6I@i7k*QBk``H@X-c%40>JtI;%QH0WPV4-(x2o%Imu@r zT8W;?_=%oX1<_W_tjXNxO+tO-dsz7KyTqmBluJo+yus?)y_$TJ)6U)53ZYU1Z zDno}>4_1GK`eGeUz=kXJkjFOHTaUqQgd*!67ekr7LgkGW*HHVbux43Be{2UkL%Eig zmh?4hR{7C4y|IPot@4R#ZgI}+;5aO8*9iY6Pm-#2TS@M&= zGKUWx@BL8H&Ro%R9cR&SH}QCE^F>H&Rje=43LTi?$$GZA5itGBfSHYG{yf)(?u*zv zo054tbQx4^4#O?h^*&`=LPGBLIhj%lOBI_#$Xq`Kw;Z)h^!7hHt}%c%nx6jd!qE=N zj1UxodmFsur<@_m2%%#HK$sk zTZGB5tVNO2*8L5I6$_L0rJ-!G_C{{^L7$+?@LU;D^Qow&LrTW4gC;Ad& zRUC-3V9>5e(rh!t49Fk zvZPYok@6PPosBWN!$vM-jE!9cGYz-VvQYEjY}OCnucO6_+HRJreHRKnnL6BSYFas_ zVM;7lA#)Ur>+_7Z3qA#8(>tn`d9LT*Mz^=O!8|~G@RY-Vx!N;Gl>g6`xiouokpdB~ zSUC3xW7Ri2u}+ABmfXi~DY~XJ8)-?AL~(S`p$sEv!3u*7br}ImviLOXXJmYLEBNdi zy_xXEJE08lYnCE{{Ct@@w^rk#eyB0-Jk`rPc&Y8|Q$uPG!cJ(G-*A6hbf+H&6ifZX zG1-bHqvaActp)up(ML6zfO#^#Y`N9a*mZLLBU8&xXj26aDfm|I?B-`^@JrljujJRu zP_w)Aq~Kky*f^DEnOYZkD0OqP`pgVr*$GTB?s`Uuu%c{z@Tt?&5Eo!v3DHs-STa8Z zSGW@A6_>fDrUz*ZrBicctj^+TLw82m)NpgK)MS3YPQ(q!0>1NYfTYatiB8@!5`o~o zb4()bzG;}a=1wUGog^GgQE?>XIu+6R>+?rjv>zt73gHw}R(+b817CkVdbL?@#&U%) zhAL|R8DL^w^9^t)r1hCo@Sx+Naiu#T?(1B`r?&o9w-g@-_<)%At{#%kthaw({pdvo z8xNj%)b-lh@fCJr+dL69Out`aj2N5V&8QL*r;_=U4w8CfiNqzN@^x2y5w2A_L^IB5 z?K{3o0Nc>5eN3+a_J@k zYGapNbaGJt&^Ga3wigzc%(myr(h*~|uIsS_J|)m(WDqwV7&=xNnY~<{NC9g*v{85S z+9^-2?b(N1R@kBbGYU_)BRP1A)7Cdc#t&qd-)?Hyx&0i9s4~2)tm$Q8Wu==@)~@C_ z`4B%W%=D*1IEFrW?V=J#nG3snZ6cO)4Q9Sb%wXyC37ITeNB$YUV7wWo9G4RMbD*UYr;+s^Tovd(Sq5Su{Z@u?<8 z;kL(*hK~vuxb+e*PmGbYhhr>`ZD{bKy2lOQ)I_!LoW_Ctke9r8N>7p0SIQP*Dn&Wr z$aIz7c_iPiM*?(Z+1cA$P9@-2ifo!S6+{$a^P3ycXhZ=1tCC2qaAb{vSjjHiNcC*xobq?a#~4b?hL#$l_)U+rP^(#SyOP+=${%3@a0!f zehqym1^AvIbbpj3##;&ET_(JDA8h+MCZJTUMEbi$Y?Rh${T}d>T``n0yKgRLNM*F> z2MXCwXnjD%4@ZGYNPeUeHQ%mU4(00-2)P38{?3i^>-O$%s+OFz=!!Kn95q{41>peO z8uiIMtu%TE%Q==kNcfpAD#%{_%00nON-kN?p2_qJbmy=)6kM_{uC5j3vZSC83s>X> zHL*}NqETiVV6s)o5vl)4-p0K*Q+7Mi$rewKU5WAlg)wIQlYL_87h%R2RUqAc$ z;O$E)oThQCxXfmO{bzNSD+jgpuQSNF(*Dpqi0@iG?Y^FIzOM@!Z-{^FTUt`P^^N7G z`{R~zm@S1#2M4!|PgC?z!udSY_%mBl*$_PteGU{e2f2oVndARJ8pUyxkv5 zLq_pmoPUFK|l#WkNW@(qby9b`ke5TGOn%cLz8(AM_xm}6k*9&JH z{e%1(T^Ng3iQw(a4)k)oJ?W$i3g*onV0D!J#B79Bqg2WXZ05k^`TWI$GTopbVUHEG z^oc&}Yj9gjf{j5F#xfVS zrW9=qmSwZI2(>=--!}fW`44mM-w!~^)WRL0h^7~78KI46^mH?Qr}x4SYEt`mg3-mk z(;?(dp5;llkru1B{vl$5S0;WHST)VztclkKwNtaJ4=1q5{}zOcsiQhN@;qbCSumLo z@4TLU!e8Ega(XGR*rm>{6g4cg<_;I#0iq@Qg%YhmRpIeyu&)67RkPOa2)96*gt%@0@voTD z-f;Bx{^;V?($+oRJ)v7^7q&?XRFKQ4JJYb{rd+!Gwv`PE2e_8NNhkBea3SN7oaRNF z;u7xS79MEjM!%KM79Ybi7PH`SA=F9Z?whE~Eh*?hbEI_-ICc&tFadTYX_ptV%}+x@ zA>Z$k_tC{yC=>$aZTGz%8GCGj8d}wycOf&=-Z|P%@OGhjUqcF=Yq6FenKDhfkBSB5 zgXY0diDT-q@KOpMdQq;SBRakvPJz@VVx|h5HDw7vtH1IYnVC9iGmHW-rYcSDlIJ}( z-?hW8yF1EEDs-i939f6|UUwMJeD%VWO<-`W@_U zii&ynU0R{=SYvI|uV{Z6k=za#0)1T)8$qr;k@RfRFv~J~ zV$%by8^%>m3Wlx3{J#HT&AtBLT{8a%_#oneW6te_(u>(Qg}**Y(0N2xInxUkWzVs(Ks+PX{tHZZ?3o?sH?h_@*M>7n1 zW!tbX&jZwvJp40;8{r2BnT4krsq?gNJ&c0hmYDoJdv*qiNqhf>aYtyry8&y~VGzsD zkI=3C@>O3(BrcUDfXcx<>~&nq2>*HdHDGm}$HuHuy3d8kQ7Y#!`jmK}cN(qv}an3&JMaB`{Fs&@JZv zH6~UTO{4ABu0|bZhsTa&(bivMtne=@sNfeX0^=Y2ynF!it1^7uwNcsCrS8w7sQQk< zVCL18b$PIV>cahJ7_Ou5@Ozol`JJw}J-;m#M%}DDQEEi8$!ArwU=H#F0|J;UtQ~F1 z&rC2h2Z_=cH4V>}?i^4{`3%KS(edtf*nN{$WsA-wifA5Hm@~1W|)HV*5CJu2i3e@)1 zy->!gD^T8k9N?OO)8t%Xq<#y?+{2QO&&mD__uq&Kn7;XIliu!e{0_j7F224BA+Y2C zwQ5S(a%|A*^3V&38le%hE-cOO;inpZ4tN&)8@4vj)g|z{oD&czjV#7_PMaqk4xnxU zIxx_9ynr=$KM8qwh#`$P;8YB5gsN@LvJ{%e%{S;T}{#~t# zXde)qM43c^S~2i#4DmkfJsEx5WOVTLYJFCnbd5BlPqVDNCLxD_JW{)>k z@wLq`5D~dHI_L?#-v_ZCVR0ce3j8wb(X%YmY1s$e?Z>I|6f^OX6B|6S)bF*UN_ZyW z6@A~=E$Oi*cxfw@^rn5KAS>(A-Acywzraf5Kt|zTf|&)vp>B!~A1|*qH~sRt+J5ex z&E4(R0P{mdKzk)l{Pi`_Gy}$gs~5)|&$`&~;1xn<59jQMjVUVB`ZAQ3 z_n%X}hJ`?_-_0#s8hKretT^SGQdUryu$L@fz1{duyQF&(i~Z_YPq=kkp-iRGz_|zx zq~5@1{gzgR$t93A*tUby(ey-rQ(#TEqXNfdmzHjZblv3_Ig=PbIyDw)!j{3EHk~m< zJ~ekkX`R>1(T7TLB0#XiMk%DlmZNNZFa*T^aesK4q2=gxw?&9!6jr*UYCil6T0!xg z>ZHcLSxK3J*Jb|_)B*t+cA5`fSaDws17;EJ(L}mm9_IXRQbv9 z*9LBy0y=b z0f*(rQ1ZTmtY~vd_Ql_l1^%%M;+&5AAFm0-y$QnUWP?}G_b)()<#Ly5-b?N;hB9+d zC#}{VdgC~36nf|)9;d;AY#a`TmfB*r>C{}`PmH1%5OdXV*2=5P>2$qhVR=j63NZ?K z^uOdY7bmjn3?E|)Eje(Mbo4rF1h!7`G^wL%jFX$vnRt$x8rkPfOzB@}#LA<}!jGFo zv)j+Y8{MneDc<#cTf6mWVpS2oLNsWh4J4{gOCX+1GJ4oz4#RKa94BCD|Qlib&VBjd4#0xcMKe<5>OPIF{t|%pH*@k!LP3ni70*Vsp z?d;oZ)+rIc3slR@JLO0K9JY^km(KM=7ogdn+b3Y1$c~%NmI|g`EI(BA)yx&|sS2kg zU#^%`nL2+7gKvz(rF}eHJQAkG+C8j>D|NknXePEy8fCM`FCD5D#R@KB}=pc*5E{z3O;K{N*7E?UKqcwcl)XbS(Ul6|YLs(|9s ztc9o0=bHJJde1ZM%n3Gt2$n2Aeefy?@-fE)MxR zh52YcO7mnSrCiM+!KSgt@n%qx>@g95#8hKBJ`o%u%M|nCSlg{uUu^pDI8yGLw>XwljzjcvnxV_rB zEyE!*$lYB^CFoRXAxY=*u0NUgA1*}Xk%BS)mnz{9P*x3|bz(Ne{|Tw(LM-R19D3l% z>6r~ghWwYbAOy`d&Gk?X%c0x+#P62t#dZ#sVz1{7h9c>sLK0~)DY+&^IsubL+Jo8m zW`QX)r#_vf-{zPni4sH6%i)DzGghxnVI79Jy*}w(#?WS6x=^5h0B4}|E#@St%8uhK z=unt%E z0x0MZX3(emhs8hkW4f6tne)%#2A)TdocRd(bF_O7L4XQv(W&D}%FJY{m8%O8JI}iX zDw|Mze^%f0r7>o@L%~l`8U3#__~ZZia@kAelP5_ni{NpJ!!gOSZl10=J0`&_CL<#- z_wNtxiCL3B!Nv)x89QIHDv)uwSEW%KvZ#HDmSLG-l*e_DfA{qpJ-wd_7j8plLrKWN z%XnDpxpV7Wg7BKT3rYG^Uv0+7@yxl-3hSyd>~*`CzJ<=%Z7Rb#B0-#{ykdGDqYd{lUTpP z=H~&wGGoVub}4Lw?0_ay(6Z+ z5s6UNk>)+5m`HqFVDeu!%vzTqYd9*RI%-y*W75 zFZWl~_x@#O^T#Q`-x{;KyE`8GAO7ACFE0ePEse2GtTY~x$)zT&l>ew$*mdPH(Fj#o zI(wuokO-By#{edDWP*KfM|4#vJIrcIY=OdTvvJ@OEgl;eXXiihz$W1HYSr;?ZMpcP zRh8qH!+BzJHUG1pM+5CB29KRPeRYE6FZ3mc7!rb~0oWP0`!D;Q*bbl$UEJIx$wU}J z53W({k@vzxdxtqNT_NI`SRTlK##DLj;WM`(bDTCx8Sj|Af3iOZ?wY>`Vm1y&0W{Hm zQqJ^&v}VpfR&5>0WVsCX@qrt>q(gX4?=uk|u9MS(_b^|Rl&8*i^vm63p`segF`>3# zEOHLupxvXX8?}yl&*HOsHCx(^j9L5M>RI`Y9EtSNM{L#5M;oCgKxwE=E$hhIj(RJ% z`<8Jg^ihCNph?K#4lLZf@5RsW_eG3viX0@j=Bd^~IL|=iuWR~LlO~CBCpGNtcfewp zju+2TTX{P2Jlaq)SR#A{JR&u@y_HP+YxB=W`Z6uEhnw74s>tn&$23DE9cMWIo~3An zwZ-Bq1Q*Y-Pux>%6lhAK<gdIHwj|f57z%1qovY0g6 z<}=%8e;9}cf#cKD{6IP-Zhos1&s?XgBLYjz$qA^9OO@B=C_*RKjJ`O+lZSGD&>XQo z2?8CylEqWp=by{et7IfTBcyAL0V*OXh;%3*IgsuV z5`suNQbM{%4@Q00p7;6Q_kWMWo89+yUg!C%GYzlu$7(w%{l#tYt3P^9ffS@$mgT9D zhKO!)CjpaKy65pG=La^!G%#!||x5>HP1D9^BDCEkvbhy35aEun|>L z5mjWjQdwUwOKfrSs{@9oz%Bk1`8v@2WtE@hHBw|N*=)hVMsD>e6x*^MLtbLbqJ!2p zv$8fjmfQgvW7#-0__HOT=ruPtj{qzS)pC_V#%D{oF6r1y>utX{_vFnG%BCC4B zWkH)A{K3!ZL>4xyqong?TLD%Jt9N~dGIQco1&r?kyaQ)68he1HYXQX?~G;4 zA5>x)fE=6W89a}Jf#Gojr^2=RtrUCkyJx0C! zIs?h<>J`F8OBWJPE&F+?LPNQ@xH+b-~;e_w(7nmy<1>fbH z%%*gwtJp%Krmd&Z`2op$iZ(Zo`XJAj1tg8vsIpAmBbG45N{@mfY{AFp#1XOtE4;aR zHjuV=b0!eIzB1heWXW?E4uR;5S7{dKH%K{sVQa$)&!#kwOWs1?t)6y>qa4=qPESkI zG;v%byeH+ug02463YDcq3^(|RAam;Ccjg15K`wO&V|+8&I8*6|7U?b^< z4Hs;DDei~ikS~2LhjmZ(4OC6oUy5|1j0^k>dI#h^Kt8}u^owRX{3zL5L^V74SBlHQ~vJ6 z3z2>PWA#kqqPa<&0!m)4md@_EWTjiH0GN=5hT~562QA^MDe=*V%u4cpH4?Pzj`2O-!c0Qe`v&d<1Y>a{&0tVq&+vVYL@{)Ot1} zh`Rr}3(@tVsnF%^f#yB*hWB#*PQC4&doOHL%>{g%71F1rv*gppt71Hen;qp={@%fN zBjVl@h0(eCka2m}ik&-b6sez$n4YC&WN4d(fW^Z|RmEbKU`efQ1&_VJ`;QF$h1=@A zaf{PW9uR1!7l1LL_bsN)1

qlhdr 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±j}!zRc;^^#YC{@K@d9o!4(D=IVZlo$*Fp~U!& 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

1@nq$Zbr#cRDNG3htHec8#_rei(BKC$CO?X% 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

VIbWpx1o#%x+GqM&js?4~(5`M^?e0(hG`J^S z&)l)YtOzRa1DnWT(-F3%5w`IelCXA)dHfV}ao)c`J0;WMF=!yvEz`ljs#_2TjADy& z{sY8x#P|-e1UC{4RuD+wa}4qh)<@8QMj52N@)Ctix;`5B3+ilOVEAH5!g>h7G#DXV zJ2?bZFn4#SsGmQJ{hG9=;S%P~vcdz7dwFS}9BRNM7|fr83=av=^Fz?y5qBT3=}_$9 z!prFXs%Vzsw19#URmyWQMa;p)&ct6Q_W#XSP#O0B3%f=#rq3~N!QcdgoC}*_^dktg zi+vcOJ$;&cH%KW1=D5zg-?-qPMhx8*%)Oh+lRYV&=f~BsCM(fP#DN?f7@T0QLiFK< zUeDBeb5z@qb;F8=u!D{l3x^Ke_@?wlH8p0?1f>Q{b@yYyN&E2dNM3bS6?TTT+|9;2 z&{zN$2VM=?Bk~D}pKpIWL<)hz2~Q2l2I;QHiV@v^fF|%Y2$#@RPkt`cATSm9T6M-? zLrpvB;Hvqsj1Z3oo=vLu?2Gnbj-M_i>!e6G6*jQ*o4+g)5y&;HJMMg*`_yv{oC(Z> zh;*1pqahu}8v-E3z4JY{igPQ!7#n^1&8gjG{Pv4dQc~D2|H*nu{{_asW9b0yBZVcL zDvZ4V$3n1i@x$6kiL$`I&*t7WA7vZ~^HFDn zZXz>D_r^oiBv1}HM+(1bO<>8!v7tTdlsd49((Eu!rVP>-!05fS7i2iCZ%^R+rDdQ! zcF@a$Wus-MJ%TNeB%*wTLZ1j)Bx+NtOO`d^F{s5as7;|3SG@6Xa2@~e!bo=}5y#1n zVmcTAT7alPqc!E7fKnY%Rbtt1-@cu2i1pR-1|o;N>vjBsr2FD}7@v=2Ppw(K;nU4`&c_038``c zOmT=I9{vFzGviy{`=d|7YIt$ILZ-hQ!_X9h7=~0>`B%l|ofZgw*bYSEf}LqS;PRrJ zS2cL1((;!5$uKV~xtOHaiCJ&oVrocz%{e7yWgREBunv}3zAt9R(v~bXXc9xG!QowD z^4nnr2R``n&f_!h$eUfbaFKCbrs8*<@A<~2UDTgH3&*x>N;Njr38!`sf+=x`aZR!kNAE>-Ja!TUj0k6U4fkWmk za#R!v(FYYy+eEq(WVVmA(8dMe2>lbdp@!NEL-B*5RRz}#DSqviFT}wL;&7k}INneo zH%3^YCkV+E&O1f96KeOg_SrN5&kX1hL{bc71TQqMjlRvwPeT{1u$_}#_|FaOEz0O& z02T|55#${8{(lKoB$U*|iV=kXTn%TO=;$HBz@>%21Fr0n>DU9)v+O?h5(u3WFRSPo z5(B}4F3EdLUhur9=2@=Nb`$Ek!lBNMYZ#KAZ>6fhUcCFe3W`d^1wyyLrwFkeW?ICG zPfE)1joY>jcM_h7n;Wy?K1D!7<;k!as_a)7Adcb-DFiSlW>c5GEx6}%?e4AI z3$i%#aA@1YOEi_ zJpo8{M+Xhj!r=hm`dkYgZkoZx9e?1oh+Ji~_K-A0*RO0&mqm>#Oev3;Pq|J>j-Upp zIn@ERc@;N`aNH0@`0GSzrLL}i;~-H#Ifw%k##&5g9PHAxhTM zY301OWr5e$ztzh(#n^}5US2X36L$l&8)#5+c#lQ5uK#g`@}8?i|8)~a253SNgvWeX z6i!{3iBe~YF2N5UKkAgL3DsVF&weAwqf^w)KQ;+Gie)?elyRE%r`%z`2*47zCu$Co zuwtFTNst2wgaemLWVMj5YdqU*zQ-A}Ed|J`#G{^fDWwQn8S^RLph02++#Qes(iSZG ztUWA(0dGRC?eQg&WhX<|Dd(2_sx>aN3m(LE9aIQpqNKo;il)OsE;cK|mO=7v&F&>h zkLf44=Sj!V^1uk^5^PGXeL>}2CbMQT{3{h@VQK1*gcmpYIlL1!;l(vfsHGp2V)FyBrqlvu$G}t6iif<>vDR|CSv2l3$#5U~(=0&u$ChuQ|ezyO~KYsn1 zb-7gofGMgP!bj2gAXbJCQhkQ8uQyRc8tGd3V>EwWfg74XQak>>NnfOu*@k0~b|hGH zGj4wh7cn;_8xo>U-;?Sxwj0bf0jPz98h1Uoq=~Q2&9s@VHU?&_@LPcZJpc?3kp)2& z3txaCi5UD&lnlgMbjJ<{;js-|KKfItAM^ETDZ6fT^Y^qp^8u6)sO$j6@A(+Ij3d_Cw^=&7F2 z=HT}d!wk_#lbNQ*w0#1}hXfpc$#X_)cvoA7kDtHK_=aPl`fl?9#g^99BF-)l zg`!r%vXU-u5q8w(;7m!qi|qr523zk6F!$RYupVLK3sDL_44(Yj3fB3lV#v4S&inRL@R8&+~L zV>k->=-SQ=KxK!6hNlR|9Gk6XORQsFT(Hb6Lv&nl(&~wf*Ws7g+9nJ6x~#A(i28E; z6Im@_Bq-Hz!+;_LX}CP;CK#b6_mCd6c&dB!T}@4XO}!kuBYWA-C%^S?(Hc)vt3uU7 zyk3KU;XFXVP#7NJolt|3hQ1Tx`=`#}?gK_aqVDpLr@e5E*}6e)CNG&K_rIq};wEx~ z-Z4~&7*d3sY-EqOrR9xZ2vk%RHt<;$4jFg~o!-`Ax2rch2!GQ?5D^70HGi$1)0jb5Fq3vSsGQub1Iv0J8f4?hK@kQZB!gW4?O7FGR+Qjapzot&8`hR& zoGD*t5lK&%>%eo!SMCu#^AWK~Z6r`XuAr5tw^1ByY2?YI*h;I{H7mbMR=CL9#eM!n z(Ez~z(m|?jWl$`7)V{Xv_b4pvy4*d~mjO=&B8>?Gi+ecvbcDVdQzSqhvrc{YQ>1nrhr!4a@4K_uuxKB1|EHTZ?Si^JAL~~XNBAb`qKlpRA03#4qKFFh z5o=3%c{*hExKSf1`gHa#m5;&!6aW~7vLx8bT*m2>%xa4K?8F0<3T#dbL(~kA_D;Cj=>lFvC11|E%J^siul3iHPZQ7rFv9Rz*Ou3%$|* z#r`biw95_pCS2e2rw6Q4f=ha z^NOCF!H}E)P(b0>FXp(qkY(aDQuUr#Wx&{UfA&#ImIDJ46@;vQ=QK2iN^(r~0y~w9 z#pO;S=>L;Z>eD@dzY-}7|3fr5IMCc-os8yv=bhOaEapiVB8dC4|9+T`BQ^*?KU7fX zUbg-cBuy9jrca~f0s51+Ui8XBoAAnro(7RfS@{c&8AN3NHQ&FYy(J-!%NDfFM_uTT z<%FR%1BjrhJV^V?-!c<%PNf12jBmaJL7k96xBuf;g6>H$hqfK z%@;u`&^ZYt2nyl=&Alol?2=|LXu5KpA2eME8t`qj3OCH%{wi;8bqd(P;!bgruC9A57XdNhv zKuHU7s$_VI)Feid-q?s}n1v=JC(r72i~;-ZD_-#f;}|G~%XpR$5xe&Bnj$U03*R*L zbz^6>^E!Xp2biA#JF>AE65Sw$#xO(t7Ca9SPI;|*Y-f&L)csWjX;O=q3G9w?F}Cun ztP{R2%94I5bIypzttT~<4`W(07&2trh?6*GxNb?;7S#C==_o6&LfnNvTx2Ch*jLy` z)}PY66-WYVjAiyMx#R$uf%U1U`by&3-_r5b_(fn42)vGC140@R9j6z0$(zh}m@rt% z!JaE4WJTE#`^bzb1q4j)hkf^|AtVEl1^_Wt2eAj>FFcw0o|ucqvy@GIwarpt@5Ki$ zGk!6mQvOnvJ=YL92)P7r7LPeTqr2n&F3ffvdtd|L?_ zE@@USLo!ZEO9V}FBsz0)(_oIvn_9GCL+lP6&sSN6&m%W>C)xPYn~%-x&i}2QOvB71 zfmt&fR^d)W(1&x2L8hb6+6#|Ph#%Oz4uM|T{2z3UL!*T;5YTVp!7Yz~s}Wp$SzstK zvX`9)o@#ACmZ13}-62S@L8t&oU3J@ASxZv(Q#P}|yndC!<`R_ti0SBEEQ!g&urNNp ze>3N2OG>x7{%@Kd63j07YJ5EqMs z4UCF`nmgfdW5*(~z<&vz5%F4_&3NXQ=oX-MP)WhKsc){yHVb?sjQ8N>2@x>x1&G~x zdvAHTs^=W~;a^f|HqO(n8wqinwF0_FC^IflHpzWat*dxwl+F@abO`$yz6gk+u=m6N zk#b@Mstz#GrZ1k>7sI*^PUfQ3vun`!bkICJjv=V{_MlNtc6JddkN&gkfm1CnYa-PR z0Q?<_MA8tCo_a_^|Cm~gvn8#@R32>a-C1rCN$Rh}Fct;0+~H?$k$tm@n0o@bwbEym z{oJ1CQPG%;yV+Q3>O@9EK1~cHVlB*@aSlN9;v%4)34jjRgm}uhM3B-zUd|DCn?rnj z)^tdH^~2&`*=W9>|K9vh;6yg?jp_T z^YVz@Yf4{=L_I*Mq&Pdq)qaa}HtDY}9@gnJ1kA*Gn;IRCq4|qDU1x2C0_Y=j!Qoaf zkT?a#W)ChBU8AiHe%?wUOmbSl)zu0n%po`6a zpmmcq^|Dp1R818~7OQkt<}fNah3r7fkM30sZagr?trFU~T3W`Pxf6ftjHR7!nIq8S zKEb=rZKhvQoIGjy;?6d%P~YY)kUW__dzo(d(x1_%xR>b>^aRRIVV{4CZl5bX2YTU6 ze*Y;}QQMbf9wK`HTn=&k2uTYQaEn`ri_nZDL@|nJeM+2CFJa?`^bj)egs8FUirFsMe& zp37=ZIj%6My>-ZlmxfZd%PFM|$L#R8@bbqh>V^(OvgratuJu>g!c7=42aI83 z1+P^`dGgt*D1<(818-&aD?v8%CgYLFUussJmbB&5)utN`*Vpnv)QreT3`icCtbPTx zjTA(_grAt0Z~$sUnidfaz_=rKCYS^+S>XSK35A+GO6S86Us=(fadqvEb*fzO+IX}9 zPGAkR+Tv=Vk3>8anav_|mRj6>jSFLE);7>(-3Nq>T)dzmdR$YhRsw2elas|}VYoSD zc_QXFw_aLt(@V4>AoTf>t+Q<*=f=f8?>i@x%h?Sf^-WHFq?O7sNFY%1BVgllM*E-YcFbJ;@)W_>X)G_nz*rFgsDvYBm)`v z&Ry}Q`40l8YwsnkWK;0YyLiihc!P4Yo)Dx_mT*O*8?rtm;0Vlkwym4=iyv3=XT8_N zHQ95mbK-^PLh!=8ho1;WWU?T{l4gSmmx~7>Q-7iY#EY0eq3Z*(w`sSm_wYBp2zs`fCea@2B8&L?c2&L&8QT@2)J~$@uR>Tj2g(2~U;Ko)>eJN?_v(42unWvFf@~NTeFY(ZW$^vQRGbpj4 zGb*2S+_XKvH>aGFx1vHdqj@;PTqoi(Q9L>l6N=hC%0XH*^dvY2!9eGd%0KPUn6a}7 zvYAIm*tbIWqE{hUS#&N!-*oQnem$-rF(iMu2vtlE!M+M|y4)w7H=FgePG?Ij>!jDP zUUS+KMi+Tz!oH5S5mg(`b?OOHApxfWaE-hW87=k~)Dcu^47ahMg}BjI=a+&fABs3* z&@Pb8qszVC7uRAiVJ%mCsk4A6T|$FQQ=Yp_lLckpy>=W|w4VV4fxP9b=7v+X|6&;5 za%LM0u?-3@Jf7U~nVtJ0L|VEfwLh}w{6xpjs@nj4D7^1qD&+@VfXuwk>-U(WsQ-tb zrc2x`4BHYTGDEO4a zm5*O*%K_2IgqXjmK7SlIP3;7Sd+F;Yq!B>#8WP-I$XQe!whd!cqRA=Sa_Y`LR`rNR zGgdS^vIv;ZhfTeMdV}jet^%w!u5XZ2I4Dq^AeTn1W}Vgi<_Q_GL0Cqr6ymNRHIIDNhU@M3qHpm5ae`2?OW8=-4aXcSeDeW=e|NWU6SuV}V6w-Swpq<1)S7DooB4S5Dm9!gdey}0vGg6vx_ zlf&|B9}8>C&kcp``d0!mt_MEaC@ipn;`GQwE%c)R|H4#dqOXUzE&Zo$?4A7hh03~V zSIf^^Z>R^TmbO(tO>DY+EN2zG-goUHX%(gd;${Qq*y^|cP65h9vZK47w|C4Mw*FS_ zQx>c|n9kpOvh$MkGCu#mRi9L}&#{z2;?%QRt_q?m^wyE?B?Kr$TVj-kYqB-U@}4R` z8zKQBFR<$!QUN7J?qLmF3v|rSp<_gyXn`(fRMuE4B*e%$amgdsAwnfgZ2}ez!P7UJ zi8Qw7GP6FwP0&(<3>VEk(CB$ zfzqbUd942HzbsUYpYhaKA&MOWGN5e}$O^#LC^b-Fz;civ*XZ|0A%)My@X~f=M;Kt% z+GHw6*l8|>d&xMBM?oBd#|F_0ib|y4RJOKNQ5IY(m~T)8;)a(gr6a}p()c^XGzEkE zuww2^$lihEpn(*&b5JnU&}14(TFt%9J*(gf57xB|!`TerEK zSuWHAVf2Bt#Ni!SYv0_H(I*-x>ROMPX?BR@3Y3^uQr%fm2oW+FY$5@nsqz(2G{~5M zg#z{=-Mr}a$5P^6z0Sdz74d1Dxn>)EI%}WPQ%j)2B@QC=yg)Ub>zm#oOjsKqQmn4bVNLx1oAG*g3mL+c%IIs6c! zDS-a~Bsa=>NdoIv8Y_uuhl^DzWd(XyD*4!}V9TIbLF)U4iEt1Ag-8n%M<7BorC<>y z0RVJdk%)*oPP=I0M(J&j>fm}(5AD7GX`I!^zS%jP(^!4_EOB(CpdY{f6(yj&tLEN6 z=aO1&j|&r9OhV2>Rw94&b(7?@n@73sH?W7`%u8&u&^FlqoJG<{_L-f}>Xy*%cK;CJ z1FNUHUiYdWOWTA=?RcbAf}|6q6^P5X`O0y6#6XWa{ROa&Sqn|lX*9-?eqdt922>X_ zc5s`bfB^N&C%l8|T>*F*yfGVPYDLY>R+q4O9z#ace;B!fG|=IRG4|;FFh`#tq5t4F z2s9V+1wJlCbR&{x_hlX_{Kv$<;#2+EpNDckPv(9eG9N~xNe9PMXnuA(j}X6*H$0Ca zjf5!O>h(FOP!KDT9w6WJt+5_#E*(5sJjixHQ6xN?>LEwGQ?FjFTR=H*Q}izg!j#W; zd4gJ-Ul>wVB=9ym`EE4$Y7)kfD?xS5_M+ECX!Um_S=Q(KL7V|^CUen@r5Kkf!%*NB zFz6r!9ReQ&ndlnW7q8p9b3Ug!$1d3O7ro3ZvmA#Q5Iz7f1jxpFAX$MKhILPP)8&6! zcVM0lfdqMc&L8RM_b=~T^*6k4HAB~Ir}CB3?@i!Iz+b=t3l?_p=mrWye9`c!WTwWl zAJoL@UxjC_40i`#(J-};;aphLq=Zxi-kS&WmJp$VgP^WOMMO3fOvM1#KX|uAIOG1m z?Yuah#*C8RuU$8`(wF5bP%~O4bgwQ=UM0kOuo|4Kk+rG5d$Rab?!)IV&(`~X=uB%E zK<ddqg^w})ELBxBcybBp|4`)}M`pfygvbiXR+Gy)!i zC7S5K1XPoHpV~O0^5M!V-q8H zT=boIzJoXK-I@dj2Z}Sqe|$U{ynyeHWh8zvkg4uXFw>A@=(TKm&jW1Wgtr7 z_CRKvOYaDg2c8mXm&Np^IZ>@ikve~_Se64zMTfiB@tk*cjs39|;zp8M|Md^vTb4V1 zsJ^{xoV8Agr7Th?(Euk6)Y&L8|Ctj;%sPrL+LcsyoAOrpXQ`^f$$$5Fq5 z=uf$<_;tP6))_AisrnVNNdK!};oJ13&7w^3y%h(3IcC^|{02XJ5Obklio;E-_&h-E zW$>08{F3w1TzmV#As5NgbtvLtbV96)$cR<2foNextqq_C0S;?}?zCDP4)~}&KgAiu zkGADrNh@@~U!A#kwQpjJ>beLDz+Bwuy;@$tU-4w{hRFTM&L#x%aN7s?f`C9??IOn+ z4Gfda);J*!*~tj$A0%>+L^`YMteAbyhHL;&054+30<|nlS#YuPdd?uJ#qy7#j)s|wds}a1giWL4=@X5rixhqX)jf`OS<+C>-`|piXT%$z} z^w>4z;mu#kXjx0+Ko0`mNMcK(G?oFEDYR*18FtG(CXO>WT$sD&w^D&ik)bBb(v34^ zwi&u76euV*0CAzJAlLo7cP&RJP{IOecU`z7!wEDEpa4L}M)3Fxb5ngon}_~hhdN~9 z577dH-b6l(>eHa8#LRfT<>#aStEADN>W7XVB_sYl5sPB36809jci0PCS%h$g-lpiA z8XFVyXB?xa>-j4b`M>m_v~o0SrBQ3`ef3PhB6M$!lM* zohSVtkaUb>>!um+lpo3XZMf3u+eZk!9ez|iivqM0qx)UfrUzh``wpAivYf_m*VAOr zw^=dYTI{l~Od#9h*tQ=}b|bLE(TaS@^XG3H1gzN?_&0ILqT|>2PK#{8u}cdc>f5F8 zztIhe9x6xzKqaLZw_=bTh@jJHlL<+e)EhZi%1#yy`s!t`)F!>xVR@L23u!e3IJl`u z_qU`Z88t|D57Mp7?OGRFOz*pyvbC(Z2RYXNm={7W11o`ooG(_1dEVJ8B}+PD5J4;7Wg&=)R#HNTiH?yf{7`0x8yFd(OL6l93IUz6uIPZa!zM%Z-L^M= zB&xwQ+V94mt^iCd}1?o9?h zx3&HAw-N|M!vGHd;Xf`zQV|#atjZna^|<>*Pfc#-f(%1o`OL3UKU5bai6%1DWk=#e zD%sFdO$Y%WAVU!Q_q}A5V(9usO~T@5V&aR5?P%@qWtTSe_NO~-QrNS$t0RMP69k_y z$cUp!mq%3$1O!)&wL}N5E7GD0w@n&XOJz{8xaN}8rUNl{f0#)lpb6|kS3xXf+3#Bn4y6p!WLdu?iTYYBTo$q*g}_o zTlerA&)qCG+pVGrqsmjrvz6OyAw))ot`PpzAe9W-qh8a;OrF=<;1(5F)$i0iiO@Fs zFJ%q{Ltp{xy9aitxKIKRQ)rac2(QEs8(cTaUFsYfa?fLDQN@uK!YQ0V61TRH7(=XD zI;K3AruxQ0-3KkE$t<YmS6d2{0P&Vxchg{S12?;A@n42rD zZ`7G+J+Zb3+eiSuhcg=6&_#q6{mG=eDzATi2%R{%2gD7r;rZeygKf?`sR3qT*i(IR zzIy4Q+S1Zu;SyURoah=$B>Gh??boK5VbpsGwmgy^*iE4sR8~&T8z&ph49bRZieG^y zA<|tYv)NDWhC=KY)_q>CBdY<(hJL1j#OEL2&`0Sl?OUkga+J2HL_wZG2}Z0_pw+-0 zLaYQV<474Z7L5hC!(byI7B9(EnQaRq^WJ^9iJ9YkBfb#wbaOb($n*6>*K$zRzeMtd zN(eB{)~JsOj_-2Qkk;l zCocB6({HcN?Og6|c3461 z4=smnHXZ^2S>VhB{sQGa5r3G;uq@C5TEgiA_JPt6PaQtxht}+{J?{gn1V~9BmBpQF zJ*6ux&g0*yX^Fn&C#Els|E8w_fJ0`Epf8Cc(hoC%80k2`qA2vBwIKx++WlY#6#a=f zXM>Ih{{~*4>c0elfKS`7Eb(Gi^SJ4-TA-H1ZZPZF)j3ltc@2!K*-NXIN5Dem^#PF+ zARXaB|Hq*gewxG*5Gr7#oOR|57*#ZI)?zDBz9SO_MT?XaoZWvdF^HqfRVp!JqRM0l zo`Qa@dYjv))2yPR+!=1DgKvT1Hqi+IG6771gKzb^Y1E_Y%NMFHh1D(ja+Zk>|7WRu z?)zVEsN}5j4CQ&vyqpJr!1=+VA}hl)#Alr7av9PsrxUu-U1Xr2ximR-qch3%T!(?_P#wC|n@uvEK3bivE1qtu?QbbEcG;Il^zP znKBi7tB}_aPc#cB$3&Q1QIsJK0M0`4NML7~r-5paq7~9AA-;ai^W7}KpBj^goOfw1BnV25dSZvP*`LK zUFfNw^= z4NA_B#lTp@$EaD`0)58 zHL9{@{f7@*-fuK1owUhnRtUHj!}8i}x_%G)Yd~kGW`rodwoB%d|uWz zz$A>X>UzGZAGLKBH3A?8M35Tmr1}cO8_t*~6=Jy$0$S+TK#3k$W3NM%sizeQ*GC35 zGcq5EOfD~7E39`0ggdRxu~X`}lX0yfX#aZCD$|m*GJF{B8^GIrEOAn?^r#uG<-8NV zA^Vv@-hL}K0^jdFKa#KK@UF4$rzitC#f^xY2!1~Ij+|%VD?NY&@V}m8h+{?A6Lw-| z4_vAej-k#3fK_1+d&;;V@_Pi*8Y3$twVNYGg@&A+u6|j zRR(Yl_h2+5Q;GPEsIoIMt>lmMF#6aL1UI; zZ%m~n7M{pI5YY6VUQE72q-#^i8$gMHzvaGf=&maAESf5j*t+4_t3%#X)L(@^!I>z3 z9UL(qr)#?PZ1~&3 z!G$e~C?XC6idBGjf%Jy3*ih&6_=N&jgwO&g=7K5Fax$m*i>U3*;ya#N56^b%h`7cs zFs>mNxqo$x$-F--XaX}{R$seay5*X|u%E^JJz07DdYAl1qK6pk9k<*(&Aw|(Za(I! zf-nJ*!m63RRGD_?f=v2)TdzWt*)R&>BqMm>Dvnk{gtNT~k0%+kDmug+t=C!4lT;VbB)hPt&YFdRQLZ{F5HMVE)VKhObVvswt43EZhz;0B z;DmlVh1`tbB&qe^oO44A_S*L7Vp~9Dfu30qGOUxJP4Ud3FhTmJV%36C2#zwcvn%BK zZ&lR8j7_~ZFhk)jDXbSKsV6=^I4wQDIukph7>Xqw7zG|@d0R7RGL)m4blKl0X-Yilo;IusRwh}L?bJ|b+z}Otf2j1qIobq=EQ=% zr{B39yqrN#Z&uJCtuv}Pz`MKljt)Dck9lck^Oe(l+TtRW4{dARv>hLY_5A2wlI1j9 z8P0W$HGF?Y_l}5Nf@jOC5{qxe?)mt8OhGAD?n8+E3yAl*!o4IN|@bWv`}o@q@dh4)jI)|Yv9X`WCD>GfH7$%BegR6Kv5wfp8ki( zApeGG_W$DJb($+0#6-GMIfG313~tNt^J_et#B)!@>TZ)seu4^Z!>}XEsF?QDQ79Ae zT;PcpXNcHPRt35Uvk~KX?V9i0B4thoneR7hEV!1%O)lZgHrNc2V(ZTksa?c3|A}D_ z=>F()r8Uh7iZ6hD_!DS?!NJHF7fiiPcqLUTD99KItuA_dqD;knpsAKPe^R_Yll!`* zuj0i^TdtWx)FsIQi|my3*f7r?s#kN$)h26FbnwkEngP=x=6|3I7;YHw1wsY!PNwzR z`PWT=$MkQhu*JS9jO1|MET6>wD9E5Iq@(Zm^Ql;`US4y<@O773+?gyjw#^tr%;mVy ztmh>7_bo+Mb$hGD3DoGS>e^X++XkVSd9(h0g%}T4xihawO&>*BC`yME+hj@r#d3R7 z-FevY!d(yn9VtSL?oOGNqCr>pn$xs;C~|!{@uK0P+1*Q_k$Mpxty3RV4IPyE;I)Wv z7bN7}cq8;qTsSM{him3_ZDD67cg%LblaslG&+~ycv(Gub-iEPltC(Ias%M19j~_qE zd$JAPo7PF*yJ;}&Mq|ruW6NS(LA^|@e`n@HO3C)cIWhTm52O6j3{>u|$T5~`y!lmJcU2)BO`PtiDKWxZ`)vX(};6Bf>ZNSc-*^1+ws+vF_GYSybtW$$@Hi z79kf{slbNu?AX;Z=M5q^HGgf5EFuRc%^;7$tlD5^Mf)<=M1D%!yESVe$YfMh zp!`txp>+B)oA)KG#zxZ*MFi4S+_Z!hK|@@~ifb<-&kqi&UD(YW@l2c}5Ii-~4@$9w zlN({A@oQ0*AwgDExq5tm_j!smeb1x)VCvEgQZ2d}n7g$^A`hU+9NACP4)Y_`ciK)fSy=Am)(wxGhr(TLje722TH!a4Tr^sFH~pfTce@KF>PCgQMJe<6&w$gtyb5YoaKA#6;s?X;<;G8RE&WN zrn7FKtwCoK?!fEw8EK9{l9+(MMNKR=l1XYZB~|!(ENohg^wT=fW@&6B|>)uR?-@ zLLUy#fge^tKylDY#*`ONv{Y|-AZeTJ2V0)8Qt)e>3pqM?KK$BKG~p5_N)zA~f&{GMf_ndzh?j+flACwxINg?fwQeqR0?Cs7SGGA8J3Ld6UFBwV&x1#54PT5GY#O`CP| z{$lv0opGn}N9*h#(du*Tn}qi(6`Q>*@svi50Bie{6Zkh6L=5K%G~__0mTdp(oEIQ3 z7FJAa@LYCQ8=nDejd5p#r=hm7x-OT#kv&^y`9A<~^n-T=;OL#Jn$8ww9!2YpeDl`OjP0noGq>kn z__kgprZ21WR&0{6%im>-53kv26o>dkI4F@a8z>^`5v-5P-{-xPJ=s$iv!oAJo%^da z(9y4sJRJol$_a=jz*Z2m1-o4>HL}p2Lkf#+0=02@v(oYjX=Z!@l!eHY-v2}~)?fXv~8(g~#uggYp=op$+y5ei$GQTkQP zcZ0nF?Ge8ySAX`dzPCIxtr}gR|4(P$!$Ez~K=O`#J0U>8^1`kMu7?F_kB7vvP(?aLAsmNU(3)3E~se`hnL8%!^IRWO!Qnb5$`}UwoUqrw=wY|b$2WcE? z+2stEwD7_WYIO+^FZ?1%oV3b+fQ|f==;n{+6=d)SrFzD0AW9*bzzZ!eC~LuZ5&k7g z=k!K3fQvv`jU?|C4DB)B{Q&<6`5}@+aDQa52?PVMxIrWYR)?$?N2?slJP=9*d8cya zb+_ocY?d#%lEN9qC5FQJPldYio+Ca2r2-2z0-zyn#Y!8uXfVm0CcV=y;PlN`wO-Lk z?O(r@7gM1>2Qy0?%L!$mua7Q0drb7gbE3+%2(1o&1a&_g!?2St(~5Nc5ZXwTC8+p_ z6ANtTu^Hi4g%Qdq-%2`*a9@#;XK2(%2Sd4{tgz=x!K(K2@W-Ob)4h7#r_A!9DHOR- z_){n2(NhZsKm$nBfCxZ40<4ic%%ZYj5jMs(RUrVSiAOs08u(CrH1Pc)p8rM@LOpZ1 z4I@=2J>-0Sp@RuG%hUZoT)hW4*X`d1{AMYj$=aq zhry{Z;SWQIP0$Rq7OMdktXS>80V<2y;5ghKT|R6BumBzfC)-lGOYIQ9i38#1kcF51 zjglArAr?d52d7rN-==~4YITS*VQU$swr4Nnv=YmUHvE=Gm+s0g#V-9i_nj=AA^tDa z@PFa#IoQ>rbOcijB5~x$(%wpf?qtQPAB&#de&ZeyTTg5=3u8a8?FW{}F^*=-VLk?) z;<+-K!f#hx=E;*tNHE5nBAM@5Mgk$90tZG?4S^lOs)J~gxLa2|verbJjgh5DgP!56 zeAJxMR4|WRt(qTI_bHUcFV6hI(2soft2}&gXR3Kf+3twJ=rKWOKSjO|9u++6O z7DKxay*@eyX$HE&zGW=tD{&&3-gi9~Ao!^!E&Z%t6qH>U&;(OvN9DxrM6WTjL#>Ny zU1DFv#XSAo3!fpj!VSPFBOwnD5h|z^w`vT*k)N2K|MAqea`Tg-EcCy3{~5k@KC4YO zoF;nLMZmX6DFuE@vP6D?2}JI>N0m}{})D9^4<@I{;`K39gTv-1n9h$HuZsgW@ zhMT2YE7_3?GJt@)nEi!f;mc7-1c@F3(FbGzlqQXia!=GlurLX}0Sd#n0c!r8G{$|+ zwNyi$hsnOWYPhTbb77&mX7kkKg{znQg*6@$hrQ3fxLaoYN_|?17@p#ucR>{cybbyu z3I*g6E*-%kp^Sj02=zpL+X3%?##yMk(+SBISnhtQ&;2`D1l9jr(|7QnnVBlNj&gw) zmw@>JW&sumN!DM}gun7$bPIr+|6h489B&|sfan-ChTd0!!1473%3Z0e=iBMN{pKCO zH#z=624WfV%9gJYH;*=Z|vSJ1x_l2hztI1(~PE)z5Vc*x-bu-mfc6xsL zsym6y2UnGB5<`iBI}UXTXiNZSVwB!-k29^DfsSjW2cP&w2^lNg`+6K!FBq7B!wBX) z@yIQmc%bfws1S2E3j$>7;q2L>k8ge+Pq3n)>W+7*GWjbWzbAAA(&&7zF?K)&%2Dqsc{V(&$9y2mbnE)H95)%Dx-r?96<ULRx_f>S)FVyxZZ1Dxm zgDew6gV5?H%xXR%V0z^gKzoEHY- z9K6E?^?3s@b%rVG=*gk8b$hMzpdK*Rvaz#!E-`;*?F*vj49nug3pD#sXIqcXf!(1oOO1Y^6r*M}^TLePem zff|wz%qYQj+$4G@3*`*fbT^j$H7INBD}fzN5Z8=9SO2$d?Xm?XL@3<93iR!G!>Z@t zw8-~B^@;C+f=Sn@^z1_>`@kLL{aivrM#j=yrr+*Cm{?g!g(EPaW%+FFt%TYA$-QT(T@ znJ_24xMS(@7sbEc35X3~FvO~2pV|v(xIR$wP$+3VHf@g%Ni3UKYEe)QIl6N*Q4e+$ zc@wG~I3f}Ot_XP@7Mir*Eo zhYFy6rK=WJ$M4<>GUX}Ub;y=9?CeCIA^OtRA=Kc=S>?o$hd2KA?R(Du*emX0?e;R} zsbqN$X)FQMP%+@`5`7!-zUk~F+GNowu&!3ZGuYMQa$;G8dCM^~;zYF4P6ZXU$Sjd( zt!g+27oL(8pI_FZ-mh=7dTmUz=1rK|11++kt*i|50py0ozdypw(4g)XwHtz^6w!(} zj`DtLQfr>V<+jqTM5u%>Aj(FZU~o!UcB*c>6?{;UQ9aXrQL1Y-8^zt-+$1-O z5jhrEQCtnI4ZAh`pQ%eJc{UIok%|2zzi*q)m7*o==s4T3>B!z$qNF>@#hT{?l_4?J zwXVQU7RV<+tCLYzfE}Wt`oP@+->1F^APvpNt3d87bs6HCJX_8xAZB>&P@4eqhFHrs z|Fyk+STzOed&pkmb~UnwY`?NrEU=7wpVQ}#j*<12>8JPZO%l9UZEQ#j8is`myVLgF zK*mg-KCtHoYPQ_n7aSd*@|;2$taoZ{cieJB4s5i9iOl`_lKCI|yEE;5Z6E?1EEv%% z55L1vR9LtHelI?M!^KYrFR?>$x;Dl4-#P>zr%TFL)|O(-DP zpoYl(a0kk%5az(QiS>QngJ%_>V>`8s9S$;ouz&GlT2=v zk*yXO!|mH&GA{xzo(G&pr~Ks2Q=%Nz+BT+vvU?%@{{QRmK_>Re{w`D9!ZIu~akcvI z)^4+9B~0gdLRo>WgV}upyY*Pa%9WMKt2~G#h}~z9)P6|jhlGV}D@VfpqK`e6pMF|a zHjF=VzSGZiGA{eSJC8 z3)a{kV=OImv34``#Qn0&@Uryf=%2Xy()atND3&^%4MzfBbqxOtu5kU@01GULM!jrw`X7raw6y300DnSG2c{Mctr)k8a?5{0QD2W?X2V+(k_4%` zdR%!Z&0rplks4UteXzTX{{fIYpZ>fm#{OL9ThM|)s7Go%D9%tLqr!!t|F6FU+8)T4 zPo({h%Vjuln$8eFvR_&nGH#OWR#l<~G4zLvUI=Z!*eY*rQ zbe9|!l(U4`qvPXDPwa%gsJ2!kJj-&1Tj3dO;|1i%vqe{qs1LL@`Q&xbZqU=9&?Kk_ zHobr_zz!o*(<^MYL=n30ZJ!{+>wf09WgXa|sPgOZ;Wo8h-T-{>%7Q(`7ak=ZJ~})M zVgSM-vQ(IRfj`0<$}r{9DyZb1F5WRs*P8reP|j}kbUxX#y|PwxMWjzki-ZJl4~


_DsaD-1jU zVEH@nuwt@<;vJU}tUN>VI~8m1z8RW`&=At3=19kl$0-9?xo#g|bwmh&_O zp5U|u#zt$%Zy!)u)js?P|xO67vW|OSzJAf zQ%6M3HBz52Z>DyH%pHduL=Yx@ASeZX0f_`e1-Vg;%T5e*Z+?j>+jqI0zlJLWk=3mU z@TVYRK?$s|u*7r@cog~v5NH?gQ*qjAMqS>#VXLEA!duT0J~cZ5Ea(I8x$!E|r$c>% z4-Zu>`etBG>GqgtAvzuh31B{8x%Hg-saTain?NQ_76oD77*akIAeP?+At!FvL;-}E zVW?%GR>VJ~0tEW2v~$&%X?%1eS47WA)cAm`M*m<)755fdod|SWhft`au|fi9h!iY3 zUVK=}l{Lg3^d6_z@S)vnQpBo{n1CCG$^st`Xeaa^2sOB+@WH`Fg~Se~9oT;hgkR>L z(krJuXw)N)F0ZNscnCj+^XQ_mHHqy#FatI+$#UUoLHS8zD#UT)Ho=|*&&V!DxH4@Z ztKkv5NxuZ${U4vi<&BzTL=%J;W`tX;_uC4X3$I5>31kdcg>(dPuFPNeynskp@GCI} z-|x#mnyHgomXB{%R8$mZY`?s_>NRc>WLt=TnDiW&$;-$pW*7608n;}b!@&W8v4nJW z92$acQ1bBkt38D@G61Pxm3MIcy8}k-QA)4}C;q@#Nr-OXxDyi;JRoY}q9gwW^B+Im z=1TcpZ-@6mwUL+C9OV?{nTyu8z;+x&r{h z-;o>{L5i1YS=W|xy?-?XQsar8e586GCtD(Es1&Pga0PTS@>FzPoE-$jGg|hp5a2is znIb!~tGXTWde9`G4#pXRR0lU0>Jajt5STPHO3^T;HDL;y=7}T9D}K!=8Oq9p9^iBI z8zA7DJ)uBp25cs<4{r*m9->Mj$HSClrVi1n0RM$ID!}0tIo=WYy0Mmp7;K2FM;(kH z0xiUk9Nc9J%Z90_8@wM*i9d?z#<-E&ag?5De!k(|f-1+A!qg9bEexUIyyBLKQ4}k) zpO(HGQ+VO~*7Il&AyyhKDU{=v@F2a6HuHO*{PAr$?16+@y$7?wXxW#S)3F0zMDWD3 zz!epWy%{#bEp_G$<>FXDW*?_X0U+EJKUo3>L7Rrtk6$g%#8l+7qFYF#twUHnud>XTtfnARg4#uQ1XG<@N5WnSax@l1rM4D1jDGI&^IvRM&78|VW53aA(Ko~ZJr7?T6%U&54S%?7qs z>w}OcqzfR(;xusZXhYgt^8;+A!p%+iFNY_1Hw4)g?$maJ6ahxkpJ?w3Jb;t{yOOjX zpZw~FImB{1?);LIyMG@0n)KBXa4&jGt7njELN^FgNw7Qr&uyn?BHrwESp0ww1?>yO#_+(!021so2^$Fa zYUstV2@ES>-fbJ2@f6{Oi0d@S{Kpp~yDUMFIAL`eU>d2O8w;WKbW#7+L0tr6jjC?3 zWOYV4UU{M*!554dbTS4!)-l~HW-X|$MpFnh7K*zg$qhSnvA$+UA!D%8lAQM_M~9bA z=bw^&20Ga~u@|eIB5$_sFQ+KRvGVbL{`u@*h9k#~2YCNw;GSn(zhe_G?^?z)R7_f& z*TlHxpKaP66_-rI(95J-t(`Ae?G*i_0eB6zsNr$_|QvNruX^NoGN2>F0IJpb)KNhpz?s$1Ed+Au(!8V-s5c>k7^75+rRtg z&jT#tL3(d#D)$*b%}-L8EeA=883(kacM7BNG@Jeb9f*UoH>Ts%=@LUBT{Fg@^7AM@ zVGkHI@HG;Nq#i##Pa+y?_QXlAVIS| z!YCRj0bF>5Pr|uj$IoT)lB@MR>e1)3VzMnfwCi07e57YN%*%=gIb>8e}Bf;5Si8EEcrT{+`@@U-DghZl>M8 zIaLzgswrHpd~GYfkmyy?85gW=QE;kDITHo;%IQ*oU=T6Mhe@da-?W4x1%GGH9PN7Xd+9nE zLq+UV-3)Icm1&M=)vEC_LOkzsZR>{B@bMYEZQuNHf0cimF7Ku$yXURmYGjPbIDs|P zWz>hqES7DZr zatDJ-Z1DZ!7A)0tqyMn!wfCpW4XXd(CR*Cqr0RW6J8M?YQ7Q<$_34=tlMAdgm5mWs zYH+jxJQx;vZ6hLS48h~=io_942PPgX3+YhWz>*hj{^!dRCZ)&k#i&0x9d5!-yNRzY zz}|PrKrDhnfunEGrQX4o3=g>HI^O5`f-S>MNsD4IOMda<#qILsvcrR-f=oBD6vA0c zsw;-ZI)meb2GuK2qL}AbRJ;?+5DUrA$cQq+0>lB=f`TyYiy6qjE*oNOjqUBU2t5z9 zs&|esvwVNGLoq%0K+K7>0r`{PX?p2a^+`tGrYZJt5Rigu7RthreqUAlmNSBGDc|}a z*U9%jOlNpFuD;J+vUbtUDXOescQc*G@w6tFQWQq_;yf@PxZ3~nOktRyu7@~6O4`jN-GDaO7eWmM6Jo04lKf8t9H$4qF40$nE6MlWw~oY#iN##Kn&6RVXK9Srrpbab4U{F-XI zhjq#m5^^h*J_ItHcEtL zKOkYm@XN59i@Q5IcAY=^EXvf%5{mNyIPs=ULM`o^WX=yvcQ2?8?vuFOmfj zOV}{rU_JWhOKUOHx!Dh~pA_tnMBeSVy5fSEb3)*SM}$Z_7O#Zf2eF4&2?&BP0s3z) z9!h=woEc;|Uv#i}S8jg3P97^at08J)fQ*o(!hvNfmH&irRPyUg@MbJ&^p5Efztz%X zR1eEW<{4IPluDZPUpsKU+Zuy|7-GdlCx#zLuMVMLMoa(CpGtmya^$g##97kom_LH$ z;(=bZ_?^Fyj&l#H*w_dh>@R$FIi_3mPB276%Tk&E0Q7&S3t?yQP765z9|$!+~Sp2GA%nb%j+4@dRx}>(0|6S4`5H=B}#aeb4}9t+`{s{s*8ub zzfOCTHKX;HrgGBqq$~G;ZcUdhPgmOKlIV($uhK9!2*q8ztEii&9Nq+Uct>bE(Sn;*@F|TOs?zA_LOqjO>I&85!9L-<*6UL=L>Dz`#JHLV()Ghwll+)-;d(*jr?n8Y^+L;MWz_qL7>G ztEt-B+biJ93x@uE7(#Syw^EeupN_ON1<j^~$D{Azr3ld)VG+T^Wf>LS0HY4V znk+Yp(;!ZmSWLLm)Bu_#-YyW5sXyo&Q8j6kF;omTeAXl2Z&=WFb2{Z*zG3PIqmgi&P|dZsUCqpcHNe`>maWXPq^7a zb&6z#g)die`LR<4Cd+OzX^ZOU=t#e%b8cz2bsW2u3&I4fIp2=OqAr7o2%D$~iVY5f zT!@{K_sXs$C-0!4qk~+8og6+E8g0G8o|GP=+e)V+uM3~Fy?%}=^?oDw4HHB+L=?NW z0+_kbKL2`LbUP>kltv4+n3o{|1x|}W79v7ixb#F=@$%&v7>9*!7bn3O8XltD+J@Ob z%cBi&I05e`DJfZZfA1rm4hJs7xN_|eakb~$=8}8he*CBd?JS-^N=XS9&L;~y`;}_bl;K*I*y!KSrrA#Q znF^nHXEm!|7oA!+GwiadxAm}WRpVis!|C32cS{2{SP)BoPLHlt1??m0t$N(a?WAAlGyXJTCUY`X*UIu>l4#{~KcQNwj#tl8g*!?){){PfRI5U{yKuOd8e{+3C*o39ZvYRx%<}6N|B4~OyL*u3c{kL7l8HXQ)Mn>z zo}1rkgtOY}c+`U4@jeGTdnw#!@MDN}15XiH8j7oZeQYc&_o#Y+oN{bz?bkbMctBU+ zm-RNPhw{QF;=jnaH%?>Df@~HAUxwP%p}Ufn{MLZwkx)>KAmE@9d4p*X{JxGq-faW! z-tY3_*gu0lQNE2oy#pM5>|D>S3eDLM6vwt3at0OA9*i+_WPxaFXm1ZCK5RgbRwmx) zDoy8Jj2B#--j+IE@R<`mkPD>`BATqTFR5Z{t|I>7sgwST77N&~efxGEM2xoC50O66 zZZ~08j{|5m(Xr#L;{+Etc2o8C_TrY1kyrSU;?tr8Gyw(#8?oGBEl9M=>0enKdh;W7 z^Y9ub4T+;bLP_jp-^eRH+n)+-J+u2AOVy9XxzVcW;%S*#3`Wn_k8dv--#%%;&>i+o zQcR6E!4wXPBRj#}0qQ`nKmVd@C7}`Y z{h_swQ_O$wiQjIwt`%wvlGsoa;iolmzra`=h#dUa=tr{*3j>g(oHwN87{A^FNM#Jm z52#4|%}7xajA-zAg)P-@RXmQlU4T_-8b$~AA;t+UQ^ zfQ%CO|MXtOhg{oU1Zq&Q&^Ew0`qSM?V=)O+os&oeB+ujFL!CoDgoz1?+>pABY;0^P z`D3`Ez$-8!a87|;XRsST5&8D3w2r^q4bH`h1MX5gkA%%X><`=jU2VUjV@c=%vnNi zYT9n!8D$DD`Ec#vy5rAmZ>hU5X%r|f!c&GeY2l~EI20clgOgKJZe(9+#mkOKA*7d& zR<3C^R-gOxPIs^%C8zIE+yabu_ICwjyzu;ZdiaihB3as@t?W=L#|p78&i&I#)sj zm;~y>F+vFqLJfc@O5yVIazqm2vujvF{^`*vFWKPEvqml8GvpX|`E#KU!Qn%YH5G|d zT+&QFDbYAuz>330J^v?Z=`h(V%I*cqjiYm>Qhe+t>>>J!AHqh@SlFW`3GmrWzlt2W z%_z~xV(}8QQgVuk_sRv5!BTF#4{;>V`+A?eyW|p`_ph>5;e{=fb0wXe#f@ zN3mWP+*Rbq%*13F&qEJz6p zfNjJ>9Y$pbA7KCl+ecB!gBk&=1iY)?_aLf{tgNgTIu7lbq4fn)1;O#?i9o)QRdz2< z2r`k~JQ!a?{G%PDp{FO4QjhW_Lh0OllM(63>OBh!IC2oDJTz)R-(6f>&|zQxVnsG^ z2&b#`#dNoY8lI@RWmin+jH>q3isSTp0l;~%_2lFRN zj4#dAxfgHo!|pD({z+h&q|Xej?|b=UX=zZmgpF)Y1$-GkUyNGpcjB}(u%gaizDA3` zV!Meg34&u-$&6?hkZuty25W8T!}~^_cQriaT(-^lhtr=3dAQ(0M@m`*82g}>LdAmo zuN0(#=89MbVDE&30|WNV^bZW2SL9=*UFWXZI^r&*w5M;}LtA~;Q=q>sj_-c>5G14I zzlC)cEIR8t4m(O6R_lVr!s7WhnY^OezTVz`PoQ8&q5s?FiTWNe2$6WBq6R?(vFh#f z?&=QHOKPTj9`7`@X6LcHQYTF>?%W^zcXsOV-7{^O( zp+@@grpH#RLKeEx0+3Z)$2psZCbiuvaa?RwzS5}WBN}KFWA>z*7P(|!&Jv86M!OH! zd)6J!8r(zbr#8o_d4tYb9iOS3q)ab3JwrOc&ZC&tBV!AX_ z&c(^-g60M{0!cNfqslzzTOijw_afFCuf?sQVE60Q4BhNRRpW};O#}AwETQdty1)lzD1j3)=;JDac?{| z9JpIG&2k@cyl zN$G`#$TAZJq#IFvhM!b`TSZ6)$s55>(ZF9u11t;|Zmu&i4UCJA=f9*F)ew zhyFQ?0irm>j_5~TzW7CPUMa*5jGD`b0e2u=W{Ra4r-xMkIO7Birt z5;cfbK6vgz_Pl%dE<$_|a0|TN;ia$FKQ}6=?Tt}yX6RZJ_h)&5Y5_JsyQ#WG@E+1d zZmUjxKt+eH0kb!Z(uiTF93@z3V!rpf0w?C3eYRgfp<3Dh*pfBpKW~JHHi9y)X z0H@1M{|J%Sr8gkkz->VO1Q-ix7u7W=NbMNkpB*N`Y+JIrNAJ7ysJ&X1XnhrIuGRK^ zB7qLT(jXxDKlm3yjdJSKbjh2Jf4ZnnT2FI3es+p3vL21N`O4lp&=(yn0tS9A(KG`? zJd!LAWX4wXkgzZX7~+>rNjq@sX&~Z5NniiNeR6ub8!;Uw5;9Xu(HeZ?`g(*s6TtrKCXX z!^J``NvrZ|fVcS#R0;>T={jDtv00N;uY%}+>rKbHKP2aBE-Q2aWbrAk916boXLw}9 z4zkgr=0NlSp#{`$9bijr{fY4BQZ^}l-_xt3?KG+xWu@MOq5EtR-Z;U6gKZymklK+$ zuHuA(Ou^-~sxCw?QE@>5rwhDVV@r$c+a0w9hRuofsS^exe|Rl&BkL_zmaU+VYT0VK zdkEEV%PYp{>a`fS0pcVnewmkY@JnZfar1V&|B7|8`>1wVPtIaAV#_+F>btkF{ky0r z6nNO3D=T!g`j#jhOIHj`61WES(XBUCmTV&vhK{4>D+kI0HdMi+0mGr~E|)8;KA>Oc zTAHZ}V*lb^j*~q*H)oZRWU!7Am_~1u!P7!t zo|zeHwD$fkZIsS2=PPNLsZuYYJCwX_?d`pt_=6&o13!lV2ADiP6huyV3&3#kLh)Mv zRX6+Rx$2(S8tsi2F~KjJlt#CU(j3PNdW>Oxx~Xo?VmnRcDU@IOp)BA?o;|}F5=D;R z5G+B7fv^D-8i9}8UF$p-`4P{~K0^zKVOG9M%gn{bj*hws`pTf#vULJ-*?Z0V|-QLQo(29GN$KJ1U zaT=>VtP6e(7wA<~#uiWP(%&$SvTim%Hm2gjW2Zr`pyJ|Ulck@T2ovBQ@Qjs@9ovLQ z+hXxPv){Ol4K*r&VE`k@MF55HIw5bvTguJNJ^enD`|}H>r-!q&6njM@N(uS+@#9Bw zltE|U6X6!15_ET?Bc)I# z{5Z0(%u^=8@y!6?C=vHD<${P${EyK#mHwmP_TfZ&b~ZiuCEzq-&Nu(f-_o~ptXTta zz%z^%Y(%uhb%zyUVj?F#^!eBpG$zt{;jyb+PPCAHEfNd}Y zgHFMcJACc;;0}5Aq{)WX;3iSX{1N1gvSX-Hn{FfNL$22DE`vq76!y3^d6rM%iV#!9>J4TB30Mc3L)-WwkSguwBn_$ zocj2~0wSI*PYvUGmJkCHCEg6UbWB;mp7hd>EcSYsdNtF8;KdNCX$It}&g2W^aJ15q zOPVNMbJl?79mnR(Ii-?l0f*{2lG$a@4{(&!XCf-&7lxpY-)kuq~kW8jA8 z8Fj|e>-LQE(ZQ$QO=a{S?20d`#%kGZcyFpXOxIH|C57Rmdsz38rwOiLt=e1PX)L5|}M z0r5$9+4_t7b4%!;F_aZzCN4NyU|*@uMpyG9Nun~aT?6}%M>Jc5m7e^d?S7bW%hw?h z=e}X>9^})oFgiGELW6imLSd0J>#igJ2~Db0rVrObB+3y4t!#|7hCA`;P~L;c#ZW&W zXXxG`r@EHedsuUOK>uzf%~tC?7QjeqVW6Fk?QqQKcscxMfIqn?mf z)f`{(s>6uk-!UJ_Oyqm^8k18bt%Ow1+Z&BoCV2>KT0=>i!SixXg3&OTkx+7hWjbDL!MJT|R z246<}s~rluBDwB(FW8`BejLozmqP)y_Zo*LU8ZO<>MjV6(0n(AAylqy_g(oq2MnfI zNC3owdnmdy92wka0IC2I5hO6oA~~ul^*c_&^E$}mb0~o!WWakS(6!bcMDCU%)m{nb zL=hpzHe;l>%xNdf|Mo)=2{$BsM-;-SpCN~(P=Nm7<$HD7)iz4|VG#m!RPevKBnJU@gb1t}D2jDCrb-E(IR%ZzVJF?`Q@FWrd z4}oST{$vYcnWFv%sOt#zzARcv$;;aYX(yiAqAW}$jy+%vsadPL>NqSI zpfw392UDJz-n)ZUgZDa&nJu>`)nr{pIrG8E8l2?lk2anV; z`MVVz{Z5cc$+(S<(ePMW6T%1{D*0_ug+K>}1!$~(5DS60WXpZBdTfS)TGnwr7EB@# zVcA!MTPSP-BXYU~6Q z_0rO`9~aZj2SP(chjew>;Ae)80(uJe1oN9*3oiYNPI^PqG2F7H-o3y;_zbnr_|0=` zDWsGde-cE5VKcQBvTM2?yJ?+0d$wxP4}@z*qPvR=S*d3!_Tkv=iEiqXKOA@>eYKQT zjbk>rQ;8dB0#LRT>u8zN-n&hbJxq!vZ0%hf^Vi?TlI!CPAKjNm62W*{V@Rm7&Q2Eb zG#HNjKK87VCvrUsK$Jz0_qe`xkaO00>ZB+88+vk4qiS~CNR^m>L`tsA8^YaZI%@lB zkyk>RNkgM0G!av}yx!7GH_2`U=)$N5 zuAf(#S`#Vtrqsrbnk8~e+qbYb&c2=#e1V~AB9!hahaA3b8`mK*j`sT&XsesrM^P7U z4^EI6v@`YR0?^8M^)17MKF5R1%;AVICng5psI}tIs>jHxt@eZgAnrTlCOSMHk0tTu z(+!N*89TUcWt`y;hZZP<87CoT4uOAf+-OSw?l-#fJsTo9F#ZEg;+~6~SzJf=#w32B zX-R!VJLWW;h<+0tS8G|GcY#R6I$;(H6nh7djO2iUj{DR5P*58gWj`OBBNC)BcfV z^yy+fK zPyEjNRefjVNuIm8BaecdZ%Q1-i-05*1}f0DLHYcCwwum+jr`L4uV230mM*;K&}Rj_ z4BwL|7TD=@^G1MP0P9T|&Mi=*dB6U-mI{pFva_?ZhQ9G_i!hDAo518(&Khhq%Akq>X%yKKvpgT~I zjjld@kc-Uo!^Q8xgR4}#u4hH7@}@_%;MjB6?TfDN52Q)q&pgW-B6T$*6$L>0OHO>w z=<1UkNAZC`FQxmo9&Lmb#PN35f4lK`th5Z#^8E_PsESB#dLy*c_MWCe_zo|q=AlP} z06S%N-v*`4p0>g)Xg@0^s464HvbR3w!RW-&aLIx=fTp%zeSjf2J$p8TSM=$V>a@C^ zcoR^lN8?Q+rN9l8u}>!992@x@1lX)U!{<4WUiLbss41lc$1$=aM8i4?_YStc0S7 ztVbIevAeTJjlBkA9AF@c;A;0)!A;p}-}Z({-W@rUb4F-Wo2X*Q7kmeNp*}!x2=zpv zj?w@tU1__U?0gkN;;|8UZZ->YLhNK<8|1C&M85|gW3+rwmCJW}CY`d=M(0u1)6-M9 zcuBdt%=I$Z8Wh*kaKey_?sQG?&VKM<9cu6kPj6FwFR++N>9A>&TEg#$6OCzS%p{aA zE?#QkR)<|m{CjXY>OpJHP4xJq_CTMEFO=zk&mm&&JCfS&jdNYqt-bCT2VE6M5q+a8 z@;;F+LT~o;sX`zlwgTa2ArAn|A-{K=2C5ne;xcX;eotg|GQzuAi7K{5$|EdJoeUho zcjGFPbyUvVMzAs4H6*R|z0SFXF32i~CTgof`g+KhGu?K%GsU&OX<4J@&|lCOUHfn= zgz6bB$2W(Z7vn>Lbp*E}ar#vs7N-{tx&pw1j>EbZQe{FLQZDl+daN!P+f?GvyNXP6 z_>4Fwm2U^JV{l8^F^KlJhxDsqTl+Pa)ANubI$zIqOcyI=Zc*hX3qdGjQ_o8{!060n zT)H+V+qpRP9D61_`vW!wm3#Cj2MJ@*1qDoFhrGbn!BCxyOs`804ky!M3{R*IoP{Y3 zfIFCHpoasEL3~4iPGdBWBN9)BNYNoD)OvjJ(<&uigLs1|b$#*3DF&#+To$2_fAY@3 zY@>9y4nv>J<4=$>C9A_3?7_Z!7&!rSi?SPfNX)xfjxIC>47G`{(BKoUrW zrBKnvN8-Q$F~XPrhW57+3n;e~o8N;aBDv45)TZsAx4V4hu0UnG{6(KAh9gtlR$r;4ckRC(I_e?`t;s2~Zr#1x zP+&qKL2R1=cmZ?O%{F>~vk@Z3Li1Nz7ou#Un50?xS<#fcaUW{0mi?;n+4>15`X0}Z z+*0Qer?(Lf+sKO+;?XuMX9Geec8ke*5%by#0IjXT{BNkD}1^LZA z_k;5DGVU7LNg>_MT7Qq(6p9h9#cy2S&n-=!gMtxesAEvz9%gX zBvGVeCYS!y`(an>M&uLt@%RPqzchXmB_v8KkFQ@lu7x9OsTh@J`}`0Xe~zCgjl2|mI-+Ow>hN%Nac^QiakP)w4WXi%I%!)Z{p zt)2Zs?Ur3P3@_XdIzUO6cdTIx-qE>xsW)~Nqdd2AHsClwTY%$Gap039YOPF99sqTW zAB@K$sT#WHB{RL#wL&_}?;$*KF}z!@N_;PzmoK-2NF=&l6%}fZYrb*c_P*&k(LMEQ*NaWlC%O}<-O}z7kr(<;WD}%(h>E~t z!Ldb2jO{2|Z?r5>bz|5AbTp7Dv$kM}z8#)ATma2w#wPpM{s(TcAipnqct`ZEKul5osy(jr+yry6;v3lGsa7FNb_HN1g{Q5CJ@}cdz}4S zs`C$B2T}yIMCfp*AIiZeV~p8voH=OqhAS#uzFV_>hi$Mf&9&cqy3li6rkt&>J8Rr(PXMHYS{F(pz02}MhGr_Ct7Sy@v zWK$#}vTM0*WUmKGvW1d0f;gD?0(gKD4GJKPfspNg?RF{v+KEheS+#wVnR?PnfxTS? zrX4aVV$Rcton_g%qE;!>VBfDES$YN~1N8EMCm@_#p=1E-;-LUMM<eEg_v@RX2s_zt9&V4-n+{7drj z;wb@}{m)*>>_e5A`4bO~Y(0eXc338HS#;Ac_z(pRnNeNY>d4D@aI!%U$^*UOHFimh zP-H_#hOfQHuyg@vW6T5G{C8SW2Rok}+R;5y7`N1Ken_ckR4mH$T^i{Ryzk|0#v1pb7AtYWRxSaRuH5pa8Uk#ldeVrydxrb6pKv_Zv zr4`lgCDO%ZdRV;V&+%p2@0igqMt~sS+tIPu?!N{kf>fY{fDA>1o&T_Zfl+qMpT3K= z-lH=3BYcv^WBvZb5)p|g$h&zc_N15p|Ibif-(9q_s6&f0hq2rRdR+;Z;g}aVFv*{`JPg<*mb;P zIVc4%c>h;hjX5BwyCEh-MUP8|<3W@KJw3dr4FKTk@v)|!?~)9#E{PhAKE!;fq(r@b z+^YygxR^wbgT$j~M~I>ULt!F^$oY;wJ#)V(tM%hY;lF-rSnveK128*Iuuevd`7lgO z8|7a@SUOv^?&`pSNi1K`nj^cXNEjueZMbkf2v*veOg9O3$x9YhXAV6}Y#0!=ucF>? zZZ2>P0A@Ob`3PVy07fWC2X7$ND%ye@459Eh zD@|`U6uqceQX2aUFV2%=fWHdelhpVf|Fm8~ObB$i?m7yr0p}ljzz<{8izJv_8fW&4 zl!dJhq*tznjXwg0CmP*2+}Ap^}j-F96#hdv76s>MZ*i2^@juH zg>t^!Q%)Uqr|Q0+Fd^M68+L5j{15XC#Jh%Ue_2sjqj3fgfr{kIh&jwY%y(c}(MRgo z#?uL{h1Q1(THKJUi78rqe5(IbFy)-#K`WT;U;Q_9D!?(}FC}=gw7}-mAJaC>%=+g) zuF-epv+Uwv|R{Y~9wX#Y1 zw*0(v{?=DV9#JR<)~eRrBL~kgOOZJN8@FP=;-Ic5$j=HFqNFu~SZtca&(Yv6y5VjWFo1`W8@0Xn1H-wqq%*PYE_M2shctv@WON@?+Om`-Ifzs8{ST27{=hE~ zARGHk#~8X@4hQ!aZaI48-&_IA#`)!V3NM=*d0hZ%$9e-5Z?O$tp z*>v~TfYzZ3f}yM|ensgB3=bikXz1zb{|WjJX1TEXJ-yegD3vvY?X3^7>L8Ye_$d>H znc3MqKvMywq5v`P&;;s&)L@?TM?)Z}?n@mgk4dpe)DD@(`oN<0|1hK{YAP&x~K&62&~Egmhhx3#pUld-z8O7XSh4eiJzN zXXE9xz1ui1&3t=l#MAAf2MDONRKCULk5BO)I+xV)(5$GRS$LXcCIBJr?g>b}{%&mn zAOvYL^r%FqcVBDZ!6{{y5c8|s-#Ql>ibaUy2@oA*{H^V8Ut$WZzf_#*t%6;^ z>pV^vVE|)`EKVT<+;H(F0`jo13y_ciy`)X5s5Ta^W47B?4TTX&Cx3^XVV|6?JqX<` zi1o)!(WZ_f3m80RXHKaC`a7>QZ&UN|5tdm;nG?$NWo|A?@pt@{1$ z?&YSp4tNf7!Nglr{^=Rh&m*)q$CDl6ViB>pYiw<|(v(c|291U!1!)?V&XMY#Q35_|@0(ijBhU z-B^8(Pn#NkuIOZ89)l(X()NnKv)<@7G5zw`mscqq032F^g6R(Lp(5VOv!y?aqLH}y zyU^ANC3<|{AU&j4|DwA;d3i9E43t7}!v$UqT|&YybyoNCQ>=R`E^Fk&V`Z3^6awgfCr3&Lk7lTIyJPI6gj}{$yuhS*e$bC`-sS|80IE zEEbwzI`Dp=d&XBHG{BuxKc5PgHuXKCc@J;bbei{2C*Da`GuPP6lVE0fh>O@0OeW`7 zux#jgUsua5 zZNu!!eW%-lh$KZjy9gLa`MLRub8Xu4=izWhA>!%HSXMtY>UB4yiR4r0;Ro}eT!+V^ zWM==xZ_38vSkz1=C9s$I5^CM8;IqV5AM<+AvX{r=snAI4m!;lBxkGP5%Otq04b~gJ zbPJ^mAo4N97Rbl2@^VE14~@7`E%&?Bo>Q4@6;v7`CpNlML=WrY_5vXc3h0M$HJ9ta z_R?C*)7C$DqNA$hau2$3e5w;3(_{3 z<$#_W-w_2e(j{7O^#{)|yOHk$s0y|Q&1M`;$5`Lfy_~+oOj6FW?ib@vGYvzdK$*wJ z!~N#NI#9%LLBvK`mIf?Omh0M>CI~@pf$BEMsL49dRlo6%{KHyjt~`erhLP@9JHlcy zxPmu>LIG3>WmN!vY{G3FBdG7ix&~1KU|QHxY$Gau*!=kWuYwf5@lvhn+a9yM z-kJP)zB6hMIsZHX69EC-aJd|a;*2&h0almiUri61N$Xbm-<44om7l<@3yza`jAN?emi@5Ym zKT~XI4k~aIg``;sR;n#@RX98tX5N?cBBvC+pzHdVhC<8AYgR-z>l#`*I-^IXJeBQ^ zY|}=t%)t6UOu^Dl?GqBZ0JA0d*phK00MU?DB9tN$Sefz0m$_PC+SV4TfAa5VXT4g6 zz_y?yg@Ty>Qaw4he?^giMFKS;nooPCQ#s(E7r3Es)j$mkCS)hQu&pc*4<0=aruwlv zA{l~jz_lwQvat1n{B1P(=P{>hIM=0T_jnufZvv1x`%dUaWuR|jh;mRyo7423Kb$8s zdp?ddb!!R5W_X>f$DkxEFyB>U(L6Xpme9(*hd*P&6Dkx0pn;s`(cYY$ohD**eXdhN zq5$6zu+WD~mc_!;3o|RB4k()biWHIDknI7bm@3csy`H#zvqDWybHt)rw!Rnc*@WuH z0&5u35YaQdD^bWop@e`8gdIAiHOmrMHI7Ie@zwraAE+a4)0j4IdDdz@%_T0ZF!lM= zT=;NVG?6tJ-}BMNptK^bcTXE;1qSL$^{h!l*0I~&elU_Z5*+g z?;AZ8uXsc0fkg^S@3rNz{{`JW&`u)6&CjPou@8~S#Y4xuKVz2R0t$AFBZ+OlZgvQL zoX{IJCK@WT9{{OyWiKZrB$yI`$_MavDkk<<%A5dfrv!6-Gx{ey5{ASweTA>yE_<{e z_ZLczW%wxEyb3Yz&T@{&U_N@>pz>1>s+v>3gk=PB>~dbOQq9hkdtSIM42wCw4iWwx z$BK-H8W@!0Jlfuc69z_b12F@Gjcu5UY#0ANJ4{X_Qyte+AX=E%xb z2$aZDcO+dR%z+^!Y@k;pF&GFV`FVM6XaklZt!reB2$SgJ<(5<@;ng@yauFh#c)p&mdNisi-^NGEMXv7oii15u6%6kufg&VseCTJ%*428IfWQ1gGg)HzP|LO)9{bDIb8SL%ASAZ!;BpROpZ zn$?u`we&J|Nab#6`6&I|MVSOsrAy7SZ+%bnV3V36UUv+Ct-Pt&7G%12!9^%Q(T9=( z!ongbp8{jhD=pR)Xnip+LKcA}#meOT%9T*>6_)A!{<@B{Tkvo&=RZ&Y%RFRtJ)!*D zq0RYdoentvmcuq5on6^KeKujI#?#q>NwcL85VsHTYinnh%v9{J{Mxzb*50Fn5sJiJ6 z*L}8GQB(my!4ZU58I>>!rp%2yZyZXh$5N{7sCDlzHp2~z?m&ndckxtK*6FicwAt4r%=OM;HKSU>?BZcM@nN4LbAG(M;^I|ZpK@k(9UcqimOV#?>K#&P?oq4|>G8GO>00FpOyuD3-(a4PtY1p) ztTJwH17`$&9C6LSNG*zdOzj~eUrsZ>4E_k01KJ)4*DUkCY|-7C@bON~50RoLcphY8 z5uGzsK*WOwW;C*Iceyo+u47@Q)_MUvhYT4(9!<;~oSma3M{(DI27?4a|4rNkSG+hP zRbA5i{vXEPI~?o34;xojwp2ohtiCE)$;gU?q=h0$vWpN|QBp?PQdy~xB9yI?ME1x^ zM)od5A{G74H$C_B{P{bM>*%=e@^xL;=kxx&#(AEv^A#Bn-?Lfh7oE3uVax}i$i@Cf z=^3=$MC|}@8VDcSqZLHhC4IP$D-?BP`}w)BwOCvSjUz%3i_ltF*F>s`a)UF5{nx&e zGj_cFRTEM8+!Zi_hH%FL>Mlq=3k-({Y5KR zS{qnbpvk(4*xEwxhHc@9T4`DQK&`;RkO!|62@6&*wRUSzwlEO&(temhyA6i#Pc^~j z-}jU(d#)-kL@E>eO)o55>&N)XW+f=Tn%W3Q`g$fRb5H&*Df(7VDgeHgmj7GJS7roUu#@4MdP5vAF>e{ulH3sJvu+L56G`IF0w9Oe{gDxj57Ms-m7UDul1xDhd?Z=talbQ5=i@tedM( z=Wk^a4A}2{iLEvbl3qJI;sgox=85eYPLFpz5(ti!g&v0R52)SnPY$xiLm$9dp|u9S ziqzP``EK@T`DX{iDMkAT;&O?7J}hje=WJ|N0qucvmLxR1xDN4__oOD7Bf8vg+r23+ zK5Y^E1uGfP%h<+7W-#c`$ZSk;nTjv-Ly((gOo(DS!T?(avTvf8Xx%PA0p}lA4`GM* zRds&BF3+hVHw-V`Q|&P&O$(Yb7B8{&^l3`@HEY(ImlZ&FG#g%J{zS5=yPFv&0CI?@ zdPYLEA!$Jr79EU{5M207rMa-u1%F~8hPA;*3xmrczn&o$EIxzUyfqoe ziRcu=HFf_O?&2ytK~&}tECD>$mxavi|0R$yh=5M5rSVAUDwo0RYRl}|i>ZsxeqQ!& zEqu8qVZ*>AU-W?a6iWP;SHN}x4#s_fo)fka)!~~O#f66^(3BwBf9mLX_dg>akX_h& z-v}}2t@nE=1qE3~y)Kz~h=jw-&40aB6AK%~3YCgr9GHjFYv!&!GCS72ifU7fp zA_P`I>*>jp-D(E-7%cDN4_}mOj|d?_AgYsQ0q=G_Hzp|pLv{U zY$BFcbQ~V!E|X|RLBzxh-G-I~f5GtCs59CWKPyWFG#pi@xine1q&_+USOKhl#yEtPcy88Q|F<1cZ{<-EUW5a~9g5i5}l5Jx^Qig1TJQt~`8{X+1~9Th5+Xl*H|$Wv8@B7!o@u)LJlpkRfIU9# z)7MNizN8g`y&N7yIDsK>1IvlZFZ22IWM(dF?_>j%m3pH3xoyXuuEzyHT!!`$2@25} z{=iZ3j>Zcb<~ES#d;06Sh_KP&@3DYbElcm=VR6Hbt1Z=98No4lr#Qh9gDoO~-TSVw zng4}bm#rEQ1O8>-NJ_VAKOB@6KN6IT#jK*_0UN|pcsMwy_)-tr!!H-H*jeMSr|0j7 zADsjP_MX3p@)+)al>b@98)7i>o}m0bbbZsI03#^wbEW z@{5=<-QI5-f1y)u$Cuo3y++AxAt_AB59b{#Yvgk^#Q}eyI^XvrQ$|hw4SKTXsrWJ4B}|@CE~F-UN`4|QVRnA46=%DG z`Wbm8aG!OgB4rBP1$t04Vh$aveUj9XG$&}L~py_Jl6p>aOZX1BF zj4{CuCZ9@9`x{gY+LF4y%}Snw_P`6!4Py=vIRt+a;Xk|?Uv8wUmponb{()TXRpDO3 zCg1vS&-MxZ%$gpU>7rF!r(XBVDc ztiX=eX^Ox~59}e=Y|}Nm%DV4ZTY^_*eOpP9Wh!MFve<)ju*C5`;oUIFOE zU_8up^?&Jiw3D4rNN(Hny1-#59S_HQGr8=ZeRFGFSt3#;?8TuSYMc|s`NSEpW%+11 z3zd$M9ERqt{e-JV1!nHt{*yH)KSJAf1{-p)Q07ZfQs>^eO;uFFQD>1HkjmjFk@M(U zrB>D<&Y(`!)dhpbtcLrI62huGJEG5anMZ3}kK>HEtY+>hAOHBurYbIMRry*Q$J)dd zM71_X_0D|}b^CRnw?t_lDLl1x{ixiQ^wX8B6P76-{m@ON%}3Nz+v^_&Vh%X(W>I9n|RZH8r-m zS-ii47;28J+PdXX;P#>A8IqJs0>oD;wx#NLC^Ox972GI_8(N!m6wm0PD3vsT{j%uo-As$EiEN4)CBa9J3 z8C@5Qg~N|d`|P!jydspM>XfH|0lZw zk^KDCs{^#Qx9|W4!K88{Eb8vu%%*=^eys~d*fkm(07f<+VyOJl9`4$s`QZ+=A4dwb zHUKD-U)IBrW#Skt%|Br10MQGP2uM66ORdFm6$3mGK1oh~oaGxg9(57I_N-Q0bR*1* zj8)$TyAuFf;UeR?Zh5+Qa8tX&;F7uIq}kp$yCl)jwBw&AyUz_i95S~*0roRXnsp6! zY+TLD6_ScYClCG>)y%{Em28yWdE$~5aD%% z)tFYT4D^xTqa%zHA@k?UwY`#l$m7t<60cj#f-l#|I#F1Sz?9t{LpFgY7 z59#U}w$k8Av7@<|TiF}GUj5Qqe=Eh?-?7IWj~tJ6m>O4A!l z^M%izdrq055_($t8s0$Wo9r2$158r8Bw%ySxpB%LklDJcz<8KGZRY0wp!x&Xaz#r? z5Q7ra2U^1LDjUh#yE+tN(GpWuq*uIFN;Z-f=x_XASfgkY%)QezY#cyPGm`-&zq?o} zgU3%lGq%nWOR3gf_(_N|05e1%>l{;K-o=kYaq`=@QkLcsBATCuq#!S3+e=wE6k@nr zQWF!#F5Qu9c1frNuJvHA_socxg7^~(gQ!!`7!eq?)fV+Xx;I$!9Z&xGoFSw{c9Z6v zbz^qAqBiKDimn8f#wko)=QzJ{;XGSnuxtjv4l+)N{su=4sy;kCsd4kOv&6-K{npS? zSw+q|%0{{ZX0HA?;YIht(g)|_n>Q}R?dD1vB70njWaQE-VxPG@D!01#soYsSFDSAx zrK1iS5*o5}v$G+M-+erK+`dD(fKqAp&-{@3B3XpUpeH`b(<2gW)Gu6vD}^J z$4MijCrd}te5KD(^ZPpP4tv{xAIIq)Eu8tihysv8Md~_&E(|c>6Aa%*&1}^}uk89h zcd#;+TljPZBgf+;&oV-*PE46V_Cmaj>6N?%ZVZ%vw#WM6l#Ou zo&UYEYT_zJ?cpy#J}odTT!*|e&P|&VX^PdiJiVnNSkBNE08|QGMGpNny}hhM*Eq%C zvPn0H^ay|u%q^VJ{@FvKvxb@goec)zp}Hc=Z(%zM5EB07KCcJvZ<}=QSAx~^(A7$+ zEdl|r`7+lP%siC2s?{-ru7N~5fZJpr+zPn8Lj3^1V3KBCgez>50Y7f!dA<S?-pezH+gCX@v>s7PvQ#?mx*9GEkx<@Y zHDsy^nZJk14&fd%4PJ}K5`L{GX{l?r2)bNwS;R318foQN<*Sb8Nz2+tdRMO{V#7l0 zhYc$>NK9TwtVfHA4*Q=w7(8eQg9W1rOgUpp1-$b=34`Zkq5qx4EEzt=@-t9L@0jie z<4nXfSQr9n6Ee+t83qJZ*gAu7hwU01+Y-Tj8Kuv?OW4*gI3)TgH{@8aRXI;vdFME`h;9D;nV!PkUSePjhfE(LTb{pZYgPkiy`W zgd(Ft-ME6>toYd1Enio$X`EGIJAqCdX9rqKVh0Cm1r#)v1Q=~-fw0TBXGJ)flQos~ z_SX&WND`CueXjpq>iMg%gqdev{HG|(@l%)*r>1b*b3gV?1GFu?m%o*ys;s7fD*5yo zpX)R}n>bce_?8fjyGcOSTYcezOiE=b*1~G_bT-xXi~ybS!YKwlzdRU0i?Rt-JFY59 zD5Zpk+iS~T^vQ=5vWV@UXt3-)Nk$;6%!!JubHeRg2`vhKKOc@u_<0hYqNAUtQUq2E zpu>gWAHh7Sz8Uw0M5XmNQ@_^S%5xQy>QeY>InyueYLu-y*^RadG)(`{&MWRkcgqT> z>zq>@(dvIv)qEiF3>rpgAdkX4wBfp{;6JEl(G zG)_o;m53oB-ErZ4YK91*IPo30Dq^)iG5ErN0^by(Kj~@sD?9UOt@%EwcP>}Mt3&XM z@%WJ-$Q#-;6kX|@@&r)!C)|EJ^+1rQEsNKDD2YyzloiH1R@T9h_9+)*TqR2J+{K1uL zV8(Hqdx?ty*~ds`q9Vl`K$v6}RTK2scPuXGB>aw|byKiX;lNN* z&Cs=)p}kO{gP{MPI-&K{?_a;LQ4d{lblhtyAZ!xEdq3dsV)5VIUSh0k@R+7)pRnJk zQ((x~H{v}xvOGP!C47|e@9J_bKqRQ)XBUt;)ViST#m$LV)6Ol3&H`miWUJ^gaN>-N zj4+(8nr)7CQW9`Fi@(2n)4TE@4Vr=fLOg?Iens>9yax6KqO>1suR5ulq>6 z5_Sn>-`GoPZs{WEq>vn1&(=4sDz6vu zotFCK{)DBr`)nLL(V-!X&w6<2-%9+up!`>a2vYSHxp&|D2D#pI+p8CF@FD4E5R_hp zh=5s>@m!J#xQUwNYhT_l(f>f~hPOVz^kZ3HNDKYY?;X0jx;S=0bGzQ?6!md1+AHrX z?m4|7NgxXOg}5I>vQ4f7f-Ictu)Ac_8%duMYdtVr;;E##!-y^F0(_MfC7nw4vs>@C z0H#B>I>_$c(R`lXo@}cQ4I4ir&)AOX5U$2AHIeG-B9t|BY5(xnVV3aMh6==^)r4)P<{l z4ef5DH(6PGvu|Px6o8=AteePj$hyRu6r?V;+GO8h-FO>DO}h@q=Fsy~=Avu|P|ov2 zQAS3CM;LY;kaKXF2Qg6Q^K|{wkj3b$ca-Gj=Qq!s&4>dhp`$Cq@wNNaW$_gHf$dd8 zPVllPS?d1%$5KM1k)yw1{Q*CI*FfkH7R7-SOFsJI!M1_NVIRE^?BN1e;PFEgCTmza z$b{M+qiiQfhy}{Zq$CvO&s>-X-W}A@7O?fy%Y9R1xN!|OpkoAXjcS+3~LlC*)zNK=1M{7yuKtS{uq$N{s(%L47 z@}bb!WS;)OT(nd@{YU1IRoofs7*wH2R~K=L(0jxJBX?)#+wzKamfsI~@F3}|N#+hqMHPfY@p}dDN^^(ub*&=FQYRl>l9CVEz z{J5KFmmzOKLj=(TUc}rMc7I?a$sQG9aSkRGsf8m2I|ekfDpAwmIl^&)haQ*#VuuS^ zZO5J1(XD$QhK+5tB7?ww12pTv;NZEGjIKS{bBmgdE8*|&D}Q%-kKOeSRGNa~pkV~I zr}ZrTPtv>`_-WiGD1Syf5wCmf*VX#*?%g~7A*U6`L1K@HNQ2J(m>4IcM(M>Er-+;c zzE%v(pUyhHxGJCH$OpEE{6t)=_BJ*O}|r6f37HHC5sFU%%AJbh?g6TJ=W7Vyo9 z-QBCtWQZ9i$3T=Yyp0eD%L?n^AP58y(A4NSgBbQggoV}cNHK7ILUQfipwFo#k2JD!b58Yn9$@SRHhWq0Rd=c6T6k;Va--PPH85u_(r< ze_ZOKt}_G7O%U#JX#Z4a{;A#(|H-(hsJ+2Gv1CaR3$|d8OeU>h&DZ z6Q@VO+@cjcg|{Kg85=sZ6nn}b$P$a6n%VaNZo&gg%m^Xx2U7_S3mO(er{{M9S0pE6 z8GU%kp!_-)Dm5G^+(0z1M7aU^Ix{DwI*r57Tf@rL86_pA(qwZ&m3c(HSPWW<>S}tj zwi;;+B1!n7KqN`@81cFt!?5e)RXF)?Kp2=S(gNxNvL~oyu`qG+;-$M%P^UuSWw#X{ z37{P07_h~JU;|#6n3w{Nj#zL!zQ^)LB@jw@{E!(lfG{{PWMcp(ETAXI3;HrTssb1W ziU;H>xV#C|c6_SXpopBm6On_f)FCBaLIc z5v>$?8QfN1bW^O!nXT_AIH99zm?!;wro`5hiali5cbQC%KM;imA2 z&d<#y>zS0sholAQX$G8BvJ5fAfDvG=4Q?201zu?6WDf}nB3em_D$$OWIIZ@1@ZUUy zf@D}!HzhHe_y<7y1Hr@ih;4(VV#A2Lqe;DIQ-~0}P#_IL6u<=YNR269w9L=~go6+U{;P|o7;?7^7n{_f!YV*}eMOwzMY52~jq_y~rsY=`>4 zdNsf>Imyy5q+p=-bP2FvtcSyl`*>Widg7P3^Olg^Prdp5EphW=fYKGRMhGKvvyW*V zp_PL8Xfr@T`-L|+>BKR0`ej=YGv?;?+G-0fc4I~xF33Nryp~dr7tv!S*j<3tul#dp z5S|b|@dCC6EL87j1j9)TmcgMQJw2Gd4{YAJpnz&14g*=r0o?~sFa#zD#_>L=*MPd9 zb%7=?b@*neS7Bj~-bbgQ9e@FFF8=2$k-_BwQ4_qb zu!#u9O5*UgC6gzpd?B0!q9pg?P#|2cTcq&d z?0i{+lLtls*f+3iTn}Vu$U;yPHffh-S6;T9=N)*)H*-g@!eRbn-Jo5%$(63feQXhA zGSk?hVS|=t)0nd~|5Lyo_%5J+EtI6{Z*+Wro9m+0N)w)+JK|H-Ih@$&qWk~KqzlG# z$$TTxn};hGL$Za*X%;A@D?I)pPDA+31fQ9Vw(0`U?r(m(dx>ek*An5J6EL{~C+hNt zA5{D+fCYyA#Q~2W}qVo|YM33W}Cb8{t59>%ITAPG}dN&TLT??ms+(<>vmS z2;L~tTE=XkmBLG$17ri9e%)OUv|%vGmeOp4T9|9QaJ0EPm>xm=66v3%tCcsd>z@@= zeT>74^1~F(CgK;;3$6>ACz@zz0-3IV6!;!J%075`?dNNZayo*`QdNr!TWDEoe=G}LvdPyDZJ$+9v!f-2F|GZ=f~;4yLi23kNoM|yu{x>{WJXU z6$p`m*_#j_1)^Mp#tad8`De&ULGgE9rc-WlS5PM|QR@8~^DO!FL9@MC&)%C!0QSa@ zBf&#^wsK}B{)$1}0OMd*;r=!98E4+{&|q`f(+JSSw@{{`JOX$LPysXwk|5xen1lP) zA1;4?&3prZgkQNSDgMk<&!G453q)b3C#txPc1P7Oftz0;HwAu*2O7s7tpZ_;5!zv> z0wZd0obkG}(Bo{Q*ZlH0FHZ(E2++%8r$h!W^CzSr3!_SKf}HUvEwiIK77zj=!^!krp#7FEN7MO|6 z9YpNJGnwPEj$!@jJpoFBSXpUmf>O^n8*9oY4>_4=I^6jb96ZV)^zw={*Lz5Wan)9A z0r2Ees6YjPg9YP;Wq&zl?c@lWFC4BcF97riL0k3!4=|8OJ<)yJl%Vp)bbi!9&B22L zlRGjFg6ghs5jKBwr~K+R5r~9=bwFZ^JI}-y-#%p-MVD`dxQ(&3UoW|G;~tWE#u+pb4hB~O$EJ+GTT z#H#C;`!R~1)qY;T_(X)oUXQa8CV>PP*AxVQMRbB+egOWKZoba>X(P*)9vo(1{iG1Z zxg|B|#~wIqLgR`V6TEQjqO@JisPF&DfFc=vI^3Rc%5cCTSUO;+ZFEkY^OLNDg{~mm zLszcd>q2W)B)NjgJK18qYrPfwb6a6e)_Zz}Y@dmlDx3!oZ#|rH7aC{CUa)p;#K+=X z!sas7gK?KoFd~Y8T8C0b4VF+e1+xx2>gPQdOEQ?o`#SX&jS^)`{|h2E}^IS}o?I zqEYyOv&_O9-ltF(HU}tiLM;aHHcc6cJN+w6uTTT{4+>^WiHW2PowJdtjON!r2H}Z8 zM5OK}+#hm9ZFBYR8l<)Ampr*ek)|U{gR>T@y!6wKlAhdi2`G;5z8(~ELqU2mWSe;$ zd+r8d%!(;7oax%t^;I8PD7*dg{wXz&&Cme@^9C@*gJ}|U)+GNzE|16$?-D@Ygmv1r ziyAw?q$p#iADPE(&fOsq;CnNtluucWkDkNFoIk90)utKuY5s&A|MiR*{Krbm_3ie6 zEG#KF7@Pk3zuFlPk=bbd04_i0C^9bkZ98DtdfvQ5M_|R5J-pwnBk>C(6s zM>Yg)6VbXs;bz>Pg?{ z$@iuFLCcV|*71vrpLhG>1GrMmTtN4tou(8J#~;10(QEU^@vT7Rqn#9*Jl)3DW*#v_|x`SOL5^gVphJvjUj`aqzgAI+3hi)otU|A|yAD%M?^gArhIaA>IirYhE_c7cDTJ=$T#TAe+LR(Dd@JcF5Zdb z?0RPi@+#M+qjiSEc<)u2k6NjV z3KeWnHa97Ae8S|tDY#r}sfKIg$KlcIB>Wyag!y)uRi44jGFxC$ca2#b8z${-ug=XE z|MfDkaMCGgZ!P0j;U(rz=6#}1k}POk51&WIM>~VG4O#Hmx04^&MU@m>3>p7k1S$i9 zM1Lusk+0zos5r|t>m|o(G-+-{)XGoR25L;6t}!)cx8;c%ye~ZOfU%5{C#9l= zc3gxBfd&$=C7N=yMktVpxU&8+at(t2hd-R~>*>3nrXzfD)HwXrwB^Xn&%r?&S39{d zHFeDJWp1v%&;hNQO_SSqLd5zAdrZ>%47R5n{jXzb8#)KLN%#xXZH=(qYtJr?@n*h} z8#&(uvo$r(jcX^bUa>cYmoBloz(Im2wI%Jg?JH*1lX6g6%028-{nX{YvrCLJn3(~{ z>0dg|tWut%PGm=uj!uiCelX?Q0QD-JlxCbY|JBqO7{s|FEE*n6Bm$&PJXAYG;1hu^ zXl07P`N8^QmpOvUQ8SQd9AzR}7P7hr+%U2JSRTzITsQjtz(6{J%Hu9Uae!2VO7m-+ z6o*R^Mhv5M5Q@y;ghoa*2#$-jKiIMXAuJ%hv{vngZ_W4Tq{-5}#6}p4@4DerY8oeE z{szl`5=(s*d07A+z*??p{te`JknE>V3ZEEIZ)jGx`yeZhW z3xynkcHUx@O-=Q+I;2DU^^5qviF1q|p0Ql)427mRmHxF8`La!r zL0hb98*{qjHO3jtl;6rC@v+MHc1=FqmAyL#RVTwD^%%nL*y4_(2PB?U02%@D0+XJo z%4oDEI#QVC04d;L^z*yq0dZyG>^(@)<^>&2gchy=w>r$~FWk6D=|iysdDuJ_X?tjs z-}!X1XL%X&O6uOI@tEV{0US$BnuI@H4U61nFHW?6VNv?Y*FBe) z-?wk4$B}r0a#^}D=U<0wL_;emUwt-7*>FlX;Mfgg4)t~nuI8-1CaYg?m#U4^wOd=M ziWiQL@MrMF;5erZFa&h8C>*ePf+)p+*{tA!cKoG`k;B)_H^E$vXjcL7qx(UfgQ89) z`KS-7APnv$6he)J%H3D3-OBr1KuJRswYrU<*J5DF6)8i9?k_^g&%mML_>z5xa9WE$ zhN&iEL5?9l@QqM0{98=p@>;z9KFUMXYLHwbJ^?$$eTSRG8K6_Z&$omJ$o~WlO;*-p zWF?z5`eA*+Ce&4^r%*ftq8G;?^NM-X5k)eO3m5_LJYhkwdhD6OHi*md2)t_MI0$j; zqaBTCRLFK<`10Ygz`r0Il4h9A4qy}5R2<|MiE8d+!IZ8@WmW#4g4K@;mfi@g*Q7B? zFb@#Vp63Yq^&!Y=%5rJDeFST`TGt>W+aGHhL)a*|R<))bpZ=i6!b30Uu;8owlk!jZ z)MB?>#!W{NbYo9i$5Y$zp*N{J*Q168I)jOfw^Q?E3lxA*TxT*1t&h?j9~+1SgkoTH zF|4B+j|C*j)tpscVGlLc1J?PQL)Gt9&q!UqFVR@+*!#zGs`Zo-1-6?-B=U3_GWs!e zNLHfaD|Y9T%wXxe;AVK7ac2%3k;vQm9>UsBKyv86SMZuWK z1$N(O-C>+u#Pjv!Nna=-z>orMLX(Y-6HpA8G9W%oFtNcyb5G~7-=&CMokbM|Cmhx# z6=dDoL#tvJ5<8(x~lzQk=L0j5*`CjC7|4RWfbaUl8Jy zp(Ce#-`M#*>;alc^Ymok}o;$QAquN*KT==y(>Jsep0f#JXcdctQ! z5CLpRKzQg7pcfvC%eHi|mXTKv4Sa6~2npN{UI+(x5xy9{fh$@&1a-2+2j>V!7R45@ zrncV=1p*;iL-ryJC&bg-VK%o8BOiptAg)qB+hBqmyA+2}(>=V(GoBC!pI#h4?l@I6 zf4^0d)3x$Q$#ujtwM>bS#~-a@SCl-^wL$}h@*991N-K;zKW+Tj)5DV3cw+aB`17!x+a90?}?oN7!w6iPSIfC6+N9zPIXgFF16^6I>;+M=|a z?F9+1fJRMprQBiH{c(DL+YyQsrV@BRpbWrLaC&efiO(1}ciG^edDOaC>KS{M-GzRh z!js$zY5$%-bv!U47)JJ)2qb2cvER0B&xcC!+RF5GFvWB~=?TCFT|g-K?t z{!Z~2`X&K4&>e94`9HOpl$4rOJHQP@Ku8AhKoju;PT~=(O;TSuTo0u0QO-DTrSbNd z6;9|`bQI*jg&#IP8@NsccI$1lPJSTsp@ks=&UoFn*^3)&fB3{sY?_Ihh>uhqE(&{A zwZcGSE%^Xu59)P@i3X@{bgb6j43dw=%lgK^;39Ibc`vFQviD#x^3Xe+cn^=5JXIm) zaolmdJ(&n6vO*3<^ievYXpr|;rF)D@2nO&5s{d0S8M&)ueMUEj%tsWJH~^+*W~)K6 zAPR2}=zhRGZ@*{U`*kI;YKhcBIwE&{d5vJ~_*USWxP63BftUCo&fS)|)<8Ja-PvP1z=Rw+K*Df*&B{0{Xtot8g0hd$QZl|Aebe3xy0Zz1ejP(0rP7VN%!}McJ9XG?XwGAu$f~ zCJ?zG#^Gadg{@eG{NJ6dKMz%okR308h~ZmD32=fa&7inLuK?;9Q@PAu_YpNf$&@ia zKRFhsl6$^r>&)DicL#4GIuX$hPE+eyfM=~|1H8yuI*7{Nqx&Ike>*- zMmMG|y&^K8&eRp>9aHeIAdqv+@;*K^+!ibLlDD!zE~%8#=ClX8nU}@jq0>aS`}_Hc z@7WWC!EX7#zA*6naTU*@&)$c!EqNFO8i4`+V1mA`&#%Hlm;l)y}6;)(1WBL33Eu(QdViFu18Q2aB*)5O) zOHuKkycY;P1qCVq69J}Ma*Xb*3u_MH`>93%U!8_Yi}U}MmX z4(>y(6*fwfRpIvb%rN&s3xnPUofJ~AUU6(p%(c(<$}Oj3!0~eIh7~S9Zzvcsm?ilc zD2eUZff3Z^IdAd=!b%Hx{mMGu&}Y(T#Kks|hYEo^%*ca!W!vJ`mN|( ziD#SZk2gWDh^^7q^#(RwucPBrJFR_#owKvjC>?n$4;dYWdbMeRT`=|zqA29>?-Sub50sMj6P`bE zzp#J%WO-DL1*l-+HGBE;>*`e+tC?$8m4nkQXnp=qoR(g{rS&u!7Lwo<{OO=x^Gv3r}` zW=_tBJCrI}KP76{AKLQn-gH^Pv$*%|XX6xg-t3YQ{8nmr^1b5mvcqj-&V<~DcPLOo z{M~BhpjXNNPqXb)A5C-{z>jfhQJbR_CRpM>6F%Z@z3ff9w#NK|S%~L;<2%277;U=Y z#e{N+teM7>DensReKh2a{B`(Z&PumSOiimaf*9%ys~Lb#1vUFC9B4t*suD@KUNiLQ z5>2#_tvCk?5nZc5W>ll5;&FwG^|%J+kZ=$-9^+=KD3(Ws!Ph^jPkY&gq882DuRkmP zx7t`zFVV*wBX2v}a)dHv++t05#CKw~`q>@_x%tdVa`2+Ga|YuWnRqsDPEk`ctddI% zw|<;mVcG(D4WuJ$f$|zm{uAv$UWmoD6gVc}F>_dc;q0O425Qid1oOb-K^{)o{P%Zv z`daO(jqGH&j1rT?IqZA~mxOr}pj@jcz(tUP0fMZOT}?O9Qpd+Lm7ltJd%LCN!pu=@ zlYzZRQPC-j8b{v@1UrMYD4L~AGF&MXJcBf_vj9=ts68L2YgmH>W^E4WRT@|2+xrTI?H=B# z+8`I2foHwUX~zHmyg*kf?Pnf7j$#fLC_JLM!R%Zjelk1M6>i2_T+ zr9cTniUS-%GJ^+T26Pchb%;hh>LEsl_RLzr=%dVcx0QRb{oV)D=X=9{LHG6aC=ErG zc~(eTp(7j~13L%4Nj$Rr6?^Jre6)X>C%F{3S_<|EXkqSC=}@2cYGD z%WpkBNio;m-Tl&{I-)ln@<;gkXJf|bKw`yngrq+~sB=Kufn6ikLkRg~c`XF$sD}}g z08pc^A)pyrMIwfBt2<{xiE9n)k>{+jTuIc%lnHe!t9y=Xtp!sg3+E3cvjMOH3x*mD z>^OnjD9xmz9sxgvvxg;*>w>% zI(XA!48Y9MaV?o?g`FUXHz+>vwEz&JMp1&X^b$Cy;4V;4BJtzZ5+Lj+ zMaLDEss7*a8=SGTcKRGCx*+fDa!{V3S0U=r`QJpYySx^FSS+o~1Hy{C6p6uu=F`3# zvQ4-E7!@RKCssfmF~ko)k?6uV5~8)WC~;RcKX&fimuPuj7(LmQWJ6y@pctjA90aXE z$_BW!^9a1j!kK)_BMaa<0R^G|qDER4P( zF7HqQ!nq%2v)S9~*WzITxi|f{p*yxB|4p0AQs&p+Z+2-#K0feL_@JjpebHTp{-TXy z*Z2~9*DyY~`Y5DkfPd?1d1V=%*;N}|%@5N!JFK@@yZ-Qc=kirnz4tG^eC!!3t<{-y zV9xTi_Z4HM&2Os4>}pAU*0n#d$2RoyW_IbRDZ9LZV~yQMuc7mV^+-(?WVE zWJI(#knH1jqRBzkKvrAh5)c#_R%fU;*l#@zEwjyiXD*55#y7{s)!C)LNVt9v7nk>~ zF`be)m;D~+{Fg*%MQP~br_fx7T|vX9@ARMO@M#oaA`vqY@JAwssnE45i0wosK_QAi z@eJ&I(X6t*=at-m<_|cnB$bd=tiTz$d3m>>7DQYI3(m-6#_`4ZWuDlg)gDB_}v6&C^SadK>VMc8Jw$GoqBv%2@aHs*xgdgY#g++XknoV?0eHc?WT8O zp!+x+MBnkS8dosTecHKmCsdsu(BenKWFhP_N zqL=_k5=9~c3#PJQS@~M+PK1I64#}2qX<3(@-=q2S+(n~amdYptyB53ai=b0tD*I7J zdE(hS-D_Q$T~~XFigtWI|I8h#RoGt~m0vgVrg@e_@*Hlc^rvB<ml01E44UWFwZi&nc3U25@oN-QHQAi2|RyY@##2!J;lGaysJfYbs(2sFQF7Tf9op49>AvW!S5X)v>H#M~;KR$#Zg0@(EqcjE|3ij>NJHvc2W!S^Ct2(C=$o&)+RdxssxK zO$}hJ~csw~lSryRS1sk^2GfDRJgJ#7x8D`@#Dv9Ru96ywYn20d4zeb`vFM{d{cZ@e8F8|o$uck ziaJ<~0m+I=2G$Gmuhn(~-dZDZn%NL}{3P2?{^&b{N+L7CvZvkl@G^Zo0M`g}$?VUrly+x@}+i~;`fC;xNy>3OeA^r^n?O*1p17tk@{OojAnA-{72=2k!-`i zvJ(q}hK0tzuj;KvooBl_jGimQONBcePS+d(Od504FG_G6T<%@;jWjO&EZX@RW^Ry2 z@`nBcq)-kL0~M5D=>`D06I}-Y6NApRX`?oXce$c#8_RHGujsaWcHV_R&S-lHqB)H% z7|;7IB2y{PomKX;y6;Y7oBJ!{@9xN|UY;p8T4GxP$J1SxC-!>}?kBKLpYg|9SxzDe z-_-3(kWsgu-`#C5sa;shPN3Y9-Lr?_e?z?hW5};3o zcaGr}b^bGVb*+wMwrvWENK6z!TZ0!eGndOWNA}L` zL){53<+mTzBRqUyV!*<}gKUxash)P)Z4zU1jcn#brKmV2DB3=4aTRQz0)KAsH~xLm zevgTWx9>57!PJBhg%gP;VLWc^%NJiTN-$_lKX@-OFIzTnyU@hCn5h$Yt~Po*Ou@R( z4@v>3HPdA95h^}=8t)%;lnl$^Ul*wE99li(6f@L)lb)-5q51Oe2t`LoG@g0E2NuWq ziPTJvtAN~bbYSS&n9|i#TSc+RRn06CUic$7bH&H^RDRZs>*{(HZtlfEwc2C1==z&} zWow2<-^hv$6uIgz-=oI;o8Mz0ef-&CVXXX(teX6F1+vKIzho zN_n=qFYTl(3d^PDYr#p4=LmS_{++7(>7K!%sq*x8ZwQ&Fb{rC)o zSZJlTDJm=4#5wQx+4DHd1y!_p{?CbJZi36%DlQ2xGjxkMu=yJojbs$-9VBvFJ27!C`o`xZ(9Y$KeRjZ9w zwrEknjB8~pY@>ICx%CP7TlV$wgCSMR)WY>kY+M7tnIdNg?X7c?9n?(;$TfS@T35U2 z8o>Dj$Ghc=3%Vg-@=(Hdu-rCph}V1AXW3&ViJ;C62?+&SR8$WDP*6bR^q4~{hgb{l zpl*x(e*a-ZVwt>b<2PwIU17Qhi`q8eOX92M+kbvXP#DziFsH#(0SYS!D$#Sd0 zO&NDoj*qO}i?tN9&sY_1DToAqvJy-riaJi2;e`%U&5%KY8sRTcQ5M)n^h8yGq&C zsQSxRM&WdogOVggP|j|Quj2v7OpUV&i1zCc$bmI(fAs8~&-C-KLLT!E^X|Zpt)GX? zK8(^7WD@GG_`zdg$T30f7y;QMk^sqP1CFG2Lim%dg7<0EDYE~-FMFwYBODPtihu_TN;qfkVcatf+Rmpeg(f%+gHC|Xp=;}J9tSwRkP6%ZP z98)pE60Z5-D&@DL?^Gzt$(TCFw#;z*#_hRZH$C;6$t_6Jo zUOCP|X-~RYGPa~G`-5$^F8bjSZ&E4v0IqR|{ORu0Sd3HQfH3XYbi#!)F|wCt3;{O% zqZ~^?i2c$2cpYJzXxHSU#7c8T&xmATlx~0Tbjda6dH+tzpQEDz%=d=npWxD?@z`ZX zx{pT^lPrbJn(Qa=2ra`3HU-U<=kE_2D3w(?9YMD)%iJ$5Xt4;A6X_YzhXeu-<4vRp2Jvv@wXf9bnxb_~KG2{EiqZKD`$wemCuF_n@jw(>BuH%KXjNtPGBq{A= zpk8AOR4OO=<>GZuY-fi}f;>wWPuexj)mvZC70$@S);%&xC$iy(hY$Z{Z4)-Xc%Fj_ z3JXf${oXSoSJ)`NeawHe0g$t6o%QZ$9cUt6Rt`z%0-LvX(-jNUy^;37Qj)WSK^)~Y zh@(5*;Z8I7hyXPx9z^f3iuTdswkun!9yE+BirzslP2Z?Ug^?z|lPR<9fKVGi4+-;a<6r2f~W8f+%((%6`X<}+3V-a(6@9gt;n}EZa?wM-KRTMJ$?NVZ~FuQBGkmcddr6-Mk_Nj zkJ8d^G1XWa8x_b`!^j4{KxAQFXa%Oxo_b-t>Z$L6v{nq!VFN_d`P2QiLM%`^o%$Uc z`AX~I;pcM-cFc{@s0t8&(6+!%_y(rB>fgNOVkuBqtZ#w>OYM~wG9E%VP6xR{mf4L4{X?#mXl&0xk|46@(+>m+dmEXyH`&@^8@sztARQ+~E(sJ7G+wvP{^`D3gaSN9l zXA!{@?}nQCUv&s*1BVFjf$;Uw=IifB54JOgPv1m2!@Y?RUF z9(YzYlbdMHUc22a$x)#iw;P2rdP2ZoqytAWX17fQa~REI1W*8{1cM>GLx=W_iA;Zg z*_T@E@`Vao>>lX`=e9P-d< ze%0X6h2x*guX$@NP2%Ui4qO&mD4GeF&cmz|u?m}?k@)ZK?XzdFu7n%`N6+ zC=#)e5KOTsuGXJgk=jf5`BA%Y+=XhXfRNU(UuuOYk(KcC(=&IA3Sg632tI8fe!WH*9 zBR=cYGd8AGGG-$(5X^>~nJn!D?ukz*D=$yHzqFGdZ3rf0CfpT}C}DNVe6YN*6nEwe zyIBRVm0H*xwP!}*j%=Eqi8u}rk3k3nqctK@kF26_%Qz?#?gO@CMdG7JbKm(&_?(}h zL}c2Ru=fAZqG#l->8uF4_}&x*E_?v#9Ju*0fU77)hSNdG8L;3<8nh4=k-mgjzERTu<3r3vJ@1-8nCom12Gyx zI0+)=lB($j@L}|=7&zk3=9=K-A0-Jt%6Eb@ zMe|F;z$EMAFMwj~j}EVRg%Ktk%u&bEVL`SdiYd^G0K072d@;uMwxu$C`-toJL@QYa{}1@+3!gkMh+Y^QH>jMR;fkMl_FfSUl1Z!~9mvQc_J z5Fj)WP(K#9e;59zghd~MOoecan~8Z#42AZ0{t(lwB0i~Te^YGyodI}yZxa_G@{PR{Z1;y(>Lpv+(!ljTs zGr+MMB@-lreht|Cl~`O{yla2>iiDh;);21`n$|QRNe|O!;B)dssSpqvE*+RTWZu+6AwfY9sFsX5-#@UI zt?kY^Z-+CI-^|IagpZ9kY-?CgFH|4r9sjY^dFA&7Vwx8~$YZxZ(-jS7fk9G_O)~#K zvfc!&=d|tr&sLU{v7}@P*_%+d3T4lh5kj(LOQBt~S`s3%42cqAlBPnTw2KldNz$Sf z6)GturGBrg=6Rm~as2P2WA1zI8TIY+xt8;MZznB39bZN|f%Lx>W2>8DL*0VRX8dp% zM0NpSn`U)?HVENb$XHkfx!p}IEl+Fq|4-s*5TtmoC8DEuHZDdeKdm>d5GFoo`9rxEnX|B4|Fm0;8x1~>2QoWX|)!s|# zD}vJct@g~}iLyr6)jQPWR(mSo!gF}Z(aiP8L(AL}GGT9~TZChzeYF~5H@(wWcQQK~ z5a?4`nc(+eN5H;^ozaz$ppiRIW7eN9bbLK`t1F~KF*g)VNI2s{Z?_QTd+UeZQN(+XA&vgm~cn=vCd+?laQjK z{-!kLxT3m2P{0+0YI8v+nX%`O4|q3Mcj~yzCHvGhyKD{z6$W=LuCL4!E16XTq=ws4 z3~9*zLIu&8yBjUfrIQ&xBal}}9($g%>+oO`4{L4nry5GL>QMh-HxMaQq$6?rab!__ za#})JaNJDCo{D5LGBTye%i>ih48OkDgv=C9M*U+?i()6`y?`IKHArI6H~>+P?Unkw7qS^Tb<*y0W5`8HMahK==%82@&PaflREdo3TPN{slIethxycs zqQ*%qzM3`AXz2Xs$_)(-foe?RaOwif!?p+DLQzL>C4*GlNN;2Lu`dOS`q$`-YLClD zKXcQQynp6k5zLv)(vfq5LpDpwu0EAN?%}~MeYJ*^tb~#mPa$~S!&`~g@JAHt0IRBA zCp1@r5ws5iwk!^|0WYpp*vY7OhQG8vcB$W1y1}7Sf4j8wqf#1uijC=-#LCU)vd)3GK|CfL+cke(x03#cs=v-?9tv+pa?+rNL<-eErhHy-OP0KsPn1G(S+E2(8rL#b*lT!!JZJtY zk8Fw7LsFjEl26Su)uQ0WT0M#etO7s*2%*SRI>2}vVk3#SCBc-jb4fny21#ZImee+W zBc`HiZfmuVP+~8c)$Ss8m=GujP`w=jdlQMr(V^9(?2JT%`^S9~cIa=L< zA8k>Pm3sKGXMlCX?9`obX_79;;`#BTxFWzW z_Ku(Pg*)nr(|V+hEiR6j8;2PH((Bb+ZSXi?K&%m@LWtK@zZwtQweu58H5fbO(}ib! zchh49a+xZUH~xIwU$5_yP2uAmVq%LMQE!N+`@b$%dNpC9i!OCMre@8i!l+>4=Pb9e zDK}fF;~3s>{tr6A#r6x2Fbka-GFzW_Exq<%;vel4HPXof=ET7|iaB2lv`0_MZsdj* z3P_p}Z`WUkXp@Bz6@e6bv;EDUt#`8)Zw~M9q>->2#lH?R_1_P#NBiFY^7&DCK2m)#l5WI9);9z}-uK-%|#|#^-$_6NXN?5gD^XZ=aZv zV;FN|@xr!b{%c_ubnAbTwwI`1$qjufwtPnbZ4ZeM&!}P(?945Pe zoR!}lZRcD0gaqXHlD#VPPDX=?1q{ z1&1TOKkHoM#K(zgz3?c0Yc4OpHE&#nO2qkY7?ILvHmOv;j{q3qX1lv)$+$kcDYm5lP_AEZ?C;#`^MLFm=+Rm^4JfB2&@)nLrJM0Rx^yNfB0DK)y*5L-_N$G zes{Y#e-ms0a65noXFacu3?hRrv#dC|=!7t~Iz`X^p zJL&X-+<9R-1u4e$1y7l(H|oQ>!e4USL1;3p3#(^LpB`P^FmTYI;)eaL4K-SEwOS^3 zrza`AHd^)g(>f=M?X!oS*&+Wu9I+YKCA{cC9i5}od&Uz+C87BH-tSxIah*e zyUI*-E;oz!wFhW0Z5o7lnBR~$&gsMf%F#r@O?CFlzU#kTYH}=%e$0;95NC4z!*3^L zkEp~Cc~3?Cb0AMko9iOeuVvEAp9!*8a=wIlG}PaSRAY|#gB>F(ad!sfsBFEc64^P$ zzwvu1j=EBVt~ErskYxbc4YJP}yoCaU4j7L#QW#tLI(pLoeA-?wp8hIRPm{J50w2&v zpc0Tl{>npi-hlI5)oXdM!SbLi!7=)r?Vi54tgPE&WE9e_Xq7Ok;Anqb9kpPY*&mh&4iK#`hra#`E zs{QlWZ*v@mVL9KT%VvaDRyMqlK3AD+qmW*(>YzYPLR)`@mWrI_O7H!z4Ty28obL5> znh7H4#9F7Bh1bs;N$0DiF~CmhEmB}%kl_aBYyle)(n2KJ++;j*>GL2b?%DY4p$)Rms;3La8aKS&w=I{JxOM&$*i1J0%<4Yv=U?H+0ngbk^u z!V+8-v_~Q>h_q*9e`wBx(E~h%lJq}N1A0-Zmr?m8g)&d>Vo1Gq=?W8zz4BLFux_v% zw{6Us3Xl1^mc-L}O6J1J3-urE4#3>64N>pSY4l#tI(TC$0mcDYqba3;nSP!b(s)Kv zVL?%qo5Ay`Nxx0)wmwid8s(j3_H?-C^h1WhcK;eZy1Z+%ku+4wgP*{);rHy(wzQYO zfZS;K%pYDhm>TuI>Va&tPv?;6EqnEw7{LS!Mzf9ZPhf`4`uV1n(GNfU>i@ zkUNZ)l*|9k15Ec%?!G+RLCX`bWJpK!*s@>`NM)$IdKPV+b8fwAe_@pz)O0^EFi?Gc zp((fzUFf95=J3-AOW@b9Wz#DT{IWS=2w04NgI@~we8E_C_OJY-al z+@5D=4Z;pf_lcWGppWv>C6JS}iO488-Gw5THO`u9Z?s-ql{|OTtDBmaF86G)Wf4K4 zJ0hx6G~)7{(^D_Tbd^$AxKDsR1qDge1>D<0AHWHUr-6zQZ)QNI2ukMn5nd%qLPAJ> z44B!0>jYM3rCu7HB`=X{4HJ~V^*ye31fq2FEA?(?eZKH4MK5YgC|<$1Ylgd=oSH)( zbvdJ9l#`j#U=eBb&v|FGkwR~Yoe%`b_aLfB>R-jQ^W3lmz5Pn=Vr{T@K&HkJ8CD5D z5=J#3)16`WZn8K0eY)JZ=M(KG%)4vWT&*6Z@Hr^^3wpf?6DAPF@5*ui%jMn!?}oBr zKT^Ot7_d>8&cz@eS{q#>#9s(jsA0h$#6uH^%++nxCc{}cLn1DJ#5p6$JhMS*p9mz- z9xN{(vGYQ`2vC?_`%)bYWTxNK0@$)6ezc#7D4MM#($A~4rWQ8$+(5H`rM$JZDXvwk z&GfK%d;y3=U7~@y#v|ikz_EP&_b%L~=x}%FyQ@sk?;xk{g!Q$UR7BY8Wh1Q}Fzuh2a?etLy?? z*Sc+oN0*okdbe`~+muX_?P@a`Kb!+$=Yxgc?Az!Y*))H2-v`QJS2}M$m!d2Lotzhh z8Pj0{6a!Zn{@6@%BQq(`Qlg<$-(YTLDFp4T}#lH9R#;q!54-5l>c=6q|ux|7dWlMZ*zfgkTU9r#~48TfH)Sm`zd zx7=q!n#!}1k(CwCr!9|csc!&|M0A#Q=39+s?wejPr*QF&`>U931QmcSbkEBL{>v#x zIA6s&Z=ZTh*)1l8G7fFY$S6yDo!d7UAcBZjh;^qvQ@tM59pO%#Vc`A=gRfA4R_9<@ zY9FsEtm>PnECjbq1j^IRAe|Q9My=nLJ4Sa&oh(>0(IZ-e1_tc6tezht^LnfzU`M|3 z93r~)HB8$yrtde+>7`-6c6ZnBF5Ck=ZqDx##*kaBi}S0L2~C^6ui*nWtB5^c<&{x$B) zoURwwimZD>!$$#EHvsPuN@ng`SzuZn=`s3V{$(Y3zSPZ$RN zRLkMuiKm;%+}s$?P`&RfnhnZcwEo)I+GE+wMrG%a%nBChx`^v(2Ukw&)hT?93d-2Y zU3XlcaIPiVXsEM(hxL%!I_2#X!G+I^0tlTWl}LNE!JDhdUJtYleAvD4f@Y~T)y9B& zhrpF)7ab)INKRq3JlSmJdhJ>L5acX!v(a48m(tv0ltW##-Q-NXLe4tbv5Uv|)_>$e zlg1k7y8pa&2_*Wc+ql;CcIihZ-TRRyQ&O&!)aT6l4EB^ znri(c-Y#0v-Bq(aI)a)~+_5YhVEx_I3+htXEO-)yioI

OFObVu1PDX8PcUmtj)1h>5D1yFV&#(2K8~ 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

K@qHevILZ zki@x)-4jehR>958Cl?4(j)Hq=3_QeyoxPOE?80g@PhRA%jEv(bJ)(3Q%pIkRQSu+Xsl!&PT&e$9)69@+xpGVNsWsPwb~vWJEEAawB) zVS%??oea$a&2dEV#l*~g`k9yN5`NIr%88V5*8aS64>YV!S@%ah8_w-!69y+tbe(!M zIFk`maAzwR9hBV20jC0|>H~rR_<$pbBh(B(3i@oLGEA9BUa}V)54Zp{X->b`Bhcbc zUp#X9rb28t|2f7)fr}!$l?Qz6K#SjA1`qBtp$7d>*R4&NNs3O^m%?TP?W(896oE8+xlbhl)3bAJ<#SgDaUij33-v*Nvxsw|4?09KcpK}AygCig|{2m4_~RUhUO_$1q&8W{R)^=kATE53_(e3$@`-l3+_VMT1>vU?Ku82*Vwm-C?N58Y*AFlD>3Yd?sMM z=aJxG>Rx&mgA-8?F1{!dD#M(q^sCJfy~rJ($F2OTYN6|;zQno1V+H3P?oyu^Jzulq zLcb$2CZcj9r!nUNr)L1DHxZXw0>bw4gD!FL9 z%92f)PSKq8PxjB>2<>HLTsi|d5#pZJZf;cWW%%?C4#mfN?g1QH3g!;L%s&

hA(jdssmA?5FmORB8=kED@DNKrcSqb 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 Am;e9( literal 0 HcmV?d00001 diff --git a/tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_5061d76b_0.png b/tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_c368c1ef_0.png similarity index 100% rename from tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_5061d76b_0.png rename to tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_c368c1ef_0.png diff --git a/tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_5061d76b_1.png b/tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_c368c1ef_1.png similarity index 100% rename from tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_5061d76b_1.png rename to tutorials/W0D2_PythonWorkshop2/static/W0D2_Tutorial1_Solution_c368c1ef_1.png diff --git a/tutorials/W0D2_PythonWorkshop2/student/W0D2_Tutorial1.ipynb b/tutorials/W0D2_PythonWorkshop2/student/W0D2_Tutorial1.ipynb index 7447ec4..009a421 100644 --- a/tutorials/W0D2_PythonWorkshop2/student/W0D2_Tutorial1.ipynb +++ b/tutorials/W0D2_PythonWorkshop2/student/W0D2_Tutorial1.ipynb @@ -47,9 +47,10 @@ "source": [ "---\n", "## Tutorial objectives\n", + "\n", "We learned basic Python and NumPy concepts in the previous tutorial. These new and efficient coding techniques can be applied repeatedly in tutorials from the NMA course, and elsewhere.\n", "\n", - "In this tutorial, we'll introduce spikes in our LIF neuron and evaluate the refractory period's effect in spiking dynamics!\n" + "In this tutorial, we'll introduce spikes in our LIF neuron and evaluate the refractory period's effect in spiking dynamics!" ] }, { @@ -296,7 +297,7 @@ "\n", "
\n", "\n", - "Another important statistic is the sample [histogram](https://en.wikipedia.org/wiki/Histogram). For our LIF neuron it provides an approximate representation of the distribution of membrane potential $V_m(t)$ at time $t=t_k\\in[0,t_{max}]$. For $N$ realizations $V\\left(t_k\\right)$ and $J$ bins is given by:\n", + "Another important statistic is the sample [histogram](https://en.wikipedia.org/wiki/Histogram). For our LIF neuron, it provides an approximate representation of the distribution of membrane potential $V_m(t)$ at time $t=t_k\\in[0,t_{max}]$. For $N$ realizations $V\\left(t_k\\right)$ and $J$ bins is given by:\n", "\n", "
\n", "\\begin{equation}\n", @@ -573,7 +574,7 @@ "execution": {} }, "source": [ - "A spike takes place whenever $V(t)$ crosses $V_{th}$. In that case, a spike is recorded and $V(t)$ resets to $V_{reset}$ value. This is summarized in the *reset condition*:\n", + "A spike occures whenever $V(t)$ crosses $V_{th}$. In that case, a spike is recorded, and $V(t)$ resets to $V_{reset}$ value. This is summarized in the *reset condition*:\n", "\n", "\\begin{equation}\n", "V(t) = V_{reset}\\quad \\text{ if } V(t)\\geq V_{th}\n", @@ -735,7 +736,7 @@ "for step, t in enumerate(t_range):\n", "\n", " # Skip first iteration\n", - " if step==0:\n", + " if step == 0:\n", " continue\n", "\n", " # Compute v_n\n", @@ -795,7 +796,7 @@ "execution": {} }, "source": [ - "[*Click for solution*](https://github.com/NeuromatchAcademy/precourse/tree/main/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_9aaee1d8.py)\n", + "[*Click for solution*](https://github.com/NeuromatchAcademy/precourse/tree/main/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_950b2963.py)\n", "\n" ] }, @@ -897,7 +898,7 @@ "execution": {} }, "source": [ - "Numpy arrays can be indexed by boolean arrays to select a subset of elements (also works with lists of booleans).\n", + "Boolean arrays can index NumPy arrays to select a subset of elements (also works with lists of booleans).\n", "\n", "The boolean array itself can be initiated by a condition, as shown in the example below.\n", "\n", @@ -1081,13 +1082,13 @@ "execution": {} }, "source": [ - "[*Click for solution*](https://github.com/NeuromatchAcademy/precourse/tree/main/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_5061d76b.py)\n", + "[*Click for solution*](https://github.com/NeuromatchAcademy/precourse/tree/main/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_c368c1ef.py)\n", "\n", "*Example output:*\n", "\n", - "Solution hint\n", + "Solution hint\n", "\n", - "Solution hint\n", + "Solution hint\n", "\n" ] }, @@ -1374,9 +1375,8 @@ }, "source": [ "## Coding Exercise 5: Investigating refactory periods\n", - "Investigate the effect of (absolute) refractory period $t_{ref}$ on the evolution of output rate $\\lambda(t)$. Add refractory period $t_{ref}=10$ ms after each spike, during which $V(t)$ is clamped to $V_{reset}$.\n", "\n", - "\n" + "Investigate the effect of (absolute) refractory period $t_{ref}$ on the evolution of output rate $\\lambda(t)$. Add refractory period $t_{ref}=10$ ms after each spike, during which $V(t)$ is clamped to $V_{reset}$." ] }, { @@ -1477,13 +1477,13 @@ }, "source": [ "## Interactive Demo 1: Random refractory period\n", + "\n", "In the following interactive demo, we will investigate the effect of random refractory periods. We will use random refactory periods $t_{ref}$ with\n", "$t_{ref} = \\mu + \\sigma\\,\\mathcal{N}$, where $\\mathcal{N}$ is the [normal distribution](https://en.wikipedia.org/wiki/Normal_distribution), $\\mu=0.01$ and $\\sigma=0.007$.\n", "\n", - "Refractory period samples `t_ref` of size `n` is initialized with `np.random.normal`. We clip negative values to `0` with boolean indexes. (Why?) You can double click the cell to see the hidden code.\n", + "Refractory period samples `t_ref` of size `n` is initialized with `np.random.normal`. We clip negative values to `0` with boolean indexes. (Why?) You can double-click the cell to see the hidden code.\n", "\n", - "You can play with the parameters mu and sigma and visualize the resulting simulation.\n", - "What is the effect of different $\\sigma$ values?\n" + "You can play with the parameters mu and sigma and visualize the resulting simulation. What is the effect of different $\\sigma$ values?" ] }, { @@ -1714,6 +1714,7 @@ }, "source": [ "## Coding Exercise 6: Rewriting code with functions\n", + "\n", "We will now re-organize parts of the code from the previous exercise with functions. You need to complete the function `spike_clamp()` to update $V(t)$ and deal with spiking and refractoriness" ] }, @@ -1733,7 +1734,7 @@ " v (numpy array of floats)\n", " membrane potential at previous time step of shape (neurons)\n", "\n", - " v (numpy array of floats)\n", + " i (numpy array of floats)\n", " synaptic input at current time step of shape (neurons)\n", "\n", " dt (float)\n", @@ -1747,6 +1748,7 @@ "\n", " return v\n", "\n", + "\n", "def spike_clamp(v, delta_spike):\n", " \"\"\"\n", " Resets membrane potential of neurons if v>= vth\n", @@ -1824,106 +1826,20 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": { + "colab_type": "text", "execution": {} }, - "outputs": [], "source": [ - "def ode_step(v, i, dt):\n", - " \"\"\"\n", - " Evolves membrane potential by one step of discrete time integration\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential at previous time step of shape (neurons)\n", - "\n", - " v (numpy array of floats)\n", - " synaptic input at current time step of shape (neurons)\n", - "\n", - " dt (float)\n", - " time step increment\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential at current time step of shape (neurons)\n", - " \"\"\"\n", - " v = v + dt/tau * (el - v + r*i)\n", - "\n", - " return v\n", - "\n", - "# to_remove solution\n", - "def spike_clamp(v, delta_spike):\n", - " \"\"\"\n", - " Resets membrane potential of neurons if v>= vth\n", - " and clamps to vr if interval of time since last spike < t_ref\n", - "\n", - " Args:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - "\n", - " delta_spike (numpy array of floats)\n", - " interval of time since last spike of shape (neurons)\n", - "\n", - " Returns:\n", - " v (numpy array of floats)\n", - " membrane potential of shape (neurons)\n", - " spiked (numpy array of floats)\n", - " boolean array of neurons that spiked of shape (neurons)\n", - " \"\"\"\n", + "[*Click for solution*](https://github.com/NeuromatchAcademy/precourse/tree/main/tutorials/W0D2_PythonWorkshop2/solutions/W0D2_Tutorial1_Solution_bf9f75ab.py)\n", "\n", - " # Boolean array spiked indexes neurons with v>=vth\n", - " spiked = (v >= vth)\n", - " v[spiked] = vr\n", - "\n", - " # Boolean array clamped indexes refractory neurons\n", - " clamped = (t_ref > delta_spike)\n", - " v[clamped] = vr\n", - "\n", - " return v, spiked\n", - "\n", - "\n", - "# Set random number generator\n", - "np.random.seed(2020)\n", - "\n", - "# Initialize step_end, t_range, n, v_n and i\n", - "t_range = np.arange(0, t_max, dt)\n", - "step_end = len(t_range)\n", - "n = 500\n", - "v_n = el * np.ones([n, step_end])\n", - "i = i_mean * (1 + 0.1 * (t_max / dt)**(0.5) * (2 * np.random.random([n, step_end]) - 1))\n", - "\n", - "# Initialize binary numpy array for raster plot\n", - "raster = np.zeros([n,step_end])\n", - "\n", - "# Initialize t_ref and last_spike\n", - "mu = 0.01\n", - "sigma = 0.007\n", - "t_ref = mu + sigma*np.random.normal(size=n)\n", - "t_ref[t_ref<0] = 0\n", - "last_spike = -t_ref * np.ones([n])\n", - "\n", - "# Loop over time steps\n", - "for step, t in enumerate(t_range):\n", - "\n", - " # Skip first iteration\n", - " if step==0:\n", - " continue\n", - "\n", - " # Compute v_n\n", - " v_n[:,step] = ode_step(v_n[:,step-1], i[:,step], dt)\n", - "\n", - " # Reset membrane potential and clamp\n", - " v_n[:,step], spiked = spike_clamp(v_n[:,step], t - last_spike)\n", + "*Example output:*\n", "\n", - " # Update raster and last_spike\n", - " raster[spiked,step] = 1.\n", - " last_spike[spiked] = t\n", + "Solution hint\n", "\n", - "# Plot multiple realizations of Vm, spikes and mean spike rate\n", - "with plt.xkcd():\n", - " plot_all(t_range, v_n, raster)" + "Solution hint\n", + "\n" ] }, { @@ -2024,7 +1940,7 @@ "execution": {} }, "source": [ - "Using classes helps with code reuse and reliability. Well-designed classes are like black boxes in that they receive inputs and provide expected outputs. The details of how the class processes inputs and produces outputs are unimportant.\n", + "Using classes helps with code reuse and reliability. Well-designed classes are like black boxes, receiving inputs and providing expected outputs. The details of how the class processes inputs and produces outputs are unimportant.\n", "\n", "See additional details here: [A Beginner's Python Tutorial/Classes](https://en.wikibooks.org/wiki/A_Beginner%27s_Python_Tutorial/Classes)\n", "\n", diff --git a/tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb b/tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb index 23c637d..fbd037d 100644 --- a/tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb +++ b/tutorials/W0D4_Calculus/W0D4_Tutorial1.ipynb @@ -1,2313 +1,2224 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "
\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "TacT2M82f3ZW" - }, - "source": [ - "# Tutorial 1: Differentiation and Integration\n", - "\n", - "**Week 0, Day 4: Calculus**\n", - "\n", - "**By Neuromatch Academy**\n", - "\n", - "**Content creators:** John S Butler, Arvind Kumar with help from Ella Batty\n", - "\n", - "**Content reviewers:** Aderogba Bayo, Tessy Tom, Matt McCann\n", - "\n", - "**Production editors:** Matthew McCann, Spiros Chavlis, Ella Batty" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "W6VM3JDWf3ZY" - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "FwuOlEwFf3ZY" - }, - "source": [ - "---\n", - "# Tutorial Objectives\n", - "\n", - "*Estimated timing of tutorial: 80 minutes*\n", - "\n", - "In this tutorial, we will cover aspects of calculus that will be frequently used in the main NMA course. We assume that you have some familiarity with calculus but may be a bit rusty or may not have done much practice. Specifically, the objectives of this tutorial are\n", - "\n", - "* Get an intuitive understanding of derivative and integration operations\n", - "* Learn to calculate the derivatives of 1- and 2-dimensional functions/signals numerically\n", - "* Familiarize with the concept of the neuron transfer function in 1- and 2-dimensions.\n", - "* Familiarize with the idea of numerical integration using the Riemann sum" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "JNz5Ytinf3ZY" - }, - "source": [ - "---\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "VuwJSe2wf3ZZ" - }, - "outputs": [], - "source": [ - "# @title Install dependencies\n", - "!pip install sympy --quiet" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "kuYTCN1Zf3ZZ" - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D4_T1\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "Yuug4Cpwf3ZZ" - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import scipy.optimize as opt # import root-finding algorithm\n", - "import sympy as sp # Python toolbox for symbolic maths\n", - "import matplotlib.pyplot as plt\n", - "from mpl_toolkits.mplot3d import Axes3D # Toolbox for rendring 3D figures\n", - "from mpl_toolkits import mplot3d # Toolbox for rendring 3D figures" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "XUDhmZ4Kf3Za" - }, - "outputs": [], - "source": [ - "# @title Figure Settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import ipywidgets as widgets # interactive display\n", - "from ipywidgets import interact\n", - "%config InlineBackend.figure_format = 'retina'\n", - "# use NMA plot style\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", - "my_layout = widgets.Layout()\n", - "\n", - "fig_w, fig_h = 12, 4.5\n", - "my_fontsize = 16\n", - "my_params = {'axes.labelsize': my_fontsize,\n", - " 'axes.titlesize': my_fontsize,\n", - " 'figure.figsize': [fig_w, fig_h],\n", - " 'font.size': my_fontsize,\n", - " 'legend.fontsize': my_fontsize-4,\n", - " 'lines.markersize': 8.,\n", - " 'lines.linewidth': 2.,\n", - " 'xtick.labelsize': my_fontsize-2,\n", - " 'ytick.labelsize': my_fontsize-2}\n", - "\n", - "plt.rcParams.update(my_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "k9_M_JXHf3Za" - }, - "outputs": [], - "source": [ - "# @title Plotting Functions\n", - "def move_sympyplot_to_axes(p, ax):\n", - " backend = p.backend(p)\n", - " backend.ax = ax\n", - " backend.process_series()\n", - " backend.ax.spines['right'].set_color('none')\n", - " backend.ax.spines['bottom'].set_position('zero')\n", - " backend.ax.spines['top'].set_color('none')\n", - " plt.close(backend.fig)\n", - "\n", - "\n", - "def plot_functions(function, show_derivative, show_integral):\n", - "\n", - " # For sympy we first define our symbolic variable\n", - " x, y, z, t, f = sp.symbols('x y z t f')\n", - "\n", - " # We define our function\n", - " if function == 'Linear':\n", - " f = -2*t\n", - " name = r'$-2t$'\n", - " elif function == 'Parabolic':\n", - " f = t**2\n", - " name = r'$t^2$'\n", - " elif function == 'Exponential':\n", - " f = sp.exp(t)\n", - " name = r'$e^t$'\n", - " elif function == 'Sine':\n", - " f = sp.sin(t)\n", - " name = r'$sin(t)$'\n", - " elif function == 'Sigmoid':\n", - " f = 1/(1 + sp.exp(-(t-5)))\n", - " name = r'$\\frac{1}{1+e^{-(t-5)}}$'\n", - "\n", - " if show_derivative and not show_integral:\n", - " # Calculate the derivative of sin(t) as a function of t\n", - " diff_f = sp.diff(f)\n", - " print('Derivative of', f, 'is ', diff_f)\n", - "\n", - " p1 = sp.plot(f, diff_f, show=False)\n", - " p1[0].line_color='r'\n", - " p1[1].line_color='b'\n", - " p1[0].label='Function'\n", - " p1[1].label='Derivative'\n", - " p1.legend=True\n", - " p1.title = 'Function = ' + name + '\\n'\n", - " p1.show()\n", - " elif show_integral and not show_derivative:\n", - "\n", - " int_f = sp.integrate(f)\n", - " int_f = int_f - int_f.subs(t, -10)\n", - " print('Integral of', f, 'is ', int_f)\n", - "\n", - "\n", - " p1 = sp.plot(f, int_f, show=False)\n", - " p1[0].line_color='r'\n", - " p1[1].line_color='g'\n", - " p1[0].label='Function'\n", - " p1[1].label='Integral'\n", - " p1.legend=True\n", - " p1.title = 'Function = ' + name + '\\n'\n", - " p1.show()\n", - "\n", - "\n", - " elif show_integral and show_derivative:\n", - "\n", - " diff_f = sp.diff(f)\n", - " print('Derivative of', f, 'is ', diff_f)\n", - "\n", - " int_f = sp.integrate(f)\n", - " int_f = int_f - int_f.subs(t, -10)\n", - " print('Integral of', f, 'is ', int_f)\n", - "\n", - " p1 = sp.plot(f, diff_f, int_f, show=False)\n", - " p1[0].line_color='r'\n", - " p1[1].line_color='b'\n", - " p1[2].line_color='g'\n", - " p1[0].label='Function'\n", - " p1[1].label='Derivative'\n", - " p1[2].label='Integral'\n", - " p1.legend=True\n", - " p1.title = 'Function = ' + name + '\\n'\n", - " p1.show()\n", - "\n", - " else:\n", - "\n", - " p1 = sp.plot(f, show=False)\n", - " p1[0].line_color='r'\n", - " p1[0].label='Function'\n", - " p1.legend=True\n", - " p1.title = 'Function = ' + name + '\\n'\n", - " p1.show()\n", - "\n", - "\n", - "def plot_alpha_func(t, f, df_dt):\n", - "\n", - " plt.figure()\n", - " plt.subplot(2,1,1)\n", - " plt.plot(t, f, 'r', label='Alpha function')\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('Voltage')\n", - " plt.title('Alpha function (f(t))')\n", - " #plt.legend()\n", - "\n", - " plt.subplot(2,1,2)\n", - " plt.plot(t, df_dt, 'b', label='Derivative')\n", - " plt.title('Derivative of alpha function')\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('df/dt')\n", - " #plt.legend()\n", - "\n", - "\n", - "def plot_charge_transfer(t, PSP, numerical_integral):\n", - "\n", - " fig, axes = plt.subplots(1, 2)\n", - "\n", - " axes[0].plot(t, PSP)\n", - " axes[0].set(xlabel = 't', ylabel = 'PSP')\n", - "\n", - " axes[1].plot(t, numerical_integral)\n", - " axes[1].set(xlabel = 't', ylabel = 'Charge Transferred')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "-5lr9lRsf3Za" - }, - "source": [ - "---\n", - "# Section 0: Introduction" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "WW3hrs_Rf3Za" - }, - "outputs": [], - "source": [ - "# @title Video 1: Why do we care about calculus?\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'NZwfH_dG2wI'), ('Bilibili', 'BV1F44y1z7Uk')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "u9hhEvO4f3Zb" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Why_do_we_care_about_calculus_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "Qgo1fAhWf3Zb" - }, - "source": [ - "---\n", - "# Section 1: What is differentiation and integration?\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "fqnzYQwNf3Zb" - }, - "outputs": [], - "source": [ - "# @title Video 2: A geometrical interpretation of differentiation and integration\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'uQjwr9RQaEs'), ('Bilibili', 'BV1sU4y1G7Ru')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "1u4lk-KPf3Zb" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_A_geometrical_interpretation_of_differentiation_and_integration_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "ezYZtWkAf3Zb" - }, - "source": [ - "This video covers the definition of differentiation and integration, highlights the geometrical interpretation of each, and introduces the idea of eigenfunctions.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "Calculus is a part of mathematics concerned with **continous change**. There are two branches of calculus: differential calculus and integral calculus.\n", - "\n", - "Differentiation of a function $f(t)$ gives you the derivative of that function $\\frac{d(f(t))}{dt}$. A derivative captures how sensitive a function is to slight changes in the input for different ranges of inputs. Geometrically, the derivative of a function at a certain input is the slope of the function at that input. For example, as you drive, the distance traveled changes continuously with time. The derivative of the distance traveled with respect to time is the velocity of the vehicle at each point in time. The velocity tells you the rate of change of the distance traveled at different points in time. If you have slow velocity (a small derivative), the distance traveled doesn't change much for small changes in time. A high velocity (big derivative) means that the distance traveled changes a lot for small changes in time.\n", - "\n", - "The sign of the derivative of a function (or signal) tells whether the signal is increasing or decreasing. For a signal going through changes as a function of time, the derivative will become zero when the signal changes its direction of change (e.g. from increasing to decreasing). That is, at local minimum or maximum values, the slope of the signal will be zero. This property is used in optimizing problems. But we can also use it to find peaks in a signal.\n", - "\n", - "Integration can be thought of as the reverse of differentation. If we integrate the velocity with respect to time, we can calculate the distance traveled. By integrating a function, we are basically trying to find functions that would have the original one as their derivative. When we integrate a function, our integral will have an added unknown scalar constant, $C$.\n", - "For example, if\n", - "\n", - "\\begin{equation}\n", - "g(t) = 1.5t^2 + 4t - 1\n", - "\\end{equation}\n", - "\n", - "our integral function $f(t)$ will be:\n", - "\n", - "\\begin{equation}\n", - "f(t) = \\int g(t) dt = 0.5t^3 + 2t^2 - t + C\n", - "\\end{equation}\n", - "\n", - "This constant exists because the derivative of a constant is 0 so we cannot know what the constant should be. This is an indefinite integral. If we compute a definite integral, that is the integral between two limits of the input, we will not have this unknown constant and the integral of a function will capture the area under the curve of that function between those two limits.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "kwh8lMCof3Zc" - }, - "source": [ - "### Interactive Demo 1: Geometrical understanding\n", - "\n", - "In the interactive demo below, you can pick different functions to examine in the drop-down menu. You can then choose to show the derivative function and/or the integral function.\n", - "\n", - "For the integral, we have chosen the unknown constant $C$ such that the integral function at the left x-axis limit is $0$, as $f(t = -10) = 0$. So the integral will reflect the area under the curve starting from that position.\n", - "\n", - "For each function:\n", - "\n", - "* Examine just the function first. Discuss and predict what the derivative and integral will look like. Remember that _derivative = slope of the function_, _integral = area under the curve from $t = -10$ to that $t$_.\n", - "* Check the derivative - does it match your expectations?\n", - "* Check the integral - does it match your expectations?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "N2Z0IyLXf3Zc" - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to enable the widget\n", - "function_options = widgets.Dropdown(\n", - " options=['Linear', 'Exponential', 'Sine', 'Sigmoid'],\n", - " description='Function',\n", - " disabled=False,\n", - ")\n", - "\n", - "derivative = widgets.Checkbox(\n", - " value=False,\n", - " description='Show derivative',\n", - " disabled=False,\n", - " indent=False\n", - ")\n", - "\n", - "integral = widgets.Checkbox(\n", - " value=False,\n", - " description='Show integral',\n", - " disabled=False,\n", - " indent=False\n", - ")\n", - "\n", - "def on_value_change(change):\n", - " derivative.value = False\n", - " integral.value = False\n", - "\n", - "function_options.observe(on_value_change, names='value')\n", - "\n", - "interact(plot_functions, function = function_options, show_derivative = derivative, show_integral = integral);" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "7zF0I-hAf3Zc" - }, - "source": [ - "In the demo above, you may have noticed that the derivative and integral of the exponential function are the same as the exponential function itself.\n", - "\n", - "When differentiated or integrated, some functions, like the exponential function, equal scalar times the same function. This is a similar idea to eigenvectors of a matrix being those that, when multiplied by the matrix, equal scalar times themselves, as you saw yesterday!\n", - "\n", - "When\n", - "\n", - "\\begin{equation}\n", - "\\frac{d(f(t)}{dt} = a \\cdot f(t)\\text{,}\n", - "\\end{equation}\n", - "\n", - "we say that $f(t)$ is an **eigenfunction** for derivative operator, where $a$ is a scaling factor. Similarly, when\n", - "\n", - "\\begin{equation}\n", - "\\int f(t)dt = a \\cdot f(t)\\text{,}\n", - "\\end{equation}\n", - "\n", - "we say that $f(t)$ is an **eigenfunction** for integral operator.\n", - "\n", - "As you can imagine, working with eigenfunctions can make mathematical analysis easy." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "EXA0mCAQf3Zc" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Geometrical_understanding_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "waVi1nTkf3Zc" - }, - "source": [ - "---\n", - "# Section 2: Analytical & Numerical Differentiation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "9EfXEFkxf3Zc" - }, - "outputs": [], - "source": [ - "# @title Video 3: Differentiation\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'sHogZISXGuQ'), ('Bilibili', 'BV14g41137d5')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "gp5v4UNZf3Zc" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Differentiation_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "SYVDa-JQf3Zc" - }, - "source": [ - "\n", - "In this section, we will delve into how we actually find the derivative of a function, both analytically and numerically.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "vdsryncaf3Zd" - }, - "source": [ - "When we find the derivative analytically, we obtain the exact formula for the derivative function.\n", - "\n", - "To do this, instead of having to do some fancy math every time, we can often consult [an online resource](https://en.wikipedia.org/wiki/Differentiation_rules) for a list of common derivatives, in this case, our trusty friend Wikipedia.\n", - "\n", - "If I told you to find the derivative of $f(t) = t^3$, you could consult that site and find in Section 2.1 that if $f(t) = t^n$, then $\\frac{d(f(t))}{dt} = nt^{n-1}$. So you would be able to tell me that the derivative of $f(t) = t^3$ is $\\frac{d(f(t))}{dt} = 3t^{2}$.\n", - "\n", - "This list of common derivatives often contains only very simple functions. Luckily, as we'll see in the next two sections, we can often break the derivative of a complex function down into the derivatives of more simple components." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "ptcwB2F6f3Zd" - }, - "source": [ - "### Section 2.1.1: Product Rule\n", - "\n", - "Sometimes we encounter functions which are the product of two functions that both depend on the variable.\n", - "How do we take the derivative of such functions? For this we use the [Product Rule](https://en.wikipedia.org/wiki/Product_rule).\n", - "\n", - "\\begin{align}\n", - "f(t) &= u(t) \\cdot v(t) \\\\ \\\\\n", - "\\frac{d(f(t))}{dt} &= v \\cdot \\frac{du}{dt} + u \\cdot \\frac{dv}{dt}\n", - "\\end{align}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "W_LkDLzlf3Zd" - }, - "source": [ - "#### Coding Exercise 2.1.1: Derivative of the postsynaptic potential alpha function\n", - "\n", - "Let's use the product rule to get the derivative of the postsynaptic potential alpha function. As we saw in Video 3, the shape of the postsynaptic potential is given by the so-called alpha function:\n", - "\n", - "\\begin{equation}\n", - "f(t) = t \\cdot \\text{exp}\\left( -\\frac{t}{\\tau} \\right)\n", - "\\end{equation}\n", - "\n", - "Here $f(t)$ is a product of $t$ and $\\text{exp} \\left(-\\frac{t}{\\tau} \\right)$. So we can have $u(t) = t$ and $v(t) = \\text{exp} \\left( -\\frac{t}{\\tau} \\right)$ and use the product rule!\n", - "\n", - "We have defined $u(t)$ and $v(t)$ in the code below for the variable $t$, an array of time steps from 0 to 10. Define $\\frac{du}{dt}$ and $\\frac{dv}{dt}$, then compute the full derivative of the alpha function using the product rule. You can always consult Wikipedia to figure out $\\frac{du}{dt}$ and $\\frac{dv}{dt}$!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "wQ9LmAOOf3Zd" - }, - "outputs": [], - "source": [ - "########################################################################\n", - "## TODO for students\n", - "## Complete all ... in code below and remove\n", - "raise NotImplementedError(\"Calculate the derivatives\")\n", - "########################################################################\n", - "\n", - "# Define time, time constant\n", - "t = np.arange(0, 10, .1)\n", - "tau = 0.5\n", - "\n", - "# Compute alpha function\n", - "f = t * np.exp(-t/tau)\n", - "\n", - "# Define u(t), v(t)\n", - "u_t = t\n", - "v_t = np.exp(-t/tau)\n", - "\n", - "# Define du/dt, dv/dt\n", - "du_dt = ...\n", - "dv_dt = ...\n", - "\n", - "# Define full derivative\n", - "df_dt = ...\n", - "\n", - "# Visualize\n", - "plot_alpha_func(t, f, df_dt)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "Q8PCjzi4f3Zd" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Define time, time constant\n", - "t = np.arange(0, 10, .1)\n", - "tau = 0.5\n", - "\n", - "# Compute alpha function\n", - "f = t * np.exp(-t/tau)\n", - "\n", - "# Define u(t), v(t)\n", - "u_t = t\n", - "v_t = np.exp(-t/tau)\n", - "\n", - "# Define du/dt, dv/dt\n", - "du_dt = 1\n", - "dv_dt = -1/tau * np.exp(-t/tau)\n", - "\n", - "# Define full derivative\n", - "df_dt = u_t * dv_dt + v_t * du_dt\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_alpha_func(t, f, df_dt)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "7Q2bnV20f3Zd" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Derivative_of_the_postsynaptic_potential_alpha_function_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "Kwbh_DR_f3Zd" - }, - "source": [ - "### Section 2.1.2: Chain Rule\n", - "\n", - "We often encounter situations in which the variable $a$ changes with time ($t$) and affects another variable $r$. How can we estimate the derivative of $r$ with respect to $a$, i.e., $\\frac{dr}{da} = ?$\n", - "\n", - "To calculate $\\frac{dr}{da}$ we use the [Chain Rule](https://en.wikipedia.org/wiki/Chain_rule).\n", - "\n", - "\\begin{equation}\n", - "\\frac{dr}{da} = \\frac{dr}{dt}\\cdot\\frac{dt}{da}\n", - "\\end{equation}\n", - "\n", - "We calculate the derivative of both variables with respect to $t$ and divide that derivative of $r$ by that derivative of $a$.\n", - "\n", - "We can also use this formula to simplify taking derivatives of complex functions! We can make an arbitrary function $t$ to compute more simple derivatives and multiply, as seen in this exercise." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "-oILTPd_f3Zd" - }, - "source": [ - "#### Math Exercise 2.1.2: Chain Rule\n", - "\n", - "Let's say that:\n", - "\n", - "\\begin{equation}\n", - "r(a) = e^{a^4 + 1}\n", - "\\end{equation}\n", - "\n", - "What is $\\frac{dr}{da}$? This is a more complex function, so we can't simply consult a table of common derivatives. Can you use the chain rule to help?\n", - "\n", - "**Hint:** We didn't define $t$, but you could set $t$ equal to the function in the exponent." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "p2tAWNiZf3Zd" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "\n", - "We can set t equal to the exponent:\n", - "t = a^4 + 1\n", - "r(t) = e^t\n", - "\n", - "\n", - "Then:\n", - " dt/da = 4a^3\n", - " dr/dt = e^t\n", - "\n", - "Now we can use the chain rule:\n", - "dr/da = dr/dt * dt/da\n", - " = e^t(4a^3)\n", - " = 4a^3e^{a^4 + 1}\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "8c4VPwPYf3Ze" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Chain_Rule_Math_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "YEo06nXDf3Ze" - }, - "source": [ - "### Section 2.2.3: Derivatives in Python using SymPy\n", - "\n", - "There is a useful Python library for getting the analytical derivatives of functions: SymPy. We actually used this in Interactive Demo 1, under the hood.\n", - "\n", - "See the following cell for an example of setting up a SymPy function and finding the derivative." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "UXwn36dyf3Ze" - }, - "outputs": [], - "source": [ - "# For sympy we first define our symbolic variables\n", - "f, t = sp.symbols('f, t')\n", - "\n", - "# Function definition (sigmoid)\n", - "f = 1/(1 + sp.exp(-(t-5)))\n", - "\n", - "# Get the derivative\n", - "diff_f = sp.diff(f)\n", - "\n", - "# Print the resulting function\n", - "print('Derivative of', f, 'is ', diff_f)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "7gARkApDf3Ze" - }, - "source": [ - "## Section 2.2: Numerical Differentiation\n", - "\n", - "*Estimated timing to here from start of tutorial: 30 min*\n", - "\n", - "Formally, the derivative of a function $\\mathcal{f}(x)$ at any value $a$ is given by the finite difference (FD) formula:\n", - "\n", - "\\begin{equation}\n", - "FD = \\frac{f(a+h) - f(a)}{h}\n", - "\\end{equation}\n", - "\n", - "As $h\\rightarrow 0$, the $FD$ approaches the actual value of the derivative. Let's check this.\n", - "\n", - "**Note:** The numerical estimate of the derivative will result\n", - "in a time series whose length is one short of the original time series." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "a1D5KI6zf3Ze" - }, - "source": [ - "### Interactive Demo 2.2: Numerical Differentiation of the Sine Function\n", - "\n", - "Below, we find the numerical derivative of the sine function for different values of $h$ and compare the result to the analytical solution.\n", - "\n", - "* What values of $h$ result in more accurate numerical derivatives?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "kauWhSZFf3Ze" - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to enable the widget.*\n", - "def numerical_derivative_demo(h = 0.2):\n", - " # Now lets create a sequence of numbers which change according to the sine function\n", - " dt = 0.01\n", - " tx = np.arange(-10, 10, dt)\n", - " sine_fun = np.sin(tx)\n", - "\n", - " # symbolic diffrentiation tells us that the derivative of sin(t) is cos(t)\n", - " cos_fun = np.cos(tx)\n", - "\n", - " # Numerical derivative using difference formula\n", - " n_tx = np.arange(-10,10,h) # create new time axis\n", - " n_sine_fun = np.sin(n_tx) # calculate the sine function on the new time axis\n", - " sine_diff = (n_sine_fun[1:] - n_sine_fun[0:-1]) / h\n", - "\n", - " fig = plt.figure()\n", - " ax = plt.subplot(111)\n", - " plt.plot(tx, sine_fun, label='sine function')\n", - " plt.plot(tx, cos_fun, label='analytical derivative of sine')\n", - "\n", - " with plt.xkcd():\n", - " # notice that numerical derivative will have one element less\n", - " plt.plot(n_tx[0:-1], sine_diff, label='numerical derivative of sine')\n", - " plt.xlim([-10, 10])\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('f(x) or df(x)/dt')\n", - " ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.05),\n", - " ncol=3, fancybox=True)\n", - " plt.show()\n", - "\n", - "_ = widgets.interact(numerical_derivative_demo, h = (0.01, 0.5, .02))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "ffcSdzAMf3Zh" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "The smaller the value of $h$, the better the estimate of the derivative of the\n", - "function at $x=a$.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "pnDxCTuTf3Zi" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Numerical_Differentiation_of_the_Sine_Function_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "yLkazqeWf3Zi" - }, - "source": [ - "## Section 2.3: Transfer Function and Gain of a Neuron\n", - "\n", - "*Estimated timing to here from start of tutorial: 34 min*\n", - "\n", - "When we inject a constant current (DC) into a neuron, its firing rate changes as a function of the strength of the injected current. This is called the **input-output transfer function** or just the *transfer function* or *I/O Curve* of the neuron. For most neurons, this can be approximated by a sigmoid function, e.g.:\n", - "\n", - "\\begin{equation}\n", - "rate(I) = \\frac{1}{1+\\text{exp}(-a \\cdot (I-\\theta))} - \\frac{1}{\\text{exp}(a \\cdot \\theta)} + \\eta\n", - "\\end{equation}\n", - "\n", - "where $I$ is injected current, $rate$ is the neuron firing rate and $\\eta$ is noise (Gaussian noise with zero mean and $\\sigma$ standard deviation).\n", - "\n", - "*You will visit this equation in a different context in Week 3*\n", - "\n", - "The slope of a neuron input-output transfer function, i.e., $\\frac{d(r(I))}{dI}$, is called the **gain** of the neuron, as it tells how the neuron output will change if the input is changed. In other words, the slope of the transfer function tells us in which range of inputs the neuron output is most sensitive to changes in its input." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "c4UqcVL5f3Zi" - }, - "source": [ - "### Interactive Demo 2.3: Calculating the Transfer Function and Gain of a Neuron\n", - "\n", - "In the following demo, you can estimate the gain of the following neuron transfer function using numerical differentiation. We will use our timestep as h. See the cell below for a function that computes the rate via the formula above and then the gain using numerical differentiation. In the following cell, you can play with the parameters $a$ and $\\theta$ to change the shape of the transfer function (and see the resulting gain function). You can also set $I_{mean}$ to see how the slope is computed for that value of I. In the left plot, the red vertical lines are the two values of the current being used to calculate the slope, while the blue lines point to the corresponding output firing rates.\n", - "\n", - "Change the parameters of the neuron transfer function (i.e., $a$ and $\\theta$) and see if you can predict the value of $I$ for which the neuron has a maximal slope and which parameter determines the peak value of the gain.\n", - "\n", - "1. Ensure you understand how the right plot relates to the left!\n", - "2. How does $\\theta$ affect the transfer function and gain?\n", - "3. How does $a$ affect the transfer function and gain?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "0FW7LW6Kf3Zi" - }, - "outputs": [], - "source": [ - "def compute_rate_and_gain(I, a, theta, current_timestep):\n", - " \"\"\" Compute rate and gain of neuron based on parameters\n", - "\n", - " Args:\n", - " I (ndarray): different possible values of the current\n", - " a (scalar): parameter of the transfer function\n", - " theta (scalar): parameter of the transfer function\n", - " current_timestep (scalar): the time we're using to take steps\n", - "\n", - " Returns:\n", - " (ndarray, ndarray): rate and gain for each possible value of I\n", - " \"\"\"\n", - "\n", - " # Compute rate\n", - " rate = (1+np.exp(-a*(I-theta)))**-1 - (1+np.exp(a*theta))**-1\n", - "\n", - " # Compute gain using a numerical derivative\n", - " gain = (rate[1:] - rate[0:-1])/current_timestep\n", - "\n", - " return rate, gain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "If0V6Vzif3Zi" - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to enable the widget\n", - "\n", - "def plot_rate_and_gain(a, theta, I_mean):\n", - "\n", - " current_timestep = 0.1\n", - "\n", - " # Compute I\n", - " I = np.arange(0, 8, current_timestep)\n", - "\n", - " rate, gain = compute_rate_and_gain(I, a, theta, current_timestep)\n", - " I_1 = I_mean - current_timestep/2\n", - " rate_1 = (1+np.exp(-a*(I_1-theta)))**-1 - (1+np.exp(a*theta))**-1\n", - " I_2 = I_mean + current_timestep/2\n", - " rate_2 = (1+np.exp(-a*(I_2-theta)))**-1 - (1+np.exp(a*theta))**-1\n", - "\n", - " input_range = I_2-I_1\n", - " output_range = rate_2 - rate_1\n", - "\n", - " # Visualize rate and gain\n", - " plt.subplot(1,2,1)\n", - " plt.plot(I,rate)\n", - " plt.plot([I_1,I_1],[0, rate_1],color='r')\n", - " plt.plot([0,I_1],[rate_1, rate_1],color='b')\n", - " plt.plot([I_2,I_2],[0, rate_2],color='r')\n", - " plt.plot([0,I_2],[rate_2, rate_2],color='b')\n", - " plt.xlim([0, 8])\n", - " low, high = plt.ylim()\n", - " plt.ylim([0, high])\n", - "\n", - " plt.xlabel('Injected current (au)')\n", - " plt.ylabel('Output firing rate (normalized)')\n", - " plt.title('Transfer function')\n", - "\n", - " plt.text(2, 1.3, 'Output-Input Ratio =' + str(np.round(1000*output_range/input_range)/1000), style='italic',\n", - " bbox={'facecolor': 'red', 'alpha': 0.5, 'pad': 10})\n", - " plt.subplot(1,2,2)\n", - " plt.plot(I[0:-1], gain)\n", - " plt.plot([I_mean, I_mean],[0,0.6],color='r')\n", - " plt.xlabel('Injected current (au)')\n", - " plt.ylabel('Gain')\n", - " plt.title('Gain')\n", - " plt.xlim([0, 8])\n", - " low, high = plt.ylim()\n", - " plt.ylim([0, high])\n", - "\n", - "_ = widgets.interact(plot_rate_and_gain, a = (0.5, 2.0, .02), theta=(1.2,4.0,0.1), I_mean= (0.5,8.0,0.1))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "eXgyVuUNf3Zj" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) $\\theta$ shifts the neuron transfer function along the x-axis -- reducing $theta$ moves the transfer function leftwards.\n", - "So changing $\\theta$ will affect the value of $I$ at which the neuron has the maximal slope.\n", - "Smaller the $\\theta$ smaller the value for maximum slope.\n", - "\n", - "2) $a$ controls the steepness of the neuron transfer function -- increasing $a$ makes the curve more steeper.\n", - "Therefore, $a$ determines the maximum value of the slope.\n", - "\n", - "Note: $a$ and $\\theta$ do not determine the maximum value of the transfer function itself.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "-0G8jJeef3Zj" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Calculating_the_transfer_function_and_gain_of_a_neuron_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "pUEKL3MNf3Zj" - }, - "source": [ - "---\n", - "# Section 3: Functions of Multiple Variables\n", - "\n", - "*Estimated timing to here from start of tutorial: 44 min*\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "aXy7S_1Wf3Zj" - }, - "outputs": [], - "source": [ - "# @title Video 4: Functions of multiple variables\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'Mp_uNNNiQAI'), ('Bilibili', 'BV1Ly4y1M77D')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "j1F7DU_5f3Zj" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Functions_of_multiple_variables_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "Ndww73njf3Zj" - }, - "source": [ - "This video covers what partial derivatives are.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "In the previous section, you looked at function of single variable $t$ or $x$. In most cases, we encounter functions of multiple variables. For example, in the brain, the firing rate of a neuron is a function of both excitatory and inhibitory input rates. In the following, we will look into how to calculate derivatives of such functions.\n", - "\n", - "When we take the derrivative of a multivariable function with respect to one of the variables it is called the **partial derivative**. For example if we have a function:\n", - "\n", - "\\begin{align}\n", - "f(x,y) = x^2 + 2xy + y^2\n", - "\\end{align}\n", - "\n", - "The we can define the partial derivatives as\n", - "\n", - "\\begin{align}\n", - "\\frac{\\partial(f(x,y))}{\\partial x} = 2x + 2y + 0 \\\\\\\\\n", - "\\frac{\\partial(f(x,y))}{\\partial y} = 0 + 2x + 2y\n", - "\\end{align}\n", - "\n", - "In the above, the derivative of the last term ($y^2$) with respect to $x$ is zero because it does not change with respect to $x$. Similarly, the derivative of $x^2$ with respect to $y$ is also zero.\n", - "
\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "FbUu-n2qf3Zk" - }, - "source": [ - "Just as with the derivatives we saw earlier, you can get partial derivatives through either an analytical method (finding an exact equation) or a numerical method (approximating)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "jEUEx5aKf3Zk" - }, - "source": [ - "### Interactive Demo 3: Visualize partial derivatives\n", - "\n", - "In the demo below, you can input any function of $x$ and $y$ and then visualize both the function and partial derivatives.\n", - "\n", - "We visualized the 2-dimensional function as a surface plot in which the function values are rendered as color. Yellow represents a high value, and blue represents a low value. The height of the surface also shows the numerical value of the function. A complete description of 2D surface plots and why we need them can be found in Bonus Section 1.1. The first plot is that of our function. And the two bottom plots are the derivative surfaces with respect to $x$ and $y$ variables.\n", - "\n", - "1. Ensure you understand how the plots relate to each other - if not, review the above material\n", - "2. Can you come up with a function where the partial derivative with respect to x will be a linear plane, and the derivative with respect to y will be more curvy?\n", - "3. What happens to the partial derivatives if no terms involve multiplying $x$ and $y$ together?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "T4zMt6wMf3Zk" - }, - "outputs": [], - "source": [ - "# @markdown Execute this widget to enable the demo\n", - "\n", - "# Let's use sympy to calculate Partial derivatives of a function of 2-variables\n", - "@interact(f2d_string = 'x**2 + 2*x*y + y**2')\n", - "def plot_partial_derivs(f2d_string):\n", - " f, x, y = sp.symbols('f, x, y')\n", - "\n", - " f2d = eval(f2d_string)\n", - " f2d_dx = sp.diff(f2d,x)\n", - " f2d_dy = sp.diff(f2d,y)\n", - "\n", - " print('Partial derivative of ', f2d, 'with respect to x is', f2d_dx)\n", - " print('Partial derivative of ', f2d, 'with respect to y is', f2d_dy)\n", - "\n", - " p1 = sp.plotting.plot3d(f2d, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='f(x,y)',title='Our function')\n", - "\n", - " p2 = sp.plotting.plot3d(f2d_dx, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='df(x,y)/dx',title='Derivative w.r.t. x')\n", - "\n", - " p3 = sp.plotting.plot3d(f2d_dy, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='df(x,y)/dy',title='Derivative w.r.t. y')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "cLwCCU7vf3Zk" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) Change terms involving x to just be linear (a constant times x, or a constant times x times some function of y).\n", - "Change a term involving y to involve more than y or y^2. For example, x + 2*x*y + y**3\n", - "\n", - "2) If there are no terms involving both x and y, the partial derivative with respect to x will\n", - " just depend on x and the partial derivative with respect to y will just depend on y.\n", - "\"\"\";" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "vF1chLsVf3Zk" - }, - "source": [ - "To see an application of the numerical calculation of partial derivatives to understand a neuron driven by excitatory and inhibitory inputs, see Bonus Section 1!\n", - "\n", - "We will use the partial derivative several times in the course. For example partial derivative are used the calculate the Jacobian of a system of differential equations. The Jacobian is used to determine the dynamics and stability of a system. This will be introduced in the second week while studying the dynamics of excitatory and inhibitory population interactions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "Q0j7SA99f3Zk" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Visualize_partial_derivatives_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "Imv-FOE5f3Zl" - }, - "source": [ - "---\n", - "# Section 4: Numerical Integration\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "NATlkGfpf3Zl" - }, - "outputs": [], - "source": [ - "# @title Video 5: Numerical Integration\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'cT0_CbD_h9Q'), ('Bilibili', 'BV1p54y1H7zt')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "FGQ_3nGCf3Zl" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Numerical_Integration_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "B32nL6y9f3Zl" - }, - "source": [ - "This video covers numerical integration and specifically Riemann sums.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "Geometrically, integration is the area under the curve. This interpretation gives two formal ways to calculate the integral of a function numerically.\n", - "\n", - "**[Riemann sum](https://en.wikipedia.org/wiki/Riemann_sum)**:\n", - "If we wish to integrate a function $f(t)$ with respect to $t$, then first we divide the function into $n$ intervals of size $dt = a-b$, where $a$ is the starting of the interval. Thus, each interval gives a rectangle with height $f(a)$ and width $dt$. By summing the area of all the rectangles, we can approximate the area under the curve. As the size $dt$ approaches to zero, our estimate of the integral approcahes the analytical calculation. Essentially, the Riemann sum is cutting the region under the curve in vertical stripes, calculating area of the each stripe and summing them up.\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "-yuPq7cIf3Zl" - }, - "source": [ - "## Section 4.1: Demonstration of the Riemann Sum\n", - "\n", - "*Estimated timing to here from start of tutorial: 60 min*\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "FyqtNb9ff3Zl" - }, - "source": [ - "### Interactive Demo 4.1: Riemann Sum vs. Analytical Integral with changing step size\n", - "\n", - "Below, we will compare numerical integration using the Riemann Sum with the analytical solution. You can change the interval size $dt$ using the slider.\n", - "\n", - "1. What values of dt result in the best numerical integration?\n", - "2. What is the downside of choosing that value of $dt$?\n", - "3. With large dt, why are we underestimating the integral (as opposed to overestimating?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "DCIZCjVef3Zm" - }, - "outputs": [], - "source": [ - "# @markdown Run this cell to enable the widget!\n", - "def riemann_sum_demo(dt = 0.5):\n", - " step_size = 0.1\n", - " min_val = 0.\n", - " max_val = 10.\n", - " tx = np.arange(min_val, max_val, step_size)\n", - "\n", - " # Our function\n", - " ftn = tx**2 - tx + 1\n", - " # And the integral analytical formula calculates using sympy\n", - " int_ftn = tx**3/3 - tx**2/2 + tx\n", - "\n", - " # Numerical integration of f(t) using Riemann Sum\n", - " n = int((max_val-min_val)/dt)\n", - " r_tx = np.zeros(n)\n", - " fun_value = np.zeros(n)\n", - " for ii in range(n):\n", - " a = min_val+ii*dt\n", - " fun_value[ii] = a**2 - a + 1\n", - " r_tx[ii] = a;\n", - "\n", - " # Riemann sum is just cumulative sum of the fun_value multiplied by the\n", - " r_sum = np.cumsum(fun_value)*dt\n", - " with plt.xkcd():\n", - " plt.figure(figsize=(20,5))\n", - " ax = plt.subplot(1,2,1)\n", - " plt.plot(tx,ftn,label='Function')\n", - "\n", - " for ii in range(n):\n", - " plt.plot([r_tx[ii], r_tx[ii], r_tx[ii]+dt, r_tx[ii]+dt], [0, fun_value[ii], fun_value[ii], 0] ,color='r')\n", - "\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('f(t)')\n", - " plt.title('f(t)')\n", - " plt.grid()\n", - "\n", - " plt.subplot(1,2,2)\n", - " plt.plot(tx,int_ftn,label='Analytical')\n", - " plt.plot(r_tx+dt,r_sum,color = 'r',label='Riemann Sum')\n", - " plt.xlabel('Time (au)')\n", - " plt.ylabel('int(f(t))')\n", - " plt.title('Integral of f(t)')\n", - " plt.grid()\n", - " plt.legend()\n", - " plt.show()\n", - "\n", - "\n", - "_ = widgets.interact(riemann_sum_demo, dt = (0.1, 1., .02))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "7EHfVa1wf3Zm" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) The smallest value of dt results in the best numerical integration (most similar\n", - " to the analytical solution)\n", - "\n", - "2) We have to compute the area of more rectangles.\n", - "\n", - "3) Here we used the forward Riemann sum, and it results in an underestimate of the\n", - " integral for positive values of $T$ and underestimate for negative values of\n", - " $T$. We could also use the backward Riemann sum, and that will give an overestimate\n", - " of the integral for positive values of $T$.\n", - "\"\"\";" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "SZiFfEHxf3Zm" - }, - "source": [ - "There are other methods of numerical integration, such as\n", - "**[Lebesgue integral](https://en.wikipedia.org/wiki/Lebesgue_integral)** and **Runge Kutta**. In the Lebesgue integral, we divide the area under the curve into horizontal stripes. That is, instead of the independent variable, the range of the function $f(t)$ is divided into small intervals. In any case, the Riemann sum is the basis of Euler's integration method for solving ordinary differential equations - something you will do in a later tutorial today." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "_1MMFiUef3Zm" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Riemann_Sum_vs_Analytical_Integral_with_changing_step_size_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "alSysG_Jf3Zm" - }, - "source": [ - "## Section 4.2: Neural Applications of Numerical Integration\n", - "\n", - "*Estimated timing to here from start of tutorial: 68 min*\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "4Ju_l1lLf3Zm" - }, - "source": [ - "### Coding Exercise 4.2: Calculating Charge Transfer with Excitatory Input\n", - "\n", - "An incoming spike elicits a change in the post-synaptic membrane potential (PSP), which can be captured by the following function:\n", - "\n", - "\\begin{equation}\n", - "PSP(t) = J \\cdot t \\cdot \\text{exp}\\left(-\\frac{t-t_{sp}}{\\tau_{s}}\\right)\n", - "\\end{equation}\n", - "\n", - "where $J$ is the synaptic amplitude, $t_{sp}$ is the spike time and $\\tau_s$ is the synaptic time constant.\n", - "\n", - "Estimate the total charge transferred to the postsynaptic neuron during a PSP with amplitude $J=1.0$, $\\tau_s = 1.0$ and $t_{sp} = 1$ (that is the spike occurred at $1$ ms). The total charge will be the integral of the PSP function." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "DlOaqWFPf3Zm" - }, - "outputs": [], - "source": [ - "########################################################################\n", - "## TODO for students\n", - "## Complete all ... in code below and remove\n", - "raise NotImplementedError(\"Calculate the charge transfer\")\n", - "########################################################################\n", - "\n", - "# Set up parameters\n", - "J = 1\n", - "tau_s = 1\n", - "t_sp = 1\n", - "dt = .1\n", - "t = np.arange(0, 10, dt)\n", - "\n", - "# Code PSP formula\n", - "PSP = ...\n", - "\n", - "# Compute numerical integral\n", - "# We already have PSP at every time step (height of rectangles). We need to\n", - "#. multiply by width of rectangles (dt) to get areas\n", - "rectangle_areas = ...\n", - "\n", - "# Cumulatively sum rectangles (hint: use np.cumsum)\n", - "numerical_integral = ...\n", - "\n", - "# Visualize\n", - "plot_charge_transfer(t, PSP, numerical_integral)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "g3hXDdBYf3Zn" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set up parameters\n", - "J = 1\n", - "tau_s = 1\n", - "t_sp = 1\n", - "dt = .1\n", - "t = np.arange(0, 10, dt)\n", - "\n", - "# Code PSP formula\n", - "PSP = J * t * np.exp(- (t-t_sp)/tau_s)\n", - "\n", - "# Compute numerical integral\n", - "# We already have PSP at every time step (height of rectangles). We need to\n", - "#. multiply by width of rectangles (dt) to get areas\n", - "rectangle_areas = PSP *dt\n", - "\n", - "# Cumulatively sum rectangles (hint: use np.cumsum)\n", - "numerical_integral = np.cumsum(rectangle_areas)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_charge_transfer(t, PSP, numerical_integral)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "v5Lp4LbMf3Zn" - }, - "source": [ - "You can see from the figure that the total charge transferred is a little over 2.5." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "VM89npQgf3Zn" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Calculating_Charge_Transfer_with_Excitatory_Input_exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "Pm6yOD1Nf3Zn" - }, - "source": [ - "---\n", - "# Section 5: Differentiation and Integration as Filtering Operations\n", - "\n", - "*Estimated timing to here from start of tutorial: 75 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "vQ0ln-Tnf3Zn" - }, - "outputs": [], - "source": [ - "# @title Video 6: Filtering Operations\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '7_ZjlT2d174'), ('Bilibili', 'BV1Vy4y1M7oT')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "uNKjyPFEf3Zn" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Filtering_Operations_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "sHQvPYXIf3Zo" - }, - "source": [ - "This video covers a different interpretation of differentiation and integration: viewing them as filtering operations.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "In the above, we used the notions that geometrically integration is the area under the curve and differentiation is the slope of the curve. There is another interpretation of these two operations.\n", - "\n", - "As we calculate the derivative of a function, we take the difference of adjacent values of the function. This results in the removal of common part between the two values. As a consequence, we end up removing the unchanging part of the signal. If we now think in terms of frequencies, differentiation removes low frequencies, or slow changes. That is, differentiation acts as a high pass filter.\n", - "\n", - "Integration does the opposite because in the estimation of an integral we keep adding adjacent values of the signal. So, again thinking in terms of frequencies, integration is akin to the removal of high frequencies or fast changes (low-pass filter). The shock absorbers in your bike are an example of integrators.\n", - "\n", - "We can see this behavior the demo below. Here we will not work with functions, but with signals. As such, functions and signals are the same. Just that in most cases our signals are measurements with respect to time." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "_kGtaT7mf3Zo" - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to see visualization\n", - "h = 0.01\n", - "tx = np.arange(0,2,h)\n", - "noise_signal = np.random.uniform(0, 1, (len(tx)))*0.5\n", - "x1 = np.sin(0.5*np.pi*tx) + noise_signal # This will generate a 1 Hz sin wave\n", - "# In the signal x1 we have added random noise which contributs the high frequencies\n", - "\n", - "# Take the derivative equivalent of the signal i.e. subtract the adjacent values\n", - "x1_diff = (x1[1:] - x1[:-1])\n", - "\n", - "# Take the integration equivalent of the signal i.e. sum the adjacent values. And divide by 2 (take average essentially)\n", - "x1_integrate = (x1[1:] + x1[:-1])/2\n", - "\n", - "# Plotting code\n", - "plt.figure(figsize=(15,10))\n", - "plt.subplot(3,1,1)\n", - "plt.plot(tx,x1,label='Original Signal')\n", - "#plt.xlabel('Time (sec)')\n", - "plt.ylabel('Signal Value(au)')\n", - "plt.legend()\n", - "\n", - "plt.subplot(3,1,2)\n", - "plt.plot(tx[0:-1],x1_diff,label='Differentiated Signal')\n", - "# plt.xlabel('Time (sec)')\n", - "plt.ylabel('Differentiated Value(au)')\n", - "plt.legend()\n", - "\n", - "plt.subplot(3,1,3)\n", - "plt.plot(tx,x1,label='Original Signal')\n", - "plt.plot(tx[0:-1],x1_integrate,label='Integrate Signal')\n", - "plt.xlabel('Time (sec)')\n", - "plt.ylabel('Integrate Value(au)')\n", - "plt.legend()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "rOTJkbSRf3Zo" - }, - "source": [ - "Notice how the differentiation operation amplifies the fast changes which were contributed by noise. By contrast, the integration operation suppresses the fast-changing noise. We will further smooth the signal if we perform the same operation of averaging the adjacent samples on the orange trace. Such sums and subtractions form the basis of digital filters." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "7dQWDg3Gf3Zo" - }, - "source": [ - "---\n", - "# Summary\n", - "\n", - "*Estimated timing of tutorial: 80 minutes*\n", - "\n", - "* Geometrically, integration is the area under the curve, and differentiation is the slope of the function\n", - "* The concepts of slope and area can be easily extended to higher dimensions. We saw this when we took the derivative of a 2-dimensional transfer function of a neuron\n", - "* Numerical estimates of both derivatives and integrals require us to choose a time step $h$. The smaller the $h$, the better the estimate, but more computations are needed for small values of $h$. So there is always some tradeoff\n", - "* Partial derivatives are just the estimate of the slope along one of the many dimensions of the function. We can combine the slopes in different directions using vector sum to find the direction of the slope\n", - "* Because the derivative of a function is zero at the local peak or trough, derivatives are used to solve optimization problems\n", - "* When thinking of signal, integration operation is equivalent to smoothening the signals (i.e., removing fast changes)\n", - "* Differentiation operations remove slow changes and enhance the high-frequency content of a signal" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "LCM50wunf3Zo" - }, - "source": [ - "---\n", - "# Bonus Section 1: Numerical calculation of partial derivatives\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "hcXseSHlf3Zo" - }, - "source": [ - "## Bonus Section 1.1: Understanding 2D plots\n", - "\n", - "Let's take the example of a neuron driven by excitatory and inhibitory inputs. Because this is for illustrative purposes, we will not go into the details of the numerical range of the input and output variables.\n", - "\n", - "In the function below, we assume that the firing rate of a neuron increases monotonically with an increase in excitation and decreases monotonically with an increase in inhibition. The inhibition is modeled as a subtraction. As for the 1-dimensional transfer function, we assume that we can approximate the transfer function as a sigmoid function.\n", - "\n", - "We can use the same numerical differentiation as before to evaluate the partial derivatives, but now we apply it to each row and column separately." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "5HLCH1G0f3Zo" - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to visualize the neuron firing rate surface\n", - "def sigmoid_function(x,a,theta):\n", - " '''\n", - " Population activation function.\n", - "\n", - " Expects:\n", - " x : the population input\n", - " a : the gain of the function\n", - " theta : the threshold of the function\n", - "\n", - " Returns:\n", - " the population activation response F(x) for input x\n", - " '''\n", - " # add the expression of f = F(x)\n", - " f = (1+np.exp(-a*(x-theta)))**-1 - (1+np.exp(a*theta))**-1\n", - "\n", - " return f\n", - "\n", - "# Neuron Transfer function\n", - "step_size = 0.1\n", - "exc_input = np.arange(2,9,step_size)\n", - "inh_input = np.arange(0,7,step_size)\n", - "exc_a = 1.2\n", - "exc_theta = 2.4\n", - "inh_a = 1.\n", - "inh_theta = 4.\n", - "\n", - "rate = np.zeros((len(exc_input),len(inh_input)))\n", - "\n", - "for ii in range(len(exc_input)):\n", - " for jj in range(len(inh_input)):\n", - " rate[ii,jj] = sigmoid_function(exc_input[ii],exc_a,exc_theta) - sigmoid_function(inh_input[jj],inh_a,inh_theta)*0.5\n", - "\n", - "with plt.xkcd():\n", - " X, Y = np.meshgrid(exc_input, inh_input)\n", - " fig = plt.figure(figsize=(12,12))\n", - " ax1 = fig.add_subplot(2,2,1)\n", - " lg_txt = 'Inhibition = ' + str(inh_input[0])\n", - " ax1.plot(exc_input,rate[:,0],label=lg_txt)\n", - " lg_txt = 'Inhibition = ' + str(inh_input[20])\n", - " ax1.plot(exc_input,rate[:,20],label=lg_txt)\n", - " lg_txt = 'Inhibition = ' + str(inh_input[40])\n", - " ax1.plot(exc_input,rate[:,40],label=lg_txt)\n", - " ax1.legend()\n", - " ax1.set_xlabel('Excitatory input (au)')\n", - " ax1.set_ylabel('Neuron output rate (au)');\n", - "\n", - " ax2 = fig.add_subplot(2,2,2)\n", - " lg_txt = 'Excitation = ' + str(exc_input[0])\n", - " ax2.plot(inh_input,rate[0,:],label=lg_txt)\n", - " lg_txt = 'Excitation = ' + str(exc_input[20])\n", - " ax2.plot(inh_input,rate[20,:],label=lg_txt)\n", - " lg_txt = 'Excitation = ' + str(exc_input[40])\n", - " ax2.plot(inh_input,rate[40,:],label=lg_txt)\n", - " ax2.legend()\n", - " ax2.set_xlabel('Inhibitory input (au)')\n", - " ax2.set_ylabel('Neuron output rate (au)');\n", - "\n", - " ax3 = fig.add_subplot(2, 1, 2, projection='3d')\n", - " surf= ax3.plot_surface(Y.T, X.T, rate, rstride=1, cstride=1,\n", - " cmap='viridis', edgecolor='none')\n", - " ax3.set_xlabel('Inhibitory input (au)')\n", - " ax3.set_ylabel('Excitatory input (au)')\n", - " ax3.set_zlabel('Neuron output rate (au)');\n", - " fig.colorbar(surf)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "WkpVNMRgf3Zp" - }, - "source": [ - "The **Top-Left** plot shows how the neuron output rate increases as a function of excitatory input (e.g., the blue trace). However, as we increase inhibition, expectedly, the neuron output decreases, and the curve is shifted downwards. This constant shift in the curve suggests that the effect of inhibition is subtractive, and the amount of subtraction does not depend on the neuron output.\n", - "\n", - "We can alternatively see how the neuron output changes with respect to inhibition and study how excitation affects that. This is visualized in the **Top-Right** plot.\n", - "\n", - "This type of plotting is very intuitive, but it becomes very tedious to visualize when there are larger numbers of lines to be plotted. A nice solution to this visualization problem is to render the data as color, as surfaces, or both.\n", - "\n", - "This is what we have done in the plot at the bottom. The color map on the right shows the neuron's output as a function of inhibitory and excitatory input. The output rate is shown both as height along the z-axis and as the color. Blue means a low firing rate, and yellow represents a high firing rate (see the color bar).\n", - "\n", - "In the above plot, the output rate of the neuron goes below zero. This is, of course, not physiological, as neurons cannot have negative firing rates. In models, we either choose the operating point so that the output does not go below zero, or we clamp the neuron output to zero if it goes below zero. You will learn about it more in Week 2." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "QTBofq-hf3Zp" - }, - "source": [ - "## Bonus Section 1.2: Numerical partial derivatives\n", - "\n", - "We can now compute the partial derivatives of our transfer function in response to excitatory and inhibitory input. We do so below!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "_ilQ5NfXf3Zp" - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell implement our neural transfer function, `plot_2d_neuron_transfer_function`, in respond to excitatory and inhibitory input\n", - "def plot_2d_neuron_transfer_function(exc_a, exc_theta, inh_a, inh_theta):\n", - " # Neuron Transfer Function\n", - " step_size = 0.1\n", - " exc_input = np.arange(1,10,step_size)\n", - " inh_input = np.arange(0,7,step_size)\n", - "\n", - " rate = np.zeros((len(exc_input),len(inh_input)))\n", - " for ii in range(len(exc_input)):\n", - " for jj in range(len(inh_input)):\n", - " rate[ii,jj] = sigmoid_function(exc_input[ii],exc_a,exc_theta) - sigmoid_function(inh_input[jj],inh_a,inh_theta)*0.5\n", - "\n", - " # Derivative with respect to excitatory input rate\n", - " rate_de = np.zeros((len(exc_input)-1,len(inh_input)))# this will have one row less than the rate matrix\n", - " for ii in range(len(inh_input)):\n", - " rate_de[:,ii] = (rate[1:,ii] - rate[0:-1,ii])/step_size\n", - "\n", - " # Derivative with respect to inhibitory input rate\n", - " rate_di = np.zeros((len(exc_input),len(inh_input)-1))# this will have one column less than the rate matrix\n", - " for ii in range(len(exc_input)):\n", - " rate_di[ii,:] = (rate[ii,1:] - rate[ii,0:-1])/step_size\n", - "\n", - "\n", - " X, Y = np.meshgrid(exc_input, inh_input)\n", - " fig = plt.figure(figsize=(20,8))\n", - " ax1 = fig.add_subplot(1, 3, 1, projection='3d')\n", - " surf1 = ax1.plot_surface(Y.T, X.T, rate, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", - " ax1.set_xlabel('Inhibitory input (au)')\n", - " ax1.set_ylabel('Excitatory input (au)')\n", - " ax1.set_zlabel('Neuron output rate (au)')\n", - " ax1.set_title('Rate as a function of Exc. and Inh');\n", - " ax1.view_init(45, 10)\n", - " fig.colorbar(surf1)\n", - "\n", - " Xde, Yde = np.meshgrid(exc_input[0:-1], inh_input)\n", - " ax2 = fig.add_subplot(1, 3, 2, projection='3d')\n", - " surf2 = ax2.plot_surface(Yde.T, Xde.T, rate_de, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", - " ax2.set_xlabel('Inhibitory input (au)')\n", - " ax2.set_ylabel('Excitatory input (au)')\n", - " ax2.set_zlabel('Neuron output rate (au)');\n", - " ax2.set_title('Derivative wrt Excitation');\n", - " ax2.view_init(45, 10)\n", - " fig.colorbar(surf2)\n", - "\n", - " Xdi, Ydi = np.meshgrid(exc_input, inh_input[:-1])\n", - " ax3 = fig.add_subplot(1, 3, 3, projection='3d')\n", - " surf3 = ax3.plot_surface(Ydi.T, Xdi.T, rate_di, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", - " ax3.set_xlabel('Inhibitory input (au)')\n", - " ax3.set_ylabel('Excitatory input (au)')\n", - " ax3.set_zlabel('Neuron output rate (au)');\n", - " ax3.set_title('Derivative wrt Inhibition');\n", - " ax3.view_init(15, -115)\n", - " fig.colorbar(surf3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "18k4QcXPf3Zp" - }, - "outputs": [], - "source": [ - "plot_2d_neuron_transfer_function(exc_a=1.2, exc_theta=2.4, inh_a=1, inh_theta=4)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "Ycrutkqvf3Zp" - }, - "source": [ - "Is this what you expected? Change the parameters in the function to generate the 2-D transfer function of the neuron for different excitatory and inhibitory $a$ and $\\theta$ and test your intuitions. Can you relate this shape of the partial derivative surface to the gain of the 1-D transfer function of a neuron (Section 2)?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "RSlm7-54f3Zp" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "Here excitation and inhibition are interacting additively. That is, if you change $\\theta$ and $a$ for excitation,\n", - "it will not affect the derivative wrt to inhibition and vice versa.\n", - "\n", - "The effect of varying $\\theta$ and $a$ on the derivative wrt to excitation and inhibition is identical to what you for 1-D transfer function\n", - "$\\theta$ shifts the neuron transfer function along the x-axis -- reducing $theta$ moves the transfer function leftwards.\n", - "So by changing $\\theta$ for excitation you will shift the derivative surface left/right along the excitation axis. Same for inhibition.\n", - "\n", - "$a$ controls the steepness of the neuron transfer function -- increasing $a$ makes the curve more steeper.\n", - "So by changing $a$ for excitation you will vary the width of the notch of the derivative surface.\n", - "For inhibition, by varying $a$ you will vary the width of the ridge in the drivative surface.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "RSyrrZQqf3Zq" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Numerical_partial_derivatives_Bonus_Discussion\")" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [ - "nq-hVvP1MZqa", - "KCWR4wciMZqb", - "tcLiKp-AMZqc", - "8-bz4N8eMZqd", - "9-BbBcZPMZqf", - "L0_P-VNEMZqf", - "Yx7vP4zvMZqg", - "RACJEWwMMZqg", - "dXFmgh9UMZqh", - "pMQS0U54MZqi", - "ZLb9XPF_VvPm", - "c0oilOjOV0KZ" - ], - "name": "W0D4_Tutorial1", - "provenance": [], - "toc_visible": true, - "include_colab_link": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "execution": {}, + "id": "view-in-github" + }, + "source": [ + "\"Open   \"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "# Tutorial 1: Differentiation and Integration\n", + "\n", + "**Week 0, Day 4: Calculus**\n", + "\n", + "**By Neuromatch Academy**\n", + "\n", + "**Content creators:** John S Butler, Arvind Kumar with help from Ella Batty\n", + "\n", + "**Content reviewers:** Aderogba Bayo, Tessy Tom, Matt McCann\n", + "\n", + "**Production editors:** Matthew McCann, Spiros Chavlis, Ella Batty" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Tutorial Objectives\n", + "\n", + "*Estimated timing of tutorial: 80 minutes*\n", + "\n", + "In this tutorial, we will cover aspects of calculus that will be frequently used in the main NMA course. We assume that you have some familiarity with calculus but may be a bit rusty or may not have done much practice. Specifically, the objectives of this tutorial are\n", + "\n", + "* Get an intuitive understanding of derivative and integration operations\n", + "* Learn to calculate the derivatives of 1- and 2-dimensional functions/signals numerically\n", + "* Familiarize with the concept of the neuron transfer function in 1- and 2-dimensions.\n", + "* Familiarize with the idea of numerical integration using the Riemann sum" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Install dependencies\n", + "!pip install sympy --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D4_T1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import scipy.optimize as opt # import root-finding algorithm\n", + "import sympy as sp # Python toolbox for symbolic maths\n", + "import matplotlib.pyplot as plt\n", + "from mpl_toolkits.mplot3d import Axes3D # Toolbox for rendring 3D figures\n", + "from mpl_toolkits import mplot3d # Toolbox for rendring 3D figures" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Figure Settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import ipywidgets as widgets # interactive display\n", + "from ipywidgets import interact\n", + "%config InlineBackend.figure_format = 'retina'\n", + "# use NMA plot style\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", + "my_layout = widgets.Layout()\n", + "\n", + "fig_w, fig_h = 12, 4.5\n", + "my_fontsize = 16\n", + "my_params = {'axes.labelsize': my_fontsize,\n", + " 'axes.titlesize': my_fontsize,\n", + " 'figure.figsize': [fig_w, fig_h],\n", + " 'font.size': my_fontsize,\n", + " 'legend.fontsize': my_fontsize-4,\n", + " 'lines.markersize': 8.,\n", + " 'lines.linewidth': 2.,\n", + " 'xtick.labelsize': my_fontsize-2,\n", + " 'ytick.labelsize': my_fontsize-2}\n", + "\n", + "plt.rcParams.update(my_params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Plotting Functions\n", + "def move_sympyplot_to_axes(p, ax):\n", + " backend = p.backend(p)\n", + " backend.ax = ax\n", + " backend.process_series()\n", + " backend.ax.spines['right'].set_color('none')\n", + " backend.ax.spines['bottom'].set_position('zero')\n", + " backend.ax.spines['top'].set_color('none')\n", + " plt.close(backend.fig)\n", + "\n", + "\n", + "def plot_functions(function, show_derivative, show_integral):\n", + "\n", + " # For sympy we first define our symbolic variable\n", + " x, y, z, t, f = sp.symbols('x y z t f')\n", + "\n", + " # We define our function\n", + " if function == 'Linear':\n", + " f = -2*t\n", + " name = r'$-2t$'\n", + " elif function == 'Parabolic':\n", + " f = t**2\n", + " name = r'$t^2$'\n", + " elif function == 'Exponential':\n", + " f = sp.exp(t)\n", + " name = r'$e^t$'\n", + " elif function == 'Sine':\n", + " f = sp.sin(t)\n", + " name = r'$sin(t)$'\n", + " elif function == 'Sigmoid':\n", + " f = 1/(1 + sp.exp(-(t-5)))\n", + " name = r'$\\frac{1}{1+e^{-(t-5)}}$'\n", + "\n", + " if show_derivative and not show_integral:\n", + " # Calculate the derivative of sin(t) as a function of t\n", + " diff_f = sp.diff(f)\n", + " print('Derivative of', f, 'is ', diff_f)\n", + "\n", + " p1 = sp.plot(f, diff_f, show=False)\n", + " p1[0].line_color='r'\n", + " p1[1].line_color='b'\n", + " p1[0].label='Function'\n", + " p1[1].label='Derivative'\n", + " p1.legend=True\n", + " p1.title = 'Function = ' + name + '\\n'\n", + " p1.show()\n", + " elif show_integral and not show_derivative:\n", + "\n", + " int_f = sp.integrate(f)\n", + " int_f = int_f - int_f.subs(t, -10)\n", + " print('Integral of', f, 'is ', int_f)\n", + "\n", + "\n", + " p1 = sp.plot(f, int_f, show=False)\n", + " p1[0].line_color='r'\n", + " p1[1].line_color='g'\n", + " p1[0].label='Function'\n", + " p1[1].label='Integral'\n", + " p1.legend=True\n", + " p1.title = 'Function = ' + name + '\\n'\n", + " p1.show()\n", + "\n", + "\n", + " elif show_integral and show_derivative:\n", + "\n", + " diff_f = sp.diff(f)\n", + " print('Derivative of', f, 'is ', diff_f)\n", + "\n", + " int_f = sp.integrate(f)\n", + " int_f = int_f - int_f.subs(t, -10)\n", + " print('Integral of', f, 'is ', int_f)\n", + "\n", + " p1 = sp.plot(f, diff_f, int_f, show=False)\n", + " p1[0].line_color='r'\n", + " p1[1].line_color='b'\n", + " p1[2].line_color='g'\n", + " p1[0].label='Function'\n", + " p1[1].label='Derivative'\n", + " p1[2].label='Integral'\n", + " p1.legend=True\n", + " p1.title = 'Function = ' + name + '\\n'\n", + " p1.show()\n", + "\n", + " else:\n", + "\n", + " p1 = sp.plot(f, show=False)\n", + " p1[0].line_color='r'\n", + " p1[0].label='Function'\n", + " p1.legend=True\n", + " p1.title = 'Function = ' + name + '\\n'\n", + " p1.show()\n", + "\n", + "\n", + "def plot_alpha_func(t, f, df_dt):\n", + "\n", + " plt.figure()\n", + " plt.subplot(2,1,1)\n", + " plt.plot(t, f, 'r', label='Alpha function')\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('Voltage')\n", + " plt.title('Alpha function (f(t))')\n", + " #plt.legend()\n", + "\n", + " plt.subplot(2,1,2)\n", + " plt.plot(t, df_dt, 'b', label='Derivative')\n", + " plt.title('Derivative of alpha function')\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('df/dt')\n", + " #plt.legend()\n", + "\n", + "\n", + "def plot_charge_transfer(t, PSP, numerical_integral):\n", + "\n", + " fig, axes = plt.subplots(1, 2)\n", + "\n", + " axes[0].plot(t, PSP)\n", + " axes[0].set(xlabel = 't', ylabel = 'PSP')\n", + "\n", + " axes[1].plot(t, numerical_integral)\n", + " axes[1].set(xlabel = 't', ylabel = 'Charge Transferred')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 0: Introduction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 1: Why do we care about calculus?\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'NZwfH_dG2wI'), ('Bilibili', 'BV1F44y1z7Uk')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Why_do_we_care_about_calculus_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 1: What is differentiation and integration?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 2: A geometrical interpretation of differentiation and integration\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'uQjwr9RQaEs'), ('Bilibili', 'BV1sU4y1G7Ru')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_A_geometrical_interpretation_of_differentiation_and_integration_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "This video covers the definition of differentiation and integration, highlights the geometrical interpretation of each, and introduces the idea of eigenfunctions.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "Calculus is a part of mathematics concerned with **continous change**. There are two branches of calculus: differential calculus and integral calculus.\n", + "\n", + "Differentiation of a function $f(t)$ gives you the derivative of that function $\\frac{d(f(t))}{dt}$. A derivative captures how sensitive a function is to slight changes in the input for different ranges of inputs. Geometrically, the derivative of a function at a certain input is the slope of the function at that input. For example, as you drive, the distance traveled changes continuously with time. The derivative of the distance traveled with respect to time is the velocity of the vehicle at each point in time. The velocity tells you the rate of change of the distance traveled at different points in time. If you have slow velocity (a small derivative), the distance traveled doesn't change much for small changes in time. A high velocity (big derivative) means that the distance traveled changes a lot for small changes in time.\n", + "\n", + "The sign of the derivative of a function (or signal) tells whether the signal is increasing or decreasing. For a signal going through changes as a function of time, the derivative will become zero when the signal changes its direction of change (e.g. from increasing to decreasing). That is, at local minimum or maximum values, the slope of the signal will be zero. This property is used in optimizing problems. But we can also use it to find peaks in a signal.\n", + "\n", + "Integration can be thought of as the reverse of differentation. If we integrate the velocity with respect to time, we can calculate the distance traveled. By integrating a function, we are basically trying to find functions that would have the original one as their derivative. When we integrate a function, our integral will have an added unknown scalar constant, $C$.\n", + "For example, if\n", + "\n", + "\\begin{equation}\n", + "g(t) = 1.5t^2 + 4t - 1\n", + "\\end{equation}\n", + "\n", + "our integral function $f(t)$ will be:\n", + "\n", + "\\begin{equation}\n", + "f(t) = \\int g(t) dt = 0.5t^3 + 2t^2 - t + C\n", + "\\end{equation}\n", + "\n", + "This constant exists because the derivative of a constant is 0 so we cannot know what the constant should be. This is an indefinite integral. If we compute a definite integral, that is the integral between two limits of the input, we will not have this unknown constant and the integral of a function will capture the area under the curve of that function between those two limits.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 1: Geometrical understanding\n", + "\n", + "In the interactive demo below, you can pick different functions to examine in the drop-down menu. You can then choose to show the derivative function and/or the integral function.\n", + "\n", + "For the integral, we have chosen the unknown constant $C$ such that the integral function at the left x-axis limit is $0$, as $f(t = -10) = 0$. So the integral will reflect the area under the curve starting from that position.\n", + "\n", + "For each function:\n", + "\n", + "* Examine just the function first. Discuss and predict what the derivative and integral will look like. Remember that _derivative = slope of the function_, _integral = area under the curve from $t = -10$ to that $t$_.\n", + "* Check the derivative - does it match your expectations?\n", + "* Check the integral - does it match your expectations?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to enable the widget\n", + "function_options = widgets.Dropdown(\n", + " options=['Linear', 'Exponential', 'Sine', 'Sigmoid'],\n", + " description='Function',\n", + " disabled=False,\n", + ")\n", + "\n", + "derivative = widgets.Checkbox(\n", + " value=False,\n", + " description='Show derivative',\n", + " disabled=False,\n", + " indent=False\n", + ")\n", + "\n", + "integral = widgets.Checkbox(\n", + " value=False,\n", + " description='Show integral',\n", + " disabled=False,\n", + " indent=False\n", + ")\n", + "\n", + "def on_value_change(change):\n", + " derivative.value = False\n", + " integral.value = False\n", + "\n", + "function_options.observe(on_value_change, names='value')\n", + "\n", + "interact(plot_functions, function = function_options, show_derivative = derivative, show_integral = integral);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "In the demo above, you may have noticed that the derivative and integral of the exponential function are the same as the exponential function itself.\n", + "\n", + "When differentiated or integrated, some functions, like the exponential function, equal scalar times the same function. This is a similar idea to eigenvectors of a matrix being those that, when multiplied by the matrix, equal scalar times themselves, as you saw yesterday!\n", + "\n", + "When\n", + "\n", + "\\begin{equation}\n", + "\\frac{d(f(t)}{dt} = a \\cdot f(t)\\text{,}\n", + "\\end{equation}\n", + "\n", + "we say that $f(t)$ is an **eigenfunction** for derivative operator, where $a$ is a scaling factor. Similarly, when\n", + "\n", + "\\begin{equation}\n", + "\\int f(t)dt = a \\cdot f(t)\\text{,}\n", + "\\end{equation}\n", + "\n", + "we say that $f(t)$ is an **eigenfunction** for integral operator.\n", + "\n", + "As you can imagine, working with eigenfunctions can make mathematical analysis easy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Geometrical_understanding_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 2: Analytical & Numerical Differentiation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 3: Differentiation\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'sHogZISXGuQ'), ('Bilibili', 'BV14g41137d5')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Differentiation_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "\n", + "In this section, we will delve into how we actually find the derivative of a function, both analytically and numerically.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "When we find the derivative analytically, we obtain the exact formula for the derivative function.\n", + "\n", + "To do this, instead of having to do some fancy math every time, we can often consult [an online resource](https://en.wikipedia.org/wiki/Differentiation_rules) for a list of common derivatives, in this case, our trusty friend Wikipedia.\n", + "\n", + "If I told you to find the derivative of $f(t) = t^3$, you could consult that site and find in Section 2.1 that if $f(t) = t^n$, then $\\frac{d(f(t))}{dt} = nt^{n-1}$. So you would be able to tell me that the derivative of $f(t) = t^3$ is $\\frac{d(f(t))}{dt} = 3t^{2}$.\n", + "\n", + "This list of common derivatives often contains only very simple functions. Luckily, as we'll see in the next two sections, we can often break the derivative of a complex function down into the derivatives of more simple components." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Section 2.1.1: Product Rule\n", + "\n", + "Sometimes we encounter functions which are the product of two functions that both depend on the variable.\n", + "How do we take the derivative of such functions? For this we use the [Product Rule](https://en.wikipedia.org/wiki/Product_rule).\n", + "\n", + "\\begin{align}\n", + "f(t) &= u(t) \\cdot v(t) \\\\ \\\\\n", + "\\frac{d(f(t))}{dt} &= v \\cdot \\frac{du}{dt} + u \\cdot \\frac{dv}{dt}\n", + "\\end{align}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "#### Coding Exercise 2.1.1: Derivative of the postsynaptic potential alpha function\n", + "\n", + "Let's use the product rule to get the derivative of the postsynaptic potential alpha function. As we saw in Video 3, the shape of the postsynaptic potential is given by the so-called alpha function:\n", + "\n", + "\\begin{equation}\n", + "f(t) = t \\cdot \\text{exp}\\left( -\\frac{t}{\\tau} \\right)\n", + "\\end{equation}\n", + "\n", + "Here $f(t)$ is a product of $t$ and $\\text{exp} \\left(-\\frac{t}{\\tau} \\right)$. So we can have $u(t) = t$ and $v(t) = \\text{exp} \\left( -\\frac{t}{\\tau} \\right)$ and use the product rule!\n", + "\n", + "We have defined $u(t)$ and $v(t)$ in the code below for the variable $t$, an array of time steps from 0 to 10. Define $\\frac{du}{dt}$ and $\\frac{dv}{dt}$, then compute the full derivative of the alpha function using the product rule. You can always consult Wikipedia to figure out $\\frac{du}{dt}$ and $\\frac{dv}{dt}$!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "########################################################################\n", + "## TODO for students\n", + "## Complete all ... in code below and remove\n", + "raise NotImplementedError(\"Calculate the derivatives\")\n", + "########################################################################\n", + "\n", + "# Define time, time constant\n", + "t = np.arange(0, 10, .1)\n", + "tau = 0.5\n", + "\n", + "# Compute alpha function\n", + "f = t * np.exp(-t/tau)\n", + "\n", + "# Define u(t), v(t)\n", + "u_t = t\n", + "v_t = np.exp(-t/tau)\n", + "\n", + "# Define du/dt, dv/dt\n", + "du_dt = ...\n", + "dv_dt = ...\n", + "\n", + "# Define full derivative\n", + "df_dt = ...\n", + "\n", + "# Visualize\n", + "plot_alpha_func(t, f, df_dt)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Define time, time constant\n", + "t = np.arange(0, 10, .1)\n", + "tau = 0.5\n", + "\n", + "# Compute alpha function\n", + "f = t * np.exp(-t/tau)\n", + "\n", + "# Define u(t), v(t)\n", + "u_t = t\n", + "v_t = np.exp(-t/tau)\n", + "\n", + "# Define du/dt, dv/dt\n", + "du_dt = 1\n", + "dv_dt = -1/tau * np.exp(-t/tau)\n", + "\n", + "# Define full derivative\n", + "df_dt = u_t * dv_dt + v_t * du_dt\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_alpha_func(t, f, df_dt)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Derivative_of_the_postsynaptic_potential_alpha_function_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Section 2.1.2: Chain Rule\n", + "\n", + "We often encounter situations in which the variable $a$ changes with time ($t$) and affects another variable $r$. How can we estimate the derivative of $r$ with respect to $a$, i.e., $\\frac{dr}{da} = ?$\n", + "\n", + "To calculate $\\frac{dr}{da}$ we use the [Chain Rule](https://en.wikipedia.org/wiki/Chain_rule).\n", + "\n", + "\\begin{equation}\n", + "\\frac{dr}{da} = \\frac{dr}{dt}\\cdot\\frac{dt}{da}\n", + "\\end{equation}\n", + "\n", + "We calculate the derivative of both variables with respect to $t$ and divide that derivative of $r$ by that derivative of $a$.\n", + "\n", + "We can also use this formula to simplify taking derivatives of complex functions! We can make an arbitrary function $t$ to compute more simple derivatives and multiply, as seen in this exercise." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "#### Math Exercise 2.1.2: Chain Rule\n", + "\n", + "Let's say that:\n", + "\n", + "\\begin{equation}\n", + "r(a) = e^{a^4 + 1}\n", + "\\end{equation}\n", + "\n", + "What is $\\frac{dr}{da}$? This is a more complex function, so we can't simply consult a table of common derivatives. Can you use the chain rule to help?\n", + "\n", + "**Hint:** We didn't define $t$, but you could set $t$ equal to the function in the exponent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "\n", + "We can set t equal to the exponent:\n", + "t = a^4 + 1\n", + "r(t) = e^t\n", + "\n", + "\n", + "Then:\n", + " dt/da = 4a^3\n", + " dr/dt = e^t\n", + "\n", + "Now we can use the chain rule:\n", + "dr/da = dr/dt * dt/da\n", + " = e^t(4a^3)\n", + " = 4a^3e^{a^4 + 1}\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Chain_Rule_Math_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Section 2.2.3: Derivatives in Python using SymPy\n", + "\n", + "There is a useful Python library for getting the analytical derivatives of functions: SymPy. We actually used this in Interactive Demo 1, under the hood.\n", + "\n", + "See the following cell for an example of setting up a SymPy function and finding the derivative." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# For sympy we first define our symbolic variables\n", + "f, t = sp.symbols('f, t')\n", + "\n", + "# Function definition (sigmoid)\n", + "f = 1/(1 + sp.exp(-(t-5)))\n", + "\n", + "# Get the derivative\n", + "diff_f = sp.diff(f)\n", + "\n", + "# Print the resulting function\n", + "print('Derivative of', f, 'is ', diff_f)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 2.2: Numerical Differentiation\n", + "\n", + "*Estimated timing to here from start of tutorial: 30 min*\n", + "\n", + "Formally, the derivative of a function $\\mathcal{f}(x)$ at any value $a$ is given by the finite difference (FD) formula:\n", + "\n", + "\\begin{equation}\n", + "FD = \\frac{f(a+h) - f(a)}{h}\n", + "\\end{equation}\n", + "\n", + "As $h\\rightarrow 0$, the $FD$ approaches the actual value of the derivative. Let's check this.\n", + "\n", + "**Note:** The numerical estimate of the derivative will result\n", + "in a time series whose length is one short of the original time series." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 2.2: Numerical Differentiation of the Sine Function\n", + "\n", + "Below, we find the numerical derivative of the sine function for different values of $h$ and compare the result to the analytical solution.\n", + "\n", + "* What values of $h$ result in more accurate numerical derivatives?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to enable the widget.*\n", + "def numerical_derivative_demo(h = 0.2):\n", + " # Now lets create a sequence of numbers which change according to the sine function\n", + " dt = 0.01\n", + " tx = np.arange(-10, 10, dt)\n", + " sine_fun = np.sin(tx)\n", + "\n", + " # symbolic diffrentiation tells us that the derivative of sin(t) is cos(t)\n", + " cos_fun = np.cos(tx)\n", + "\n", + " # Numerical derivative using difference formula\n", + " n_tx = np.arange(-10,10,h) # create new time axis\n", + " n_sine_fun = np.sin(n_tx) # calculate the sine function on the new time axis\n", + " sine_diff = (n_sine_fun[1:] - n_sine_fun[0:-1]) / h\n", + "\n", + " fig = plt.figure()\n", + " ax = plt.subplot(111)\n", + " plt.plot(tx, sine_fun, label='sine function')\n", + " plt.plot(tx, cos_fun, label='analytical derivative of sine')\n", + "\n", + " with plt.xkcd():\n", + " # notice that numerical derivative will have one element less\n", + " plt.plot(n_tx[0:-1], sine_diff, label='numerical derivative of sine')\n", + " plt.xlim([-10, 10])\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('f(x) or df(x)/dt')\n", + " ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.05),\n", + " ncol=3, fancybox=True)\n", + " plt.show()\n", + "\n", + "_ = widgets.interact(numerical_derivative_demo, h = (0.01, 0.5, .02))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "The smaller the value of $h$, the better the estimate of the derivative of the\n", + "function at $x=a$.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Numerical_Differentiation_of_the_Sine_Function_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 2.3: Transfer Function and Gain of a Neuron\n", + "\n", + "*Estimated timing to here from start of tutorial: 34 min*\n", + "\n", + "When we inject a constant current (DC) into a neuron, its firing rate changes as a function of the strength of the injected current. This is called the **input-output transfer function** or just the *transfer function* or *I/O Curve* of the neuron. For most neurons, this can be approximated by a sigmoid function, e.g.:\n", + "\n", + "\\begin{equation}\n", + "rate(I) = \\frac{1}{1+\\text{exp}(-a \\cdot (I-\\theta))} - \\frac{1}{\\text{exp}(a \\cdot \\theta)} + \\eta\n", + "\\end{equation}\n", + "\n", + "where $I$ is injected current, $rate$ is the neuron firing rate and $\\eta$ is noise (Gaussian noise with zero mean and $\\sigma$ standard deviation).\n", + "\n", + "*You will visit this equation in a different context in Week 3*\n", + "\n", + "The slope of a neuron input-output transfer function, i.e., $\\frac{d(r(I))}{dI}$, is called the **gain** of the neuron, as it tells how the neuron output will change if the input is changed. In other words, the slope of the transfer function tells us in which range of inputs the neuron output is most sensitive to changes in its input." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 2.3: Calculating the Transfer Function and Gain of a Neuron\n", + "\n", + "In the following demo, you can estimate the gain of the following neuron transfer function using numerical differentiation. We will use our timestep as h. See the cell below for a function that computes the rate via the formula above and then the gain using numerical differentiation. In the following cell, you can play with the parameters $a$ and $\\theta$ to change the shape of the transfer function (and see the resulting gain function). You can also set $I_{mean}$ to see how the slope is computed for that value of I. In the left plot, the red vertical lines are the two values of the current being used to calculate the slope, while the blue lines point to the corresponding output firing rates.\n", + "\n", + "Change the parameters of the neuron transfer function (i.e., $a$ and $\\theta$) and see if you can predict the value of $I$ for which the neuron has a maximal slope and which parameter determines the peak value of the gain.\n", + "\n", + "1. Ensure you understand how the right plot relates to the left!\n", + "2. How does $\\theta$ affect the transfer function and gain?\n", + "3. How does $a$ affect the transfer function and gain?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "def compute_rate_and_gain(I, a, theta, current_timestep):\n", + " \"\"\" Compute rate and gain of neuron based on parameters\n", + "\n", + " Args:\n", + " I (ndarray): different possible values of the current\n", + " a (scalar): parameter of the transfer function\n", + " theta (scalar): parameter of the transfer function\n", + " current_timestep (scalar): the time we're using to take steps\n", + "\n", + " Returns:\n", + " (ndarray, ndarray): rate and gain for each possible value of I\n", + " \"\"\"\n", + "\n", + " # Compute rate\n", + " rate = (1+np.exp(-a*(I-theta)))**-1 - (1+np.exp(a*theta))**-1\n", + "\n", + " # Compute gain using a numerical derivative\n", + " gain = (rate[1:] - rate[0:-1])/current_timestep\n", + "\n", + " return rate, gain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to enable the widget\n", + "\n", + "def plot_rate_and_gain(a, theta, I_mean):\n", + "\n", + " current_timestep = 0.1\n", + "\n", + " # Compute I\n", + " I = np.arange(0, 8, current_timestep)\n", + "\n", + " rate, gain = compute_rate_and_gain(I, a, theta, current_timestep)\n", + " I_1 = I_mean - current_timestep/2\n", + " rate_1 = (1+np.exp(-a*(I_1-theta)))**-1 - (1+np.exp(a*theta))**-1\n", + " I_2 = I_mean + current_timestep/2\n", + " rate_2 = (1+np.exp(-a*(I_2-theta)))**-1 - (1+np.exp(a*theta))**-1\n", + "\n", + " input_range = I_2-I_1\n", + " output_range = rate_2 - rate_1\n", + "\n", + " # Visualize rate and gain\n", + " plt.subplot(1,2,1)\n", + " plt.plot(I,rate)\n", + " plt.plot([I_1,I_1],[0, rate_1],color='r')\n", + " plt.plot([0,I_1],[rate_1, rate_1],color='b')\n", + " plt.plot([I_2,I_2],[0, rate_2],color='r')\n", + " plt.plot([0,I_2],[rate_2, rate_2],color='b')\n", + " plt.xlim([0, 8])\n", + " low, high = plt.ylim()\n", + " plt.ylim([0, high])\n", + "\n", + " plt.xlabel('Injected current (au)')\n", + " plt.ylabel('Output firing rate (normalized)')\n", + " plt.title('Transfer function')\n", + "\n", + " plt.text(2, 1.3, 'Output-Input Ratio =' + str(np.round(1000*output_range/input_range)/1000), style='italic',\n", + " bbox={'facecolor': 'red', 'alpha': 0.5, 'pad': 10})\n", + " plt.subplot(1,2,2)\n", + " plt.plot(I[0:-1], gain)\n", + " plt.plot([I_mean, I_mean],[0,0.6],color='r')\n", + " plt.xlabel('Injected current (au)')\n", + " plt.ylabel('Gain')\n", + " plt.title('Gain')\n", + " plt.xlim([0, 8])\n", + " low, high = plt.ylim()\n", + " plt.ylim([0, high])\n", + "\n", + "_ = widgets.interact(plot_rate_and_gain, a = (0.5, 2.0, .02), theta=(1.2,4.0,0.1), I_mean= (0.5,8.0,0.1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) $\\theta$ shifts the neuron transfer function along the x-axis -- reducing $theta$ moves the transfer function leftwards.\n", + "So changing $\\theta$ will affect the value of $I$ at which the neuron has the maximal slope.\n", + "Smaller the $\\theta$ smaller the value for maximum slope.\n", + "\n", + "2) $a$ controls the steepness of the neuron transfer function -- increasing $a$ makes the curve more steeper.\n", + "Therefore, $a$ determines the maximum value of the slope.\n", + "\n", + "Note: $a$ and $\\theta$ do not determine the maximum value of the transfer function itself.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Calculating_the_transfer_function_and_gain_of_a_neuron_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 3: Functions of Multiple Variables\n", + "\n", + "*Estimated timing to here from start of tutorial: 44 min*\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 4: Functions of multiple variables\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'Mp_uNNNiQAI'), ('Bilibili', 'BV1Ly4y1M77D')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Functions_of_multiple_variables_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "This video covers what partial derivatives are.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "In the previous section, you looked at function of single variable $t$ or $x$. In most cases, we encounter functions of multiple variables. For example, in the brain, the firing rate of a neuron is a function of both excitatory and inhibitory input rates. In the following, we will look into how to calculate derivatives of such functions.\n", + "\n", + "When we take the derrivative of a multivariable function with respect to one of the variables it is called the **partial derivative**. For example if we have a function:\n", + "\n", + "\\begin{align}\n", + "f(x,y) = x^2 + 2xy + y^2\n", + "\\end{align}\n", + "\n", + "The we can define the partial derivatives as\n", + "\n", + "\\begin{align}\n", + "\\frac{\\partial(f(x,y))}{\\partial x} = 2x + 2y + 0 \\\\\\\\\n", + "\\frac{\\partial(f(x,y))}{\\partial y} = 0 + 2x + 2y\n", + "\\end{align}\n", + "\n", + "In the above, the derivative of the last term ($y^2$) with respect to $x$ is zero because it does not change with respect to $x$. Similarly, the derivative of $x^2$ with respect to $y$ is also zero.\n", + "
\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "Just as with the derivatives we saw earlier, you can get partial derivatives through either an analytical method (finding an exact equation) or a numerical method (approximating)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 3: Visualize partial derivatives\n", + "\n", + "In the demo below, you can input any function of $x$ and $y$ and then visualize both the function and partial derivatives.\n", + "\n", + "We visualized the 2-dimensional function as a surface plot in which the function values are rendered as color. Yellow represents a high value, and blue represents a low value. The height of the surface also shows the numerical value of the function. A complete description of 2D surface plots and why we need them can be found in Bonus Section 1.1. The first plot is that of our function. And the two bottom plots are the derivative surfaces with respect to $x$ and $y$ variables.\n", + "\n", + "1. Ensure you understand how the plots relate to each other - if not, review the above material\n", + "2. Can you come up with a function where the partial derivative with respect to x will be a linear plane, and the derivative with respect to y will be more curvy?\n", + "3. What happens to the partial derivatives if no terms involve multiplying $x$ and $y$ together?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute this widget to enable the demo\n", + "\n", + "# Let's use sympy to calculate Partial derivatives of a function of 2-variables\n", + "@interact(f2d_string = 'x**2 + 2*x*y + y**2')\n", + "def plot_partial_derivs(f2d_string):\n", + " f, x, y = sp.symbols('f, x, y')\n", + "\n", + " f2d = eval(f2d_string)\n", + " f2d_dx = sp.diff(f2d,x)\n", + " f2d_dy = sp.diff(f2d,y)\n", + "\n", + " print('Partial derivative of ', f2d, 'with respect to x is', f2d_dx)\n", + " print('Partial derivative of ', f2d, 'with respect to y is', f2d_dy)\n", + "\n", + " p1 = sp.plotting.plot3d(f2d, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='f(x,y)',title='Our function')\n", + "\n", + " p2 = sp.plotting.plot3d(f2d_dx, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='df(x,y)/dx',title='Derivative w.r.t. x')\n", + "\n", + " p3 = sp.plotting.plot3d(f2d_dy, (x, -5, 5), (y, -5, 5),show=True,xlabel='x', ylabel='y', zlabel='df(x,y)/dy',title='Derivative w.r.t. y')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) Change terms involving x to just be linear (a constant times x, or a constant times x times some function of y).\n", + "Change a term involving y to involve more than y or y^2. For example, x + 2*x*y + y**3\n", + "\n", + "2) If there are no terms involving both x and y, the partial derivative with respect to x will\n", + " just depend on x and the partial derivative with respect to y will just depend on y.\n", + "\"\"\";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "To see an application of the numerical calculation of partial derivatives to understand a neuron driven by excitatory and inhibitory inputs, see Bonus Section 1!\n", + "\n", + "We will use the partial derivative several times in the course. For example partial derivative are used the calculate the Jacobian of a system of differential equations. The Jacobian is used to determine the dynamics and stability of a system. This will be introduced in the second week while studying the dynamics of excitatory and inhibitory population interactions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Visualize_partial_derivatives_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 4: Numerical Integration\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 5: Numerical Integration\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'cT0_CbD_h9Q'), ('Bilibili', 'BV1p54y1H7zt')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Numerical_Integration_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "This video covers numerical integration and specifically Riemann sums.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "Geometrically, integration is the area under the curve. This interpretation gives two formal ways to calculate the integral of a function numerically.\n", + "\n", + "**[Riemann sum](https://en.wikipedia.org/wiki/Riemann_sum)**:\n", + "If we wish to integrate a function $f(t)$ with respect to $t$, then first we divide the function into $n$ intervals of size $dt = a-b$, where $a$ is the starting of the interval. Thus, each interval gives a rectangle with height $f(a)$ and width $dt$. By summing the area of all the rectangles, we can approximate the area under the curve. As the size $dt$ approaches to zero, our estimate of the integral approcahes the analytical calculation. Essentially, the Riemann sum is cutting the region under the curve in vertical stripes, calculating area of the each stripe and summing them up.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 4.1: Demonstration of the Riemann Sum\n", + "\n", + "*Estimated timing to here from start of tutorial: 60 min*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 4.1: Riemann Sum vs. Analytical Integral with changing step size\n", + "\n", + "Below, we will compare numerical integration using the Riemann Sum with the analytical solution. You can change the interval size $dt$ using the slider.\n", + "\n", + "1. What values of dt result in the best numerical integration?\n", + "2. What is the downside of choosing that value of $dt$?\n", + "3. With large dt, why are we underestimating the integral (as opposed to overestimating?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Run this cell to enable the widget!\n", + "def riemann_sum_demo(dt = 0.5):\n", + " step_size = 0.1\n", + " min_val = 0.\n", + " max_val = 10.\n", + " tx = np.arange(min_val, max_val, step_size)\n", + "\n", + " # Our function\n", + " ftn = tx**2 - tx + 1\n", + " # And the integral analytical formula calculates using sympy\n", + " int_ftn = tx**3/3 - tx**2/2 + tx\n", + "\n", + " # Numerical integration of f(t) using Riemann Sum\n", + " n = int((max_val-min_val)/dt)\n", + " r_tx = np.zeros(n)\n", + " fun_value = np.zeros(n)\n", + " for ii in range(n):\n", + " a = min_val+ii*dt\n", + " fun_value[ii] = a**2 - a + 1\n", + " r_tx[ii] = a;\n", + "\n", + " # Riemann sum is just cumulative sum of the fun_value multiplied by the\n", + " r_sum = np.cumsum(fun_value)*dt\n", + " with plt.xkcd():\n", + " plt.figure(figsize=(20,5))\n", + " ax = plt.subplot(1,2,1)\n", + " plt.plot(tx,ftn,label='Function')\n", + "\n", + " for ii in range(n):\n", + " plt.plot([r_tx[ii], r_tx[ii], r_tx[ii]+dt, r_tx[ii]+dt], [0, fun_value[ii], fun_value[ii], 0] ,color='r')\n", + "\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('f(t)')\n", + " plt.title('f(t)')\n", + " plt.grid()\n", + "\n", + " plt.subplot(1,2,2)\n", + " plt.plot(tx,int_ftn,label='Analytical')\n", + " plt.plot(r_tx+dt,r_sum,color = 'r',label='Riemann Sum')\n", + " plt.xlabel('Time (au)')\n", + " plt.ylabel('int(f(t))')\n", + " plt.title('Integral of f(t)')\n", + " plt.grid()\n", + " plt.legend()\n", + " plt.show()\n", + "\n", + "\n", + "_ = widgets.interact(riemann_sum_demo, dt = (0.1, 1., .02))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) The smallest value of dt results in the best numerical integration (most similar\n", + " to the analytical solution)\n", + "\n", + "2) We have to compute the area of more rectangles.\n", + "\n", + "3) Here we used the forward Riemann sum, and it results in an underestimate of the\n", + " integral for positive values of $T$ and underestimate for negative values of\n", + " $T$. We could also use the backward Riemann sum, and that will give an overestimate\n", + " of the integral for positive values of $T$.\n", + "\"\"\";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "There are other methods of numerical integration, such as\n", + "**[Lebesgue integral](https://en.wikipedia.org/wiki/Lebesgue_integral)** and **Runge Kutta**. In the Lebesgue integral, we divide the area under the curve into horizontal stripes. That is, instead of the independent variable, the range of the function $f(t)$ is divided into small intervals. In any case, the Riemann sum is the basis of Euler's integration method for solving ordinary differential equations - something you will do in a later tutorial today." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Riemann_Sum_vs_Analytical_Integral_with_changing_step_size_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 4.2: Neural Applications of Numerical Integration\n", + "\n", + "*Estimated timing to here from start of tutorial: 68 min*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Coding Exercise 4.2: Calculating Charge Transfer with Excitatory Input\n", + "\n", + "An incoming spike elicits a change in the post-synaptic membrane potential (PSP), which can be captured by the following function:\n", + "\n", + "\\begin{equation}\n", + "PSP(t) = J \\cdot t \\cdot \\text{exp}\\left(-\\frac{t-t_{sp}}{\\tau_{s}}\\right)\n", + "\\end{equation}\n", + "\n", + "where $J$ is the synaptic amplitude, $t_{sp}$ is the spike time and $\\tau_s$ is the synaptic time constant.\n", + "\n", + "Estimate the total charge transferred to the postsynaptic neuron during a PSP with amplitude $J=1.0$, $\\tau_s = 1.0$ and $t_{sp} = 1$ (that is the spike occurred at $1$ ms). The total charge will be the integral of the PSP function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "########################################################################\n", + "## TODO for students\n", + "## Complete all ... in code below and remove\n", + "raise NotImplementedError(\"Calculate the charge transfer\")\n", + "########################################################################\n", + "\n", + "# Set up parameters\n", + "J = 1\n", + "tau_s = 1\n", + "t_sp = 1\n", + "dt = .1\n", + "t = np.arange(0, 10, dt)\n", + "\n", + "# Code PSP formula\n", + "PSP = ...\n", + "\n", + "# Compute numerical integral\n", + "# We already have PSP at every time step (height of rectangles). We need to\n", + "#. multiply by width of rectangles (dt) to get areas\n", + "rectangle_areas = ...\n", + "\n", + "# Cumulatively sum rectangles (hint: use np.cumsum)\n", + "numerical_integral = ...\n", + "\n", + "# Visualize\n", + "plot_charge_transfer(t, PSP, numerical_integral)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set up parameters\n", + "J = 1\n", + "tau_s = 1\n", + "t_sp = 1\n", + "dt = .1\n", + "t = np.arange(0, 10, dt)\n", + "\n", + "# Code PSP formula\n", + "PSP = J * t * np.exp(- (t-t_sp)/tau_s)\n", + "\n", + "# Compute numerical integral\n", + "# We already have PSP at every time step (height of rectangles). We need to\n", + "#. multiply by width of rectangles (dt) to get areas\n", + "rectangle_areas = PSP *dt\n", + "\n", + "# Cumulatively sum rectangles (hint: use np.cumsum)\n", + "numerical_integral = np.cumsum(rectangle_areas)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_charge_transfer(t, PSP, numerical_integral)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "You can see from the figure that the total charge transferred is a little over 2.5." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Calculating_Charge_Transfer_with_Excitatory_Input_exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 5: Differentiation and Integration as Filtering Operations\n", + "\n", + "*Estimated timing to here from start of tutorial: 75 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 6: Filtering Operations\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '7_ZjlT2d174'), ('Bilibili', 'BV1Vy4y1M7oT')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Filtering_Operations_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "This video covers a different interpretation of differentiation and integration: viewing them as filtering operations.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "In the above, we used the notions that geometrically integration is the area under the curve and differentiation is the slope of the curve. There is another interpretation of these two operations.\n", + "\n", + "As we calculate the derivative of a function, we take the difference of adjacent values of the function. This results in the removal of common part between the two values. As a consequence, we end up removing the unchanging part of the signal. If we now think in terms of frequencies, differentiation removes low frequencies, or slow changes. That is, differentiation acts as a high pass filter.\n", + "\n", + "Integration does the opposite because in the estimation of an integral we keep adding adjacent values of the signal. So, again thinking in terms of frequencies, integration is akin to the removal of high frequencies or fast changes (low-pass filter). The shock absorbers in your bike are an example of integrators.\n", + "\n", + "We can see this behavior the demo below. Here we will not work with functions, but with signals. As such, functions and signals are the same. Just that in most cases our signals are measurements with respect to time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to see visualization\n", + "h = 0.01\n", + "tx = np.arange(0,2,h)\n", + "noise_signal = np.random.uniform(0, 1, (len(tx)))*0.5\n", + "x1 = np.sin(0.5*np.pi*tx) + noise_signal # This will generate a 1 Hz sin wave\n", + "# In the signal x1 we have added random noise which contributs the high frequencies\n", + "\n", + "# Take the derivative equivalent of the signal i.e. subtract the adjacent values\n", + "x1_diff = (x1[1:] - x1[:-1])\n", + "\n", + "# Take the integration equivalent of the signal i.e. sum the adjacent values. And divide by 2 (take average essentially)\n", + "x1_integrate = (x1[1:] + x1[:-1])/2\n", + "\n", + "# Plotting code\n", + "plt.figure(figsize=(15,10))\n", + "plt.subplot(3,1,1)\n", + "plt.plot(tx,x1,label='Original Signal')\n", + "#plt.xlabel('Time (sec)')\n", + "plt.ylabel('Signal Value(au)')\n", + "plt.legend()\n", + "\n", + "plt.subplot(3,1,2)\n", + "plt.plot(tx[0:-1],x1_diff,label='Differentiated Signal')\n", + "# plt.xlabel('Time (sec)')\n", + "plt.ylabel('Differentiated Value(au)')\n", + "plt.legend()\n", + "\n", + "plt.subplot(3,1,3)\n", + "plt.plot(tx,x1,label='Original Signal')\n", + "plt.plot(tx[0:-1],x1_integrate,label='Integrate Signal')\n", + "plt.xlabel('Time (sec)')\n", + "plt.ylabel('Integrate Value(au)')\n", + "plt.legend()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "Notice how the differentiation operation amplifies the fast changes which were contributed by noise. By contrast, the integration operation suppresses the fast-changing noise. We will further smooth the signal if we perform the same operation of averaging the adjacent samples on the orange trace. Such sums and subtractions form the basis of digital filters." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Summary\n", + "\n", + "*Estimated timing of tutorial: 80 minutes*\n", + "\n", + "* Geometrically, integration is the area under the curve, and differentiation is the slope of the function\n", + "* The concepts of slope and area can be easily extended to higher dimensions. We saw this when we took the derivative of a 2-dimensional transfer function of a neuron\n", + "* Numerical estimates of both derivatives and integrals require us to choose a time step $h$. The smaller the $h$, the better the estimate, but more computations are needed for small values of $h$. So there is always some tradeoff\n", + "* Partial derivatives are just the estimate of the slope along one of the many dimensions of the function. We can combine the slopes in different directions using vector sum to find the direction of the slope\n", + "* Because the derivative of a function is zero at the local peak or trough, derivatives are used to solve optimization problems\n", + "* When thinking of signal, integration operation is equivalent to smoothening the signals (i.e., removing fast changes)\n", + "* Differentiation operations remove slow changes and enhance the high-frequency content of a signal" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Bonus Section 1: Numerical calculation of partial derivatives\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Bonus Section 1.1: Understanding 2D plots\n", + "\n", + "Let's take the example of a neuron driven by excitatory and inhibitory inputs. Because this is for illustrative purposes, we will not go into the details of the numerical range of the input and output variables.\n", + "\n", + "In the function below, we assume that the firing rate of a neuron increases monotonically with an increase in excitation and decreases monotonically with an increase in inhibition. The inhibition is modeled as a subtraction. As for the 1-dimensional transfer function, we assume that we can approximate the transfer function as a sigmoid function.\n", + "\n", + "We can use the same numerical differentiation as before to evaluate the partial derivatives, but now we apply it to each row and column separately." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to visualize the neuron firing rate surface\n", + "def sigmoid_function(x,a,theta):\n", + " '''\n", + " Population activation function.\n", + "\n", + " Expects:\n", + " x : the population input\n", + " a : the gain of the function\n", + " theta : the threshold of the function\n", + "\n", + " Returns:\n", + " the population activation response F(x) for input x\n", + " '''\n", + " # add the expression of f = F(x)\n", + " f = (1+np.exp(-a*(x-theta)))**-1 - (1+np.exp(a*theta))**-1\n", + "\n", + " return f\n", + "\n", + "# Neuron Transfer function\n", + "step_size = 0.1\n", + "exc_input = np.arange(2,9,step_size)\n", + "inh_input = np.arange(0,7,step_size)\n", + "exc_a = 1.2\n", + "exc_theta = 2.4\n", + "inh_a = 1.\n", + "inh_theta = 4.\n", + "\n", + "rate = np.zeros((len(exc_input),len(inh_input)))\n", + "\n", + "for ii in range(len(exc_input)):\n", + " for jj in range(len(inh_input)):\n", + " rate[ii,jj] = sigmoid_function(exc_input[ii],exc_a,exc_theta) - sigmoid_function(inh_input[jj],inh_a,inh_theta)*0.5\n", + "\n", + "with plt.xkcd():\n", + " X, Y = np.meshgrid(exc_input, inh_input)\n", + " fig = plt.figure(figsize=(12,12))\n", + " ax1 = fig.add_subplot(2,2,1)\n", + " lg_txt = 'Inhibition = ' + str(inh_input[0])\n", + " ax1.plot(exc_input,rate[:,0],label=lg_txt)\n", + " lg_txt = 'Inhibition = ' + str(inh_input[20])\n", + " ax1.plot(exc_input,rate[:,20],label=lg_txt)\n", + " lg_txt = 'Inhibition = ' + str(inh_input[40])\n", + " ax1.plot(exc_input,rate[:,40],label=lg_txt)\n", + " ax1.legend()\n", + " ax1.set_xlabel('Excitatory input (au)')\n", + " ax1.set_ylabel('Neuron output rate (au)');\n", + "\n", + " ax2 = fig.add_subplot(2,2,2)\n", + " lg_txt = 'Excitation = ' + str(exc_input[0])\n", + " ax2.plot(inh_input,rate[0,:],label=lg_txt)\n", + " lg_txt = 'Excitation = ' + str(exc_input[20])\n", + " ax2.plot(inh_input,rate[20,:],label=lg_txt)\n", + " lg_txt = 'Excitation = ' + str(exc_input[40])\n", + " ax2.plot(inh_input,rate[40,:],label=lg_txt)\n", + " ax2.legend()\n", + " ax2.set_xlabel('Inhibitory input (au)')\n", + " ax2.set_ylabel('Neuron output rate (au)');\n", + "\n", + " ax3 = fig.add_subplot(2, 1, 2, projection='3d')\n", + " surf= ax3.plot_surface(Y.T, X.T, rate, rstride=1, cstride=1,\n", + " cmap='viridis', edgecolor='none')\n", + " ax3.set_xlabel('Inhibitory input (au)')\n", + " ax3.set_ylabel('Excitatory input (au)')\n", + " ax3.set_zlabel('Neuron output rate (au)');\n", + " fig.colorbar(surf)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "The **Top-Left** plot shows how the neuron output rate increases as a function of excitatory input (e.g., the blue trace). However, as we increase inhibition, expectedly, the neuron output decreases, and the curve is shifted downwards. This constant shift in the curve suggests that the effect of inhibition is subtractive, and the amount of subtraction does not depend on the neuron output.\n", + "\n", + "We can alternatively see how the neuron output changes with respect to inhibition and study how excitation affects that. This is visualized in the **Top-Right** plot.\n", + "\n", + "This type of plotting is very intuitive, but it becomes very tedious to visualize when there are larger numbers of lines to be plotted. A nice solution to this visualization problem is to render the data as color, as surfaces, or both.\n", + "\n", + "This is what we have done in the plot at the bottom. The color map on the right shows the neuron's output as a function of inhibitory and excitatory input. The output rate is shown both as height along the z-axis and as the color. Blue means a low firing rate, and yellow represents a high firing rate (see the color bar).\n", + "\n", + "In the above plot, the output rate of the neuron goes below zero. This is, of course, not physiological, as neurons cannot have negative firing rates. In models, we either choose the operating point so that the output does not go below zero, or we clamp the neuron output to zero if it goes below zero. You will learn about it more in Week 2." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Bonus Section 1.2: Numerical partial derivatives\n", + "\n", + "We can now compute the partial derivatives of our transfer function in response to excitatory and inhibitory input. We do so below!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell implement our neural transfer function, `plot_2d_neuron_transfer_function`, in respond to excitatory and inhibitory input\n", + "def plot_2d_neuron_transfer_function(exc_a, exc_theta, inh_a, inh_theta):\n", + " # Neuron Transfer Function\n", + " step_size = 0.1\n", + " exc_input = np.arange(1,10,step_size)\n", + " inh_input = np.arange(0,7,step_size)\n", + "\n", + " rate = np.zeros((len(exc_input),len(inh_input)))\n", + " for ii in range(len(exc_input)):\n", + " for jj in range(len(inh_input)):\n", + " rate[ii,jj] = sigmoid_function(exc_input[ii],exc_a,exc_theta) - sigmoid_function(inh_input[jj],inh_a,inh_theta)*0.5\n", + "\n", + " # Derivative with respect to excitatory input rate\n", + " rate_de = np.zeros((len(exc_input)-1,len(inh_input)))# this will have one row less than the rate matrix\n", + " for ii in range(len(inh_input)):\n", + " rate_de[:,ii] = (rate[1:,ii] - rate[0:-1,ii])/step_size\n", + "\n", + " # Derivative with respect to inhibitory input rate\n", + " rate_di = np.zeros((len(exc_input),len(inh_input)-1))# this will have one column less than the rate matrix\n", + " for ii in range(len(exc_input)):\n", + " rate_di[ii,:] = (rate[ii,1:] - rate[ii,0:-1])/step_size\n", + "\n", + "\n", + " X, Y = np.meshgrid(exc_input, inh_input)\n", + " fig = plt.figure(figsize=(20,8))\n", + " ax1 = fig.add_subplot(1, 3, 1, projection='3d')\n", + " surf1 = ax1.plot_surface(Y.T, X.T, rate, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", + " ax1.set_xlabel('Inhibitory input (au)')\n", + " ax1.set_ylabel('Excitatory input (au)')\n", + " ax1.set_zlabel('Neuron output rate (au)')\n", + " ax1.set_title('Rate as a function of Exc. and Inh');\n", + " ax1.view_init(45, 10)\n", + " fig.colorbar(surf1)\n", + "\n", + " Xde, Yde = np.meshgrid(exc_input[0:-1], inh_input)\n", + " ax2 = fig.add_subplot(1, 3, 2, projection='3d')\n", + " surf2 = ax2.plot_surface(Yde.T, Xde.T, rate_de, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", + " ax2.set_xlabel('Inhibitory input (au)')\n", + " ax2.set_ylabel('Excitatory input (au)')\n", + " ax2.set_zlabel('Neuron output rate (au)');\n", + " ax2.set_title('Derivative wrt Excitation');\n", + " ax2.view_init(45, 10)\n", + " fig.colorbar(surf2)\n", + "\n", + " Xdi, Ydi = np.meshgrid(exc_input, inh_input[:-1])\n", + " ax3 = fig.add_subplot(1, 3, 3, projection='3d')\n", + " surf3 = ax3.plot_surface(Ydi.T, Xdi.T, rate_di, rstride=1, cstride=1, cmap='viridis', edgecolor='none')\n", + " ax3.set_xlabel('Inhibitory input (au)')\n", + " ax3.set_ylabel('Excitatory input (au)')\n", + " ax3.set_zlabel('Neuron output rate (au)');\n", + " ax3.set_title('Derivative wrt Inhibition');\n", + " ax3.view_init(15, -115)\n", + " fig.colorbar(surf3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "plot_2d_neuron_transfer_function(exc_a=1.2, exc_theta=2.4, inh_a=1, inh_theta=4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "Is this what you expected? Change the parameters in the function to generate the 2-D transfer function of the neuron for different excitatory and inhibitory $a$ and $\\theta$ and test your intuitions. Can you relate this shape of the partial derivative surface to the gain of the 1-D transfer function of a neuron (Section 2)?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "Here excitation and inhibition are interacting additively. That is, if you change $\\theta$ and $a$ for excitation,\n", + "it will not affect the derivative wrt to inhibition and vice versa.\n", + "\n", + "The effect of varying $\\theta$ and $a$ on the derivative wrt to excitation and inhibition is identical to what you for 1-D transfer function\n", + "$\\theta$ shifts the neuron transfer function along the x-axis -- reducing $theta$ moves the transfer function leftwards.\n", + "So by changing $\\theta$ for excitation you will shift the derivative surface left/right along the excitation axis. Same for inhibition.\n", + "\n", + "$a$ controls the steepness of the neuron transfer function -- increasing $a$ makes the curve more steeper.\n", + "So by changing $a$ for excitation you will vary the width of the notch of the derivative surface.\n", + "For inhibition, by varying $a$ you will vary the width of the ridge in the drivative surface.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Numerical_partial_derivatives_Bonus_Discussion\")" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "nq-hVvP1MZqa", + "KCWR4wciMZqb", + "tcLiKp-AMZqc", + "8-bz4N8eMZqd", + "9-BbBcZPMZqf", + "L0_P-VNEMZqf", + "Yx7vP4zvMZqg", + "RACJEWwMMZqg", + "dXFmgh9UMZqh", + "pMQS0U54MZqi", + "ZLb9XPF_VvPm", + "c0oilOjOV0KZ" + ], + "include_colab_link": true, + "name": "W0D4_Tutorial1", + "provenance": [], + "toc_visible": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb b/tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb index f9c9feb..a86e9dc 100644 --- a/tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb +++ b/tutorials/W0D4_Calculus/W0D4_Tutorial2.ipynb @@ -1,1892 +1,1824 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "id": "08842220", - "metadata": { - "execution": {}, - "id": "08842220" - }, - "source": [ - "# Tutorial 2: Differential Equations\n", - "\n", - "**Week 0, Day 4: Calculus**\n", - "\n", - "**By Neuromatch Academy**\n", - "\n", - "**Content creators:** John S Butler, Arvind Kumar with help from Rebecca Brady\n", - "\n", - "**Content reviewers:** Swapnil Kumar, Sirisha Sripada, Matthew McCann, Tessy Tom\n", - "\n", - "**Production editors:** Matthew McCann, Ella Batty" - ] - }, - { - "cell_type": "markdown", - "id": "gK0EQXTiH5W2", - "metadata": { - "execution": {}, - "id": "gK0EQXTiH5W2" - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "id": "7d80998b", - "metadata": { - "execution": {}, - "id": "7d80998b" - }, - "source": [ - "---\n", - "# Tutorial Objectives\n", - "\n", - "*Estimated timing of tutorial: 45 minutes*\n", - "\n", - "A great deal of neuroscience can be modeled using differential equations, from gating channels to single neurons to a network of neurons to blood flow to behavior. A simple way to think about differential equations is they are equations that describe how something changes.\n", - "\n", - "The most famous of these in neuroscience is the Nobel Prize-winning Hodgkin-Huxley equation, which describes a neuron by modeling the gating of each axon. But we will not start there; we will start a few steps back.\n", - "\n", - "Differential Equations are mathematical equations that describe how something like a population or a neuron changes over time. Differential equations are so useful because they can generalize a process such that one equation can be used to describe many different outcomes.\n", - "The general form of a first-order differential equation is:\n", - "\n", - "\\begin{equation}\n", - "\\frac{d}{dt}y(t) = f\\left( t,y(t) \\right)\n", - "\\end{equation}\n", - "\n", - "which can be read as \"the change in a process $y$ over time $t$ is a function $f$ of time $t$ and itself $y$\". This might initially seem like a paradox as you are using a process $y$ you want to know about to describe itself, a bit like the MC Escher drawing of two hands painting [each other](https://en.wikipedia.org/wiki/Drawing_Hands). But that is the beauty of mathematics - this can be solved sometimes, and when it cannot be solved exactly, we can use numerical methods to estimate the answer (as we will see in the next tutorial).\n", - "\n", - "In this tutorial, we will see how __differential equations are motivated by observations of physical responses__. We will break down the population differential equation, then the integrate and fire model, which leads nicely into raster plots and frequency-current curves to rate models.\n", - "\n", - "**Steps:**\n", - "- Get an intuitive understanding of a linear population differential equation (humans, not neurons)\n", - "- Visualize the relationship between the change in population and the population\n", - "- Breakdown the Leaky Integrate and Fire (LIF) differential equation\n", - "- Code the exact solution of a LIF for a constant input\n", - "- Visualize and listen to the response of the LIF for different inputs" - ] - }, - { - "cell_type": "markdown", - "id": "kq1fWqhyNJ3i", - "metadata": { - "execution": {}, - "id": "kq1fWqhyNJ3i" - }, - "source": [ - "---\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1-6UCje-fVs6", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "1-6UCje-fVs6" - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D4_T2\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5985801", - "metadata": { - "execution": {}, - "id": "a5985801" - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d40abcd8", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "d40abcd8" - }, - "outputs": [], - "source": [ - "# @title Figure Settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import IPython.display as ipd\n", - "from matplotlib import gridspec\n", - "import ipywidgets as widgets # interactive display\n", - "%config InlineBackend.figure_format = 'retina'\n", - "# use NMA plot style\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", - "my_layout = widgets.Layout()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6XJo_I6_NbJg", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "6XJo_I6_NbJg" - }, - "outputs": [], - "source": [ - "# @title Plotting Functions\n", - "\n", - "def plot_dPdt(alpha=.3):\n", - " \"\"\" Plots change in population over time\n", - " Args:\n", - " alpha: Birth Rate\n", - " Returns:\n", - " A figure two panel figure\n", - " left panel: change in population as a function of population\n", - " right panel: membrane potential as a function of time\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " time=np.arange(0, 10 ,0.01)\n", - " fig = plt.figure(figsize=(12,4))\n", - " gs = gridspec.GridSpec(1, 2)\n", - "\n", - " ## dpdt as a fucntion of p\n", - " plt.subplot(gs[0])\n", - " plt.plot(np.exp(alpha*time), alpha*np.exp(alpha*time))\n", - " plt.xlabel(r'Population $p(t)$ (millions)')\n", - " plt.ylabel(r'$\\frac{d}{dt}p(t)=\\alpha p(t)$')\n", - "\n", - " ## p exact solution\n", - " plt.subplot(gs[1])\n", - " plt.plot(time, np.exp(alpha*time))\n", - " plt.ylabel(r'Population $p(t)$ (millions)')\n", - " plt.xlabel('time (years)')\n", - " plt.show()\n", - "\n", - "\n", - "def plot_V_no_input(V_reset=-75):\n", - " \"\"\"\n", - " Args:\n", - " V_reset: Reset Potential\n", - " Returns:\n", - " A figure two panel figure\n", - " left panel: change in membrane potential as a function of membrane potential\n", - " right panel: membrane potential as a function of time\n", - " \"\"\"\n", - " E_L=-75\n", - " tau_m=10\n", - " t=np.arange(0,100,0.01)\n", - " V= E_L+(V_reset-E_L)*np.exp(-(t)/tau_m)\n", - " V_range=np.arange(-90,0,1)\n", - " dVdt=-(V_range-E_L)/tau_m\n", - "\n", - " with plt.xkcd():\n", - " time=np.arange(0, 10, 0.01)\n", - " fig = plt.figure(figsize=(12, 4))\n", - " gs = gridspec.GridSpec(1, 2)\n", - "\n", - " plt.subplot(gs[0])\n", - " plt.plot(V_range,dVdt)\n", - " plt.hlines(0,min(V_range),max(V_range), colors='black', linestyles='dashed')\n", - " plt.vlines(-75, min(dVdt), max(dVdt), colors='black', linestyles='dashed')\n", - " plt.plot(V_reset,-(V_reset - E_L)/tau_m, 'o', label=r'$V_{reset}$')\n", - " plt.text(-50, 1, 'Positive')\n", - " plt.text(-50, -2, 'Negative')\n", - " plt.text(E_L - 1, max(dVdt), r'$E_L$')\n", - " plt.legend()\n", - " plt.xlabel('Membrane Potential V (mV)')\n", - " plt.ylabel(r'$\\frac{dV}{dt}=\\frac{-(V(t)-E_L)}{\\tau_m}$')\n", - "\n", - " plt.subplot(gs[1])\n", - " plt.plot(t,V)\n", - " plt.plot(t[0],V_reset,'o')\n", - " plt.ylabel(r'Membrane Potential $V(t)$ (mV)')\n", - " plt.xlabel('time (ms)')\n", - " plt.ylim([-95, -60])\n", - "\n", - " plt.show()\n", - "\n", - "\n", - "## LIF PLOT\n", - "def plot_IF(t, V,I,Spike_time):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " V : membrane Voltage\n", - " I : Input\n", - " Spike_time : Spike_times\n", - " Returns:\n", - " figure with three panels\n", - " top panel: Input as a function of time\n", - " middle panel: membrane potential as a function of time\n", - " bottom panel: Raster plot\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(12, 4))\n", - " gs = gridspec.GridSpec(3, 1, height_ratios=[1, 4, 1])\n", - "\n", - " # PLOT OF INPUT\n", - " plt.subplot(gs[0])\n", - " plt.ylabel(r'$I_e(nA)$')\n", - " plt.yticks(rotation=45)\n", - " plt.hlines(I,min(t),max(t),'g')\n", - " plt.ylim((2, 4))\n", - " plt.xlim((-50, 1000))\n", - "\n", - " # PLOT OF ACTIVITY\n", - " plt.subplot(gs[1])\n", - " plt.plot(t,V)\n", - " plt.xlim((-50, 1000))\n", - " plt.ylabel(r'$V(t)$(mV)')\n", - "\n", - " # PLOT OF SPIKES\n", - " plt.subplot(gs[2])\n", - " plt.ylabel(r'Spike')\n", - " plt.yticks([])\n", - " plt.scatter(Spike_time, 1 * np.ones(len(Spike_time)), color=\"grey\", marker=\".\")\n", - " plt.xlim((-50, 1000))\n", - " plt.xlabel('time(ms)')\n", - " plt.show()\n", - "\n", - "\n", - "## Plotting the differential Equation\n", - "def plot_dVdt(I=0):\n", - " \"\"\"\n", - " Args:\n", - " I : Input Current\n", - " Returns:\n", - " figure of change in membrane potential as a function of membrane potential\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " E_L = -75\n", - " tau_m = 10\n", - " V = np.arange(-85, 0, 1)\n", - " g_L = 10.\n", - " fig = plt.figure(figsize=(6, 4))\n", - "\n", - " plt.plot(V,(-(V-E_L) + I*10) / tau_m)\n", - " plt.hlines(0, min(V), max(V), colors='black', linestyles='dashed')\n", - " plt.xlabel('V (mV)')\n", - " plt.ylabel(r'$\\frac{dV}{dt}$')\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d88bf487", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "d88bf487" - }, - "outputs": [], - "source": [ - "# @title Helper Functions\n", - "\n", - "## EXACT SOLUTION OF LIF\n", - "def Exact_Integrate_and_Fire(I,t):\n", - " \"\"\"\n", - " Args:\n", - " I : Input Current\n", - " t : time\n", - " Returns:\n", - " Spike : Spike Count\n", - " Spike_time : Spike time\n", - " V_exact : Exact membrane potential\n", - " \"\"\"\n", - "\n", - " Spike = 0\n", - " tau_m = 10\n", - " R = 10\n", - " t_isi = 0\n", - " V_reset = E_L = -75\n", - " V_exact = V_reset * np.ones(len(t))\n", - " V_th = -50\n", - " Spike_time = []\n", - "\n", - " for i in range(0, len(t)):\n", - "\n", - " V_exact[i] = E_L + R*I + (V_reset - E_L - R*I) * np.exp(-(t[i]-t_isi)/tau_m)\n", - "\n", - " # Threshold Reset\n", - " if V_exact[i] > V_th:\n", - " V_exact[i-1] = 0\n", - " V_exact[i] = V_reset\n", - " t_isi = t[i]\n", - " Spike = Spike+1\n", - " Spike_time = np.append(Spike_time, t[i])\n", - "\n", - " return Spike, Spike_time, V_exact" - ] - }, - { - "cell_type": "markdown", - "id": "O2Q5d9D2hLZq", - "metadata": { - "execution": {}, - "id": "O2Q5d9D2hLZq" - }, - "source": [ - "---\n", - "# Section 0: Introduction" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "DIZfUH4rQNSR", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "DIZfUH4rQNSR" - }, - "outputs": [], - "source": [ - "# @title Video 1: Why do we care about differential equations?\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'LhX-mUd8lPo'), ('Bilibili', 'BV1v64y197bW')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "Or-35flNhTBo", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "Or-35flNhTBo" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Why_do_we_care_about_differential_equations_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "AcwNWVdsQgLz", - "metadata": { - "execution": {}, - "id": "AcwNWVdsQgLz" - }, - "source": [ - "---\n", - "# Section 1: Population differential equation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "LRwtiFPVQ3Pz", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "LRwtiFPVQ3Pz" - }, - "outputs": [], - "source": [ - "# @title Video 2: Population differential equation\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'czgGyoUsRoQ'), ('Bilibili', 'BV1pg41137CU')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "WLss9oNLhSI-", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "WLss9oNLhSI-" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Population_differential_equation_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "286b4298", - "metadata": { - "execution": {}, - "id": "286b4298" - }, - "source": [ - "This video covers our first example of a differential equation: a differential equation which models the change in population.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "To get an intuitive feel of a differential equations, we will start with a population differential equation, which models the change in population [1], that is human population not neurons, we will get to neurons later. Mathematically it is written like:\n", - "\\begin{align*}\n", - "\\\\\n", - "\\frac{d}{dt}\\,p(t) &= \\alpha p(t),\\\\\n", - "\\end{align*}\n", - "\n", - "where $p(t)$ is the population of the world and $\\alpha$ is a parameter representing birth rate.\n", - "\n", - "Another way of thinking about the models is that the equation\n", - "\\begin{align*}\n", - "\\\\\n", - "\\frac{d}{dt}\\,p(t) &= \\alpha p(t),\\\\\n", - "\\text{can be written as:}\\\\\n", - "\\text{\"Change in Population\"} &= \\text{ \"Birth rate times Current population.\"}\n", - "\\end{align*}\n", - "\n", - "The equation is saying something reasonable maybe not the perfect model but a good start.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "njGc4mc4ASXb", - "metadata": { - "execution": {}, - "id": "njGc4mc4ASXb" - }, - "source": [ - "### Think! 1.1: Interpretating the behavior of a linear population equation\n", - "\n", - "Using the plot below of change of population $\\frac{d}{dt} p(t) $ as a function of population $p(t)$ with birth-rate $\\alpha=0.3$, discuss the following questions:\n", - "\n", - "1. Why is the population differential equation known as a linear differential equation?\n", - "2. How does population size affect the rate of change of the population?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e37083e", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "8e37083e" - }, - "outputs": [], - "source": [ - "# @markdown Execute the code to plot the rate of change of population as a function of population\n", - "p = np.arange(0, 100, 0.1)\n", - "\n", - "with plt.xkcd():\n", - "\n", - " dpdt = 0.3*p\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(p, dpdt)\n", - " plt.xlabel(r'Population $p(t)$ (millions)')\n", - " plt.ylabel(r'$\\frac{d}{dt}p(t)=\\alpha p(t)$')\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "il7f6AWkXk0B", - "metadata": { - "execution": {}, - "id": "il7f6AWkXk0B" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. The plot of $\\frac{dp}{dt}$ is a line, which is why the differential\n", - " equation is known as a linear differential equation.\n", - "\n", - " 2. As the population increases, the change of population increases. A\n", - " population of 20 has a change of 6 while a population of 100 has a change of\n", - " 30. This makes sense - the larger the population the larger the change.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1whSBsnBhbAG", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "1whSBsnBhbAG" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Interpretating_the_behavior_of_a_linear_population_equation_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "id": "5846bbb5", - "metadata": { - "execution": {}, - "id": "5846bbb5" - }, - "source": [ - "## Section 1.1: Exact solution of the population equation" - ] - }, - { - "cell_type": "markdown", - "id": "uqcubpZNBIGl", - "metadata": { - "execution": {}, - "id": "uqcubpZNBIGl" - }, - "source": [ - "### Section 1.1.1: Initial condition\n", - "\n", - "The linear population differential equation is an initial value differential equation because we need an initial population value to solve it. Here we will set our initial population at time $0$ to $1$:\n", - "\n", - "\\begin{equation}\n", - "p(0) = 1\n", - "\\end{equation}\n", - "\n", - "Different initial conditions will lead to different answers but will not change the differential equation. This is one of the strengths of a differential equation." - ] - }, - { - "cell_type": "markdown", - "id": "oYBK4NWYBL5t", - "metadata": { - "execution": {}, - "id": "oYBK4NWYBL5t" - }, - "source": [ - "### Section 1.1.2: Exact Solution\n", - "\n", - "To calculate the exact solution of a differential equation, we must integrate both sides. Instead of numerical integration (as you delved into in the last tutorial), we will first try to solve the differential equations using analytical integration. As with derivatives, we can find analytical integrals of simple equations by consulting [a list](https://en.wikipedia.org/wiki/Lists_of_integrals). We can then get integrals for more complex equations using some mathematical tricks - the harder the equation, the more obscure the trick.\n", - "\n", - "The linear population equation\n", - "\\begin{equation}\n", - "\\frac{d}{dt}p(t) = \\alpha p(t), \\, p(0)=P_0\n", - "\\end{equation}\n", - "\n", - "has the exact solution:\n", - "\n", - "\\begin{equation}\n", - "p(t) = P_0 e^{\\alpha t}.\n", - "\\end{equation}\n", - "\n", - "The exact solution written in words is:\n", - "\n", - "\\begin{equation}\n", - "\\text{\"Population\"} = \\text{\"grows/declines exponentially as a function of time and birth rate\"}.\n", - "\\end{equation}\n", - "\n", - "Most differential equations do not have a known exact solution, so we will show how the solution can be estimated in the next tutorial on numerical methods.\n", - "\n", - "A small aside: a good deal of progress in mathematics was due to mathematicians writing taunting letters to each other, saying they had a trick to solve something better than everyone else. So do not worry too much about the tricks." - ] - }, - { - "cell_type": "markdown", - "id": "1257fcc0", - "metadata": { - "execution": {}, - "id": "1257fcc0" - }, - "source": [ - "#### Example Exact Solution of the Population Equation\n", - "\n", - "Let's consider the population differential equation with a birth rate $\\alpha=0.3$:\n", - "\n", - "\\begin{equation}\n", - "\\frac{d}{dt}p(t) = 0.3p(t)\n", - "\\end{equation}\n", - "\n", - "with the initial condition\n", - "\n", - "\\begin{equation}\n", - "p(0)=1.\n", - "\\end{equation}\n", - "\n", - "It has an exact solution\n", - "\n", - "\\begin{equation}\n", - "p(t)=e^{0.3 t}.\n", - "\\end{equation}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eeb717b8", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "eeb717b8" - }, - "outputs": [], - "source": [ - "# @markdown Execute code to plot the exact solution\n", - "t = np.arange(0, 10, 0.1) # Time from 0 to 10 years in 0.1 steps\n", - "\n", - "with plt.xkcd():\n", - "\n", - " p = np.exp(0.3 * t)\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(t, p)\n", - " plt.ylabel('Population (millions)')\n", - " plt.xlabel('time (years)')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "792841c9", - "metadata": { - "execution": {}, - "id": "792841c9" - }, - "source": [ - "## Section 1.2: Parameters of the differential equation\n", - "\n", - "*Estimated timing to here from start of tutorial: 12 min*\n", - "\n", - "One of the goals when designing a differential equation is to make it generalizable. This means that the differential equation will give reasonable solutions for different countries with different birth rates $\\alpha$." - ] - }, - { - "cell_type": "markdown", - "id": "8zD98917MDtW", - "metadata": { - "execution": {}, - "id": "8zD98917MDtW" - }, - "source": [ - "### Interactive Demo 1.2: Parameter Change\n", - "\n", - "Play with the widget to see the relationship between $\\alpha$ and the population differential equation as a function of population (left-hand side) and the population solution as a function of time (right-hand side). Pay close attention to the transition point from positive to negative.\n", - "\n", - "How do changing parameters of the population equation affect the outcome?\n", - "\n", - "1. What happens when $\\alpha < 0$?\n", - "2. What happens when $\\alpha > 0$?\n", - "3. What happens when $\\alpha = 0$?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "490c0be0", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "490c0be0" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " alpha=widgets.FloatSlider(.3, min=-1., max=1., step=.1, layout=my_layout)\n", - ")\n", - "def Pop_widget(alpha):\n", - " plot_dPdt(alpha=alpha)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "KDgZUHS5YDQp", - "metadata": { - "execution": {}, - "id": "KDgZUHS5YDQp" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. Negative values of alpha result in an exponential decrease to 0 a stable solution.\n", - " 2. Positive Values of alpha in an exponential increases to infinity.\n", - " 3. Alpha equal to 0 is a unique point known as an equilibrium point when the\n", - " dp/dt=0 and there is no change in population. This is known as a stable point.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1AgWDO58hnSv", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "1AgWDO58hnSv" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Parameter_change_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "c3a78544", - "metadata": { - "execution": {}, - "id": "c3a78544" - }, - "source": [ - "The population differential equation is an over-simplification and has some pronounced limitations:\n", - "1. Population growth is not exponential as there are limited resources, so the population will level out at some point.\n", - "2. It does not include any external factors on the populations, like weather, predators, and prey.\n", - "\n", - "These kinds of limitations can be addressed by extending the model.\n", - "\n", - "While it might not seem that the population equation has direct relevance to neuroscience, a similar equation is used to describe the accumulation of evidence for decision-making. This is known as the Drift Diffusion Model, and you will see it in more detail in the Linear System Day in Neuromatch (W2D2).\n", - "\n", - "Another differential equation similar to the population equation is the Leaky Integrate and Fire model, which you may have seen in the Python pre-course materials on W0D1 and W0D2. It will turn up later in Neuromatch as well. Below we will delve into the motivation of the differential equation." - ] - }, - { - "cell_type": "markdown", - "id": "ae82611f", - "metadata": { - "execution": {}, - "id": "ae82611f" - }, - "source": [ - "---\n", - "# Section 2: The leaky integrate and fire (LIF) model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "hGBNAqnVVY3E", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "hGBNAqnVVY3E" - }, - "outputs": [], - "source": [ - "# @title Video 3: The leaky integrate and fire model\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'ZfWO6MLCa1s'), ('Bilibili', 'BV1rb4y1C79n')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eWa8PXDrh2NB", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "eWa8PXDrh2NB" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_The_leaky_integrate_and_fire_model_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "4qgkjHOtWcEL", - "metadata": { - "execution": {}, - "id": "4qgkjHOtWcEL" - }, - "source": [ - "This video covers the Leaky Integrate and Fire model (a linear differential equation which describes the membrane potential of a single neuron).\n", - "\n", - "
\n", - " Click here for text recap of full LIF equation from video \n", - "\n", - "The Leaky Integrate and Fire Model is a linear differential equation that describes the membrane potential ($V$) of a single neuron which was proposed by Louis Édouard Lapicque in 1907 [2].\n", - "\n", - "The subthreshold membrane potential dynamics of a LIF neuron is described by\n", - "\\begin{align}\n", - "\\tau_m\\frac{dV}{dt} = -(V-E_L) + R_mI\\,\n", - "\\end{align}\n", - "\n", - "\n", - "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is membrane resistance, and $I$ is the external input current.\n", - "\n", - "
\n", - "\n", - "In the next few sections, we will break down the full LIF equation and then build it back up to get an intuitive feel of the different facets of the differential equation.\n" - ] - }, - { - "cell_type": "markdown", - "id": "c05087c6", - "metadata": { - "execution": {}, - "id": "c05087c6" - }, - "source": [ - "## Section 2.1: LIF without input\n", - "\n", - "*Estimated timing to here from start of tutorial: 18 min*\n", - "\n", - "As seen in the video, we will first model an LIF neuron without input, which results in the equation:\n", - "\n", - "\\begin{equation}\n", - "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}.\n", - "\\end{equation}\n", - "\n", - "where $\\tau_m$ is the time constant, $V$ is the membrane potential, and $E_L$ is the resting potential.\n", - "\n", - "
\n", - " Click here for further details (from video) \n", - "\n", - "Removing the input gives the equation\n", - "\n", - "\\begin{equation}\n", - "\\tau_m\\frac{dV}{dt} = -V+E_L,\n", - "\\end{equation}\n", - "\n", - "which can be written in words as:\n", - "\n", - "\\begin{align}\n", - "\\begin{matrix}\\text{\"Time constant multiplied by the} \\\\ \\text{change in membrane potential\"}\\end{matrix}&=\\begin{matrix}\\text{\"Minus Current} \\\\ \\text{membrane potential\"} \\end{matrix}+\n", - "\\begin{matrix}\\text{\"resting potential\"}\\end{matrix}.\\\\\n", - "\\end{align}\n", - "\n", - "\n", - "The equation can be re-arranged to look even more like the population equation:\n", - "\n", - "\\begin{align}\n", - "\\frac{dV}{dt} &= \\frac{-(V-E_L)}{\\tau_m}.\\\\\n", - "\\end{align}\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "XRl1cRt5ppJ1", - "metadata": { - "execution": {}, - "id": "XRl1cRt5ppJ1" - }, - "source": [ - "### Think! 2.1: Effect of membrane potential $V$ on the LIF model\n", - "\n", - "The plot the below shows the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ with the parameters set as:\n", - "* `E_L = -75`\n", - "* `V_reset = -50`\n", - "* `tau_m = 10.`\n", - "\n", - "1. What is the effect on $\\frac{dV}{dt}$ when $V>-75$ mV?\n", - "2. What is the effect on $\\frac{dV}{dt}$ when $V<-75$ mV\n", - "3. What is the effect on $\\frac{dV}{dt}$ when $V=-75$ mV?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8dd9906c", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "8dd9906c" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to plot the relationship between dV/dt and V\n", - "# Parameter definition\n", - "E_L = -75\n", - "tau_m = 10\n", - "\n", - "# Range of Values of V\n", - "V = np.arange(-90, 0, 1)\n", - "dV = -(V - E_L) / tau_m\n", - "\n", - "with plt.xkcd():\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(V, dV)\n", - " plt.hlines(0, min(V), max(V), colors='black', linestyles='dashed')\n", - " plt.vlines(-75, min(dV), max(dV), colors='black', linestyles='dashed')\n", - "\n", - " plt.text(-50, 1, 'Positive')\n", - " plt.text(-50, -2, 'Negative')\n", - " plt.text(E_L, max(dV) + 1, r'$E_L$')\n", - " plt.xlabel(r'$V(t)$ (mV)')\n", - " plt.ylabel(r'$\\frac{dV}{dt}=\\frac{-(V-E_L)}{\\tau_m}$')\n", - " plt.ylim(-8, 2)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "h0MIi-MJYdk5", - "metadata": { - "execution": {}, - "id": "h0MIi-MJYdk5" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. For $V>-75$ mV, the derivative is negative.\n", - " 2. For $V<-75$ mV, the derivative is positive.\n", - " 3. For $V=-75$ mV, the derivative is equal to $0$ is and a stable point when nothing changes.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "q0zy29uhh8lG", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "q0zy29uhh8lG" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Effect_of_membrane_potential_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "f7bb8882", - "metadata": { - "execution": {}, - "id": "f7bb8882" - }, - "source": [ - "### Section 2.1.1: Exact Solution of the LIF model without input\n", - "\n", - "The LIF model has the exact solution:\n", - "\n", - "\\begin{equation}\n", - "V(t) = E_L+(V_{reset}-E_L)e^{\\frac{-t}{\\tau_m}}\n", - "\\end{equation}\n", - "\n", - "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, and $V_{reset}$ is the initial membrane potential.\n", - "\n", - "
\n", - " Click here for further details (from video) \n", - "\n", - "Similar to the population equation, we need an initial membrane potential at time $0$ to solve the LIF model.\n", - "\n", - "With this equation\n", - "\\begin{align}\n", - "\\frac{dV}{dt} &= \\frac{-(V-E_L)}{\\tau_m}\\,\\\\\n", - "V(0)&=V_{reset},\n", - "\\end{align}\n", - "where is $V_{reset}$ is called the reset potential.\n", - "\n", - "The LIF model has the exact solution:\n", - "\n", - "\\begin{align*}\n", - "V(t)=&\\ E_L+(V_{reset}-E_L)e^{\\frac{-t}{\\tau_m}}\\\\\n", - "\\text{ which can be written as: }\\\\\n", - "\\begin{matrix}\\text{\"Current membrane} \\\\ \\text{potential}\"\\end{matrix}=&\\text{\"Resting potential\"}+\\begin{matrix}\\text{\"Reset potential minus resting potential} \\\\ \\text{times exponential with rate one over time constant.\"}\\end{matrix}\\\\\n", - "\\end{align*}\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "31910ff4", - "metadata": { - "execution": {}, - "id": "31910ff4" - }, - "source": [ - "#### Interactive Demo 2.1.1: Initial Condition $V_{reset}$\n", - "\n", - "This exercise is to get an intuitive feel of how the different initial conditions $V_{reset}$ impact the differential equation of the LIF and the exact solution for the equation:\n", - "\n", - "\\begin{equation}\n", - "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}\n", - "\\end{equation}\n", - "\n", - "with the parameters set as:\n", - "* `E_L = -75,`\n", - "* `tau_m = 10.`\n", - "\n", - "The panel on the left-hand side plots the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ and the right-hand side panel plots the exact solution $V$ as a function of time $t,$ the green dot in both panels is the reset potential $V_{reset}$.\n", - "\n", - "Pay close attention to when $V_{reset}=E_L=-75$mV.\n", - "\n", - "1. How does the solution look with initial values of $V_{reset} < -75$?\n", - "2. How does the solution look with initial values of $V_{reset} > -75$?\n", - "3. How does the solution look with initial values of $V_{reset} = -75$?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "03458759", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "03458759" - }, - "outputs": [], - "source": [ - "#@markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " V_reset=widgets.FloatSlider(-77., min=-91., max=-61., step=2,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def V_reset_widget(V_reset):\n", - " plot_V_no_input(V_reset)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "IQmxKOuRaVGr", - "metadata": { - "execution": {}, - "id": "IQmxKOuRaVGr" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. Initial Values of $V_{reset} < -75$ result in the solution increasing to\n", - " -75mV because $\\frac{dV}{dt} > 0$.\n", - " 2. Initial Values of $V_{reset} > -75$ result in the solution decreasing to\n", - " -75mV because $\\frac{dV}{dt} < 0$.\n", - " 3. Initial Values of $V_{reset} = -75$ result in a constant $V = -75$ mV\n", - " because $\\frac{dV}{dt} = 0$ (Stable point).\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "Q9OCCAmdiVYQ", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "Q9OCCAmdiVYQ" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Initial_condition_Vreset_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "5dcd105e", - "metadata": { - "execution": {}, - "id": "5dcd105e" - }, - "source": [ - "## Section 2.2: LIF with input\n", - "\n", - "*Estimated timing to here from start of tutorial: 24 min*\n", - "\n", - "We will re-introduce the input $I$ and membrane resistance $R_m$ giving the original equation:\n", - "\n", - "\\begin{equation}\n", - "\\tau_m\\frac{dV}{dt} = -(V-E_L) + \\color{blue}{R_mI}\n", - "\\end{equation}\n", - "\n", - "The input can be other neurons or sensory information." - ] - }, - { - "cell_type": "markdown", - "id": "9862281c", - "metadata": { - "execution": {}, - "id": "9862281c" - }, - "source": [ - "### Interactive Demo 2.2: The Impact of Input\n", - "\n", - "The interactive plot below manipulates $I$ in the differential equation.\n", - "\n", - "- With increasing input, how does the $\\frac{dV}{dt}$ change? How would this impact the solution?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc6ce7c7", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "fc6ce7c7" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " I=widgets.FloatSlider(3., min=0., max=20., step=2,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(I):\n", - " plot_dVdt(I=I)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "HSNtznIwY6wA", - "metadata": { - "execution": {}, - "id": "HSNtznIwY6wA" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "dV/dt becomes bigger and less of it is below 0. This means the solution will\n", - "increase well beyond what is bioligically plausible.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2S4tiJYAidIt", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "2S4tiJYAidIt" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_The_impact_of_Input_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "968432a1", - "metadata": { - "execution": {}, - "id": "968432a1" - }, - "source": [ - "### Section 2.2.1: LIF exact solution\n", - "\n", - "The LIF with a constant input has a known exact solution:\n", - "\\begin{equation}\n", - "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-t}{\\tau_m}}\n", - "\\end{equation}\n", - "\n", - "which is written as:\n", - "\n", - "\\begin{align*}\n", - "\\begin{matrix}\\text{\"Current membrane} \\\\ \\text{potential\"}\\end{matrix}=&\\text{\"Resting potential\"}+\\begin{matrix}\\text{\"Reset potential minus resting potential} \\\\ \\text{times exponential with rate one over time constant.\" }\\end{matrix}\\\\\n", - "\\end{align*}" - ] - }, - { - "cell_type": "markdown", - "id": "91b7c87a", - "metadata": { - "execution": {}, - "id": "91b7c87a" - }, - "source": [ - "The plot below shows the exact solution of the membrane potential with the parameters set as:\n", - "* `V_reset = -75,`\n", - "* `E_L = -75,`\n", - "* `tau_m = 10,`\n", - "* `R_m = 10,`\n", - "* `I = 10.`\n", - "\n", - "Ask yourself, does the result make biological sense? If not, what would you change? We'll delve into this in the next section" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "db5816d4", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "db5816d4" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to see the exact solution\n", - "dt = 0.5\n", - "t_rest = 0\n", - "\n", - "t = np.arange(0, 1000, dt)\n", - "\n", - "tau_m = 10\n", - "R_m = 10\n", - "V_reset = E_L = -75\n", - "\n", - "I = 10\n", - "\n", - "V = E_L + R_m*I + (V_reset - E_L - R_m*I) * np.exp(-(t)/tau_m)\n", - "\n", - "with plt.xkcd():\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(t,V)\n", - " plt.ylabel('V (mV)')\n", - " plt.xlabel('time (ms)')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "0a31b8d1", - "metadata": { - "execution": {}, - "id": "0a31b8d1" - }, - "source": [ - "## Section 2.3: Maths is one thing, but neuroscience matters\n", - "\n", - "*Estimated timing to here from start of tutorial: 30 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "CtWroVVASxG1", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "CtWroVVASxG1" - }, - "outputs": [], - "source": [ - "# @title Video 4: Adding firing to the LIF\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'rLQk-vXRaX0'), ('Bilibili', 'BV1gX4y1P7pZ')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "KljGW16eihTo", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "KljGW16eihTo" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Adding_firing_to_the_LIF_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "9f12c68e", - "metadata": { - "execution": {}, - "id": "9f12c68e" - }, - "source": [ - "This video first recaps the introduction of input to the leaky integrate and fire model and then delves into how we add spiking behavior (or firing) to the model.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "While the mathematics of the exact solution is exact, it is not biologically valid as a neuron spikes and definitely does not plateau at a very positive value.\n", - "\n", - "To model the firing of a spike, we must have a threshold voltage $V_{th}$ such that if the voltage $V(t)$ goes above it, the neuron spikes\n", - "$$V(t)>V_{th}.$$\n", - "We must record the time of spike $t_{isi}$ and count the number of spikes\n", - "$$t_{isi}=t, $$\n", - "$$𝑆𝑝𝑖𝑘𝑒=𝑆𝑝𝑖𝑘𝑒+1.$$\n", - "Then reset the membrane voltage $V(t)$\n", - "$$V(t_{isi} )=V_{Reset}.$$\n", - "\n", - "To take into account the spike the exact solution becomes:\n", - "\\begin{align*}\n", - "V(t)=&\\ E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},&\\qquad V(t)V_{th}\\\\\n", - "Spike=&Spike+1,&\\\\\n", - "t_{isi}=&t,\\\\\n", - "\\end{align*}\n", - "while this does make the neuron spike, it introduces a discontinuity which is not as elegant mathematically as it could be, but it gets results so that is good.\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "acc43e8a", - "metadata": { - "execution": {}, - "id": "acc43e8a" - }, - "source": [ - "### Interactive Demo 2.3.1: Input on spikes\n", - "\n", - "This exercise shows the relationship between the firing rate and the Input for the exact solution `V` of the LIF:\n", - "\n", - "\\begin{equation}\n", - "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},\n", - "\\end{equation}\n", - "\n", - "with the parameters set as:\n", - "* `V_reset = -75,`\n", - "* `E_L = -75,`\n", - "* `tau_m = 10,`\n", - "* `R_m = 10.`\n", - "\n", - "Below is a figure with three panels;\n", - "* the top panel is the input, $I,$\n", - "* the middle panel is the membrane potential $V(t)$. To illustrate the spike, $V(t)$ is set to $0$ and then reset to $-75$ mV when there is a spike.\n", - "* the bottom panel is the raster plot with each dot indicating a spike.\n", - "\n", - "First, as electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the firing rate for a single value of $I$.\n", - "\n", - "**Note:** The audio doesn't work in some browsers so don't worry about it if you can't hear anything." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "55785e26", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "55785e26" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to be able to hear the neuron\n", - "I = 3\n", - "t = np.arange(0, 1000, dt)\n", - "Spike, Spike_time, V = Exact_Integrate_and_Fire(I, t)\n", - "\n", - "plot_IF(t, V, I, Spike_time)\n", - "ipd.Audio(V, rate=len(V))" - ] - }, - { - "cell_type": "markdown", - "id": "54d5af0e", - "metadata": { - "execution": {}, - "id": "54d5af0e" - }, - "source": [ - "Manipulate the input into the LIF to see the impact of input on the firing pattern (rate).\n", - "\n", - "* What is the effect of $I$ on spiking?\n", - "* Is this biologically valid?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ebf27a22", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ebf27a22" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " I=widgets.FloatSlider(3, min=2.0, max=4., step=.1,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(I):\n", - " Spike, Spike_time, V = Exact_Integrate_and_Fire(I, t)\n", - " plot_IF(t, V, I, Spike_time)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "GjwI3QF3ZLtY", - "metadata": { - "execution": {}, - "id": "GjwI3QF3ZLtY" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. As I increases, the number of spikes increases.\n", - " 2. No, as there is a limit to the number of spikes due to a refractory period,\n", - " which is not accounted for in this model.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ouprca8SipAs", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ouprca8SipAs" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Input_on_spikes_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "c3cf4d7b", - "metadata": { - "execution": {}, - "id": "c3cf4d7b" - }, - "source": [ - "## Section 2.4 Firing Rate as a function of Input\n", - "\n", - "*Estimated timing to here from start of tutorial: 38 min*\n", - "\n", - "The firing frequency of a neuron plotted as a function of current is called an input-output curve (F–I curve). It is also known as a transfer function, which you came across in the previous tutorial. This function is one of the starting points for the rate model, which extends from modeling single neurons to the firing rate of a collection of neurons.\n", - "\n", - "By fitting this to a function, we can start to generalize the firing pattern of many neurons, which can be used to build rate models. This will be discussed later in Neuromatch." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "96bcf5e7", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "96bcf5e7" - }, - "outputs": [], - "source": [ - "# @markdown *Execture this cell to visualize the FI curve*\n", - "I_range = np.arange(2.0, 4.0, 0.1)\n", - "Spike_rate = np.ones(len(I_range))\n", - "\n", - "for i, I in enumerate(I_range):\n", - " Spike_rate[i], _, _ = Exact_Integrate_and_Fire(I, t)\n", - "\n", - "with plt.xkcd():\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(I_range,Spike_rate)\n", - " plt.xlabel('Input Current (nA)')\n", - " plt.ylabel('Spikes per Second (Hz)')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "58fdcbe8", - "metadata": { - "execution": {}, - "id": "58fdcbe8" - }, - "source": [ - "The LIF model is a very nice differential equation to start with in computational neuroscience as it has been used as a building block for many papers that simulate neuronal response.\n", - "\n", - "__Strengths of LIF model:__\n", - "+ Has an exact solution;\n", - "+ Easy to interpret;\n", - "+ Great to build network of neurons.\n", - "\n", - "__Weaknesses of the LIF model:__\n", - "- Spiking is a discontinuity;\n", - "- Abstraction from biology;\n", - "- Cannot generate different spiking patterns." - ] - }, - { - "cell_type": "markdown", - "id": "697364ff", - "metadata": { - "execution": {}, - "id": "697364ff" - }, - "source": [ - "---\n", - "# Summary\n", - "\n", - "*Estimated timing of tutorial: 45 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "_Jmpnq0mSihx", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "_Jmpnq0mSihx" - }, - "outputs": [], - "source": [ - "# @title Video 5: Summary\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'VzwLAW5p4ao'), ('Bilibili', 'BV1jV411x7t9')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "VNxbJDK5iuFA", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "VNxbJDK5iuFA" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Summary_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "x0H0zhTNWlop", - "metadata": { - "execution": {}, - "id": "x0H0zhTNWlop" - }, - "source": [ - "In this tutorial, we have seen two differential equations, the population differential equations and the leaky integrate and fire model.\n", - "\n", - "We learned about the following:\n", - "* The motivation for differential equations.\n", - "* An intuitive relationship between the solution and the differential equation form.\n", - "* How different parameters of the differential equation impact the solution.\n", - "* The strengths and limitations of the simple differential equations." - ] - }, - { - "cell_type": "markdown", - "id": "eaec6994", - "metadata": { - "execution": {}, - "id": "eaec6994" - }, - "source": [ - "---\n", - "## Links to Neuromatch Computational Neuroscience Days\n", - "\n", - "Differential equations turn up in a number of different Neuromatch days:\n", - "* The LIF model is discussed in more details in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", - "* Drift Diffusion model which is a differential equation for decision making is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", - "* Systems of differential equations are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", - "\n", - "---\n", - "## References\n", - "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", - "\n", - "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", - "\n", - "\n", - "## Bibliography\n", - "1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.\n", - "2. Strogatz, S. (2014). Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2nd edition\n", - "\n", - "### Supplemental Popular Reading List\n", - "1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.\n", - "2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.\n", - "\n", - "### Popular Podcast\n", - "1. Strogatz, S. (Host). (2020), Joy of X https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine\n" - ] - } - ], - "metadata": { - "colab": { - "name": "W0D4_Tutorial2", - "provenance": [], - "toc_visible": true, - "include_colab_link": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file + "cells": [ + { + "cell_type": "markdown", + "id": "bc470a90", + "metadata": { + "colab_type": "text", + "execution": {}, + "id": "view-in-github" + }, + "source": [ + "\"Open   \"Open" + ] + }, + { + "cell_type": "markdown", + "id": "08842220", + "metadata": { + "execution": {} + }, + "source": [ + "# Tutorial 2: Differential Equations\n", + "\n", + "**Week 0, Day 4: Calculus**\n", + "\n", + "**By Neuromatch Academy**\n", + "\n", + "**Content creators:** John S Butler, Arvind Kumar with help from Rebecca Brady\n", + "\n", + "**Content reviewers:** Swapnil Kumar, Sirisha Sripada, Matthew McCann, Tessy Tom\n", + "\n", + "**Production editors:** Matthew McCann, Ella Batty" + ] + }, + { + "cell_type": "markdown", + "id": "gK0EQXTiH5W2", + "metadata": { + "execution": {} + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "id": "7d80998b", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Tutorial Objectives\n", + "\n", + "*Estimated timing of tutorial: 45 minutes*\n", + "\n", + "A great deal of neuroscience can be modeled using differential equations, from gating channels to single neurons to a network of neurons to blood flow to behavior. A simple way to think about differential equations is they are equations that describe how something changes.\n", + "\n", + "The most famous of these in neuroscience is the Nobel Prize-winning Hodgkin-Huxley equation, which describes a neuron by modeling the gating of each axon. But we will not start there; we will start a few steps back.\n", + "\n", + "Differential Equations are mathematical equations that describe how something like a population or a neuron changes over time. Differential equations are so useful because they can generalize a process such that one equation can be used to describe many different outcomes.\n", + "The general form of a first-order differential equation is:\n", + "\n", + "\\begin{equation}\n", + "\\frac{d}{dt}y(t) = f\\left( t,y(t) \\right)\n", + "\\end{equation}\n", + "\n", + "which can be read as \"the change in a process $y$ over time $t$ is a function $f$ of time $t$ and itself $y$\". This might initially seem like a paradox as you are using a process $y$ you want to know about to describe itself, a bit like the MC Escher drawing of two hands painting [each other](https://en.wikipedia.org/wiki/Drawing_Hands). But that is the beauty of mathematics - this can be solved sometimes, and when it cannot be solved exactly, we can use numerical methods to estimate the answer (as we will see in the next tutorial).\n", + "\n", + "In this tutorial, we will see how __differential equations are motivated by observations of physical responses__. We will break down the population differential equation, then the integrate and fire model, which leads nicely into raster plots and frequency-current curves to rate models.\n", + "\n", + "**Steps:**\n", + "- Get an intuitive understanding of a linear population differential equation (humans, not neurons)\n", + "- Visualize the relationship between the change in population and the population\n", + "- Breakdown the Leaky Integrate and Fire (LIF) differential equation\n", + "- Code the exact solution of a LIF for a constant input\n", + "- Visualize and listen to the response of the LIF for different inputs" + ] + }, + { + "cell_type": "markdown", + "id": "kq1fWqhyNJ3i", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1-6UCje-fVs6", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D4_T2\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5985801", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d40abcd8", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Figure Settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import IPython.display as ipd\n", + "from matplotlib import gridspec\n", + "import ipywidgets as widgets # interactive display\n", + "%config InlineBackend.figure_format = 'retina'\n", + "# use NMA plot style\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", + "my_layout = widgets.Layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6XJo_I6_NbJg", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Plotting Functions\n", + "\n", + "def plot_dPdt(alpha=.3):\n", + " \"\"\" Plots change in population over time\n", + " Args:\n", + " alpha: Birth Rate\n", + " Returns:\n", + " A figure two panel figure\n", + " left panel: change in population as a function of population\n", + " right panel: membrane potential as a function of time\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " time=np.arange(0, 10 ,0.01)\n", + " fig = plt.figure(figsize=(12,4))\n", + " gs = gridspec.GridSpec(1, 2)\n", + "\n", + " ## dpdt as a fucntion of p\n", + " plt.subplot(gs[0])\n", + " plt.plot(np.exp(alpha*time), alpha*np.exp(alpha*time))\n", + " plt.xlabel(r'Population $p(t)$ (millions)')\n", + " plt.ylabel(r'$\\frac{d}{dt}p(t)=\\alpha p(t)$')\n", + "\n", + " ## p exact solution\n", + " plt.subplot(gs[1])\n", + " plt.plot(time, np.exp(alpha*time))\n", + " plt.ylabel(r'Population $p(t)$ (millions)')\n", + " plt.xlabel('time (years)')\n", + " plt.show()\n", + "\n", + "\n", + "def plot_V_no_input(V_reset=-75):\n", + " \"\"\"\n", + " Args:\n", + " V_reset: Reset Potential\n", + " Returns:\n", + " A figure two panel figure\n", + " left panel: change in membrane potential as a function of membrane potential\n", + " right panel: membrane potential as a function of time\n", + " \"\"\"\n", + " E_L=-75\n", + " tau_m=10\n", + " t=np.arange(0,100,0.01)\n", + " V= E_L+(V_reset-E_L)*np.exp(-(t)/tau_m)\n", + " V_range=np.arange(-90,0,1)\n", + " dVdt=-(V_range-E_L)/tau_m\n", + "\n", + " with plt.xkcd():\n", + " time=np.arange(0, 10, 0.01)\n", + " fig = plt.figure(figsize=(12, 4))\n", + " gs = gridspec.GridSpec(1, 2)\n", + "\n", + " plt.subplot(gs[0])\n", + " plt.plot(V_range,dVdt)\n", + " plt.hlines(0,min(V_range),max(V_range), colors='black', linestyles='dashed')\n", + " plt.vlines(-75, min(dVdt), max(dVdt), colors='black', linestyles='dashed')\n", + " plt.plot(V_reset,-(V_reset - E_L)/tau_m, 'o', label=r'$V_{reset}$')\n", + " plt.text(-50, 1, 'Positive')\n", + " plt.text(-50, -2, 'Negative')\n", + " plt.text(E_L - 1, max(dVdt), r'$E_L$')\n", + " plt.legend()\n", + " plt.xlabel('Membrane Potential V (mV)')\n", + " plt.ylabel(r'$\\frac{dV}{dt}=\\frac{-(V(t)-E_L)}{\\tau_m}$')\n", + "\n", + " plt.subplot(gs[1])\n", + " plt.plot(t,V)\n", + " plt.plot(t[0],V_reset,'o')\n", + " plt.ylabel(r'Membrane Potential $V(t)$ (mV)')\n", + " plt.xlabel('time (ms)')\n", + " plt.ylim([-95, -60])\n", + "\n", + " plt.show()\n", + "\n", + "\n", + "## LIF PLOT\n", + "def plot_IF(t, V,I,Spike_time):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " V : membrane Voltage\n", + " I : Input\n", + " Spike_time : Spike_times\n", + " Returns:\n", + " figure with three panels\n", + " top panel: Input as a function of time\n", + " middle panel: membrane potential as a function of time\n", + " bottom panel: Raster plot\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(12, 4))\n", + " gs = gridspec.GridSpec(3, 1, height_ratios=[1, 4, 1])\n", + "\n", + " # PLOT OF INPUT\n", + " plt.subplot(gs[0])\n", + " plt.ylabel(r'$I_e(nA)$')\n", + " plt.yticks(rotation=45)\n", + " plt.hlines(I,min(t),max(t),'g')\n", + " plt.ylim((2, 4))\n", + " plt.xlim((-50, 1000))\n", + "\n", + " # PLOT OF ACTIVITY\n", + " plt.subplot(gs[1])\n", + " plt.plot(t,V)\n", + " plt.xlim((-50, 1000))\n", + " plt.ylabel(r'$V(t)$(mV)')\n", + "\n", + " # PLOT OF SPIKES\n", + " plt.subplot(gs[2])\n", + " plt.ylabel(r'Spike')\n", + " plt.yticks([])\n", + " plt.scatter(Spike_time, 1 * np.ones(len(Spike_time)), color=\"grey\", marker=\".\")\n", + " plt.xlim((-50, 1000))\n", + " plt.xlabel('time(ms)')\n", + " plt.show()\n", + "\n", + "\n", + "## Plotting the differential Equation\n", + "def plot_dVdt(I=0):\n", + " \"\"\"\n", + " Args:\n", + " I : Input Current\n", + " Returns:\n", + " figure of change in membrane potential as a function of membrane potential\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " E_L = -75\n", + " tau_m = 10\n", + " V = np.arange(-85, 0, 1)\n", + " g_L = 10.\n", + " fig = plt.figure(figsize=(6, 4))\n", + "\n", + " plt.plot(V,(-(V-E_L) + I*10) / tau_m)\n", + " plt.hlines(0, min(V), max(V), colors='black', linestyles='dashed')\n", + " plt.xlabel('V (mV)')\n", + " plt.ylabel(r'$\\frac{dV}{dt}$')\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d88bf487", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Helper Functions\n", + "\n", + "## EXACT SOLUTION OF LIF\n", + "def Exact_Integrate_and_Fire(I,t):\n", + " \"\"\"\n", + " Args:\n", + " I : Input Current\n", + " t : time\n", + " Returns:\n", + " Spike : Spike Count\n", + " Spike_time : Spike time\n", + " V_exact : Exact membrane potential\n", + " \"\"\"\n", + "\n", + " Spike = 0\n", + " tau_m = 10\n", + " R = 10\n", + " t_isi = 0\n", + " V_reset = E_L = -75\n", + " V_exact = V_reset * np.ones(len(t))\n", + " V_th = -50\n", + " Spike_time = []\n", + "\n", + " for i in range(0, len(t)):\n", + "\n", + " V_exact[i] = E_L + R*I + (V_reset - E_L - R*I) * np.exp(-(t[i]-t_isi)/tau_m)\n", + "\n", + " # Threshold Reset\n", + " if V_exact[i] > V_th:\n", + " V_exact[i-1] = 0\n", + " V_exact[i] = V_reset\n", + " t_isi = t[i]\n", + " Spike = Spike+1\n", + " Spike_time = np.append(Spike_time, t[i])\n", + "\n", + " return Spike, Spike_time, V_exact" + ] + }, + { + "cell_type": "markdown", + "id": "O2Q5d9D2hLZq", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 0: Introduction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "DIZfUH4rQNSR", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 1: Why do we care about differential equations?\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'LhX-mUd8lPo'), ('Bilibili', 'BV1v64y197bW')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Or-35flNhTBo", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Why_do_we_care_about_differential_equations_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "AcwNWVdsQgLz", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 1: Population differential equation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "LRwtiFPVQ3Pz", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 2: Population differential equation\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'czgGyoUsRoQ'), ('Bilibili', 'BV1pg41137CU')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "WLss9oNLhSI-", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Population_differential_equation_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "286b4298", + "metadata": { + "execution": {} + }, + "source": [ + "This video covers our first example of a differential equation: a differential equation which models the change in population.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "To get an intuitive feel of a differential equations, we will start with a population differential equation, which models the change in population [1], that is human population not neurons, we will get to neurons later. Mathematically it is written like:\n", + "\\begin{align*}\n", + "\\\\\n", + "\\frac{d}{dt}\\,p(t) &= \\alpha p(t),\\\\\n", + "\\end{align*}\n", + "\n", + "where $p(t)$ is the population of the world and $\\alpha$ is a parameter representing birth rate.\n", + "\n", + "Another way of thinking about the models is that the equation\n", + "\\begin{align*}\n", + "\\\\\n", + "\\frac{d}{dt}\\,p(t) &= \\alpha p(t),\\\\\n", + "\\text{can be written as:}\\\\\n", + "\\text{\"Change in Population\"} &= \\text{ \"Birth rate times Current population.\"}\n", + "\\end{align*}\n", + "\n", + "The equation is saying something reasonable maybe not the perfect model but a good start.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "njGc4mc4ASXb", + "metadata": { + "execution": {} + }, + "source": [ + "### Think! 1.1: Interpretating the behavior of a linear population equation\n", + "\n", + "Using the plot below of change of population $\\frac{d}{dt} p(t) $ as a function of population $p(t)$ with birth-rate $\\alpha=0.3$, discuss the following questions:\n", + "\n", + "1. Why is the population differential equation known as a linear differential equation?\n", + "2. How does population size affect the rate of change of the population?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e37083e", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute the code to plot the rate of change of population as a function of population\n", + "p = np.arange(0, 100, 0.1)\n", + "\n", + "with plt.xkcd():\n", + "\n", + " dpdt = 0.3*p\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(p, dpdt)\n", + " plt.xlabel(r'Population $p(t)$ (millions)')\n", + " plt.ylabel(r'$\\frac{d}{dt}p(t)=\\alpha p(t)$')\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "il7f6AWkXk0B", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. The plot of $\\frac{dp}{dt}$ is a line, which is why the differential\n", + " equation is known as a linear differential equation.\n", + "\n", + " 2. As the population increases, the change of population increases. A\n", + " population of 20 has a change of 6 while a population of 100 has a change of\n", + " 30. This makes sense - the larger the population the larger the change.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1whSBsnBhbAG", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Interpretating_the_behavior_of_a_linear_population_equation_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "id": "5846bbb5", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 1.1: Exact solution of the population equation" + ] + }, + { + "cell_type": "markdown", + "id": "uqcubpZNBIGl", + "metadata": { + "execution": {} + }, + "source": [ + "### Section 1.1.1: Initial condition\n", + "\n", + "The linear population differential equation is an initial value differential equation because we need an initial population value to solve it. Here we will set our initial population at time $0$ to $1$:\n", + "\n", + "\\begin{equation}\n", + "p(0) = 1\n", + "\\end{equation}\n", + "\n", + "Different initial conditions will lead to different answers but will not change the differential equation. This is one of the strengths of a differential equation." + ] + }, + { + "cell_type": "markdown", + "id": "oYBK4NWYBL5t", + "metadata": { + "execution": {} + }, + "source": [ + "### Section 1.1.2: Exact Solution\n", + "\n", + "To calculate the exact solution of a differential equation, we must integrate both sides. Instead of numerical integration (as you delved into in the last tutorial), we will first try to solve the differential equations using analytical integration. As with derivatives, we can find analytical integrals of simple equations by consulting [a list](https://en.wikipedia.org/wiki/Lists_of_integrals). We can then get integrals for more complex equations using some mathematical tricks - the harder the equation, the more obscure the trick.\n", + "\n", + "The linear population equation\n", + "\\begin{equation}\n", + "\\frac{d}{dt}p(t) = \\alpha p(t), \\, p(0)=P_0\n", + "\\end{equation}\n", + "\n", + "has the exact solution:\n", + "\n", + "\\begin{equation}\n", + "p(t) = P_0 e^{\\alpha t}.\n", + "\\end{equation}\n", + "\n", + "The exact solution written in words is:\n", + "\n", + "\\begin{equation}\n", + "\\text{\"Population\"} = \\text{\"grows/declines exponentially as a function of time and birth rate\"}.\n", + "\\end{equation}\n", + "\n", + "Most differential equations do not have a known exact solution, so we will show how the solution can be estimated in the next tutorial on numerical methods.\n", + "\n", + "A small aside: a good deal of progress in mathematics was due to mathematicians writing taunting letters to each other, saying they had a trick to solve something better than everyone else. So do not worry too much about the tricks." + ] + }, + { + "cell_type": "markdown", + "id": "1257fcc0", + "metadata": { + "execution": {} + }, + "source": [ + "#### Example Exact Solution of the Population Equation\n", + "\n", + "Let's consider the population differential equation with a birth rate $\\alpha=0.3$:\n", + "\n", + "\\begin{equation}\n", + "\\frac{d}{dt}p(t) = 0.3p(t)\n", + "\\end{equation}\n", + "\n", + "with the initial condition\n", + "\n", + "\\begin{equation}\n", + "p(0)=1.\n", + "\\end{equation}\n", + "\n", + "It has an exact solution\n", + "\n", + "\\begin{equation}\n", + "p(t)=e^{0.3 t}.\n", + "\\end{equation}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eeb717b8", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute code to plot the exact solution\n", + "t = np.arange(0, 10, 0.1) # Time from 0 to 10 years in 0.1 steps\n", + "\n", + "with plt.xkcd():\n", + "\n", + " p = np.exp(0.3 * t)\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(t, p)\n", + " plt.ylabel('Population (millions)')\n", + " plt.xlabel('time (years)')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "792841c9", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 1.2: Parameters of the differential equation\n", + "\n", + "*Estimated timing to here from start of tutorial: 12 min*\n", + "\n", + "One of the goals when designing a differential equation is to make it generalizable. This means that the differential equation will give reasonable solutions for different countries with different birth rates $\\alpha$." + ] + }, + { + "cell_type": "markdown", + "id": "8zD98917MDtW", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 1.2: Parameter Change\n", + "\n", + "Play with the widget to see the relationship between $\\alpha$ and the population differential equation as a function of population (left-hand side) and the population solution as a function of time (right-hand side). Pay close attention to the transition point from positive to negative.\n", + "\n", + "How do changing parameters of the population equation affect the outcome?\n", + "\n", + "1. What happens when $\\alpha < 0$?\n", + "2. What happens when $\\alpha > 0$?\n", + "3. What happens when $\\alpha = 0$?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "490c0be0", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " alpha=widgets.FloatSlider(.3, min=-1., max=1., step=.1, layout=my_layout)\n", + ")\n", + "def Pop_widget(alpha):\n", + " plot_dPdt(alpha=alpha)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "KDgZUHS5YDQp", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. Negative values of alpha result in an exponential decrease to 0 a stable solution.\n", + " 2. Positive Values of alpha in an exponential increases to infinity.\n", + " 3. Alpha equal to 0 is a unique point known as an equilibrium point when the\n", + " dp/dt=0 and there is no change in population. This is known as a stable point.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1AgWDO58hnSv", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Parameter_change_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "c3a78544", + "metadata": { + "execution": {} + }, + "source": [ + "The population differential equation is an over-simplification and has some pronounced limitations:\n", + "1. Population growth is not exponential as there are limited resources, so the population will level out at some point.\n", + "2. It does not include any external factors on the populations, like weather, predators, and prey.\n", + "\n", + "These kinds of limitations can be addressed by extending the model.\n", + "\n", + "While it might not seem that the population equation has direct relevance to neuroscience, a similar equation is used to describe the accumulation of evidence for decision-making. This is known as the Drift Diffusion Model, and you will see it in more detail in the Linear System Day in Neuromatch (W2D2).\n", + "\n", + "Another differential equation similar to the population equation is the Leaky Integrate and Fire model, which you may have seen in the Python pre-course materials on W0D1 and W0D2. It will turn up later in Neuromatch as well. Below we will delve into the motivation of the differential equation." + ] + }, + { + "cell_type": "markdown", + "id": "ae82611f", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 2: The leaky integrate and fire (LIF) model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hGBNAqnVVY3E", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 3: The leaky integrate and fire model\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'ZfWO6MLCa1s'), ('Bilibili', 'BV1rb4y1C79n')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eWa8PXDrh2NB", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_The_leaky_integrate_and_fire_model_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "4qgkjHOtWcEL", + "metadata": { + "execution": {} + }, + "source": [ + "This video covers the Leaky Integrate and Fire model (a linear differential equation which describes the membrane potential of a single neuron).\n", + "\n", + "
\n", + " Click here for text recap of full LIF equation from video \n", + "\n", + "The Leaky Integrate and Fire Model is a linear differential equation that describes the membrane potential ($V$) of a single neuron which was proposed by Louis Édouard Lapicque in 1907 [2].\n", + "\n", + "The subthreshold membrane potential dynamics of a LIF neuron is described by\n", + "\\begin{align}\n", + "\\tau_m\\frac{dV}{dt} = -(V-E_L) + R_mI\\,\n", + "\\end{align}\n", + "\n", + "\n", + "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is membrane resistance, and $I$ is the external input current.\n", + "\n", + "
\n", + "\n", + "In the next few sections, we will break down the full LIF equation and then build it back up to get an intuitive feel of the different facets of the differential equation.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c05087c6", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 2.1: LIF without input\n", + "\n", + "*Estimated timing to here from start of tutorial: 18 min*\n", + "\n", + "As seen in the video, we will first model an LIF neuron without input, which results in the equation:\n", + "\n", + "\\begin{equation}\n", + "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}.\n", + "\\end{equation}\n", + "\n", + "where $\\tau_m$ is the time constant, $V$ is the membrane potential, and $E_L$ is the resting potential.\n", + "\n", + "
\n", + " Click here for further details (from video) \n", + "\n", + "Removing the input gives the equation\n", + "\n", + "\\begin{equation}\n", + "\\tau_m\\frac{dV}{dt} = -V+E_L,\n", + "\\end{equation}\n", + "\n", + "which can be written in words as:\n", + "\n", + "\\begin{align}\n", + "\\begin{matrix}\\text{\"Time constant multiplied by the} \\\\ \\text{change in membrane potential\"}\\end{matrix}&=\\begin{matrix}\\text{\"Minus Current} \\\\ \\text{membrane potential\"} \\end{matrix}+\n", + "\\begin{matrix}\\text{\"resting potential\"}\\end{matrix}.\\\\\n", + "\\end{align}\n", + "\n", + "\n", + "The equation can be re-arranged to look even more like the population equation:\n", + "\n", + "\\begin{align}\n", + "\\frac{dV}{dt} &= \\frac{-(V-E_L)}{\\tau_m}.\\\\\n", + "\\end{align}\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "XRl1cRt5ppJ1", + "metadata": { + "execution": {} + }, + "source": [ + "### Think! 2.1: Effect of membrane potential $V$ on the LIF model\n", + "\n", + "The plot the below shows the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ with the parameters set as:\n", + "* `E_L = -75`\n", + "* `V_reset = -50`\n", + "* `tau_m = 10.`\n", + "\n", + "1. What is the effect on $\\frac{dV}{dt}$ when $V>-75$ mV?\n", + "2. What is the effect on $\\frac{dV}{dt}$ when $V<-75$ mV\n", + "3. What is the effect on $\\frac{dV}{dt}$ when $V=-75$ mV?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dd9906c", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to plot the relationship between dV/dt and V\n", + "# Parameter definition\n", + "E_L = -75\n", + "tau_m = 10\n", + "\n", + "# Range of Values of V\n", + "V = np.arange(-90, 0, 1)\n", + "dV = -(V - E_L) / tau_m\n", + "\n", + "with plt.xkcd():\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(V, dV)\n", + " plt.hlines(0, min(V), max(V), colors='black', linestyles='dashed')\n", + " plt.vlines(-75, min(dV), max(dV), colors='black', linestyles='dashed')\n", + "\n", + " plt.text(-50, 1, 'Positive')\n", + " plt.text(-50, -2, 'Negative')\n", + " plt.text(E_L, max(dV) + 1, r'$E_L$')\n", + " plt.xlabel(r'$V(t)$ (mV)')\n", + " plt.ylabel(r'$\\frac{dV}{dt}=\\frac{-(V-E_L)}{\\tau_m}$')\n", + " plt.ylim(-8, 2)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "h0MIi-MJYdk5", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. For $V>-75$ mV, the derivative is negative.\n", + " 2. For $V<-75$ mV, the derivative is positive.\n", + " 3. For $V=-75$ mV, the derivative is equal to $0$ is and a stable point when nothing changes.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "q0zy29uhh8lG", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Effect_of_membrane_potential_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "f7bb8882", + "metadata": { + "execution": {} + }, + "source": [ + "### Section 2.1.1: Exact Solution of the LIF model without input\n", + "\n", + "The LIF model has the exact solution:\n", + "\n", + "\\begin{equation}\n", + "V(t) = E_L+(V_{reset}-E_L)e^{\\frac{-t}{\\tau_m}}\n", + "\\end{equation}\n", + "\n", + "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, and $V_{reset}$ is the initial membrane potential.\n", + "\n", + "
\n", + " Click here for further details (from video) \n", + "\n", + "Similar to the population equation, we need an initial membrane potential at time $0$ to solve the LIF model.\n", + "\n", + "With this equation\n", + "\\begin{align}\n", + "\\frac{dV}{dt} &= \\frac{-(V-E_L)}{\\tau_m}\\,\\\\\n", + "V(0)&=V_{reset},\n", + "\\end{align}\n", + "where is $V_{reset}$ is called the reset potential.\n", + "\n", + "The LIF model has the exact solution:\n", + "\n", + "\\begin{align*}\n", + "V(t)=&\\ E_L+(V_{reset}-E_L)e^{\\frac{-t}{\\tau_m}}\\\\\n", + "\\text{ which can be written as: }\\\\\n", + "\\begin{matrix}\\text{\"Current membrane} \\\\ \\text{potential}\"\\end{matrix}=&\\text{\"Resting potential\"}+\\begin{matrix}\\text{\"Reset potential minus resting potential} \\\\ \\text{times exponential with rate one over time constant.\"}\\end{matrix}\\\\\n", + "\\end{align*}\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "31910ff4", + "metadata": { + "execution": {} + }, + "source": [ + "#### Interactive Demo 2.1.1: Initial Condition $V_{reset}$\n", + "\n", + "This exercise is to get an intuitive feel of how the different initial conditions $V_{reset}$ impact the differential equation of the LIF and the exact solution for the equation:\n", + "\n", + "\\begin{equation}\n", + "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}\n", + "\\end{equation}\n", + "\n", + "with the parameters set as:\n", + "* `E_L = -75,`\n", + "* `tau_m = 10.`\n", + "\n", + "The panel on the left-hand side plots the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ and the right-hand side panel plots the exact solution $V$ as a function of time $t,$ the green dot in both panels is the reset potential $V_{reset}$.\n", + "\n", + "Pay close attention to when $V_{reset}=E_L=-75$mV.\n", + "\n", + "1. How does the solution look with initial values of $V_{reset} < -75$?\n", + "2. How does the solution look with initial values of $V_{reset} > -75$?\n", + "3. How does the solution look with initial values of $V_{reset} = -75$?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03458759", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "#@markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " V_reset=widgets.FloatSlider(-77., min=-91., max=-61., step=2,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def V_reset_widget(V_reset):\n", + " plot_V_no_input(V_reset)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "IQmxKOuRaVGr", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. Initial Values of $V_{reset} < -75$ result in the solution increasing to\n", + " -75mV because $\\frac{dV}{dt} > 0$.\n", + " 2. Initial Values of $V_{reset} > -75$ result in the solution decreasing to\n", + " -75mV because $\\frac{dV}{dt} < 0$.\n", + " 3. Initial Values of $V_{reset} = -75$ result in a constant $V = -75$ mV\n", + " because $\\frac{dV}{dt} = 0$ (Stable point).\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Q9OCCAmdiVYQ", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Initial_condition_Vreset_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "5dcd105e", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 2.2: LIF with input\n", + "\n", + "*Estimated timing to here from start of tutorial: 24 min*\n", + "\n", + "We will re-introduce the input $I$ and membrane resistance $R_m$ giving the original equation:\n", + "\n", + "\\begin{equation}\n", + "\\tau_m\\frac{dV}{dt} = -(V-E_L) + \\color{blue}{R_mI}\n", + "\\end{equation}\n", + "\n", + "The input can be other neurons or sensory information." + ] + }, + { + "cell_type": "markdown", + "id": "9862281c", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 2.2: The Impact of Input\n", + "\n", + "The interactive plot below manipulates $I$ in the differential equation.\n", + "\n", + "- With increasing input, how does the $\\frac{dV}{dt}$ change? How would this impact the solution?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc6ce7c7", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " I=widgets.FloatSlider(3., min=0., max=20., step=2,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(I):\n", + " plot_dVdt(I=I)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "HSNtznIwY6wA", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "dV/dt becomes bigger and less of it is below 0. This means the solution will\n", + "increase well beyond what is bioligically plausible.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2S4tiJYAidIt", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_The_impact_of_Input_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "968432a1", + "metadata": { + "execution": {} + }, + "source": [ + "### Section 2.2.1: LIF exact solution\n", + "\n", + "The LIF with a constant input has a known exact solution:\n", + "\\begin{equation}\n", + "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-t}{\\tau_m}}\n", + "\\end{equation}\n", + "\n", + "which is written as:\n", + "\n", + "\\begin{align*}\n", + "\\begin{matrix}\\text{\"Current membrane} \\\\ \\text{potential\"}\\end{matrix}=&\\text{\"Resting potential\"}+\\begin{matrix}\\text{\"Reset potential minus resting potential} \\\\ \\text{times exponential with rate one over time constant.\" }\\end{matrix}\\\\\n", + "\\end{align*}" + ] + }, + { + "cell_type": "markdown", + "id": "91b7c87a", + "metadata": { + "execution": {} + }, + "source": [ + "The plot below shows the exact solution of the membrane potential with the parameters set as:\n", + "* `V_reset = -75,`\n", + "* `E_L = -75,`\n", + "* `tau_m = 10,`\n", + "* `R_m = 10,`\n", + "* `I = 10.`\n", + "\n", + "Ask yourself, does the result make biological sense? If not, what would you change? We'll delve into this in the next section" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db5816d4", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to see the exact solution\n", + "dt = 0.5\n", + "t_rest = 0\n", + "\n", + "t = np.arange(0, 1000, dt)\n", + "\n", + "tau_m = 10\n", + "R_m = 10\n", + "V_reset = E_L = -75\n", + "\n", + "I = 10\n", + "\n", + "V = E_L + R_m*I + (V_reset - E_L - R_m*I) * np.exp(-(t)/tau_m)\n", + "\n", + "with plt.xkcd():\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(t,V)\n", + " plt.ylabel('V (mV)')\n", + " plt.xlabel('time (ms)')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0a31b8d1", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 2.3: Maths is one thing, but neuroscience matters\n", + "\n", + "*Estimated timing to here from start of tutorial: 30 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "CtWroVVASxG1", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 4: Adding firing to the LIF\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'rLQk-vXRaX0'), ('Bilibili', 'BV1gX4y1P7pZ')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "KljGW16eihTo", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Adding_firing_to_the_LIF_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "9f12c68e", + "metadata": { + "execution": {} + }, + "source": [ + "This video first recaps the introduction of input to the leaky integrate and fire model and then delves into how we add spiking behavior (or firing) to the model.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "While the mathematics of the exact solution is exact, it is not biologically valid as a neuron spikes and definitely does not plateau at a very positive value.\n", + "\n", + "To model the firing of a spike, we must have a threshold voltage $V_{th}$ such that if the voltage $V(t)$ goes above it, the neuron spikes\n", + "$$V(t)>V_{th}.$$\n", + "We must record the time of spike $t_{isi}$ and count the number of spikes\n", + "$$t_{isi}=t, $$\n", + "$$𝑆𝑝𝑖𝑘𝑒=𝑆𝑝𝑖𝑘𝑒+1.$$\n", + "Then reset the membrane voltage $V(t)$\n", + "$$V(t_{isi} )=V_{Reset}.$$\n", + "\n", + "To take into account the spike the exact solution becomes:\n", + "\\begin{align*}\n", + "V(t)=&\\ E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},&\\qquad V(t)V_{th}\\\\\n", + "Spike=&Spike+1,&\\\\\n", + "t_{isi}=&t,\\\\\n", + "\\end{align*}\n", + "while this does make the neuron spike, it introduces a discontinuity which is not as elegant mathematically as it could be, but it gets results so that is good.\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "acc43e8a", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 2.3.1: Input on spikes\n", + "\n", + "This exercise shows the relationship between the firing rate and the Input for the exact solution `V` of the LIF:\n", + "\n", + "\\begin{equation}\n", + "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},\n", + "\\end{equation}\n", + "\n", + "with the parameters set as:\n", + "* `V_reset = -75,`\n", + "* `E_L = -75,`\n", + "* `tau_m = 10,`\n", + "* `R_m = 10.`\n", + "\n", + "Below is a figure with three panels;\n", + "* the top panel is the input, $I,$\n", + "* the middle panel is the membrane potential $V(t)$. To illustrate the spike, $V(t)$ is set to $0$ and then reset to $-75$ mV when there is a spike.\n", + "* the bottom panel is the raster plot with each dot indicating a spike.\n", + "\n", + "First, as electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the firing rate for a single value of $I$.\n", + "\n", + "**Note:** The audio doesn't work in some browsers so don't worry about it if you can't hear anything." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55785e26", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to be able to hear the neuron\n", + "I = 3\n", + "t = np.arange(0, 1000, dt)\n", + "Spike, Spike_time, V = Exact_Integrate_and_Fire(I, t)\n", + "\n", + "plot_IF(t, V, I, Spike_time)\n", + "ipd.Audio(V, rate=len(V))" + ] + }, + { + "cell_type": "markdown", + "id": "54d5af0e", + "metadata": { + "execution": {} + }, + "source": [ + "Manipulate the input into the LIF to see the impact of input on the firing pattern (rate).\n", + "\n", + "* What is the effect of $I$ on spiking?\n", + "* Is this biologically valid?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebf27a22", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " I=widgets.FloatSlider(3, min=2.0, max=4., step=.1,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(I):\n", + " Spike, Spike_time, V = Exact_Integrate_and_Fire(I, t)\n", + " plot_IF(t, V, I, Spike_time)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "GjwI3QF3ZLtY", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. As I increases, the number of spikes increases.\n", + " 2. No, as there is a limit to the number of spikes due to a refractory period,\n", + " which is not accounted for in this model.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ouprca8SipAs", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Input_on_spikes_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "c3cf4d7b", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 2.4 Firing Rate as a function of Input\n", + "\n", + "*Estimated timing to here from start of tutorial: 38 min*\n", + "\n", + "The firing frequency of a neuron plotted as a function of current is called an input-output curve (F–I curve). It is also known as a transfer function, which you came across in the previous tutorial. This function is one of the starting points for the rate model, which extends from modeling single neurons to the firing rate of a collection of neurons.\n", + "\n", + "By fitting this to a function, we can start to generalize the firing pattern of many neurons, which can be used to build rate models. This will be discussed later in Neuromatch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96bcf5e7", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown *Execture this cell to visualize the FI curve*\n", + "I_range = np.arange(2.0, 4.0, 0.1)\n", + "Spike_rate = np.ones(len(I_range))\n", + "\n", + "for i, I in enumerate(I_range):\n", + " Spike_rate[i], _, _ = Exact_Integrate_and_Fire(I, t)\n", + "\n", + "with plt.xkcd():\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(I_range,Spike_rate)\n", + " plt.xlabel('Input Current (nA)')\n", + " plt.ylabel('Spikes per Second (Hz)')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "58fdcbe8", + "metadata": { + "execution": {} + }, + "source": [ + "The LIF model is a very nice differential equation to start with in computational neuroscience as it has been used as a building block for many papers that simulate neuronal response.\n", + "\n", + "__Strengths of LIF model:__\n", + "+ Has an exact solution;\n", + "+ Easy to interpret;\n", + "+ Great to build network of neurons.\n", + "\n", + "__Weaknesses of the LIF model:__\n", + "- Spiking is a discontinuity;\n", + "- Abstraction from biology;\n", + "- Cannot generate different spiking patterns." + ] + }, + { + "cell_type": "markdown", + "id": "697364ff", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Summary\n", + "\n", + "*Estimated timing of tutorial: 45 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "_Jmpnq0mSihx", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 5: Summary\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'VzwLAW5p4ao'), ('Bilibili', 'BV1jV411x7t9')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "VNxbJDK5iuFA", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Summary_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "x0H0zhTNWlop", + "metadata": { + "execution": {} + }, + "source": [ + "In this tutorial, we have seen two differential equations, the population differential equations and the leaky integrate and fire model.\n", + "\n", + "We learned about the following:\n", + "* The motivation for differential equations.\n", + "* An intuitive relationship between the solution and the differential equation form.\n", + "* How different parameters of the differential equation impact the solution.\n", + "* The strengths and limitations of the simple differential equations." + ] + }, + { + "cell_type": "markdown", + "id": "eaec6994", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "## Links to Neuromatch Computational Neuroscience Days\n", + "\n", + "Differential equations turn up in a number of different Neuromatch days:\n", + "* The LIF model is discussed in more details in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", + "* Drift Diffusion model which is a differential equation for decision making is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", + "* Systems of differential equations are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", + "\n", + "---\n", + "## References\n", + "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", + "\n", + "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", + "\n", + "\n", + "## Bibliography\n", + "1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.\n", + "2. Strogatz, S. (2014). Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2nd edition\n", + "\n", + "### Supplemental Popular Reading List\n", + "1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.\n", + "2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.\n", + "\n", + "### Popular Podcast\n", + "1. Strogatz, S. (Host). (2020), Joy of X https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine\n" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "W0D4_Tutorial2", + "provenance": [], + "toc_visible": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb b/tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb index d54dffc..72fce05 100644 --- a/tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb +++ b/tutorials/W0D4_Calculus/W0D4_Tutorial3.ipynb @@ -1,2684 +1,2595 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "id": "08842220", - "metadata": { - "execution": {}, - "id": "08842220" - }, - "source": [ - "# Tutorial 3: Numerical Methods\n", - "\n", - "**Week 0, Day 3: Calculus**\n", - "\n", - "**Content creators:** John S Butler, Arvind Kumar with help from Harvey McCone\n", - "\n", - "**Content reviewers:** Swapnil Kumar, Matthew McCann\n", - "\n", - "**Production editors:** Matthew McCann, Ella Batty" - ] - }, - { - "cell_type": "markdown", - "id": "v0oVvJHEMouH", - "metadata": { - "execution": {}, - "id": "v0oVvJHEMouH" - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "id": "7d80998b", - "metadata": { - "execution": {}, - "id": "7d80998b" - }, - "source": [ - "---\n", - "# Tutorial Objectives\n", - "\n", - "*Estimated timing of tutorial: 70 minutes*\n", - "\n", - "While a great deal of neuroscience can be described by mathematics, a great deal of the mathematics used cannot be solved exactly. This might seem very odd that you can writing something in mathematics that cannot be immediately solved but that is the beauty and mystery of mathematics. To side step this issue we use Numerical Methods to estimate the solution.\n", - "\n", - "In this tutorial, we will look at the Euler method to estimate the solution of a few different differential equations: the population equation, the Leaky Integrate and Fire model and a simplified version of the Wilson-Cowan model which is a system of differential equations.\n", - "\n", - "**Steps:**\n", - "- Code the Euler estimate of the Population Equation;\n", - "- Investigate the impact of time step on the error of the numerical solution;\n", - "- Code the Euler estimate of the Leaky Integrate and Fire model for a constant input;\n", - "- Visualize and listen to the response of the integrate for different inputs;\n", - "- Apply the Euler method to estimate the solution of a system of differential equations." - ] - }, - { - "cell_type": "markdown", - "id": "bwMygnJgMUp7", - "metadata": { - "execution": {}, - "id": "bwMygnJgMUp7" - }, - "source": [ - "---\n", - "# Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "BrceoWcPfYlB", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "BrceoWcPfYlB" - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D4_T3\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5985801", - "metadata": { - "execution": {}, - "id": "a5985801" - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d40abcd8", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "d40abcd8" - }, - "outputs": [], - "source": [ - "# @title Figure Settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import IPython.display as ipd\n", - "from matplotlib import gridspec\n", - "import ipywidgets as widgets # interactive display\n", - "from ipywidgets import Label\n", - "%config InlineBackend.figure_format = 'retina'\n", - "# use NMA plot style\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", - "my_layout = widgets.Layout()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "hwhtoyMMi7qj", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "hwhtoyMMi7qj" - }, - "outputs": [], - "source": [ - "# @title Plotting Functions\n", - "\n", - "time = np.arange(0, 1, 0.01)\n", - "\n", - "def plot_slope(dt):\n", - " \"\"\"\n", - " Args:\n", - " dt : time-step\n", - " Returns:\n", - " A figure of an exponential, the slope of the exponential and the derivative exponential\n", - " \"\"\"\n", - "\n", - " t = np.arange(0, 5+0.1/2, 0.1)\n", - "\n", - " with plt.xkcd():\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " # Exponential\n", - " p = np.exp(0.3*t)\n", - " plt.plot(t, p, label='y')\n", - " # slope\n", - " plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1+dt))],':og',label=r'$\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$')\n", - " # derivative\n", - " plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1))+dt*0.3*np.exp(0.3*(1))],'-k',label=r'$\\frac{dy}{dt}$')\n", - " plt.legend()\n", - " plt.plot(1+dt, np.exp(0.3*(1+dt)), 'og')\n", - " plt.ylabel('y')\n", - " plt.xlabel('t')\n", - " plt.show()\n", - "\n", - "\n", - "\n", - "def plot_StepEuler(dt):\n", - " \"\"\"\n", - " Args:\n", - " dt : time-step\n", - " Returns:\n", - " A figure of one step of the Euler method for an exponential growth function\n", - " \"\"\"\n", - "\n", - " t=np.arange(0, 1 + dt + 0.1 / 2, 0.1)\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(6,4))\n", - " p=np.exp(0.3*t)\n", - " plt.plot(t,p)\n", - " plt.plot([1,],[np.exp(0.3*1)],'og',label='Known')\n", - " plt.plot([1,1+dt],[np.exp(0.3*1),np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)],':g',label=r'Euler')\n", - " plt.plot(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),'or',label=r'Estimate $p_1$')\n", - " plt.plot(1+dt,p[-1],'bo',label=r'Exact $p(t_1)$')\n", - " plt.vlines(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),p[-1],colors='r', linestyles='dashed',label=r'Error $e_1$')\n", - " plt.text(1+dt+0.1,(np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)+p[-1])/2,r'$e_1$')\n", - " plt.legend()\n", - " plt.ylabel('Population (millions)')\n", - " plt.xlabel('time(years)')\n", - " plt.show()\n", - "\n", - "def visualize_population_approx(t, p):\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')\n", - "\n", - " plt.plot(t, p,':o', label='Euler Estimate')\n", - " plt.vlines(t, p, np.exp(0.3*t),\n", - " colors='r', linestyles='dashed', label=r'Error $e_k$')\n", - "\n", - " plt.ylabel('Population (millions)')\n", - " plt.legend()\n", - " plt.xlabel('Time (years)')\n", - " plt.show()\n", - "\n", - "## LIF PLOT\n", - "def plot_IF(t, V, I, Spike_time):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " V : membrane Voltage\n", - " I : Input\n", - " Spike_time : Spike_times\n", - " Returns:\n", - " figure with three panels\n", - " top panel: Input as a function of time\n", - " middle panel: membrane potential as a function of time\n", - " bottom panel: Raster plot\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(12,4))\n", - " gs = gridspec.GridSpec(3, 1, height_ratios=[1, 4, 1])\n", - " # PLOT OF INPUT\n", - " plt.subplot(gs[0])\n", - " plt.ylabel(r'$I_e(nA)$')\n", - " plt.yticks(rotation=45)\n", - " plt.plot(t,I,'g')\n", - " #plt.ylim((2,4))\n", - " plt.xlim((-50,1000))\n", - " # PLOT OF ACTIVITY\n", - " plt.subplot(gs[1])\n", - " plt.plot(t,V,':')\n", - " plt.xlim((-50,1000))\n", - " plt.ylabel(r'$V(t)$(mV)')\n", - " # PLOT OF SPIKES\n", - " plt.subplot(gs[2])\n", - " plt.ylabel(r'Spike')\n", - " plt.yticks([])\n", - " plt.scatter(Spike_time,1*np.ones(len(Spike_time)), color=\"grey\", marker=\".\")\n", - " plt.xlim((-50,1000))\n", - " plt.xlabel('time(ms)')\n", - " plt.show()\n", - "\n", - "def plot_rErI(t, r_E, r_I):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " r_E : excitation rate\n", - " r_I : inhibition rate\n", - "\n", - " Returns:\n", - " figure of r_I and r_E as a function of time\n", - "\n", - " \"\"\"\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(6,4))\n", - " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", - " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", - " plt.xlabel('time(ms)')\n", - " plt.legend()\n", - " plt.ylabel('Firing Rate (Hz)')\n", - " plt.show()\n", - "\n", - "\n", - "def plot_rErI_Simple(t, r_E, r_I):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " r_E : excitation rate\n", - " r_I : inhibition rate\n", - "\n", - " Returns:\n", - " figure with two panels\n", - " left panel: r_I and r_E as a function of time\n", - " right panel: r_I as a function of r_E with Nullclines\n", - "\n", - " \"\"\"\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(12,4))\n", - " gs = gridspec.GridSpec(1, 2)\n", - " # LEFT PANEL\n", - " plt.subplot(gs[0])\n", - " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", - " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", - " plt.xlabel('time(ms)')\n", - " plt.legend()\n", - " plt.ylabel('Firing Rate (Hz)')\n", - " # RIGHT PANEL\n", - " plt.subplot(gs[1])\n", - " plt.plot(r_E,r_I,'k:')\n", - " plt.plot(r_E[0],r_I[0],'go')\n", - "\n", - " plt.hlines(0,np.min(r_E),np.max(r_E),linestyles=\"dashed\",color='b',label=r'$\\frac{d}{dt}r_E=0$')\n", - " plt.vlines(0,np.min(r_I),np.max(r_I),linestyles=\"dashed\",color='r',label=r'$\\frac{d}{dt}r_I=0$')\n", - "\n", - " plt.legend(loc='upper left')\n", - "\n", - " plt.xlabel(r'$r_E$')\n", - " plt.ylabel(r'$r_I$')\n", - " plt.show()\n", - "\n", - "def plot_rErI_Matrix(t, r_E, r_I, Null_rE, Null_rI):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " r_E : excitation firing rate\n", - " r_I : inhibition firing rate\n", - " Null_rE: Nullclines excitation firing rate\n", - " Null_rI: Nullclines inhibition firing rate\n", - " Returns:\n", - " figure with two panels\n", - " left panel: r_I and r_E as a function of time\n", - " right panel: r_I as a function of r_E with Nullclines\n", - "\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(12,4))\n", - " gs = gridspec.GridSpec(1, 2)\n", - " plt.subplot(gs[0])\n", - " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", - " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", - " plt.xlabel('time(ms)')\n", - " plt.ylabel('Firing Rate (Hz)')\n", - " plt.legend()\n", - " plt.subplot(gs[1])\n", - " plt.plot(r_E,r_I,'k:')\n", - " plt.plot(r_E[0],r_I[0],'go')\n", - "\n", - " plt.plot(r_E,Null_rE,':',color='b',label=r'$\\frac{d}{dt}r_E=0$')\n", - " plt.plot(r_E,Null_rI,':',color='r',label=r'$\\frac{d}{dt}r_I=0$')\n", - " plt.legend(loc='best')\n", - " plt.xlabel(r'$r_E$')\n", - " plt.ylabel(r'$r_I$')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "SXJPvBQzik6K", - "metadata": { - "execution": {}, - "id": "SXJPvBQzik6K" - }, - "source": [ - "---\n", - "# Section 1: Intro to the Euler method using the population differential equation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "DYKXWaR7iwl9", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "DYKXWaR7iwl9" - }, - "outputs": [], - "source": [ - "# @title Video 1: Intro to numerical methods for differential equations\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'NI8c80TA7IQ'), ('Bilibili', 'BV1gh411Y7gV')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "M3V5Kju3j7Ie", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "M3V5Kju3j7Ie" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Intro_to_numerical_methods_for_differential_equations_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "R9B9JKT-PvEn", - "metadata": { - "execution": {}, - "id": "R9B9JKT-PvEn" - }, - "source": [ - "## Section 1.1: Slope of line as approximation of derivative\n", - "\n", - "*Estimated timing to here from start of tutorial: 8 min*\n", - "\n", - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "The Euler method is one of the straight forward and elegant methods to approximate a differential. It was designed by [Leonhard Euler](https://en.wikipedia.org/wiki/Leonhard_Euler) (1707-1783).\n", - "Simply put we just replace the derivative in the differential equation by the formula for a line and re-arrange.\n", - "\n", - "The slope is the rate of change between two points. The formula for the slope of a line between the points $(t_0,y(t_0))$ and $(t_1,y(t_1))$ is given by:\n", - "$$ m=\\frac{y(t_1)-y(t_0)}{t_1-t_0}, $$\n", - "which can be written as\n", - "$$ m=\\frac{y_1-y_0}{t_1-t_0}, $$\n", - "or as\n", - "$$ m=\\frac{\\Delta y_0}{\\Delta t_0}, $$\n", - "where $\\Delta y_0=y_1-y_0$ and $\\Delta t_0=t_1-t_0$ or in words as\n", - "$$ m=\\frac{\\text{ Change in y} }{\\text{Change in t}}. $$\n", - "The slope can be used as an approximation of the derivative such that\n", - "$$ \\frac{d}{dt}y(t)\\approx \\frac{y(t_0+\\Delta t)-y(t_0)}{t_0+\\Delta t-t_0}=\\frac{y(t_0+dt)-y(t_0)}{\\Delta t}$$\n", - "where $\\Delta t$ is a time-step.\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "b667c36a", - "metadata": { - "execution": {}, - "id": "b667c36a" - }, - "source": [ - "### Interactive Demo 1.1: Slope of a Line\n", - "\n", - "The plot below shows a function $y(t)$ in blue, its exact derivative $ \\frac{d}{dt}y(t)$ at $t_0=1$ in black. The approximate derivative is calculated using the slope formula $=\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$ for different time-steps sizes $\\Delta t$ in green.\n", - "\n", - "Interact with the widget to see how time-step impacts the slope's accuracy, which is the derivative's estimate.\n", - "- How does the size of $\\Delta t$ affect the approximation?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ea4a4f78", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ea4a4f78" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - "\n", - " dt=widgets.FloatSlider(1, min=0., max=4., step=.1,\n", - " layout=my_layout)\n", - "\n", - ")\n", - "\n", - "def Pop_widget(dt):\n", - " plot_slope(dt)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "gTgFRiMhc1qm", - "metadata": { - "execution": {}, - "id": "gTgFRiMhc1qm" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " The larger the time-step $dt$, the worse job the formula of the slope of a\n", - " line does to approximate the derivative.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "FZ1SZPQwj9H4", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "FZ1SZPQwj9H4" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Slope_of_a_line_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "8f92ce83", - "metadata": { - "execution": {}, - "id": "8f92ce83" - }, - "source": [ - "## Section 1.2: Euler error for a single step\n", - "\n", - "*Estimated timing to here from start of tutorial: 12 min*\n", - "\n", - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "Linking with the previous tutorial, we will use the population differential equation to get an intuitive feel of using the Euler method to approximate the solution of a differential equation, as it has an exact solution with no discontinuities.\n", - "\n", - "The population differential equation is given by\n", - "\\begin{align*}\n", - "\\\\\n", - "\\frac{d}{dt}\\,p(t) &= \\alpha p(t)\\\\\\\\\n", - "p(0)&=p_0 \\quad \\text{Initial Condition}\n", - "\\end{align*}\n", - "\n", - "where $p(t)$ is the population at time $t$ and $\\alpha$ is a parameter representing birth rate. The exact solution of the differential equation is\n", - "$$ p(t)=p_0e^{\\alpha t}.$$\n", - "\n", - "To numerically estimate the population differential equation we replace the derivate with the slope of the line to get the discrete (not continuous) equation:\n", - "\n", - "\\begin{align*}\n", - "\\\\\n", - "\\frac{p_1-p_0}{t_1-t_0} &= \\alpha p_0\\\\\\\\\n", - "p(0)&=p_0 \\quad \\text{Initial Condition}\n", - "\\end{align*}\n", - "\n", - "where $p_1$ is the estimate of $p(t_1)$. Let $\\Delta t=t_1-t_0$ be the time-step and re-arrange the equation gives\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{p_1}&=\\color{green}{p_0}+\\color{blue}{\\Delta t} (\\color{blue}{\\alpha} \\color{green}{p_0})\n", - "\\end{align*}\n", - "\n", - "where $\\color{red}{p_1}$ is the unknown future, $\\color{green}{p_0}$ is the known current population, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter and $\\color{blue}{\\alpha}$ is the given birth rate parameter.\n", - "Another way to read the re-arranged discrete equation is:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{\\text{\"Future Population\"}}&=\\color{green}{\\text{ \"Current Population\"}}+\\color{blue}{\\text{ time-step}} \\times (\\color{blue}{\\text{ \"Birth rate}}\\times \\color{green}{\\text{ Current Population\"}}).\n", - "\\end{align*}\n", - "\n", - "So pretty much, we can use the current population and add a bit of the dynamics of the differential equation to predict the future. We will make millions... But wait there is always an error.\n", - "\n", - "The solution of the Euler method $p_1$ is an estimate of the exact solution $p(t_1)$ at $t_1$ which means there is a bit of error $e_1$ which gives the equation\n", - "\\begin{align*}\n", - "p(t_1)&=p_1+e_1\\\\\\\\\n", - "\\text{Rearranging}\\\\\\\\\n", - "e_1&=p(t_1)-p_1,\\\\\n", - "\\text{Error}&=\\text{Exact-Estimate}.\n", - "\\end{align*}\n", - "\n", - "Most of the time we do not know the exact answer $p(t_1)$ and hence the size of the error $e_1$ but for the population equation we have the exact solution $ p(t)=p_0e^{\\alpha}.$\n", - "\n", - "This means we can explore what the error looks like.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "vGxg-2k6k8hM", - "metadata": { - "execution": {}, - "id": "vGxg-2k6k8hM" - }, - "source": [ - "### Interactive Demo 1.2: Euler error for a single step\n", - "\n", - "Interact with the widget to see how the time-step $\\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.\n", - "\n", - "1. What happens to the estimate $p_1$ as the time-step $\\Delta t$ increases?\n", - "2. Is there a relationship between the size of $\\Delta t$ and $e_1$?\n", - "3. How would you improve the error $e_1$?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e72c600b", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "e72c600b" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " dt=widgets.FloatSlider(1.5, min=0., max=4., step=.1,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(dt):\n", - " plot_StepEuler(dt)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "EoA90TrDdVWv", - "metadata": { - "execution": {}, - "id": "EoA90TrDdVWv" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. The larger the time-step $\\Delta t$ the more the estimate $p_1$ deviates\n", - " from the exact $p(t_1)$.\n", - "\n", - " 2. There is a linear relationship between $\\Delta t$ and $e_1$: double the time-step double the error.\n", - "\n", - " 3. Make more shorter time-steps\n", - "\"\"\";" - ] - }, - { - "cell_type": "markdown", - "id": "e78fc157", - "metadata": { - "execution": {}, - "id": "e78fc157" - }, - "source": [ - "The error $e_1$ from one time-step is known as the __local error__. For the Euler method, the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $\\mathcal{O}(\\Delta t)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "qXS6Fkkkj-xD", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "qXS6Fkkkj-xD" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Euler_error_of_single_step_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "1yr4hKV7R0JJ", - "metadata": { - "execution": {}, - "id": "1yr4hKV7R0JJ" - }, - "source": [ - "## Section 1.3: Taking more steps\n", - "\n", - "*Estimated timing to here from start of tutorial: 16 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "MZcqv_DoRsWJ", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "MZcqv_DoRsWJ" - }, - "outputs": [], - "source": [ - "# @title Video 2: Taking more steps\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'cGsXHllGMVo'), ('Bilibili', 'BV135411T7QF')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "W8HbORAlkAWj", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "W8HbORAlkAWj" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Taking_more_steps_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "972a55c4", - "metadata": { - "execution": {}, - "id": "972a55c4" - }, - "source": [ - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "In the above exercise we saw that by increasing the time-step $\\Delta t$ the error between the estimate $p_1$ and the exact $p(t_1)$ increased. The largest error in the example above was for $\\Delta t=4$, meaning the first time point was at 1 year and the second was at 5 years (as 5 - 1 = 4).\n", - "\n", - "To decrease the error, we can divide the interval $[1, 5]$ into more steps using a smaller $\\Delta t$.\n", - "\n", - "In the plot below we use $\\Delta t=1$, which divides the interval into four segments\n", - "$$n=\\frac{5-1}{1}=4,$$\n", - "giving\n", - "$$ t_0=t[0]=1, \\ t_1=t[1]=2, \\ t_2=t[2]=3, \\ t_3=t[3]=4 \\ \\text{ and } t_4=t[4]=5. $$\n", - "This can be written as\n", - "$$ t[k]=1+k\\times1, \\text{ for } k=0,\\cdots 4, $$\n", - "and more generally as\n", - "$$ t[k]=t[k]+k\\times \\Delta t, \\text{ for } k=0,\\cdots n, $$\n", - "where $n$ is the number of steps.\n", - "\n", - "Using the Euler method, the continuous population differential equation is written as a series of discrete difference equations of the form:\n", - "\\begin{align*}\n", - "\\color{red}{p[k+1]}&=\\color{green}{p[k]}+\\color{blue}{\\Delta t} (\\color{blue}{\\alpha} \\color{green}{p[k]})\\\\\n", - "&\\text{for } k=0,1,\\cdots n-1\n", - "\\end{align*}\n", - "where $\\color{red}{p[k+1]}$ is the unknown estimate of the future population at $t[k+1]$, $\\color{green}{p[k]}$ is the known current population estimate at $t_k$, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter and $\\color{blue}{\\alpha}$ is the given birth rate parameter.\n", - "\n", - "\n", - "The Euler method can be applied to all first order differential equations of the form\n", - "\\begin{align*}\n", - "\\frac{d}{dt}y(t)&=f(t,y(t)),\\\\\n", - "y(t_{0})&=y_0,\\\\\n", - "\\end{align*}\n", - "on an interval $[a,b]$.\n", - "\n", - "Using the Euler method all differential equation can be written as discrete difference equations:\n", - "\\begin{align*}\n", - "\\frac{\\color{red}{y[k+1]}-\\color{green}{y[k]}}{\\color{blue}{\\Delta t}}&=f(\\color{blue}{t[k]},\\color{green}{y[k]}),\\\\\n", - "\\text{Re-arranging}\\\\\n", - "\\color{red}{y[k+1]}&=\\color{green}{y[k]}+\\color{blue}{\\Delta t}f(\\color{blue}{t[k]},\\color{green}{y[k]}),\\\\\n", - "&\\text{ for } k=0, \\cdots n-1,\\\\\n", - "y[0]&=\\color{green}{y_0},\\\\\n", - "\\end{align*}\n", - "where $\\color{red}{y[k+1]}$ is the unknown estimate at $t[k+1]$, $\\color{green}{y[k]}$ is the known at $t_k$, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter, $\\color{blue}{t[k]}$ is the time point and $f$ is the right hand side of the differential equation.\n", - "The discrete time steps are:\n", - "\\begin{align*}\n", - "\\color{blue}{t[k]}&=\\color{blue}{t[0]}+\\color{blue}{k}\\color{blue}{\\Delta t},\\\\\n", - "n&=\\frac{b-a}{\\Delta t}\\\\\n", - "&\\text{ for } k=0, \\cdots n.\\\\\n", - "\\end{align*}\n", - "Once again this can be simply put into words:\n", - "\\begin{align*}\n", - "\\color{red}{\\text{ \"Future\" }}&=\\color{green}{\\text{ \"Current Info\" }}+\\color{blue}{\\text{ time-step } }\\times\\text{ \"Dynamics of the system which is a function of } \\color{blue}{ \\text{ time }} \\text{ and }\\color{green}{ \\text{ Current Info.\" }}\n", - "\\end{align*}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d84f02ed", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "d84f02ed" - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to visualize time steps*\n", - "dt =1\n", - "t =np.arange(1, 5+dt/2, dt) # Time from 1 to 5 years in 0.1 steps\n", - "with plt.xkcd():\n", - " fig = plt.figure(figsize=(8, 2))\n", - " plt.plot(t, 0*t, ':o')\n", - " plt.plot(t[0] ,0*t[0],':go',label='Initial Condition')\n", - " plt.text(t[0]-0.1, 0*t[0]-0.03, r'$t_0$')\n", - " plt.text(t[1]-0.1, 0*t[0]-0.03, r'$t_1$')\n", - " plt.text(t[2]-0.1, 0*t[0]-0.03, r'$t_2$')\n", - " plt.text(t[3]-0.1, 0*t[0]-0.03, r'$t_3$')\n", - " plt.text(t[4]-0.1, 0*t[0]-0.03,r'$t_4$')\n", - " plt.text(t[0]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", - " plt.text(t[1]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", - " plt.text(t[2]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", - " plt.text(t[3]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", - " plt.yticks([])#plt.ylabel('Population (millions)')\n", - " plt.xlabel('time(years)')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "279ea532", - "metadata": { - "execution": {}, - "id": "279ea532" - }, - "source": [ - "### Coding Exercise 1.3: Step, step, step\n", - "\n", - "Given the population differential equation:\n", - "\\begin{align*}\n", - "\\frac{d}{dt}\\,p(t) &= 0.3 p(t),\\\\\n", - "\\end{align*}\n", - "\n", - "and the initial condition:\n", - "\n", - "\\begin{align*}\n", - "p(t_0=1)&=e^{0.3},\n", - "\\end{align*}\n", - "\n", - "code the difference equation:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{p[k+1]}&=\\color{green}{p[k]}+\\color{blue}{\\Delta t} (\\color{blue}{0.3} \\color{green}{p[k]}),\\\\\n", - "\\color{green}{p[0]}&=e^{0.3},\\quad \\text{Initial Condition,}\\\\\n", - "&\\text{for } k=0,1,\\cdots 4,\\\\\n", - "\\end{align*}\n", - "\n", - "to estimate the population on the interval $[1,5]$ with a time-step $\\Delta t=1$, denoted by `dt` in code." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b872e66", - "metadata": { - "execution": {}, - "id": "8b872e66" - }, - "outputs": [], - "source": [ - "# Time step\n", - "dt = 1\n", - "\n", - "# Make time range from 1 to 5 years with step size dt\n", - "t = np.arange(1, 5+dt/2, dt)\n", - "\n", - "# Get number of steps\n", - "n = len(t)\n", - "\n", - "# Initialize p array\n", - "p = np.zeros(n)\n", - "p[0] = np.exp(0.3*t[0]) # initial condition\n", - "\n", - "# Loop over steps\n", - "for k in range(n-1):\n", - "\n", - " ########################################################################\n", - " ## TODO for students\n", - " ## Complete line of code and remove\n", - " raise NotImplementedError(\"Student exercise: calculate the population step for each time point\")\n", - " ########################################################################\n", - "\n", - " # Calculate the population step\n", - " p[k+1] = ...\n", - "\n", - "# Visualize\n", - "visualize_population_approx(t, p)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "LHAKHn_5UzLQ", - "metadata": { - "execution": {}, - "id": "LHAKHn_5UzLQ" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Time step\n", - "dt = 1\n", - "\n", - "# Make time range from 1 to 5 years with step size dt\n", - "t = np.arange(1, 5+dt/2, dt)\n", - "\n", - "# Get number of steps\n", - "n = len(t)\n", - "\n", - "# Initialize p array\n", - "p = np.zeros(n)\n", - "p[0] = np.exp(0.3*t[0]) # initial condition\n", - "\n", - "# Loop over steps\n", - "for k in range(n-1):\n", - "\n", - " # Calculate the population step\n", - " p[k+1] = p[k] + dt * 0.3 * p[k]\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " visualize_population_approx(t, p)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "Yvl3OBBPkCQr", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "Yvl3OBBPkCQr" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Step_step_step_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "id": "d9301447", - "metadata": { - "execution": {}, - "id": "d9301447" - }, - "source": [ - "The error is smaller for 4-time steps than taking one large time step from 1 to 5 but does note that the error is increasing for each step. This is known as __global error__ so the further in time you want to predict, the larger the error.\n", - "\n", - "You can read the theorems [here](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)." - ] - }, - { - "cell_type": "markdown", - "id": "b419bf50", - "metadata": { - "execution": {}, - "id": "b419bf50" - }, - "source": [ - "---\n", - "# Section 2: Euler method for the leaky integrate and fire\n", - "\n", - "\n", - "*Estimated timing to here from start of tutorial: 26 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "JOA7EzauxIV6", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "JOA7EzauxIV6" - }, - "outputs": [], - "source": [ - "# @title Video 3: Leaky integrate and fire\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'T85OcgY7xjo'), ('Bilibili', 'BV1k5411T7by')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "GeAbFXiSkDEd", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "GeAbFXiSkDEd" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Leaky_integrate_and_fire_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "Zjro4hXwxU8O", - "metadata": { - "execution": {}, - "id": "Zjro4hXwxU8O" - }, - "source": [ - "
\n", - " Click here for text recap of video \n", - "\n", - "The Leaky Integrate and Fire (LIF) differential equation is:\n", - "\n", - "\\begin{align}\n", - "\\frac{dV}{dt} = \\frac{-(V-E_L) + R_mI(t)}{\\tau_m}\\,\n", - "\\end{align}\n", - "\n", - "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is leak resistance and $I(t)$ is the external input current.\n", - "\n", - "The solution of the LIF can be estimated by applying the Euler method to give the difference equation:\n", - "\n", - "\\begin{align}\n", - "\\frac{\\color{red}{V[k+1]}-\\color{green}{V[k]}}{\\color{blue}{\\Delta t}}=\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", - "\\end{align}\n", - "where $V[k]$ is the estimate of the membrane potential at time point $t[k]$.\n", - "Re-arranging the equation such that all the known terms are on the right gives:\n", - "\n", - "\\begin{align}\n", - "\\color{red}{V[k+1]}=\\color{green}{V[k]}+\\color{blue}{\\Delta t}\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", - "\\text{for } k=0\\cdots n-1,\n", - "\\end{align}\n", - "\n", - "where $\\color{red}{V[k+1]}$ is the unknown membrane potential at $t[k+1]$, $\\color{green}{V[k]} $ is known membrane potential, $\\color{blue}{E_L}$, $\\color{blue}{R_m}$ and $\\color{blue}{\\tau_m}$ are known parameters, $\\color{blue}{\\Delta t}$ is a chosen time-step and $I(t_k)$ is a function for an external input current." - ] - }, - { - "cell_type": "markdown", - "id": "1efb9d89", - "metadata": { - "execution": {}, - "id": "1efb9d89" - }, - "source": [ - "## Coding Exercise 2: LIF and Euler\n", - "Code the difference equation for the LIF:\n", - "\n", - "\\begin{align}\n", - "\\color{red}{V[k+1]}=\\color{green}{V[k]}+\\color{blue}{\\Delta t}\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", - "\\text{for } k=0\\cdots n-1,\n", - "\\end{align}\n", - "\n", - "with the given parameters set as:\n", - "* `V_reset = -75,`\n", - "* `E_L = -75,`\n", - "* `tau_m = 10,`\n", - "* `R_m = 10.`\n", - "\n", - "We will then visualize the result.\n", - "The figure has three panels:\n", - "* the top panel is the sinusoidal input, $I$,\n", - "* the middle panel is the estimate membrane potential $V_k$. To illustrate a spike, $V_k$ is set to $0$ and then reset,\n", - "* the bottom panel is the raster plot with each dot indicating a spike." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4e7d29bb", - "metadata": { - "execution": {}, - "id": "4e7d29bb" - }, - "outputs": [], - "source": [ - "def Euler_Integrate_and_Fire(I, time, dt):\n", - " \"\"\"\n", - " Args:\n", - " I: Input\n", - " time: time\n", - " dt: time-step\n", - " Returns:\n", - " Spike: Spike count\n", - " Spike_time: Spike times\n", - " V: membrane potential esitmated by the Euler method\n", - " \"\"\"\n", - "\n", - " Spike = 0\n", - " tau_m = 10\n", - " R_m = 10\n", - " t_isi = 0\n", - " V_reset = E_L = -75\n", - " n = len(time)\n", - " V = V_reset * np.ones(n)\n", - " V_th = -50\n", - " Spike_time = []\n", - "\n", - " for k in range(n-1):\n", - " #######################################################################\n", - " ## TODO for students: calculate the estimate solution of V at t[i+1]\n", - " ## Complete line of codes for dV and remove\n", - " ## Run the code in Section 5.1 or 5.2 to see the output!\n", - " raise NotImplementedError(\"Student exercise: calculate the estimate solution of V at t[i+1]\")\n", - " ########################################################################\n", - "\n", - " dV = ...\n", - " V[k+1] = V[k] + dt*dV\n", - "\n", - " # Discontinuity for Spike\n", - " if V[k] > V_th:\n", - " V[k] = 0\n", - " V[k+1] = V_reset\n", - " t_isi = time[k]\n", - " Spike = Spike + 1\n", - " Spike_time = np.append(Spike_time, time[k])\n", - "\n", - " return Spike, Spike_time, V\n", - "\n", - "# Set up time step and current\n", - "dt = 1\n", - "t = np.arange(0, 1000, dt)\n", - "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", - "\n", - "# Model integrate and fire neuron\n", - "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", - "\n", - "# Visualize\n", - "plot_IF(t, V,I,Spike_time)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "55785e26", - "metadata": { - "execution": {}, - "id": "55785e26" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "def Euler_Integrate_and_Fire(I, time, dt):\n", - " \"\"\"\n", - " Args:\n", - " I: Input\n", - " time: time\n", - " dt: time-step\n", - " Returns:\n", - " Spike: Spike count\n", - " Spike_time: Spike times\n", - " V: membrane potential esitmated by the Euler method\n", - " \"\"\"\n", - "\n", - " Spike = 0\n", - " tau_m = 10\n", - " R_m = 10\n", - " t_isi = 0\n", - " V_reset = E_L = -75\n", - " n = len(time)\n", - " V = V_reset * np.ones(n)\n", - " V_th = -50\n", - " Spike_time = []\n", - "\n", - " for k in range(n-1):\n", - " dV = (-(V[k] - E_L) + R_m*I[k]) / tau_m\n", - " V[k+1] = V[k] + dt*dV\n", - "\n", - " # Discontinuity for Spike\n", - " if V[k] > V_th:\n", - " V[k] = 0\n", - " V[k+1] = V_reset\n", - " t_isi = time[k]\n", - " Spike = Spike + 1\n", - " Spike_time = np.append(Spike_time, time[k])\n", - "\n", - " return Spike, Spike_time, V\n", - "\n", - "# Set up time step and current\n", - "dt = 1\n", - "t = np.arange(0, 1000, dt)\n", - "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", - "\n", - "# Model integrate and fire neuron\n", - "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_IF(t, V,I,Spike_time)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ggAQjBCekEm7", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ggAQjBCekEm7" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_LIF_and_Euler_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "id": "0d322dc9", - "metadata": { - "execution": {}, - "id": "0d322dc9" - }, - "source": [ - "As electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the LIF neuron below. Note: this does not work on all browsers so just move on if you can't hear the audio." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cd8f60c2", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "cd8f60c2" - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to visualize the LIF for sinusoidal input*\n", - "\n", - "dt = 1\n", - "t = np.arange(0, 1000, dt)\n", - "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", - "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", - "\n", - "plot_IF(t, V,I,Spike_time)\n", - "plt.show()\n", - "ipd.Audio(V, rate=1000/dt)" - ] - }, - { - "cell_type": "markdown", - "id": "L5o2liqz3bxi", - "metadata": { - "execution": {}, - "id": "L5o2liqz3bxi" - }, - "source": [ - "---\n", - "# Section 3: Systems of differential equations\n", - "\n", - "\n", - "*Estimated timing to here from start of tutorial: 34 min*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "RkM3-RucX2c-", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "RkM3-RucX2c-" - }, - "outputs": [], - "source": [ - "# @title Video 4: Systems of differential equations\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'A3Puozl9nEs'), ('Bilibili', 'BV1XV411s76a')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "x5wo7eFJkGBS", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "x5wo7eFJkGBS" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Systems_of_differential_equations_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "dTy23IBPZxYG", - "metadata": { - "execution": {}, - "id": "dTy23IBPZxYG" - }, - "source": [ - "## Section 3.1: Using Euler to approximate a simple system\n", - "\n", - "*Estimated timing to here from start of tutorial: 40 min*" - ] - }, - { - "cell_type": "markdown", - "id": "85df22b9", - "metadata": { - "execution": {}, - "id": "85df22b9" - }, - "source": [ - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "To get to grips with solving a system of differential equations using the Euler method, we will simplify the Wilson Cowan model, a set of equations that will be explored in more detail in the Dynamical Systems day.\n", - "Looking at systems of differential equations will also allow us to introduce the concept of a phase-plane plot which is a method of investigating how different processes interact.\n", - "\n", - "In the previous section we looked at the LIF model for a single neuron. We now model a collection of neurons using a differential equation which describes the firing rate of a population of neurons.\n", - "We will model the firing rate $r$ of two types of populations of neurons which interact, the excitation population firing rate $r_E$ and inhibition population firing rate $r_I$. These firing rates of neurons regulate each other by weighted connections $w$. The directed graph below illustrates this.\n", - "\n", - "Our system of differential equations is a linear version of the Wilson Cowan model. Consider the equations,\n", - "\n", - "\\begin{align}\n", - "\\tau_E \\frac{dr_E}{dt} &= w_{EE}r_E+w_{EI}r_I, \\\\\n", - "\\tau_I \\frac{dr_I}{dt} &= w_{IE}r_E+w_{II}r_I\n", - "\\end{align}\n", - "\n", - "$r_E(t)$ represents the average activation (or firing rate) of the excitatory population at time $t$, and $r_I(t)$ the activation (or firing rate) of the inhibitory population. The parameters $\\tau_E$ and $\\tau_I$ control the timescales of the dynamics of each population. Connection strengths are given by: $w_{EE}$ (E $\\rightarrow$ E), $w_{EI}$ (I $\\rightarrow$ E), $w_{IE}$ (E $\\rightarrow$ I), and $w_{II}$ (I $\\rightarrow$ I). The terms $w_{EI}$ and $w_{IE}$ represent connections from inhibitory to excitatory population and vice versa, respectively.\n", - "\n", - "To start we will further simplify the linear system of equations by setting $w_{EE}$ and $w_{II}$ to zero, we now have the equations\n", - "\n", - "\\begin{align}\n", - "\\tau_E \\frac{dr_E}{dt} &= w_{EI}r_I, \\\\\n", - "\\tau_I \\frac{dr_I}{dt} &= w_{IE}r_E, \\qquad (1)\n", - "\\end{align}\n", - "\n", - "where $\\tau_E=100$ and $\\tau_I=120$, no internal connection $w_{EE}=w_{II}=0$, and $w_{EI}=-1$ and $w_{IE}=1$,\n", - "with the initial conditions\n", - "\n", - "\\begin{align}\n", - "r_E(0)=30, \\\\\n", - "r_I(0)=20.\n", - "\\end{align}\n", - "\n", - "The solution can be approximated using the Euler method such that we have the difference equations:\n", - "\n", - "\\begin{align*}\n", - "\\frac{\\color{red}{r_E[k+1]}-\\color{green}{r_E[k]}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\frac{\\color{red}{r_I[k+1]}-\\color{green}{r_I[k]}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "\\end{align*}\n", - "where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t[k])$ and inhibition population $r_I(t[k])$ and $\\Delta t$ is the time-step.\n", - "\n", - "Re-arranging the equation such that all the known terms are on the right gives:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{r_E[k+1]}&=\\color{green}{r_E[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\color{red}{r_I[k+1]}&=\\color{green}{r_I[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "&\\text{ for } k=0, \\cdots , n-1,\\\\\\\\\n", - "r_E[0]&=30,\\\\\n", - "r_I[0]&=20.\\\\\n", - "\\end{align*}\n", - "\n", - "where $\\color{red}{r_E[k+1]}$ and $\\color{red}{r_I[k+1]}$ are unknown, $\\color{green}{r_E[k]} $ and $\\color{green}{r_I[k]} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$ and $\\color{blue}{\\tau_E}=100ms$ and $\\color{blue}{\\tau_I}=120ms$ are known parameters and $\\color{blue}{\\Delta t}=0.01ms$ is a chosen time-step.\n", - "
\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "0xhGLDhgdq1P", - "metadata": { - "execution": {}, - "id": "0xhGLDhgdq1P" - }, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "067b5f6e", - "metadata": { - "execution": {}, - "id": "067b5f6e" - }, - "source": [ - "### Coding Exercise 3.1: Euler on a Simple System\n", - "\n", - "Our difference equations are:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{r_E[k+1]}&=\\color{green}{r_E[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\color{red}{r_I[k+1]}&=\\color{green}{r_I[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "&\\text{ for } k=0, \\cdots , n-1,\\\\\\\\\n", - "r_E[0]&=30,\\\\\n", - "r_I[0]&=20.\\\\\n", - "\\end{align*}\n", - "\n", - "where $\\color{red}{r_E[k+1]}$ and $\\color{red}{r_I[k+1]}$ are unknown, $\\color{green}{r_E[k]} $ and $\\color{green}{r_I[k]} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$ and $\\color{blue}{\\tau_E}=100ms$ and $\\color{blue}{\\tau_I}=120ms$ are known parameters and $\\color{blue}{\\Delta t}=0.01ms$ is a chosen time-step.\n", - "__Code the difference equations to estimate $r_{E}$ and $r_{I}$__.\n", - "\n", - "Note that the equations have to estimated in the same `for` loop as they depend on each other." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e1514105", - "metadata": { - "execution": {}, - "id": "e1514105" - }, - "outputs": [], - "source": [ - "def Euler_Simple_Linear_System(t, dt):\n", - " \"\"\"\n", - " Args:\n", - " time: time\n", - " dt : time-step\n", - " Returns:\n", - " r_E : Excitation Firing Rate\n", - " r_I : Inhibition Firing Rate\n", - "\n", - " \"\"\"\n", - "\n", - " # Set up parameters\n", - " tau_E = 100\n", - " tau_I = 120\n", - " n = len(t)\n", - " r_I = np.zeros(n)\n", - " r_I[0] = 20\n", - " r_E = np.zeros(n)\n", - " r_E[0] = 30\n", - "\n", - " #######################################################################\n", - " ## TODO for students: calculate the estimate solutions of r_E and r_I at t[i+1]\n", - " ## Complete line of codes for dr_E and dr_I and remove\n", - " raise NotImplementedError(\"Student exercise: calculate the estimate solutions of r_E and r_I at t[i+1]\")\n", - " ########################################################################\n", - "\n", - " # Loop over time steps\n", - " for k in range(n-1):\n", - "\n", - " # Estimate r_e\n", - " dr_E = ...\n", - " r_E[k+1] = r_E[k] + dt*dr_E\n", - "\n", - " # Estimate r_i\n", - " dr_I = ...\n", - " r_I[k+1] = r_I[k] + dt*dr_I\n", - "\n", - " return r_E, r_I\n", - "\n", - "# Set up dt, t\n", - "dt = 0.1 # time-step\n", - "t = np.arange(0, 1000, dt)\n", - "\n", - "# Run Euler method\n", - "r_E, r_I = Euler_Simple_Linear_System(t, dt)\n", - "\n", - "# Visualize\n", - "plot_rErI(t, r_E, r_I)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "JnLfmtc5ZWXz", - "metadata": { - "execution": {}, - "id": "JnLfmtc5ZWXz" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "def Euler_Simple_Linear_System(t, dt):\n", - " \"\"\"\n", - " Args:\n", - " time: time\n", - " dt : time-step\n", - " Returns:\n", - " r_E : Excitation Firing Rate\n", - " r_I : Inhibition Firing Rate\n", - "\n", - " \"\"\"\n", - "\n", - " # Set up parameters\n", - " tau_E = 100\n", - " tau_I = 120\n", - " n = len(t)\n", - " r_I = np.zeros(n)\n", - " r_I[0] = 20\n", - " r_E = np.zeros(n)\n", - " r_E[0] = 30\n", - "\n", - " # Loop over time steps\n", - " for k in range(n-1):\n", - "\n", - " # Estimate r_e\n", - " dr_E = -r_I[k]/tau_E\n", - " r_E[k+1] = r_E[k] + dt*dr_E\n", - "\n", - " # Estimate r_i\n", - " dr_I = r_E[k]/tau_I\n", - " r_I[k+1] = r_I[k] + dt*dr_I\n", - "\n", - " return r_E, r_I\n", - "\n", - "# Set up dt, t\n", - "dt = 0.1 # time-step\n", - "t = np.arange(0, 1000, dt)\n", - "\n", - "# Run Euler method\n", - "r_E, r_I = Euler_Simple_Linear_System(t, dt)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_rErI(t, r_E, r_I)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ZGYMiHlAkHbV", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ZGYMiHlAkHbV" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Euler_on_a_simple_system_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "id": "Fk-EvkAOImKR", - "metadata": { - "execution": {}, - "id": "Fk-EvkAOImKR" - }, - "source": [ - "### Think! 3.1: Simple Euler solution to the Wilson Cowan model\n", - "\n", - "1. Is the simulation biologically plausible?\n", - "2. What is the effect of combined excitation and inhibition?\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "i3L1OGeUgZEe", - "metadata": { - "execution": {}, - "id": "i3L1OGeUgZEe" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. The simulation is not biologically plausible as there are negative firing\n", - " rates but a mathematician could just say that it is firing rate relative to\n", - " baseline.\n", - " 2. Excitation and inhibition creates an oscillation.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "gyCVFYRHkIMH", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "gyCVFYRHkIMH" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Simple_Euler_solution_to_the_Wilson_Cowan_model_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "id": "Cw-R4Yd4QCTH", - "metadata": { - "execution": {}, - "id": "Cw-R4Yd4QCTH" - }, - "source": [ - "## Section 3.2: Phase Plane Plot and Nullcline\n", - "\n", - "\n", - "*Estimated timing to here from start of tutorial: 50 min*\n", - "\n", - "
\n", - " Click here for text recap of relevant part of video \n", - "\n", - "When there are two differential equations describing the interaction of two processes like excitation $r_{E}$ and inhibition $r_{I}$ that are dependent on each other they can be plotted as a function of each other, which is known as a phase plane plot. The phase plane plot can give insight give insight into the state of the system but more about that later in Neuromatch Academy.\n", - "\n", - "In the animated figure below, the panel of the left shows the excitation firing rate $r_E$ and the inhibition firing rate $r_I$ as a function of time. The panel on the right hand side is the phase plane plot which shows the inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$.\n", - "\n", - "An addition to the phase plane plot are the \"nullcline\". These lines plot when the rate of change $\\frac{d}{dt}$ of the variables is equal to $0$. We saw a variation of this for a single differential equation in the differential equation tutorial.\n", - "\n", - "As we have two differential equations we set $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0$ which gives the equations,\n", - "\n", - "\\begin{align}\n", - "0&= w_{EI}r_I, \\\\\n", - "0&= w_{IE}r_E,\\\\\n", - "\\end{align}\n", - "\n", - "these are a unique example as they are a vertical and horizontal line. Where the lines cross is the stable point which the $r_E(t)$ excitatory population and $r_I(t)$ the inhibitory population orbit around." - ] - }, - { - "cell_type": "markdown", - "id": "2ghDuf-YeG6z", - "metadata": { - "execution": {}, - "id": "2ghDuf-YeG6z" - }, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "98f3c696", - "metadata": { - "execution": {}, - "id": "98f3c696" - }, - "source": [ - "### Think! 6.1: Discuss the Plots\n", - "\n", - "1. Which representation is more intuitive (and useful), the time plot or the phase plane plot?\n", - "2. Why do we only see one circle?\n", - "3. What do the quadrants represent?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "036d3fb5", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "036d3fb5" - }, - "outputs": [], - "source": [ - "# @markdown Execute the code to plot the solution to the system\n", - "plot_rErI_Simple(t, r_E, r_I)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "Sfa1foAYhB13", - "metadata": { - "execution": {}, - "id": "Sfa1foAYhB13" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - " 1. Personal preference: both have their benefits\n", - " 2. The solution is stable.\n", - " 3. The quadrants are:\n", - " - Top right positive derivative for inhibition and negative for excitation (r_I increases, r_E decreases)\n", - " - top left negative derivative for both inhibition and excitation (r_I decrease, r_E decrease)\n", - " - bottom left negative derivative for inhibition and positive for excitation (r_I decrease, r_E increase)\n", - " - bottom right positive derivative for inhibition and excitation (r_I increase, r_E increase)\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "nDKgymrbkJJ9", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "nDKgymrbkJJ9" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Discuss_the_Plots_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "id": "5a278386", - "metadata": { - "execution": {}, - "id": "5a278386" - }, - "source": [ - "## Section 3.3: Adding internal connections\n", - "\n", - "*Estimated timing to here from start of tutorial: 57 min*\n", - "\n", - "Building up the equations in the previous section we re-introduce internal connections $w_{EE}$, $w_{II}$. The two coupled differential equations, each representing the dynamics of the excitatory or inhibitory population are now:\n", - "\n", - "\\begin{align}\n", - "\\tau_E \\frac{dr_E}{dt} &=w_{EE}r_E +w_{EI}r_I, \\\\\n", - "\\tau_I \\frac{dr_I}{dt} &=w_{IE}r_E +w_{II}r_I ,\n", - "\\end{align}\n", - "\n", - "where $\\tau_E=100$ and $\\tau_I=120$, $w_{EE}=1$ and $w_{II}=-1$, and $w_{EI}=-5$ and $w_{IE}=0.6$,\n", - "with the initial conditions\n", - "\n", - "\\begin{align}\n", - "r_E(0)=30, \\\\\n", - "r_I(0)=20.\n", - "\\end{align}\n", - "\n", - "The solutions can be approximated using the Euler method such that the equations become:\n", - "\n", - "\\begin{align*}\n", - "\\frac{\\color{red}{rE_{k+1}}-\\color{green}{rE_k}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{EE}}\\color{green}{rE_k}+\\color{blue}{w_{EI}}\\color{green}{rI_k}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\frac{\\color{red}{rI_{k+1}}-\\color{green}{rI_k}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{II}}\\color{green}{rI_k}+\\color{blue}{w_{IE}}\\color{green}{rE_k}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "\\end{align*}\n", - "where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t_k)$ and inhibition population $r_I(t_K)$ and $\\Delta t$ is the time-step.\n", - "\n", - "Re-arranging the equation such that all the known terms are on the right gives:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{rE_{k+1}}&=\\color{green}{rE_k}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EE}}\\color{green}{rE_k}+\\color{blue}{w_{EI}}\\color{green}{rI_k}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", - "\\color{red}{rI_{k+1}}&=\\color{green}{rI_k}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{II}}\\color{green}{rI_k}+\\color{blue}{w_{IE}}\\color{green}{rE_k}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", - "&\\text{ for } k=0, \\cdots n-1,\\\\\\\\\n", - "rE_0&=30,\\\\\n", - "rI_0&=20.\\\\\n", - "\\end{align*}\n", - "\n", - "where $\\color{red}{rE_{k+1}}$ and $\\color{red}{rI_{k+1}}$ are unknown, $\\color{green}{rE_{k}} $ and $\\color{green}{rI_{k}} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$, $\\color{blue}{w_{EE}}=1$, $\\color{blue}{w_{II}}=-1$ and $\\color{blue}{\\tau_E}=100$ and $\\color{blue}{\\tau_I}=120$ are known parameters and $\\color{blue}{\\Delta t}=0.1$ is a chosen time-step." - ] - }, - { - "cell_type": "markdown", - "id": "byUkHQNsJDF-", - "metadata": { - "execution": {}, - "id": "byUkHQNsJDF-" - }, - "source": [ - "### Think! 3.3: Oscillations\n", - "\n", - "\n", - "The code below implements and visualizes the linear Wilson-Cowan model.\n", - "\n", - "1. What will happen to the oscillations if the time period is extended?\n", - "2. How would you control or predict the oscillations?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e00d88fb", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "e00d88fb" - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to visualize the Linear Willson-Cowan*\n", - "\n", - "def Euler_Linear_System_Matrix(t, dt, w_EE=1):\n", - " \"\"\"\n", - " Args:\n", - " time: time\n", - " dt: time-step\n", - " w_EE: Excitation to excitation weight\n", - " Returns:\n", - " r_E: Excitation Firing Rate\n", - " r_I: Inhibition Firing Rate\n", - " N_Er: Nullclines for drE/dt=0\n", - " N_Ir: Nullclines for drI/dt=0\n", - " \"\"\"\n", - "\n", - " tau_E = 100\n", - " tau_I = 120\n", - " n = len(t)\n", - " r_I = 20*np.ones(n)\n", - " r_E = 30*np.ones(n)\n", - " w_EI = -5\n", - " w_IE = 0.6\n", - " w_II = -1\n", - "\n", - " for k in range(n-1):\n", - "\n", - " # Calculate the derivatives of the E and I populations\n", - " drE = (w_EI*r_I[k] + w_EE*r_E[k]) / tau_E\n", - " r_E[k+1] = r_E[k] + dt*drE\n", - "\n", - " drI = (w_II*r_I[k] + w_IE*r_E[k]) / tau_I\n", - " r_I[k+1] = r_I[k] + dt*drI\n", - "\n", - "\n", - " N_Er = -w_EE / w_EI*r_E\n", - " N_Ir = -w_IE / w_II*r_E\n", - "\n", - " return r_E, r_I, N_Er, N_Ir\n", - "\n", - "\n", - "dt = 0.1 # time-step\n", - "t = np.arange(0, 1000, dt)\n", - "r_E, r_I, _, _ = Euler_Linear_System_Matrix(t, dt)\n", - "plot_rErI(t, r_E, r_I)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1ACUE5rriyyR", - "metadata": { - "execution": {}, - "id": "1ACUE5rriyyR" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1. The oscillations start getting larger and larger which could in the end become uncontrollable\n", - "2. Looking at the shape of the spiral\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "z4gwxcvJkKKN", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "z4gwxcvJkKKN" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Oscillations_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "id": "261b8f71", - "metadata": { - "execution": {}, - "id": "261b8f71" - }, - "source": [ - "## Section 3.4 Phase Plane and Nullclines Part 2\n", - "\n", - "*Estimated timing to here from start of tutorial: 62 min*\n", - "\n", - "Like before, we have two differential equations so we can plot the results on a phase plane. We can also calculate the Nullclines when we set $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0$ which gives,\n", - "\\begin{align}\n", - "0&= w_{EE}r_E+w_{EI}r_I, \\\\\n", - "0&= w_{IE}r_E+w_{II}r_I,\n", - "\\end{align}\n", - "re-arranging as two lines\n", - "\\begin{align}\n", - "r_I&= -\\frac{w_{EE}}{w_{EI}}r_E, \\\\\n", - "r_I&= -\\frac{w_{IE}}{w_{II}}r_E,\n", - "\\end{align}\n", - "which crosses at the stable point.\n", - "\n", - "The panel on the left shows excitation firing rate $r_E$ and inhibition firing rate $r_I$ as a function of time. On the right side the phase plane plot shows inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$ with the Nullclines for $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0.$" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ca5f555", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "3ca5f555" - }, - "outputs": [], - "source": [ - "# @markdown *Run this cell to visualize the phase plane*\n", - "dt = 0.1 # time-step\n", - "t = np.arange(0, 1000, dt)\n", - "r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt)\n", - "plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)" - ] - }, - { - "cell_type": "markdown", - "id": "4d220e96", - "metadata": { - "execution": {}, - "id": "4d220e96" - }, - "source": [ - "### Interactive Demo 3.4: A small change changes everything\n", - "\n", - "We will illustrate that even changing one parameter in a system of differential equations can have a large impact on the solutions of the excitation firing rate $r_E$ and inhibition firing rate $r_I$.\n", - "Interact with the widget below to change the size of $w_{EE}$.\n", - "\n", - "Take note of:\n", - "1. How the solution changes for positive and negative of $w_{EE}$. Pay close attention to the axis.\n", - "2. How would you maintain a stable oscillation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "42724aca", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "42724aca" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " w_EE=widgets.FloatSlider(1, min=-1., max=2., step=.1,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(w_EE):\n", - " dt = 0.1 # time-step\n", - " t = np.arange(0,1000,dt)\n", - " r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt, w_EE)\n", - " plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "sH4Bpv7lkLFe", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "sH4Bpv7lkLFe" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Small_change_changes_everything_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "1268fd80", - "metadata": { - "execution": {}, - "id": "1268fd80" - }, - "source": [ - "---\n", - "# Summary\n", - "\n", - "*Estimated timing of tutorial: 70 minutes*\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cEcFr2llcl-l", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "cEcFr2llcl-l" - }, - "outputs": [], - "source": [ - "# @title Video 5: Summary\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '5UmGgboSc40'), ('Bilibili', 'BV1wM4y1g78M')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "uzv5CG0AkLwR", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "uzv5CG0AkLwR" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Summary_Video\")" - ] - }, - { - "cell_type": "markdown", - "id": "-JZJWqTnitQx", - "metadata": { - "execution": {}, - "id": "-JZJWqTnitQx" - }, - "source": [ - "Using the formula for the slope of a line, the solution of the differential equation can be estimated with reasonable accuracy. This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution." - ] - }, - { - "cell_type": "markdown", - "id": "a307a8fa", - "metadata": { - "execution": {}, - "id": "a307a8fa" - }, - "source": [ - "---\n", - "## Links to Neuromatch Computational Neuroscience Days\n", - "\n", - "Differential equations turn up on a number of different Neuromatch days:\n", - "* The LIF model is discussed in more detail in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", - "* Drift Diffusion model, which is a differential equation for decision making, is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", - "* Phase-plane plots are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", - "* The Wilson-Cowan model is discussed in [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", - "\n", - "## References\n", - "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", - "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", - "\n", - "\n", - "## Bibliography\n", - "1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.\n", - "2. Strogatz, S. (2014). Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2nd edition\n", - "\n", - "### Supplemental Popular Reading List\n", - "1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.\n", - "2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.\n", - "\n", - "### Popular Podcast\n", - "1. Strogatz, S. (Host). (2020), Joy of X https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine" - ] - }, - { - "cell_type": "markdown", - "id": "_bnKhPLwobY9", - "metadata": { - "execution": {}, - "id": "_bnKhPLwobY9" - }, - "source": [ - "---\n", - "# Bonus" - ] - }, - { - "cell_type": "markdown", - "id": "3c9b3f6e", - "metadata": { - "execution": {}, - "id": "3c9b3f6e" - }, - "source": [ - "---\n", - "## Bonus Section 1: The 4th Order Runge-Kutta method\n", - "\n", - "Another popular numerical method to estimate the solution of differential equations of the general form:\n", - "\n", - "\\begin{align}\n", - "\\frac{d}{dt}y=f(t,y)\n", - "\\end{align}\n", - "\n", - "is the 4th Order Runge Kutta:\n", - "\\begin{align}\n", - "k_1 &= f(t_k,y_k)\\\\\n", - "k_2 &= f(t_k+\\frac{\\Delta t}{2},y_k+\\frac{\\Delta t}{2}k_1)\\\\\n", - "k_3 &= f(t_k+\\frac{\\Delta t}{2},y_k+\\frac{\\Delta t}{2}k_2)\\\\\n", - "k_4 &= f(t_k+\\Delta t,y_k+\\Delta tk_3)\\\\\n", - "y_{k+1} &= y_{k}+\\frac{\\Delta t}{6}(k_1+2k_2+2k_3+k_4)\\\\\n", - "\\end{align}\n", - "\n", - "for $k=0,1,\\cdots,n-1$,\n", - "which is more accurate than the Euler method.\n", - "\n", - "Given the population differential equation\n", - "\\begin{align*}\n", - "\\frac{d}{dt}\\,p(t) &= 0.3 p(t),\\\\\n", - "p(t_0=1)&=e^{0.3}\\quad \\text{Initial Condition},\n", - "\\end{align*}\n", - "\n", - "the 4th Runge Kutta difference equation is\n", - "\n", - "\\begin{align*}\n", - "k_1&=0.3\\color{green}{p[k]},\\\\\n", - "k_2&=0.3(\\color{green}{p[k]}+\\frac{\\Delta t}{2}k_1),\\\\\n", - "k_3&=0.3(\\color{green}{p[k]}+\\frac{\\Delta t}{2}k_2),\\\\\n", - "k_4&=0.3(\\color{green}{p[k]}+k_3),\\\\\n", - "\\color{red}{p_{k[k]}}&=\\color{green}{p[k]}+\\frac{\\color{blue}{\\Delta t}}{6}( \\color{green}{k_1+2k_2+2k_3+k_4})\\\\\n", - "\\end{align*}\n", - "\n", - "for $k=0,1,\\cdots 4$ to estimate the population for $\\Delta t=0.5$.\n", - "\n", - "The code is implemented below. Note how much more accurate the 4th Order Runge Kutta (yellow) is compared to the Euler Method (blue). The 4th order Runge Kutta is of order 4 which means that if you half the time-step $\\Delta t$ the error decreases by a factor of $\\Delta t^4$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7b936428", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "7b936428" - }, - "outputs": [], - "source": [ - "# @markdown *Execute this cell to show the difference between the Runge Kutta Method and the Euler Method*\n", - "\n", - "\n", - "def RK4(dt=0.5):\n", - " t=np.arange(1, 5+dt/2, dt)\n", - " t_fine=np.arange(1, 5+0.1/2, 0.1)\n", - " n = len(t)\n", - " p = np.ones(n)\n", - " pRK4 = np.ones(n)\n", - " p[0] = np.exp(0.3*t[0])\n", - " pRK4[0] = np.exp(0.3*t[0])# Initial Condition\n", - "\n", - " with plt.xkcd():\n", - "\n", - " fig = plt.figure(figsize=(6, 4))\n", - " plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')\n", - "\n", - " for i in range(n-1):\n", - " dp = dt*0.3*p[i]\n", - " p[i+1] = p[i] + dp # Euler\n", - " k1 = 0.3*(pRK4[i])\n", - " k2 = 0.3*(pRK4[i] + dt/2*k1)\n", - " k3 = 0.3*(pRK4[i] + dt/2*k2)\n", - " k4 = 0.3*(pRK4[i] + dt*k3)\n", - " pRK4[i+1] = pRK4[i] + dt/6 *(k1 + 2*k2 + 2*k3 + k4)\n", - "\n", - " plt.plot(t_fine, np.exp(0.3*t_fine), label='Exact')\n", - " plt.plot(t, p,':ro', label='Euler Estimate')\n", - " plt.plot(t, pRK4,':co', label='4th Order Runge Kutta Estimate')\n", - "\n", - " plt.ylabel('Population (millions)')\n", - " plt.legend()\n", - " plt.xlabel('Time (years)')\n", - " plt.show()\n", - "\n", - "# @markdown Make sure you execute this cell to enable the widget!\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " dt=widgets.FloatSlider(0.5, min=.1, max=4., step=.1,\n", - " layout=my_layout)\n", - ")\n", - "def Pop_widget(dt):\n", - " RK4(dt)" - ] - }, - { - "cell_type": "markdown", - "id": "W7ziQulJL6Kl", - "metadata": { - "execution": {}, - "id": "W7ziQulJL6Kl" - }, - "source": [ - "**Bonus Reference 1: A full course on numerical methods in Python**\n", - "\n", - "For a full course on Numerical Methods for differential Equations you can look [here](https://github.com/john-s-butler-dit/Numerical-Analysis-Python)." - ] - }, - { - "cell_type": "markdown", - "id": "jXpJytN7dgYn", - "metadata": { - "execution": {}, - "id": "jXpJytN7dgYn" - }, - "source": [ - "---\n", - "## Bonus Section 2: Neural oscillations are a start toward understanding brain activity rather than the end\n", - "\n", - "The differential equations we have discussed above are all to simulate neuronal processes. Another way differential equations can be used is to motivate experimental findings.\n", - "\n", - "Many experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrees to a stimulus.\n", - "An issue with these studies is that there is no consistent definition of what constitutes an oscillation. Right now, it is a bit of I know one when I see one problem.\n", - "\n", - "In an essay from May 2021 in PLoS Biology, Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally when studying oscillations. The essay proposes that instead of thinking about the brain, we should look at this question from the mathematical side to motivate what can be defined as an oscillation.\n", - "\n", - "To do this, they used Stuart–Landau equations, which is a system of differential equations\n", - "\\begin{align}\n", - "\\frac{dx}{dt} &= \\lambda x-\\omega y -\\gamma (x^2+y^2)x+s\\\\\n", - "\\frac{dy}{dt} &= \\lambda y+\\omega x -\\gamma (x^2+y^2)y\n", - "\\end{align}\n", - "\n", - "where $s$ is input to the system, and $\\lambda$, $\\omega$ and $\\gamma$ are parameters.\n", - "\n", - "The Stuart–Landau equations are a well-described system of non-linear differential equations that generate oscillations. For their purpose, Dowling and Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.\n", - "In their paper, using the Stuart–Landau equations, they outline\n", - "* \"What is an oscillator?\"\n", - "* \"What an oscillator is not.\"\n", - "* \"Not all that oscillates is an oscillator.\"\n", - "* \"Not all oscillators are alike.\"\n", - "\n", - "The Euler form of the Stuart–Landau system of equations is:\n", - "\n", - "\\begin{align*}\n", - "\\color{red}{x_{k+1}}&=\\color{green}{x_k}+\\color{blue}{\\Delta t}\\big(\\lambda \\color{green}{x_k}-\\omega \\color{green}{y_k} -\\gamma (\\color{green}{x_k}^2+\\color{green}{y_k}^2)\\color{green}{x_k}+s\\big),\\\\\n", - "\\color{red}{y_{k+1}}&=\\color{green}{y_k}+\\color{blue}{\\Delta t}\\big(\\lambda \\color{green}{y_k}+\\omega \\color{green}{x_k} -\\gamma (\\color{green}{x_k}^2+\\color{green}{y_k}^2)\\color{green}{y_k} \\big),\\\\\n", - "&\\text{ for } k=0, \\cdots n-1,\\\\\n", - "x_0&=1,\\\\\n", - "y_0&=1,\\\\\n", - "\\end{align*}\n", - "\n", - "with $ \\Delta t=0.1/1000$ ms." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b9e30cc", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "5b9e30cc" - }, - "outputs": [], - "source": [ - "# @title Helper functions\n", - "def plot_Stuart_Landa(t, x, y, s):\n", - " \"\"\"\n", - " Args:\n", - " t : time\n", - " x : x\n", - " y : y\n", - " s : input\n", - " Returns:\n", - " figure with two panels\n", - " top panel: Input as a function of time\n", - " bottom panel: x\n", - " \"\"\"\n", - "\n", - " with plt.xkcd():\n", - " fig = plt.figure(figsize=(14, 4))\n", - " gs = gridspec.GridSpec(2, 2, height_ratios=[1, 4], width_ratios=[4, 1])\n", - "\n", - " # PLOT OF INPUT\n", - " plt.subplot(gs[0])\n", - " plt.ylabel(r'$s$')\n", - " plt.plot(t, s, 'g')\n", - " #plt.ylim((2,4))\n", - "\n", - " # PLOT OF ACTIVITY\n", - " plt.subplot(gs[2])\n", - " plt.plot(t ,x)\n", - " plt.ylabel(r'x')\n", - " plt.xlabel(r't')\n", - " plt.subplot(gs[3])\n", - " plt.plot(x,y)\n", - " plt.plot(x[0], y[0],'go')\n", - " plt.xlabel(r'x')\n", - " plt.ylabel(r'y')\n", - " plt.show()\n", - "\n", - "\n", - "def Euler_Stuart_Landau(s,time,dt,lamba=0.1,gamma=1,k=25):\n", - " \"\"\"\n", - " Args:\n", - " I: Input\n", - " time: time\n", - " dt: time-step\n", - " \"\"\"\n", - "\n", - " n = len(time)\n", - " omega = 4 * 2*np.pi\n", - " x = np.zeros(n)\n", - " y = np.zeros(n)\n", - " x[0] = 1\n", - " y[0] = 1\n", - "\n", - " for i in range(n-1):\n", - " dx = lamba*x[i] - omega*y[i] - gamma*(x[i]*x[i] + y[i]*y[i])*x[i] + k*s[i]\n", - " x[i+1] = x[i] + dt*dx\n", - " dy = lamba*y[i] + omega*x[i] - gamma*(x[i]*x[i] + y[i]*y[i])*y[i]\n", - " y[i+1] = y[i] + dt*dy\n", - "\n", - " return x, y" - ] - }, - { - "cell_type": "markdown", - "id": "H5inj2EKJyqN", - "metadata": { - "execution": {}, - "id": "H5inj2EKJyqN" - }, - "source": [ - "### Bonus 2.1: What is an Oscillator?\n", - "\n", - "Doelling & Assaneo (2021), using the Stuart–Landau system, show different possible states of an oscillator by manipulating the $\\lambda$ term in the equation.\n", - "\n", - "From the paper:\n", - "\n", - "> This qualitative change in behavior takes place at $\\lambda = 0$. For $\\lambda < 0$, the system decays to a stable equilibrium, while for $\\lambda > 0$, it keeps oscillating.\n", - "\n", - "This illustrates that oscillations do not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\\alpha$ band oscillations in EEG the oscillations come and go." - ] - }, - { - "cell_type": "markdown", - "id": "5MyIOK9w_qjd", - "metadata": { - "execution": {}, - "id": "5MyIOK9w_qjd" - }, - "source": [ - "#### Interactive Demo Bonus 2.1: Oscillator\n", - "\n", - "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency $\\omega$ set to $4\\times 2\\pi$, $\\gamma=1$ and $k=25$ over 3 seconds. The input to the system $s(t)$ is plotted in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$. You can manipulate $\\lambda$ to see how the oscillations change." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "LAnBGRU5crfG", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "LAnBGRU5crfG" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "dt=0.1/1000\n", - "t=np.arange(0, 3, dt)\n", - "\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " lamda=widgets.FloatSlider(1, min=-1., max=5., step=0.5,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(lamda):\n", - " s=np.zeros(len(t))\n", - " x,y=Euler_Stuart_Landau(s,t,dt,lamda)\n", - " plot_Stuart_Landa(t, x, y, s)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "PNtk7NRomDuQ", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "PNtk7NRomDuQ" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Oscillator_Bonus_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "q2OMPLOoKGtr", - "metadata": { - "execution": {}, - "id": "q2OMPLOoKGtr" - }, - "source": [ - "### Bonus 2.2 : Not all oscillators are alike" - ] - }, - { - "cell_type": "markdown", - "id": "NLOVdwN5_ypj", - "metadata": { - "execution": {}, - "id": "NLOVdwN5_ypj" - }, - "source": [ - "#### Interactive Demo Bonus 2: Stuart-Landau System\n", - "\n", - "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency of 4Hz by setting $\\omega$ to $4\\times 2\\pi$, $\\lambda=1$, $\\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\\sin(freq 2\\pi t) $ in the top panel, $x$ as a function of time in the bottom panel and on the right the phase plane plot of $x$ and $y$.\n", - "\n", - "You can manipulate the frequency $freq$ of the input to see how the oscillations change, and for frequencies $freq$ further and further from the base frequency of 4Hz, the oscillations break down.\n", - "\n", - "This shows that if you have an oscillating input into an oscillator, it does not have to respond by oscillating about could even break down. Hence the frequency of the input oscillation is important to the system.\n", - "So if you flash something at 50Hz, for example, the visual system might not follow the signal, but that does not mean the visual system is not an oscillator. It might just be the wrong frequency." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "XPuVG5f4clxt", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "XPuVG5f4clxt" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "dt=0.1/1000\n", - "t=np.arange(0, 3, dt)\n", - "\n", - "my_layout.width = '450px'\n", - "@widgets.interact(\n", - " freq=widgets.FloatSlider(4, min=0.5, max=10., step=0.5,\n", - " layout=my_layout)\n", - ")\n", - "\n", - "def Pop_widget(freq):\n", - " s = np.sin(freq * 2*np.pi * t)\n", - "\n", - " x, y = Euler_Stuart_Landau(s, t, dt, lamba=1, gamma=.1, k=50)\n", - " plot_Stuart_Landa(t, x, y, s)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "EC5oefXvmOtd", - "metadata": { - "cellView": "form", - "execution": {}, - "id": "EC5oefXvmOtd" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Stuart_Landau_System_Bonus_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "id": "1cWYP3SNL1Mh", - "metadata": { - "execution": {}, - "id": "1cWYP3SNL1Mh" - }, - "source": [ - "**Bonus Reference 2**:\n", - "\n", - "Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234. doi: [10.1371/journal.pbio.3001234](https://doi.org/10.1371/journal.pbio.3001234)" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [ - "1cWYP3SNL1Mh" - ], - "name": "W0D4_Tutorial3", - "provenance": [], - "toc_visible": true, - "include_colab_link": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file + "cells": [ + { + "cell_type": "markdown", + "id": "dfd443f6", + "metadata": { + "colab_type": "text", + "execution": {}, + "id": "view-in-github" + }, + "source": [ + "\"Open   \"Open" + ] + }, + { + "cell_type": "markdown", + "id": "08842220", + "metadata": { + "execution": {} + }, + "source": [ + "# Tutorial 3: Numerical Methods\n", + "\n", + "**Week 0, Day 3: Calculus**\n", + "\n", + "**Content creators:** John S Butler, Arvind Kumar with help from Harvey McCone\n", + "\n", + "**Content reviewers:** Swapnil Kumar, Matthew McCann\n", + "\n", + "**Production editors:** Matthew McCann, Ella Batty" + ] + }, + { + "cell_type": "markdown", + "id": "v0oVvJHEMouH", + "metadata": { + "execution": {} + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "id": "7d80998b", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Tutorial Objectives\n", + "\n", + "*Estimated timing of tutorial: 70 minutes*\n", + "\n", + "While a great deal of neuroscience can be described by mathematics, a great deal of the mathematics used cannot be solved exactly. This might seem very odd that you can writing something in mathematics that cannot be immediately solved but that is the beauty and mystery of mathematics. To side step this issue we use Numerical Methods to estimate the solution.\n", + "\n", + "In this tutorial, we will look at the Euler method to estimate the solution of a few different differential equations: the population equation, the Leaky Integrate and Fire model and a simplified version of the Wilson-Cowan model which is a system of differential equations.\n", + "\n", + "**Steps:**\n", + "- Code the Euler estimate of the Population Equation;\n", + "- Investigate the impact of time step on the error of the numerical solution;\n", + "- Code the Euler estimate of the Leaky Integrate and Fire model for a constant input;\n", + "- Visualize and listen to the response of the integrate for different inputs;\n", + "- Apply the Euler method to estimate the solution of a system of differential equations." + ] + }, + { + "cell_type": "markdown", + "id": "bwMygnJgMUp7", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "BrceoWcPfYlB", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D4_T3\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5985801", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d40abcd8", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Figure Settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import IPython.display as ipd\n", + "from matplotlib import gridspec\n", + "import ipywidgets as widgets # interactive display\n", + "from ipywidgets import Label\n", + "%config InlineBackend.figure_format = 'retina'\n", + "# use NMA plot style\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")\n", + "my_layout = widgets.Layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hwhtoyMMi7qj", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Plotting Functions\n", + "\n", + "time = np.arange(0, 1, 0.01)\n", + "\n", + "def plot_slope(dt):\n", + " \"\"\"\n", + " Args:\n", + " dt : time-step\n", + " Returns:\n", + " A figure of an exponential, the slope of the exponential and the derivative exponential\n", + " \"\"\"\n", + "\n", + " t = np.arange(0, 5+0.1/2, 0.1)\n", + "\n", + " with plt.xkcd():\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " # Exponential\n", + " p = np.exp(0.3*t)\n", + " plt.plot(t, p, label='y')\n", + " # slope\n", + " plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1+dt))],':og',label=r'$\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$')\n", + " # derivative\n", + " plt.plot([1, 1+dt], [np.exp(0.3*1), np.exp(0.3*(1))+dt*0.3*np.exp(0.3*(1))],'-k',label=r'$\\frac{dy}{dt}$')\n", + " plt.legend()\n", + " plt.plot(1+dt, np.exp(0.3*(1+dt)), 'og')\n", + " plt.ylabel('y')\n", + " plt.xlabel('t')\n", + " plt.show()\n", + "\n", + "\n", + "\n", + "def plot_StepEuler(dt):\n", + " \"\"\"\n", + " Args:\n", + " dt : time-step\n", + " Returns:\n", + " A figure of one step of the Euler method for an exponential growth function\n", + " \"\"\"\n", + "\n", + " t=np.arange(0, 1 + dt + 0.1 / 2, 0.1)\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(6,4))\n", + " p=np.exp(0.3*t)\n", + " plt.plot(t,p)\n", + " plt.plot([1,],[np.exp(0.3*1)],'og',label='Known')\n", + " plt.plot([1,1+dt],[np.exp(0.3*1),np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)],':g',label=r'Euler')\n", + " plt.plot(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),'or',label=r'Estimate $p_1$')\n", + " plt.plot(1+dt,p[-1],'bo',label=r'Exact $p(t_1)$')\n", + " plt.vlines(1+dt,np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1),p[-1],colors='r', linestyles='dashed',label=r'Error $e_1$')\n", + " plt.text(1+dt+0.1,(np.exp(0.3*(1))+dt*0.3*np.exp(0.3*1)+p[-1])/2,r'$e_1$')\n", + " plt.legend()\n", + " plt.ylabel('Population (millions)')\n", + " plt.xlabel('time(years)')\n", + " plt.show()\n", + "\n", + "def visualize_population_approx(t, p):\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')\n", + "\n", + " plt.plot(t, p,':o', label='Euler Estimate')\n", + " plt.vlines(t, p, np.exp(0.3*t),\n", + " colors='r', linestyles='dashed', label=r'Error $e_k$')\n", + "\n", + " plt.ylabel('Population (millions)')\n", + " plt.legend()\n", + " plt.xlabel('Time (years)')\n", + " plt.show()\n", + "\n", + "## LIF PLOT\n", + "def plot_IF(t, V, I, Spike_time):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " V : membrane Voltage\n", + " I : Input\n", + " Spike_time : Spike_times\n", + " Returns:\n", + " figure with three panels\n", + " top panel: Input as a function of time\n", + " middle panel: membrane potential as a function of time\n", + " bottom panel: Raster plot\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(12,4))\n", + " gs = gridspec.GridSpec(3, 1, height_ratios=[1, 4, 1])\n", + " # PLOT OF INPUT\n", + " plt.subplot(gs[0])\n", + " plt.ylabel(r'$I_e(nA)$')\n", + " plt.yticks(rotation=45)\n", + " plt.plot(t,I,'g')\n", + " #plt.ylim((2,4))\n", + " plt.xlim((-50,1000))\n", + " # PLOT OF ACTIVITY\n", + " plt.subplot(gs[1])\n", + " plt.plot(t,V,':')\n", + " plt.xlim((-50,1000))\n", + " plt.ylabel(r'$V(t)$(mV)')\n", + " # PLOT OF SPIKES\n", + " plt.subplot(gs[2])\n", + " plt.ylabel(r'Spike')\n", + " plt.yticks([])\n", + " plt.scatter(Spike_time,1*np.ones(len(Spike_time)), color=\"grey\", marker=\".\")\n", + " plt.xlim((-50,1000))\n", + " plt.xlabel('time(ms)')\n", + " plt.show()\n", + "\n", + "def plot_rErI(t, r_E, r_I):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " r_E : excitation rate\n", + " r_I : inhibition rate\n", + "\n", + " Returns:\n", + " figure of r_I and r_E as a function of time\n", + "\n", + " \"\"\"\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(6,4))\n", + " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", + " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", + " plt.xlabel('time(ms)')\n", + " plt.legend()\n", + " plt.ylabel('Firing Rate (Hz)')\n", + " plt.show()\n", + "\n", + "\n", + "def plot_rErI_Simple(t, r_E, r_I):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " r_E : excitation rate\n", + " r_I : inhibition rate\n", + "\n", + " Returns:\n", + " figure with two panels\n", + " left panel: r_I and r_E as a function of time\n", + " right panel: r_I as a function of r_E with Nullclines\n", + "\n", + " \"\"\"\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(12,4))\n", + " gs = gridspec.GridSpec(1, 2)\n", + " # LEFT PANEL\n", + " plt.subplot(gs[0])\n", + " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", + " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", + " plt.xlabel('time(ms)')\n", + " plt.legend()\n", + " plt.ylabel('Firing Rate (Hz)')\n", + " # RIGHT PANEL\n", + " plt.subplot(gs[1])\n", + " plt.plot(r_E,r_I,'k:')\n", + " plt.plot(r_E[0],r_I[0],'go')\n", + "\n", + " plt.hlines(0,np.min(r_E),np.max(r_E),linestyles=\"dashed\",color='b',label=r'$\\frac{d}{dt}r_E=0$')\n", + " plt.vlines(0,np.min(r_I),np.max(r_I),linestyles=\"dashed\",color='r',label=r'$\\frac{d}{dt}r_I=0$')\n", + "\n", + " plt.legend(loc='upper left')\n", + "\n", + " plt.xlabel(r'$r_E$')\n", + " plt.ylabel(r'$r_I$')\n", + " plt.show()\n", + "\n", + "def plot_rErI_Matrix(t, r_E, r_I, Null_rE, Null_rI):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " r_E : excitation firing rate\n", + " r_I : inhibition firing rate\n", + " Null_rE: Nullclines excitation firing rate\n", + " Null_rI: Nullclines inhibition firing rate\n", + " Returns:\n", + " figure with two panels\n", + " left panel: r_I and r_E as a function of time\n", + " right panel: r_I as a function of r_E with Nullclines\n", + "\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(12,4))\n", + " gs = gridspec.GridSpec(1, 2)\n", + " plt.subplot(gs[0])\n", + " plt.plot(t,r_E,':',color='b',label=r'$r_E$')\n", + " plt.plot(t,r_I,':',color='r',label=r'$r_I$')\n", + " plt.xlabel('time(ms)')\n", + " plt.ylabel('Firing Rate (Hz)')\n", + " plt.legend()\n", + " plt.subplot(gs[1])\n", + " plt.plot(r_E,r_I,'k:')\n", + " plt.plot(r_E[0],r_I[0],'go')\n", + "\n", + " plt.plot(r_E,Null_rE,':',color='b',label=r'$\\frac{d}{dt}r_E=0$')\n", + " plt.plot(r_E,Null_rI,':',color='r',label=r'$\\frac{d}{dt}r_I=0$')\n", + " plt.legend(loc='best')\n", + " plt.xlabel(r'$r_E$')\n", + " plt.ylabel(r'$r_I$')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "SXJPvBQzik6K", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 1: Intro to the Euler method using the population differential equation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "DYKXWaR7iwl9", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 1: Intro to numerical methods for differential equations\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'NI8c80TA7IQ'), ('Bilibili', 'BV1gh411Y7gV')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "M3V5Kju3j7Ie", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Intro_to_numerical_methods_for_differential_equations_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "R9B9JKT-PvEn", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 1.1: Slope of line as approximation of derivative\n", + "\n", + "*Estimated timing to here from start of tutorial: 8 min*\n", + "\n", + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "The Euler method is one of the straight forward and elegant methods to approximate a differential. It was designed by [Leonhard Euler](https://en.wikipedia.org/wiki/Leonhard_Euler) (1707-1783).\n", + "Simply put we just replace the derivative in the differential equation by the formula for a line and re-arrange.\n", + "\n", + "The slope is the rate of change between two points. The formula for the slope of a line between the points $(t_0,y(t_0))$ and $(t_1,y(t_1))$ is given by:\n", + "$$ m=\\frac{y(t_1)-y(t_0)}{t_1-t_0}, $$\n", + "which can be written as\n", + "$$ m=\\frac{y_1-y_0}{t_1-t_0}, $$\n", + "or as\n", + "$$ m=\\frac{\\Delta y_0}{\\Delta t_0}, $$\n", + "where $\\Delta y_0=y_1-y_0$ and $\\Delta t_0=t_1-t_0$ or in words as\n", + "$$ m=\\frac{\\text{ Change in y} }{\\text{Change in t}}. $$\n", + "The slope can be used as an approximation of the derivative such that\n", + "$$ \\frac{d}{dt}y(t)\\approx \\frac{y(t_0+\\Delta t)-y(t_0)}{t_0+\\Delta t-t_0}=\\frac{y(t_0+dt)-y(t_0)}{\\Delta t}$$\n", + "where $\\Delta t$ is a time-step.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "b667c36a", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 1.1: Slope of a Line\n", + "\n", + "The plot below shows a function $y(t)$ in blue, its exact derivative $ \\frac{d}{dt}y(t)$ at $t_0=1$ in black. The approximate derivative is calculated using the slope formula $=\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$ for different time-steps sizes $\\Delta t$ in green.\n", + "\n", + "Interact with the widget to see how time-step impacts the slope's accuracy, which is the derivative's estimate.\n", + "- How does the size of $\\Delta t$ affect the approximation?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea4a4f78", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + "\n", + " dt=widgets.FloatSlider(1, min=0., max=4., step=.1,\n", + " layout=my_layout)\n", + "\n", + ")\n", + "\n", + "def Pop_widget(dt):\n", + " plot_slope(dt)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "gTgFRiMhc1qm", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " The larger the time-step $dt$, the worse job the formula of the slope of a\n", + " line does to approximate the derivative.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "FZ1SZPQwj9H4", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Slope_of_a_line_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "8f92ce83", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 1.2: Euler error for a single step\n", + "\n", + "*Estimated timing to here from start of tutorial: 12 min*\n", + "\n", + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "Linking with the previous tutorial, we will use the population differential equation to get an intuitive feel of using the Euler method to approximate the solution of a differential equation, as it has an exact solution with no discontinuities.\n", + "\n", + "The population differential equation is given by\n", + "\\begin{align*}\n", + "\\\\\n", + "\\frac{d}{dt}\\,p(t) &= \\alpha p(t)\\\\\\\\\n", + "p(0)&=p_0 \\quad \\text{Initial Condition}\n", + "\\end{align*}\n", + "\n", + "where $p(t)$ is the population at time $t$ and $\\alpha$ is a parameter representing birth rate. The exact solution of the differential equation is\n", + "$$ p(t)=p_0e^{\\alpha t}.$$\n", + "\n", + "To numerically estimate the population differential equation we replace the derivate with the slope of the line to get the discrete (not continuous) equation:\n", + "\n", + "\\begin{align*}\n", + "\\\\\n", + "\\frac{p_1-p_0}{t_1-t_0} &= \\alpha p_0\\\\\\\\\n", + "p(0)&=p_0 \\quad \\text{Initial Condition}\n", + "\\end{align*}\n", + "\n", + "where $p_1$ is the estimate of $p(t_1)$. Let $\\Delta t=t_1-t_0$ be the time-step and re-arrange the equation gives\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{p_1}&=\\color{green}{p_0}+\\color{blue}{\\Delta t} (\\color{blue}{\\alpha} \\color{green}{p_0})\n", + "\\end{align*}\n", + "\n", + "where $\\color{red}{p_1}$ is the unknown future, $\\color{green}{p_0}$ is the known current population, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter and $\\color{blue}{\\alpha}$ is the given birth rate parameter.\n", + "Another way to read the re-arranged discrete equation is:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{\\text{\"Future Population\"}}&=\\color{green}{\\text{ \"Current Population\"}}+\\color{blue}{\\text{ time-step}} \\times (\\color{blue}{\\text{ \"Birth rate}}\\times \\color{green}{\\text{ Current Population\"}}).\n", + "\\end{align*}\n", + "\n", + "So pretty much, we can use the current population and add a bit of the dynamics of the differential equation to predict the future. We will make millions... But wait there is always an error.\n", + "\n", + "The solution of the Euler method $p_1$ is an estimate of the exact solution $p(t_1)$ at $t_1$ which means there is a bit of error $e_1$ which gives the equation\n", + "\\begin{align*}\n", + "p(t_1)&=p_1+e_1\\\\\\\\\n", + "\\text{Rearranging}\\\\\\\\\n", + "e_1&=p(t_1)-p_1,\\\\\n", + "\\text{Error}&=\\text{Exact-Estimate}.\n", + "\\end{align*}\n", + "\n", + "Most of the time we do not know the exact answer $p(t_1)$ and hence the size of the error $e_1$ but for the population equation we have the exact solution $ p(t)=p_0e^{\\alpha}.$\n", + "\n", + "This means we can explore what the error looks like.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "vGxg-2k6k8hM", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 1.2: Euler error for a single step\n", + "\n", + "Interact with the widget to see how the time-step $\\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.\n", + "\n", + "1. What happens to the estimate $p_1$ as the time-step $\\Delta t$ increases?\n", + "2. Is there a relationship between the size of $\\Delta t$ and $e_1$?\n", + "3. How would you improve the error $e_1$?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e72c600b", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " dt=widgets.FloatSlider(1.5, min=0., max=4., step=.1,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(dt):\n", + " plot_StepEuler(dt)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "EoA90TrDdVWv", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. The larger the time-step $\\Delta t$ the more the estimate $p_1$ deviates\n", + " from the exact $p(t_1)$.\n", + "\n", + " 2. There is a linear relationship between $\\Delta t$ and $e_1$: double the time-step double the error.\n", + "\n", + " 3. Make more shorter time-steps\n", + "\"\"\";" + ] + }, + { + "cell_type": "markdown", + "id": "e78fc157", + "metadata": { + "execution": {} + }, + "source": [ + "The error $e_1$ from one time-step is known as the __local error__. For the Euler method, the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $\\mathcal{O}(\\Delta t)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "qXS6Fkkkj-xD", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Euler_error_of_single_step_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "1yr4hKV7R0JJ", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 1.3: Taking more steps\n", + "\n", + "*Estimated timing to here from start of tutorial: 16 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "MZcqv_DoRsWJ", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 2: Taking more steps\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'cGsXHllGMVo'), ('Bilibili', 'BV135411T7QF')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "W8HbORAlkAWj", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Taking_more_steps_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "972a55c4", + "metadata": { + "execution": {} + }, + "source": [ + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "In the above exercise we saw that by increasing the time-step $\\Delta t$ the error between the estimate $p_1$ and the exact $p(t_1)$ increased. The largest error in the example above was for $\\Delta t=4$, meaning the first time point was at 1 year and the second was at 5 years (as 5 - 1 = 4).\n", + "\n", + "To decrease the error, we can divide the interval $[1, 5]$ into more steps using a smaller $\\Delta t$.\n", + "\n", + "In the plot below we use $\\Delta t=1$, which divides the interval into four segments\n", + "$$n=\\frac{5-1}{1}=4,$$\n", + "giving\n", + "$$ t_0=t[0]=1, \\ t_1=t[1]=2, \\ t_2=t[2]=3, \\ t_3=t[3]=4 \\ \\text{ and } t_4=t[4]=5. $$\n", + "This can be written as\n", + "$$ t[k]=1+k\\times1, \\text{ for } k=0,\\cdots 4, $$\n", + "and more generally as\n", + "$$ t[k]=t[k]+k\\times \\Delta t, \\text{ for } k=0,\\cdots n, $$\n", + "where $n$ is the number of steps.\n", + "\n", + "Using the Euler method, the continuous population differential equation is written as a series of discrete difference equations of the form:\n", + "\\begin{align*}\n", + "\\color{red}{p[k+1]}&=\\color{green}{p[k]}+\\color{blue}{\\Delta t} (\\color{blue}{\\alpha} \\color{green}{p[k]})\\\\\n", + "&\\text{for } k=0,1,\\cdots n-1\n", + "\\end{align*}\n", + "where $\\color{red}{p[k+1]}$ is the unknown estimate of the future population at $t[k+1]$, $\\color{green}{p[k]}$ is the known current population estimate at $t_k$, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter and $\\color{blue}{\\alpha}$ is the given birth rate parameter.\n", + "\n", + "\n", + "The Euler method can be applied to all first order differential equations of the form\n", + "\\begin{align*}\n", + "\\frac{d}{dt}y(t)&=f(t,y(t)),\\\\\n", + "y(t_{0})&=y_0,\\\\\n", + "\\end{align*}\n", + "on an interval $[a,b]$.\n", + "\n", + "Using the Euler method all differential equation can be written as discrete difference equations:\n", + "\\begin{align*}\n", + "\\frac{\\color{red}{y[k+1]}-\\color{green}{y[k]}}{\\color{blue}{\\Delta t}}&=f(\\color{blue}{t[k]},\\color{green}{y[k]}),\\\\\n", + "\\text{Re-arranging}\\\\\n", + "\\color{red}{y[k+1]}&=\\color{green}{y[k]}+\\color{blue}{\\Delta t}f(\\color{blue}{t[k]},\\color{green}{y[k]}),\\\\\n", + "&\\text{ for } k=0, \\cdots n-1,\\\\\n", + "y[0]&=\\color{green}{y_0},\\\\\n", + "\\end{align*}\n", + "where $\\color{red}{y[k+1]}$ is the unknown estimate at $t[k+1]$, $\\color{green}{y[k]}$ is the known at $t_k$, $\\color{blue}{\\Delta t}$ is the chosen time-step parameter, $\\color{blue}{t[k]}$ is the time point and $f$ is the right hand side of the differential equation.\n", + "The discrete time steps are:\n", + "\\begin{align*}\n", + "\\color{blue}{t[k]}&=\\color{blue}{t[0]}+\\color{blue}{k}\\color{blue}{\\Delta t},\\\\\n", + "n&=\\frac{b-a}{\\Delta t}\\\\\n", + "&\\text{ for } k=0, \\cdots n.\\\\\n", + "\\end{align*}\n", + "Once again this can be simply put into words:\n", + "\\begin{align*}\n", + "\\color{red}{\\text{ \"Future\" }}&=\\color{green}{\\text{ \"Current Info\" }}+\\color{blue}{\\text{ time-step } }\\times\\text{ \"Dynamics of the system which is a function of } \\color{blue}{ \\text{ time }} \\text{ and }\\color{green}{ \\text{ Current Info.\" }}\n", + "\\end{align*}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d84f02ed", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to visualize time steps*\n", + "dt =1\n", + "t =np.arange(1, 5+dt/2, dt) # Time from 1 to 5 years in 0.1 steps\n", + "with plt.xkcd():\n", + " fig = plt.figure(figsize=(8, 2))\n", + " plt.plot(t, 0*t, ':o')\n", + " plt.plot(t[0] ,0*t[0],':go',label='Initial Condition')\n", + " plt.text(t[0]-0.1, 0*t[0]-0.03, r'$t_0$')\n", + " plt.text(t[1]-0.1, 0*t[0]-0.03, r'$t_1$')\n", + " plt.text(t[2]-0.1, 0*t[0]-0.03, r'$t_2$')\n", + " plt.text(t[3]-0.1, 0*t[0]-0.03, r'$t_3$')\n", + " plt.text(t[4]-0.1, 0*t[0]-0.03,r'$t_4$')\n", + " plt.text(t[0]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", + " plt.text(t[1]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", + " plt.text(t[2]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", + " plt.text(t[3]+0.5, 0*t[0]+0.02, r'$\\Delta t$')\n", + " plt.yticks([])#plt.ylabel('Population (millions)')\n", + " plt.xlabel('time(years)')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "279ea532", + "metadata": { + "execution": {} + }, + "source": [ + "### Coding Exercise 1.3: Step, step, step\n", + "\n", + "Given the population differential equation:\n", + "\\begin{align*}\n", + "\\frac{d}{dt}\\,p(t) &= 0.3 p(t),\\\\\n", + "\\end{align*}\n", + "\n", + "and the initial condition:\n", + "\n", + "\\begin{align*}\n", + "p(t_0=1)&=e^{0.3},\n", + "\\end{align*}\n", + "\n", + "code the difference equation:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{p[k+1]}&=\\color{green}{p[k]}+\\color{blue}{\\Delta t} (\\color{blue}{0.3} \\color{green}{p[k]}),\\\\\n", + "\\color{green}{p[0]}&=e^{0.3},\\quad \\text{Initial Condition,}\\\\\n", + "&\\text{for } k=0,1,\\cdots 4,\\\\\n", + "\\end{align*}\n", + "\n", + "to estimate the population on the interval $[1,5]$ with a time-step $\\Delta t=1$, denoted by `dt` in code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b872e66", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# Time step\n", + "dt = 1\n", + "\n", + "# Make time range from 1 to 5 years with step size dt\n", + "t = np.arange(1, 5+dt/2, dt)\n", + "\n", + "# Get number of steps\n", + "n = len(t)\n", + "\n", + "# Initialize p array\n", + "p = np.zeros(n)\n", + "p[0] = np.exp(0.3*t[0]) # initial condition\n", + "\n", + "# Loop over steps\n", + "for k in range(n-1):\n", + "\n", + " ########################################################################\n", + " ## TODO for students\n", + " ## Complete line of code and remove\n", + " raise NotImplementedError(\"Student exercise: calculate the population step for each time point\")\n", + " ########################################################################\n", + "\n", + " # Calculate the population step\n", + " p[k+1] = ...\n", + "\n", + "# Visualize\n", + "visualize_population_approx(t, p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "LHAKHn_5UzLQ", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Time step\n", + "dt = 1\n", + "\n", + "# Make time range from 1 to 5 years with step size dt\n", + "t = np.arange(1, 5+dt/2, dt)\n", + "\n", + "# Get number of steps\n", + "n = len(t)\n", + "\n", + "# Initialize p array\n", + "p = np.zeros(n)\n", + "p[0] = np.exp(0.3*t[0]) # initial condition\n", + "\n", + "# Loop over steps\n", + "for k in range(n-1):\n", + "\n", + " # Calculate the population step\n", + " p[k+1] = p[k] + dt * 0.3 * p[k]\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " visualize_population_approx(t, p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Yvl3OBBPkCQr", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Step_step_step_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "id": "d9301447", + "metadata": { + "execution": {} + }, + "source": [ + "The error is smaller for 4-time steps than taking one large time step from 1 to 5 but does note that the error is increasing for each step. This is known as __global error__ so the further in time you want to predict, the larger the error.\n", + "\n", + "You can read the theorems [here](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)." + ] + }, + { + "cell_type": "markdown", + "id": "b419bf50", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 2: Euler method for the leaky integrate and fire\n", + "\n", + "\n", + "*Estimated timing to here from start of tutorial: 26 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "JOA7EzauxIV6", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 3: Leaky integrate and fire\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'T85OcgY7xjo'), ('Bilibili', 'BV1k5411T7by')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "GeAbFXiSkDEd", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Leaky_integrate_and_fire_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "Zjro4hXwxU8O", + "metadata": { + "execution": {} + }, + "source": [ + "
\n", + " Click here for text recap of video \n", + "\n", + "The Leaky Integrate and Fire (LIF) differential equation is:\n", + "\n", + "\\begin{align}\n", + "\\frac{dV}{dt} = \\frac{-(V-E_L) + R_mI(t)}{\\tau_m}\\,\n", + "\\end{align}\n", + "\n", + "where $\\tau_m$ is the time constant, $V$ is the membrane potential, $E_L$ is the resting potential, $R_m$ is leak resistance and $I(t)$ is the external input current.\n", + "\n", + "The solution of the LIF can be estimated by applying the Euler method to give the difference equation:\n", + "\n", + "\\begin{align}\n", + "\\frac{\\color{red}{V[k+1]}-\\color{green}{V[k]}}{\\color{blue}{\\Delta t}}=\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", + "\\end{align}\n", + "where $V[k]$ is the estimate of the membrane potential at time point $t[k]$.\n", + "Re-arranging the equation such that all the known terms are on the right gives:\n", + "\n", + "\\begin{align}\n", + "\\color{red}{V[k+1]}=\\color{green}{V[k]}+\\color{blue}{\\Delta t}\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", + "\\text{for } k=0\\cdots n-1,\n", + "\\end{align}\n", + "\n", + "where $\\color{red}{V[k+1]}$ is the unknown membrane potential at $t[k+1]$, $\\color{green}{V[k]} $ is known membrane potential, $\\color{blue}{E_L}$, $\\color{blue}{R_m}$ and $\\color{blue}{\\tau_m}$ are known parameters, $\\color{blue}{\\Delta t}$ is a chosen time-step and $I(t_k)$ is a function for an external input current." + ] + }, + { + "cell_type": "markdown", + "id": "1efb9d89", + "metadata": { + "execution": {} + }, + "source": [ + "## Coding Exercise 2: LIF and Euler\n", + "Code the difference equation for the LIF:\n", + "\n", + "\\begin{align}\n", + "\\color{red}{V[k+1]}=\\color{green}{V[k]}+\\color{blue}{\\Delta t}\\big(\\frac{-(\\color{green}{V[k]}-\\color{blue}{E_L}) + \\color{blue}{R_m}I[k]}{\\color{blue}{\\tau_m}}\\big),\\\\\n", + "\\text{for } k=0\\cdots n-1,\n", + "\\end{align}\n", + "\n", + "with the given parameters set as:\n", + "* `V_reset = -75,`\n", + "* `E_L = -75,`\n", + "* `tau_m = 10,`\n", + "* `R_m = 10.`\n", + "\n", + "We will then visualize the result.\n", + "The figure has three panels:\n", + "* the top panel is the sinusoidal input, $I$,\n", + "* the middle panel is the estimate membrane potential $V_k$. To illustrate a spike, $V_k$ is set to $0$ and then reset,\n", + "* the bottom panel is the raster plot with each dot indicating a spike." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e7d29bb", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "def Euler_Integrate_and_Fire(I, time, dt):\n", + " \"\"\"\n", + " Args:\n", + " I: Input\n", + " time: time\n", + " dt: time-step\n", + " Returns:\n", + " Spike: Spike count\n", + " Spike_time: Spike times\n", + " V: membrane potential esitmated by the Euler method\n", + " \"\"\"\n", + "\n", + " Spike = 0\n", + " tau_m = 10\n", + " R_m = 10\n", + " t_isi = 0\n", + " V_reset = E_L = -75\n", + " n = len(time)\n", + " V = V_reset * np.ones(n)\n", + " V_th = -50\n", + " Spike_time = []\n", + "\n", + " for k in range(n-1):\n", + " #######################################################################\n", + " ## TODO for students: calculate the estimate solution of V at t[i+1]\n", + " ## Complete line of codes for dV and remove\n", + " ## Run the code in Section 5.1 or 5.2 to see the output!\n", + " raise NotImplementedError(\"Student exercise: calculate the estimate solution of V at t[i+1]\")\n", + " ########################################################################\n", + "\n", + " dV = ...\n", + " V[k+1] = V[k] + dt*dV\n", + "\n", + " # Discontinuity for Spike\n", + " if V[k] > V_th:\n", + " V[k] = 0\n", + " V[k+1] = V_reset\n", + " t_isi = time[k]\n", + " Spike = Spike + 1\n", + " Spike_time = np.append(Spike_time, time[k])\n", + "\n", + " return Spike, Spike_time, V\n", + "\n", + "# Set up time step and current\n", + "dt = 1\n", + "t = np.arange(0, 1000, dt)\n", + "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", + "\n", + "# Model integrate and fire neuron\n", + "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", + "\n", + "# Visualize\n", + "plot_IF(t, V,I,Spike_time)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55785e26", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "def Euler_Integrate_and_Fire(I, time, dt):\n", + " \"\"\"\n", + " Args:\n", + " I: Input\n", + " time: time\n", + " dt: time-step\n", + " Returns:\n", + " Spike: Spike count\n", + " Spike_time: Spike times\n", + " V: membrane potential esitmated by the Euler method\n", + " \"\"\"\n", + "\n", + " Spike = 0\n", + " tau_m = 10\n", + " R_m = 10\n", + " t_isi = 0\n", + " V_reset = E_L = -75\n", + " n = len(time)\n", + " V = V_reset * np.ones(n)\n", + " V_th = -50\n", + " Spike_time = []\n", + "\n", + " for k in range(n-1):\n", + " dV = (-(V[k] - E_L) + R_m*I[k]) / tau_m\n", + " V[k+1] = V[k] + dt*dV\n", + "\n", + " # Discontinuity for Spike\n", + " if V[k] > V_th:\n", + " V[k] = 0\n", + " V[k+1] = V_reset\n", + " t_isi = time[k]\n", + " Spike = Spike + 1\n", + " Spike_time = np.append(Spike_time, time[k])\n", + "\n", + " return Spike, Spike_time, V\n", + "\n", + "# Set up time step and current\n", + "dt = 1\n", + "t = np.arange(0, 1000, dt)\n", + "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", + "\n", + "# Model integrate and fire neuron\n", + "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_IF(t, V,I,Spike_time)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ggAQjBCekEm7", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_LIF_and_Euler_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "id": "0d322dc9", + "metadata": { + "execution": {} + }, + "source": [ + "As electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the LIF neuron below. Note: this does not work on all browsers so just move on if you can't hear the audio." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd8f60c2", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to visualize the LIF for sinusoidal input*\n", + "\n", + "dt = 1\n", + "t = np.arange(0, 1000, dt)\n", + "I = np.sin(4 * 2 * np.pi * t/1000) + 2\n", + "Spike, Spike_time, V = Euler_Integrate_and_Fire(I, t, dt)\n", + "\n", + "plot_IF(t, V,I,Spike_time)\n", + "plt.show()\n", + "ipd.Audio(V, rate=1000/dt)" + ] + }, + { + "cell_type": "markdown", + "id": "L5o2liqz3bxi", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 3: Systems of differential equations\n", + "\n", + "\n", + "*Estimated timing to here from start of tutorial: 34 min*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "RkM3-RucX2c-", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 4: Systems of differential equations\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'A3Puozl9nEs'), ('Bilibili', 'BV1XV411s76a')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "x5wo7eFJkGBS", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Systems_of_differential_equations_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "dTy23IBPZxYG", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 3.1: Using Euler to approximate a simple system\n", + "\n", + "*Estimated timing to here from start of tutorial: 40 min*" + ] + }, + { + "cell_type": "markdown", + "id": "85df22b9", + "metadata": { + "execution": {} + }, + "source": [ + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "To get to grips with solving a system of differential equations using the Euler method, we will simplify the Wilson Cowan model, a set of equations that will be explored in more detail in the Dynamical Systems day.\n", + "Looking at systems of differential equations will also allow us to introduce the concept of a phase-plane plot which is a method of investigating how different processes interact.\n", + "\n", + "In the previous section we looked at the LIF model for a single neuron. We now model a collection of neurons using a differential equation which describes the firing rate of a population of neurons.\n", + "We will model the firing rate $r$ of two types of populations of neurons which interact, the excitation population firing rate $r_E$ and inhibition population firing rate $r_I$. These firing rates of neurons regulate each other by weighted connections $w$. The directed graph below illustrates this.\n", + "\n", + "Our system of differential equations is a linear version of the Wilson Cowan model. Consider the equations,\n", + "\n", + "\\begin{align}\n", + "\\tau_E \\frac{dr_E}{dt} &= w_{EE}r_E+w_{EI}r_I, \\\\\n", + "\\tau_I \\frac{dr_I}{dt} &= w_{IE}r_E+w_{II}r_I\n", + "\\end{align}\n", + "\n", + "$r_E(t)$ represents the average activation (or firing rate) of the excitatory population at time $t$, and $r_I(t)$ the activation (or firing rate) of the inhibitory population. The parameters $\\tau_E$ and $\\tau_I$ control the timescales of the dynamics of each population. Connection strengths are given by: $w_{EE}$ (E $\\rightarrow$ E), $w_{EI}$ (I $\\rightarrow$ E), $w_{IE}$ (E $\\rightarrow$ I), and $w_{II}$ (I $\\rightarrow$ I). The terms $w_{EI}$ and $w_{IE}$ represent connections from inhibitory to excitatory population and vice versa, respectively.\n", + "\n", + "To start we will further simplify the linear system of equations by setting $w_{EE}$ and $w_{II}$ to zero, we now have the equations\n", + "\n", + "\\begin{align}\n", + "\\tau_E \\frac{dr_E}{dt} &= w_{EI}r_I, \\\\\n", + "\\tau_I \\frac{dr_I}{dt} &= w_{IE}r_E, \\qquad (1)\n", + "\\end{align}\n", + "\n", + "where $\\tau_E=100$ and $\\tau_I=120$, no internal connection $w_{EE}=w_{II}=0$, and $w_{EI}=-1$ and $w_{IE}=1$,\n", + "with the initial conditions\n", + "\n", + "\\begin{align}\n", + "r_E(0)=30, \\\\\n", + "r_I(0)=20.\n", + "\\end{align}\n", + "\n", + "The solution can be approximated using the Euler method such that we have the difference equations:\n", + "\n", + "\\begin{align*}\n", + "\\frac{\\color{red}{r_E[k+1]}-\\color{green}{r_E[k]}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\frac{\\color{red}{r_I[k+1]}-\\color{green}{r_I[k]}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "\\end{align*}\n", + "where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t[k])$ and inhibition population $r_I(t[k])$ and $\\Delta t$ is the time-step.\n", + "\n", + "Re-arranging the equation such that all the known terms are on the right gives:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{r_E[k+1]}&=\\color{green}{r_E[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\color{red}{r_I[k+1]}&=\\color{green}{r_I[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "&\\text{ for } k=0, \\cdots , n-1,\\\\\\\\\n", + "r_E[0]&=30,\\\\\n", + "r_I[0]&=20.\\\\\n", + "\\end{align*}\n", + "\n", + "where $\\color{red}{r_E[k+1]}$ and $\\color{red}{r_I[k+1]}$ are unknown, $\\color{green}{r_E[k]} $ and $\\color{green}{r_I[k]} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$ and $\\color{blue}{\\tau_E}=100ms$ and $\\color{blue}{\\tau_I}=120ms$ are known parameters and $\\color{blue}{\\Delta t}=0.01ms$ is a chosen time-step.\n", + "
\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "0xhGLDhgdq1P", + "metadata": { + "execution": {} + }, + "source": [ + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "067b5f6e", + "metadata": { + "execution": {} + }, + "source": [ + "### Coding Exercise 3.1: Euler on a Simple System\n", + "\n", + "Our difference equations are:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{r_E[k+1]}&=\\color{green}{r_E[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EI}}\\color{green}{r_I[k]}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\color{red}{r_I[k+1]}&=\\color{green}{r_I[k]}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{IE}}\\color{green}{r_E[k]}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "&\\text{ for } k=0, \\cdots , n-1,\\\\\\\\\n", + "r_E[0]&=30,\\\\\n", + "r_I[0]&=20.\\\\\n", + "\\end{align*}\n", + "\n", + "where $\\color{red}{r_E[k+1]}$ and $\\color{red}{r_I[k+1]}$ are unknown, $\\color{green}{r_E[k]} $ and $\\color{green}{r_I[k]} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$ and $\\color{blue}{\\tau_E}=100ms$ and $\\color{blue}{\\tau_I}=120ms$ are known parameters and $\\color{blue}{\\Delta t}=0.01ms$ is a chosen time-step.\n", + "__Code the difference equations to estimate $r_{E}$ and $r_{I}$__.\n", + "\n", + "Note that the equations have to estimated in the same `for` loop as they depend on each other." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1514105", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "def Euler_Simple_Linear_System(t, dt):\n", + " \"\"\"\n", + " Args:\n", + " time: time\n", + " dt : time-step\n", + " Returns:\n", + " r_E : Excitation Firing Rate\n", + " r_I : Inhibition Firing Rate\n", + "\n", + " \"\"\"\n", + "\n", + " # Set up parameters\n", + " tau_E = 100\n", + " tau_I = 120\n", + " n = len(t)\n", + " r_I = np.zeros(n)\n", + " r_I[0] = 20\n", + " r_E = np.zeros(n)\n", + " r_E[0] = 30\n", + "\n", + " #######################################################################\n", + " ## TODO for students: calculate the estimate solutions of r_E and r_I at t[i+1]\n", + " ## Complete line of codes for dr_E and dr_I and remove\n", + " raise NotImplementedError(\"Student exercise: calculate the estimate solutions of r_E and r_I at t[i+1]\")\n", + " ########################################################################\n", + "\n", + " # Loop over time steps\n", + " for k in range(n-1):\n", + "\n", + " # Estimate r_e\n", + " dr_E = ...\n", + " r_E[k+1] = r_E[k] + dt*dr_E\n", + "\n", + " # Estimate r_i\n", + " dr_I = ...\n", + " r_I[k+1] = r_I[k] + dt*dr_I\n", + "\n", + " return r_E, r_I\n", + "\n", + "# Set up dt, t\n", + "dt = 0.1 # time-step\n", + "t = np.arange(0, 1000, dt)\n", + "\n", + "# Run Euler method\n", + "r_E, r_I = Euler_Simple_Linear_System(t, dt)\n", + "\n", + "# Visualize\n", + "plot_rErI(t, r_E, r_I)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "JnLfmtc5ZWXz", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "def Euler_Simple_Linear_System(t, dt):\n", + " \"\"\"\n", + " Args:\n", + " time: time\n", + " dt : time-step\n", + " Returns:\n", + " r_E : Excitation Firing Rate\n", + " r_I : Inhibition Firing Rate\n", + "\n", + " \"\"\"\n", + "\n", + " # Set up parameters\n", + " tau_E = 100\n", + " tau_I = 120\n", + " n = len(t)\n", + " r_I = np.zeros(n)\n", + " r_I[0] = 20\n", + " r_E = np.zeros(n)\n", + " r_E[0] = 30\n", + "\n", + " # Loop over time steps\n", + " for k in range(n-1):\n", + "\n", + " # Estimate r_e\n", + " dr_E = -r_I[k]/tau_E\n", + " r_E[k+1] = r_E[k] + dt*dr_E\n", + "\n", + " # Estimate r_i\n", + " dr_I = r_E[k]/tau_I\n", + " r_I[k+1] = r_I[k] + dt*dr_I\n", + "\n", + " return r_E, r_I\n", + "\n", + "# Set up dt, t\n", + "dt = 0.1 # time-step\n", + "t = np.arange(0, 1000, dt)\n", + "\n", + "# Run Euler method\n", + "r_E, r_I = Euler_Simple_Linear_System(t, dt)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_rErI(t, r_E, r_I)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ZGYMiHlAkHbV", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Euler_on_a_simple_system_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "id": "Fk-EvkAOImKR", + "metadata": { + "execution": {} + }, + "source": [ + "### Think! 3.1: Simple Euler solution to the Wilson Cowan model\n", + "\n", + "1. Is the simulation biologically plausible?\n", + "2. What is the effect of combined excitation and inhibition?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "i3L1OGeUgZEe", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. The simulation is not biologically plausible as there are negative firing\n", + " rates but a mathematician could just say that it is firing rate relative to\n", + " baseline.\n", + " 2. Excitation and inhibition creates an oscillation.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "gyCVFYRHkIMH", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Simple_Euler_solution_to_the_Wilson_Cowan_model_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "id": "Cw-R4Yd4QCTH", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 3.2: Phase Plane Plot and Nullcline\n", + "\n", + "\n", + "*Estimated timing to here from start of tutorial: 50 min*\n", + "\n", + "
\n", + " Click here for text recap of relevant part of video \n", + "\n", + "When there are two differential equations describing the interaction of two processes like excitation $r_{E}$ and inhibition $r_{I}$ that are dependent on each other they can be plotted as a function of each other, which is known as a phase plane plot. The phase plane plot can give insight give insight into the state of the system but more about that later in Neuromatch Academy.\n", + "\n", + "In the animated figure below, the panel of the left shows the excitation firing rate $r_E$ and the inhibition firing rate $r_I$ as a function of time. The panel on the right hand side is the phase plane plot which shows the inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$.\n", + "\n", + "An addition to the phase plane plot are the \"nullcline\". These lines plot when the rate of change $\\frac{d}{dt}$ of the variables is equal to $0$. We saw a variation of this for a single differential equation in the differential equation tutorial.\n", + "\n", + "As we have two differential equations we set $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0$ which gives the equations,\n", + "\n", + "\\begin{align}\n", + "0&= w_{EI}r_I, \\\\\n", + "0&= w_{IE}r_E,\\\\\n", + "\\end{align}\n", + "\n", + "these are a unique example as they are a vertical and horizontal line. Where the lines cross is the stable point which the $r_E(t)$ excitatory population and $r_I(t)$ the inhibitory population orbit around." + ] + }, + { + "cell_type": "markdown", + "id": "2ghDuf-YeG6z", + "metadata": { + "execution": {} + }, + "source": [ + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "98f3c696", + "metadata": { + "execution": {} + }, + "source": [ + "### Think! 6.1: Discuss the Plots\n", + "\n", + "1. Which representation is more intuitive (and useful), the time plot or the phase plane plot?\n", + "2. Why do we only see one circle?\n", + "3. What do the quadrants represent?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "036d3fb5", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute the code to plot the solution to the system\n", + "plot_rErI_Simple(t, r_E, r_I)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Sfa1foAYhB13", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + " 1. Personal preference: both have their benefits\n", + " 2. The solution is stable.\n", + " 3. The quadrants are:\n", + " - Top right positive derivative for inhibition and negative for excitation (r_I increases, r_E decreases)\n", + " - top left negative derivative for both inhibition and excitation (r_I decrease, r_E decrease)\n", + " - bottom left negative derivative for inhibition and positive for excitation (r_I decrease, r_E increase)\n", + " - bottom right positive derivative for inhibition and excitation (r_I increase, r_E increase)\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "nDKgymrbkJJ9", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Discuss_the_Plots_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "id": "5a278386", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 3.3: Adding internal connections\n", + "\n", + "*Estimated timing to here from start of tutorial: 57 min*\n", + "\n", + "Building up the equations in the previous section we re-introduce internal connections $w_{EE}$, $w_{II}$. The two coupled differential equations, each representing the dynamics of the excitatory or inhibitory population are now:\n", + "\n", + "\\begin{align}\n", + "\\tau_E \\frac{dr_E}{dt} &=w_{EE}r_E +w_{EI}r_I, \\\\\n", + "\\tau_I \\frac{dr_I}{dt} &=w_{IE}r_E +w_{II}r_I ,\n", + "\\end{align}\n", + "\n", + "where $\\tau_E=100$ and $\\tau_I=120$, $w_{EE}=1$ and $w_{II}=-1$, and $w_{EI}=-5$ and $w_{IE}=0.6$,\n", + "with the initial conditions\n", + "\n", + "\\begin{align}\n", + "r_E(0)=30, \\\\\n", + "r_I(0)=20.\n", + "\\end{align}\n", + "\n", + "The solutions can be approximated using the Euler method such that the equations become:\n", + "\n", + "\\begin{align*}\n", + "\\frac{\\color{red}{rE_{k+1}}-\\color{green}{rE_k}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{EE}}\\color{green}{rE_k}+\\color{blue}{w_{EI}}\\color{green}{rI_k}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\frac{\\color{red}{rI_{k+1}}-\\color{green}{rI_k}}{\\color{blue}{\\Delta t}}&=\\big(\\frac{\\color{blue}{w_{II}}\\color{green}{rI_k}+\\color{blue}{w_{IE}}\\color{green}{rE_k}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "\\end{align*}\n", + "where $r_E[k]$ and $r_I[k]$ are the numerical estimates of the firing rate of the excitation population $r_E(t_k)$ and inhibition population $r_I(t_K)$ and $\\Delta t$ is the time-step.\n", + "\n", + "Re-arranging the equation such that all the known terms are on the right gives:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{rE_{k+1}}&=\\color{green}{rE_k}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{EE}}\\color{green}{rE_k}+\\color{blue}{w_{EI}}\\color{green}{rI_k}}{\\color{blue}{\\tau_E}}\\big),\\\\\n", + "\\color{red}{rI_{k+1}}&=\\color{green}{rI_k}+\\color{blue}{\\Delta t}\\big(\\frac{\\color{blue}{w_{II}}\\color{green}{rI_k}+\\color{blue}{w_{IE}}\\color{green}{rE_k}}{\\color{blue}{\\tau_I}}\\big),\\\\\n", + "&\\text{ for } k=0, \\cdots n-1,\\\\\\\\\n", + "rE_0&=30,\\\\\n", + "rI_0&=20.\\\\\n", + "\\end{align*}\n", + "\n", + "where $\\color{red}{rE_{k+1}}$ and $\\color{red}{rI_{k+1}}$ are unknown, $\\color{green}{rE_{k}} $ and $\\color{green}{rI_{k}} $ are known, $\\color{blue}{w_{EI}}=-1$, $\\color{blue}{w_{IE}}=1$, $\\color{blue}{w_{EE}}=1$, $\\color{blue}{w_{II}}=-1$ and $\\color{blue}{\\tau_E}=100$ and $\\color{blue}{\\tau_I}=120$ are known parameters and $\\color{blue}{\\Delta t}=0.1$ is a chosen time-step." + ] + }, + { + "cell_type": "markdown", + "id": "byUkHQNsJDF-", + "metadata": { + "execution": {} + }, + "source": [ + "### Think! 3.3: Oscillations\n", + "\n", + "\n", + "The code below implements and visualizes the linear Wilson-Cowan model.\n", + "\n", + "1. What will happen to the oscillations if the time period is extended?\n", + "2. How would you control or predict the oscillations?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e00d88fb", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to visualize the Linear Willson-Cowan*\n", + "\n", + "def Euler_Linear_System_Matrix(t, dt, w_EE=1):\n", + " \"\"\"\n", + " Args:\n", + " time: time\n", + " dt: time-step\n", + " w_EE: Excitation to excitation weight\n", + " Returns:\n", + " r_E: Excitation Firing Rate\n", + " r_I: Inhibition Firing Rate\n", + " N_Er: Nullclines for drE/dt=0\n", + " N_Ir: Nullclines for drI/dt=0\n", + " \"\"\"\n", + "\n", + " tau_E = 100\n", + " tau_I = 120\n", + " n = len(t)\n", + " r_I = 20*np.ones(n)\n", + " r_E = 30*np.ones(n)\n", + " w_EI = -5\n", + " w_IE = 0.6\n", + " w_II = -1\n", + "\n", + " for k in range(n-1):\n", + "\n", + " # Calculate the derivatives of the E and I populations\n", + " drE = (w_EI*r_I[k] + w_EE*r_E[k]) / tau_E\n", + " r_E[k+1] = r_E[k] + dt*drE\n", + "\n", + " drI = (w_II*r_I[k] + w_IE*r_E[k]) / tau_I\n", + " r_I[k+1] = r_I[k] + dt*drI\n", + "\n", + "\n", + " N_Er = -w_EE / w_EI*r_E\n", + " N_Ir = -w_IE / w_II*r_E\n", + "\n", + " return r_E, r_I, N_Er, N_Ir\n", + "\n", + "\n", + "dt = 0.1 # time-step\n", + "t = np.arange(0, 1000, dt)\n", + "r_E, r_I, _, _ = Euler_Linear_System_Matrix(t, dt)\n", + "plot_rErI(t, r_E, r_I)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ACUE5rriyyR", + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1. The oscillations start getting larger and larger which could in the end become uncontrollable\n", + "2. Looking at the shape of the spiral\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "z4gwxcvJkKKN", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Oscillations_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "id": "261b8f71", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 3.4 Phase Plane and Nullclines Part 2\n", + "\n", + "*Estimated timing to here from start of tutorial: 62 min*\n", + "\n", + "Like before, we have two differential equations so we can plot the results on a phase plane. We can also calculate the Nullclines when we set $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0$ which gives,\n", + "\\begin{align}\n", + "0&= w_{EE}r_E+w_{EI}r_I, \\\\\n", + "0&= w_{IE}r_E+w_{II}r_I,\n", + "\\end{align}\n", + "re-arranging as two lines\n", + "\\begin{align}\n", + "r_I&= -\\frac{w_{EE}}{w_{EI}}r_E, \\\\\n", + "r_I&= -\\frac{w_{IE}}{w_{II}}r_E,\n", + "\\end{align}\n", + "which crosses at the stable point.\n", + "\n", + "The panel on the left shows excitation firing rate $r_E$ and inhibition firing rate $r_I$ as a function of time. On the right side the phase plane plot shows inhibition firing rate $r_I$ as a function of excitation firing rate $r_E$ with the Nullclines for $\\frac{dr_E}{dt}=0$ and $\\frac{dr_I}{dt}=0.$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ca5f555", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown *Run this cell to visualize the phase plane*\n", + "dt = 0.1 # time-step\n", + "t = np.arange(0, 1000, dt)\n", + "r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt)\n", + "plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)" + ] + }, + { + "cell_type": "markdown", + "id": "4d220e96", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 3.4: A small change changes everything\n", + "\n", + "We will illustrate that even changing one parameter in a system of differential equations can have a large impact on the solutions of the excitation firing rate $r_E$ and inhibition firing rate $r_I$.\n", + "Interact with the widget below to change the size of $w_{EE}$.\n", + "\n", + "Take note of:\n", + "1. How the solution changes for positive and negative of $w_{EE}$. Pay close attention to the axis.\n", + "2. How would you maintain a stable oscillation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42724aca", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " w_EE=widgets.FloatSlider(1, min=-1., max=2., step=.1,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(w_EE):\n", + " dt = 0.1 # time-step\n", + " t = np.arange(0,1000,dt)\n", + " r_E, r_I, N_Er, N_Ir = Euler_Linear_System_Matrix(t, dt, w_EE)\n", + " plot_rErI_Matrix(t, r_E, r_I, N_Er, N_Ir)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "sH4Bpv7lkLFe", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Small_change_changes_everything_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "1268fd80", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Summary\n", + "\n", + "*Estimated timing of tutorial: 70 minutes*\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cEcFr2llcl-l", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 5: Summary\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '5UmGgboSc40'), ('Bilibili', 'BV1wM4y1g78M')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "uzv5CG0AkLwR", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Summary_Video\")" + ] + }, + { + "cell_type": "markdown", + "id": "-JZJWqTnitQx", + "metadata": { + "execution": {} + }, + "source": [ + "Using the formula for the slope of a line, the solution of the differential equation can be estimated with reasonable accuracy. This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution." + ] + }, + { + "cell_type": "markdown", + "id": "a307a8fa", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "## Links to Neuromatch Computational Neuroscience Days\n", + "\n", + "Differential equations turn up on a number of different Neuromatch days:\n", + "* The LIF model is discussed in more detail in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", + "* Drift Diffusion model, which is a differential equation for decision making, is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", + "* Phase-plane plots are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", + "* The Wilson-Cowan model is discussed in [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", + "\n", + "## References\n", + "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", + "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", + "\n", + "\n", + "## Bibliography\n", + "1. Dayan, P., & Abbott, L. F. (2001). Theoretical neuroscience: computational and mathematical modeling of neural systems. Computational Neuroscience Series.\n", + "2. Strogatz, S. (2014). Nonlinear dynamics and chaos: with applications to physics, biology, chemistry, and engineering (studies in nonlinearity), Westview Press; 2nd edition\n", + "\n", + "### Supplemental Popular Reading List\n", + "1. Lindsay, G. (2021). Models of the Mind: How Physics, Engineering and Mathematics Have Shaped Our Understanding of the Brain. Bloomsbury Publishing.\n", + "2. Strogatz, S. (2004). Sync: The emerging science of spontaneous order. Penguin UK.\n", + "\n", + "### Popular Podcast\n", + "1. Strogatz, S. (Host). (2020), Joy of X https://www.quantamagazine.org/tag/the-joy-of-x/ Quanta Magazine" + ] + }, + { + "cell_type": "markdown", + "id": "_bnKhPLwobY9", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Bonus" + ] + }, + { + "cell_type": "markdown", + "id": "3c9b3f6e", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "## Bonus Section 1: The 4th Order Runge-Kutta method\n", + "\n", + "Another popular numerical method to estimate the solution of differential equations of the general form:\n", + "\n", + "\\begin{align}\n", + "\\frac{d}{dt}y=f(t,y)\n", + "\\end{align}\n", + "\n", + "is the 4th Order Runge Kutta:\n", + "\\begin{align}\n", + "k_1 &= f(t_k,y_k)\\\\\n", + "k_2 &= f(t_k+\\frac{\\Delta t}{2},y_k+\\frac{\\Delta t}{2}k_1)\\\\\n", + "k_3 &= f(t_k+\\frac{\\Delta t}{2},y_k+\\frac{\\Delta t}{2}k_2)\\\\\n", + "k_4 &= f(t_k+\\Delta t,y_k+\\Delta tk_3)\\\\\n", + "y_{k+1} &= y_{k}+\\frac{\\Delta t}{6}(k_1+2k_2+2k_3+k_4)\\\\\n", + "\\end{align}\n", + "\n", + "for $k=0,1,\\cdots,n-1$,\n", + "which is more accurate than the Euler method.\n", + "\n", + "Given the population differential equation\n", + "\\begin{align*}\n", + "\\frac{d}{dt}\\,p(t) &= 0.3 p(t),\\\\\n", + "p(t_0=1)&=e^{0.3}\\quad \\text{Initial Condition},\n", + "\\end{align*}\n", + "\n", + "the 4th Runge Kutta difference equation is\n", + "\n", + "\\begin{align*}\n", + "k_1&=0.3\\color{green}{p[k]},\\\\\n", + "k_2&=0.3(\\color{green}{p[k]}+\\frac{\\Delta t}{2}k_1),\\\\\n", + "k_3&=0.3(\\color{green}{p[k]}+\\frac{\\Delta t}{2}k_2),\\\\\n", + "k_4&=0.3(\\color{green}{p[k]}+k_3),\\\\\n", + "\\color{red}{p_{k[k]}}&=\\color{green}{p[k]}+\\frac{\\color{blue}{\\Delta t}}{6}( \\color{green}{k_1+2k_2+2k_3+k_4})\\\\\n", + "\\end{align*}\n", + "\n", + "for $k=0,1,\\cdots 4$ to estimate the population for $\\Delta t=0.5$.\n", + "\n", + "The code is implemented below. Note how much more accurate the 4th Order Runge Kutta (yellow) is compared to the Euler Method (blue). The 4th order Runge Kutta is of order 4 which means that if you half the time-step $\\Delta t$ the error decreases by a factor of $\\Delta t^4$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b936428", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown *Execute this cell to show the difference between the Runge Kutta Method and the Euler Method*\n", + "\n", + "\n", + "def RK4(dt=0.5):\n", + " t=np.arange(1, 5+dt/2, dt)\n", + " t_fine=np.arange(1, 5+0.1/2, 0.1)\n", + " n = len(t)\n", + " p = np.ones(n)\n", + " pRK4 = np.ones(n)\n", + " p[0] = np.exp(0.3*t[0])\n", + " pRK4[0] = np.exp(0.3*t[0])# Initial Condition\n", + "\n", + " with plt.xkcd():\n", + "\n", + " fig = plt.figure(figsize=(6, 4))\n", + " plt.plot(t, np.exp(0.3*t), 'k', label='Exact Solution')\n", + "\n", + " for i in range(n-1):\n", + " dp = dt*0.3*p[i]\n", + " p[i+1] = p[i] + dp # Euler\n", + " k1 = 0.3*(pRK4[i])\n", + " k2 = 0.3*(pRK4[i] + dt/2*k1)\n", + " k3 = 0.3*(pRK4[i] + dt/2*k2)\n", + " k4 = 0.3*(pRK4[i] + dt*k3)\n", + " pRK4[i+1] = pRK4[i] + dt/6 *(k1 + 2*k2 + 2*k3 + k4)\n", + "\n", + " plt.plot(t_fine, np.exp(0.3*t_fine), label='Exact')\n", + " plt.plot(t, p,':ro', label='Euler Estimate')\n", + " plt.plot(t, pRK4,':co', label='4th Order Runge Kutta Estimate')\n", + "\n", + " plt.ylabel('Population (millions)')\n", + " plt.legend()\n", + " plt.xlabel('Time (years)')\n", + " plt.show()\n", + "\n", + "# @markdown Make sure you execute this cell to enable the widget!\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " dt=widgets.FloatSlider(0.5, min=.1, max=4., step=.1,\n", + " layout=my_layout)\n", + ")\n", + "def Pop_widget(dt):\n", + " RK4(dt)" + ] + }, + { + "cell_type": "markdown", + "id": "W7ziQulJL6Kl", + "metadata": { + "execution": {} + }, + "source": [ + "**Bonus Reference 1: A full course on numerical methods in Python**\n", + "\n", + "For a full course on Numerical Methods for differential Equations you can look [here](https://github.com/john-s-butler-dit/Numerical-Analysis-Python)." + ] + }, + { + "cell_type": "markdown", + "id": "jXpJytN7dgYn", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "## Bonus Section 2: Neural oscillations are a start toward understanding brain activity rather than the end\n", + "\n", + "The differential equations we have discussed above are all to simulate neuronal processes. Another way differential equations can be used is to motivate experimental findings.\n", + "\n", + "Many experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrees to a stimulus.\n", + "An issue with these studies is that there is no consistent definition of what constitutes an oscillation. Right now, it is a bit of I know one when I see one problem.\n", + "\n", + "In an essay from May 2021 in PLoS Biology, Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally when studying oscillations. The essay proposes that instead of thinking about the brain, we should look at this question from the mathematical side to motivate what can be defined as an oscillation.\n", + "\n", + "To do this, they used Stuart–Landau equations, which is a system of differential equations\n", + "\\begin{align}\n", + "\\frac{dx}{dt} &= \\lambda x-\\omega y -\\gamma (x^2+y^2)x+s\\\\\n", + "\\frac{dy}{dt} &= \\lambda y+\\omega x -\\gamma (x^2+y^2)y\n", + "\\end{align}\n", + "\n", + "where $s$ is input to the system, and $\\lambda$, $\\omega$ and $\\gamma$ are parameters.\n", + "\n", + "The Stuart–Landau equations are a well-described system of non-linear differential equations that generate oscillations. For their purpose, Dowling and Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.\n", + "In their paper, using the Stuart–Landau equations, they outline\n", + "* \"What is an oscillator?\"\n", + "* \"What an oscillator is not.\"\n", + "* \"Not all that oscillates is an oscillator.\"\n", + "* \"Not all oscillators are alike.\"\n", + "\n", + "The Euler form of the Stuart–Landau system of equations is:\n", + "\n", + "\\begin{align*}\n", + "\\color{red}{x_{k+1}}&=\\color{green}{x_k}+\\color{blue}{\\Delta t}\\big(\\lambda \\color{green}{x_k}-\\omega \\color{green}{y_k} -\\gamma (\\color{green}{x_k}^2+\\color{green}{y_k}^2)\\color{green}{x_k}+s\\big),\\\\\n", + "\\color{red}{y_{k+1}}&=\\color{green}{y_k}+\\color{blue}{\\Delta t}\\big(\\lambda \\color{green}{y_k}+\\omega \\color{green}{x_k} -\\gamma (\\color{green}{x_k}^2+\\color{green}{y_k}^2)\\color{green}{y_k} \\big),\\\\\n", + "&\\text{ for } k=0, \\cdots n-1,\\\\\n", + "x_0&=1,\\\\\n", + "y_0&=1,\\\\\n", + "\\end{align*}\n", + "\n", + "with $ \\Delta t=0.1/1000$ ms." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b9e30cc", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Helper functions\n", + "def plot_Stuart_Landa(t, x, y, s):\n", + " \"\"\"\n", + " Args:\n", + " t : time\n", + " x : x\n", + " y : y\n", + " s : input\n", + " Returns:\n", + " figure with two panels\n", + " top panel: Input as a function of time\n", + " bottom panel: x\n", + " \"\"\"\n", + "\n", + " with plt.xkcd():\n", + " fig = plt.figure(figsize=(14, 4))\n", + " gs = gridspec.GridSpec(2, 2, height_ratios=[1, 4], width_ratios=[4, 1])\n", + "\n", + " # PLOT OF INPUT\n", + " plt.subplot(gs[0])\n", + " plt.ylabel(r'$s$')\n", + " plt.plot(t, s, 'g')\n", + " #plt.ylim((2,4))\n", + "\n", + " # PLOT OF ACTIVITY\n", + " plt.subplot(gs[2])\n", + " plt.plot(t ,x)\n", + " plt.ylabel(r'x')\n", + " plt.xlabel(r't')\n", + " plt.subplot(gs[3])\n", + " plt.plot(x,y)\n", + " plt.plot(x[0], y[0],'go')\n", + " plt.xlabel(r'x')\n", + " plt.ylabel(r'y')\n", + " plt.show()\n", + "\n", + "\n", + "def Euler_Stuart_Landau(s,time,dt,lamba=0.1,gamma=1,k=25):\n", + " \"\"\"\n", + " Args:\n", + " I: Input\n", + " time: time\n", + " dt: time-step\n", + " \"\"\"\n", + "\n", + " n = len(time)\n", + " omega = 4 * 2*np.pi\n", + " x = np.zeros(n)\n", + " y = np.zeros(n)\n", + " x[0] = 1\n", + " y[0] = 1\n", + "\n", + " for i in range(n-1):\n", + " dx = lamba*x[i] - omega*y[i] - gamma*(x[i]*x[i] + y[i]*y[i])*x[i] + k*s[i]\n", + " x[i+1] = x[i] + dt*dx\n", + " dy = lamba*y[i] + omega*x[i] - gamma*(x[i]*x[i] + y[i]*y[i])*y[i]\n", + " y[i+1] = y[i] + dt*dy\n", + "\n", + " return x, y" + ] + }, + { + "cell_type": "markdown", + "id": "H5inj2EKJyqN", + "metadata": { + "execution": {} + }, + "source": [ + "### Bonus 2.1: What is an Oscillator?\n", + "\n", + "Doelling & Assaneo (2021), using the Stuart–Landau system, show different possible states of an oscillator by manipulating the $\\lambda$ term in the equation.\n", + "\n", + "From the paper:\n", + "\n", + "> This qualitative change in behavior takes place at $\\lambda = 0$. For $\\lambda < 0$, the system decays to a stable equilibrium, while for $\\lambda > 0$, it keeps oscillating.\n", + "\n", + "This illustrates that oscillations do not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\\alpha$ band oscillations in EEG the oscillations come and go." + ] + }, + { + "cell_type": "markdown", + "id": "5MyIOK9w_qjd", + "metadata": { + "execution": {} + }, + "source": [ + "#### Interactive Demo Bonus 2.1: Oscillator\n", + "\n", + "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency $\\omega$ set to $4\\times 2\\pi$, $\\gamma=1$ and $k=25$ over 3 seconds. The input to the system $s(t)$ is plotted in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$. You can manipulate $\\lambda$ to see how the oscillations change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "LAnBGRU5crfG", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "dt=0.1/1000\n", + "t=np.arange(0, 3, dt)\n", + "\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " lamda=widgets.FloatSlider(1, min=-1., max=5., step=0.5,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(lamda):\n", + " s=np.zeros(len(t))\n", + " x,y=Euler_Stuart_Landau(s,t,dt,lamda)\n", + " plot_Stuart_Landa(t, x, y, s)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "PNtk7NRomDuQ", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Oscillator_Bonus_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "q2OMPLOoKGtr", + "metadata": { + "execution": {} + }, + "source": [ + "### Bonus 2.2 : Not all oscillators are alike" + ] + }, + { + "cell_type": "markdown", + "id": "NLOVdwN5_ypj", + "metadata": { + "execution": {} + }, + "source": [ + "#### Interactive Demo Bonus 2: Stuart-Landau System\n", + "\n", + "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency of 4Hz by setting $\\omega$ to $4\\times 2\\pi$, $\\lambda=1$, $\\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\\sin(freq 2\\pi t) $ in the top panel, $x$ as a function of time in the bottom panel and on the right the phase plane plot of $x$ and $y$.\n", + "\n", + "You can manipulate the frequency $freq$ of the input to see how the oscillations change, and for frequencies $freq$ further and further from the base frequency of 4Hz, the oscillations break down.\n", + "\n", + "This shows that if you have an oscillating input into an oscillator, it does not have to respond by oscillating about could even break down. Hence the frequency of the input oscillation is important to the system.\n", + "So if you flash something at 50Hz, for example, the visual system might not follow the signal, but that does not mean the visual system is not an oscillator. It might just be the wrong frequency." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "XPuVG5f4clxt", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "dt=0.1/1000\n", + "t=np.arange(0, 3, dt)\n", + "\n", + "my_layout.width = '450px'\n", + "@widgets.interact(\n", + " freq=widgets.FloatSlider(4, min=0.5, max=10., step=0.5,\n", + " layout=my_layout)\n", + ")\n", + "\n", + "def Pop_widget(freq):\n", + " s = np.sin(freq * 2*np.pi * t)\n", + "\n", + " x, y = Euler_Stuart_Landau(s, t, dt, lamba=1, gamma=.1, k=50)\n", + " plot_Stuart_Landa(t, x, y, s)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "EC5oefXvmOtd", + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Stuart_Landau_System_Bonus_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "id": "1cWYP3SNL1Mh", + "metadata": { + "execution": {} + }, + "source": [ + "**Bonus Reference 2**:\n", + "\n", + "Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234. doi: [10.1371/journal.pbio.3001234](https://doi.org/10.1371/journal.pbio.3001234)" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "1cWYP3SNL1Mh" + ], + "include_colab_link": true, + "name": "W0D4_Tutorial3", + "provenance": [], + "toc_visible": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial1.ipynb b/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial1.ipynb index 4ccc633..4d9a800 100644 --- a/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial1.ipynb +++ b/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial1.ipynb @@ -27,7 +27,7 @@ "\n", "**Content reviewers:** Aderogba Bayo, Tessy Tom, Matt McCann\n", "\n", - "**Production editors:** Matthew McCann, Ella Batty" + "**Production editors:** Matthew McCann, Spiros Chavlis, Ella Batty" ] }, { @@ -50,14 +50,12 @@ "\n", "*Estimated timing of tutorial: 80 minutes*\n", "\n", - "In this tutorial, we will cover aspects of calculus that will be frequently used in the main NMA course. We assume that you have some familiarty with calculus, but may be a bit rusty or may not have done much practice. Specifically the objectives of this tutorial are\n", + "In this tutorial, we will cover aspects of calculus that will be frequently used in the main NMA course. We assume that you have some familiarity with calculus but may be a bit rusty or may not have done much practice. Specifically, the objectives of this tutorial are\n", "\n", - "* Get an intuitive understanding of derivative and integration operations\n", - "* Learn to calculate the derivatives of 1- and 2-dimensional functions/signals numerically\n", - "* Familiarize with the concept of neuron transfer function in 1- and 2-dimensions.\n", - "* Familiarize with the idea of numerical integration using Riemann sum\n", - "\n", - "\n" + "* Get an intuitive understanding of derivative and integration operations\n", + "* Learn to calculate the derivatives of 1- and 2-dimensional functions/signals numerically\n", + "* Familiarize with the concept of the neuron transfer function in 1- and 2-dimensions.\n", + "* Familiarize with the idea of numerical integration using the Riemann sum" ] }, { @@ -494,13 +492,13 @@ "source": [ "### Interactive Demo 1: Geometrical understanding\n", "\n", - "In the interactive demo below, you can pick different functions to examine in the drop down menu. You can then choose to show the derivative function and/or the integral function.\n", + "In the interactive demo below, you can pick different functions to examine in the drop-down menu. You can then choose to show the derivative function and/or the integral function.\n", "\n", "For the integral, we have chosen the unknown constant $C$ such that the integral function at the left x-axis limit is $0$, as $f(t = -10) = 0$. So the integral will reflect the area under the curve starting from that position.\n", "\n", "For each function:\n", "\n", - "* Examine just the function first. Discuss and predict what the derivative and integral will look like. Remember that derivative = slope of function, integral = area under curve from $t = -10$ to that t.\n", + "* Examine just the function first. Discuss and predict what the derivative and integral will look like. Remember that _derivative = slope of the function_, _integral = area under the curve from $t = -10$ to that $t$_.\n", "* Check the derivative - does it match your expectations?\n", "* Check the integral - does it match your expectations?" ] @@ -550,9 +548,9 @@ "execution": {} }, "source": [ - "In the demo above you may have noticed that the derivative and integral of the exponential function is same as the exponential function itself.\n", + "In the demo above, you may have noticed that the derivative and integral of the exponential function are the same as the exponential function itself.\n", "\n", - "Some functions like the exponential function, when differentiated or integrated, equal a scalar times the same function. This is a similar idea to eigenvectors of a matrix being those that, when multipled by the matrix, equal a scalar times themselves, as you saw yesterday!\n", + "When differentiated or integrated, some functions, like the exponential function, equal scalar times the same function. This is a similar idea to eigenvectors of a matrix being those that, when multiplied by the matrix, equal scalar times themselves, as you saw yesterday!\n", "\n", "When\n", "\n", @@ -679,15 +677,11 @@ "execution": {} }, "source": [ - "## Section 2.1: Analytical Differentiation\n", - "\n", - "*Estimated timing to here from start of tutorial: 20 min*\n", + "When we find the derivative analytically, we obtain the exact formula for the derivative function.\n", "\n", - "When we find the derivative analytically, we are finding the exact formula for the derivative function.\n", + "To do this, instead of having to do some fancy math every time, we can often consult [an online resource](https://en.wikipedia.org/wiki/Differentiation_rules) for a list of common derivatives, in this case, our trusty friend Wikipedia.\n", "\n", - "To do this, instead of having to do some fancy math every time, we can often consult [an online resource](https://en.wikipedia.org/wiki/Differentiation_rules) for a list of common derivatives, in this case our trusty friend Wikipedia.\n", - "\n", - "If I told you to find the derivative of $f(t) = t^3$, you could consult that site and find in Section 2.1, that if $f(t) = t^n$, then $\\frac{d(f(t))}{dt} = nt^{n-1}$. So you would be able to tell me that the derivative of $f(t) = t^3$ is $\\frac{d(f(t))}{dt} = 3t^{2}$.\n", + "If I told you to find the derivative of $f(t) = t^3$, you could consult that site and find in Section 2.1 that if $f(t) = t^n$, then $\\frac{d(f(t))}{dt} = nt^{n-1}$. So you would be able to tell me that the derivative of $f(t) = t^3$ is $\\frac{d(f(t))}{dt} = 3t^{2}$.\n", "\n", "This list of common derivatives often contains only very simple functions. Luckily, as we'll see in the next two sections, we can often break the derivative of a complex function down into the derivatives of more simple components." ] @@ -717,7 +711,7 @@ "source": [ "#### Coding Exercise 2.1.1: Derivative of the postsynaptic potential alpha function\n", "\n", - "Let's use the product rule to get the derivative of the post-synaptic potential alpha function. As we saw in Video 3, the shape of the postsynaptic potential is given by the so called alpha function:\n", + "Let's use the product rule to get the derivative of the postsynaptic potential alpha function. As we saw in Video 3, the shape of the postsynaptic potential is given by the so-called alpha function:\n", "\n", "\\begin{equation}\n", "f(t) = t \\cdot \\text{exp}\\left( -\\frac{t}{\\tau} \\right)\n", @@ -725,7 +719,7 @@ "\n", "Here $f(t)$ is a product of $t$ and $\\text{exp} \\left(-\\frac{t}{\\tau} \\right)$. So we can have $u(t) = t$ and $v(t) = \\text{exp} \\left( -\\frac{t}{\\tau} \\right)$ and use the product rule!\n", "\n", - "We have defined $u(t)$ and $v(t)$ in the code below, in terms of the variable $t$ which is an array of time steps from 0 to 10. Define $\\frac{du}{dt}$ and $\\frac{dv}{dt}$, the compute the full derivative of the alpha function using the product rule. You can always consult wikipedia to figure out $\\frac{du}{dt}$ and $\\frac{dv}{dt}$!" + "We have defined $u(t)$ and $v(t)$ in the code below for the variable $t$, an array of time steps from 0 to 10. Define $\\frac{du}{dt}$ and $\\frac{dv}{dt}$, then compute the full derivative of the alpha function using the product rule. You can always consult Wikipedia to figure out $\\frac{du}{dt}$ and $\\frac{dv}{dt}$!" ] }, { @@ -820,7 +814,7 @@ "source": [ "### Section 2.1.2: Chain Rule\n", "\n", - "Many times we encounter situations in which the variable $a$ is changing with time ($t$) and affecting another variable $r$. How can we estimate the derivative of $r$ with respect to $a$ i.e. $\\frac{dr}{da} = ?$\n", + "We often encounter situations in which the variable $a$ changes with time ($t$) and affects another variable $r$. How can we estimate the derivative of $r$ with respect to $a$, i.e., $\\frac{dr}{da} = ?$\n", "\n", "To calculate $\\frac{dr}{da}$ we use the [Chain Rule](https://en.wikipedia.org/wiki/Chain_rule).\n", "\n", @@ -828,9 +822,9 @@ "\\frac{dr}{da} = \\frac{dr}{dt}\\cdot\\frac{dt}{da}\n", "\\end{equation}\n", "\n", - "That is, we calculate the derivative of both variables with respect to t and divide that derivative of $r$ by that derivative of $a$.\n", + "We calculate the derivative of both variables with respect to $t$ and divide that derivative of $r$ by that derivative of $a$.\n", "\n", - "We can also use this formula to simplify taking derivatives of complex functions! We can make an arbitrary function t so that we can compute more simple derivatives and multiply, as we will see in this exercise." + "We can also use this formula to simplify taking derivatives of complex functions! We can make an arbitrary function $t$ to compute more simple derivatives and multiply, as seen in this exercise." ] }, { @@ -847,9 +841,9 @@ "r(a) = e^{a^4 + 1}\n", "\\end{equation}\n", "\n", - "What is $\\frac{dr}{da}$? This is a more complex function so we can't simply consult a table of common derivatives. Can you use the chain rule to help?\n", + "What is $\\frac{dr}{da}$? This is a more complex function, so we can't simply consult a table of common derivatives. Can you use the chain rule to help?\n", "\n", - "**Hint:** we didn't define t but you could set t equal to the function in the exponent." + "**Hint:** We didn't define $t$, but you could set $t$ equal to the function in the exponent." ] }, { @@ -877,7 +871,7 @@ "dr/da = dr/dt * dt/da\n", " = e^t(4a^3)\n", " = 4a^3e^{a^4 + 1}\n", - "\"\"\"" + "\"\"\";" ] }, { @@ -903,7 +897,7 @@ "\n", "There is a useful Python library for getting the analytical derivatives of functions: SymPy. We actually used this in Interactive Demo 1, under the hood.\n", "\n", - "See the following cell for an example of setting up a sympy function and finding the derivative." + "See the following cell for an example of setting up a SymPy function and finding the derivative." ] }, { @@ -957,9 +951,9 @@ "source": [ "### Interactive Demo 2.2: Numerical Differentiation of the Sine Function\n", "\n", - "Below, we find the numerical derivative of the sine function for different values of $h$, and and compare the result the analytical solution.\n", + "Below, we find the numerical derivative of the sine function for different values of $h$ and compare the result to the analytical solution.\n", "\n", - "- What values of h result in more accurate numerical derivatives?" + "* What values of $h$ result in more accurate numerical derivatives?" ] }, { @@ -1043,7 +1037,7 @@ "\n", "*Estimated timing to here from start of tutorial: 34 min*\n", "\n", - "When we inject a constant current (DC) in a neuron, its firing rate changes as a function of strength of the injected current. This is called the **input-output transfer function** or just the *transfer function* or *I/O Curve* of the neuron. For most neurons this can be approximated by a sigmoid function e.g.,:\n", + "When we inject a constant current (DC) into a neuron, its firing rate changes as a function of the strength of the injected current. This is called the **input-output transfer function** or just the *transfer function* or *I/O Curve* of the neuron. For most neurons, this can be approximated by a sigmoid function, e.g.:\n", "\n", "\\begin{equation}\n", "rate(I) = \\frac{1}{1+\\text{exp}(-a \\cdot (I-\\theta))} - \\frac{1}{\\text{exp}(a \\cdot \\theta)} + \\eta\n", @@ -1053,7 +1047,7 @@ "\n", "*You will visit this equation in a different context in Week 3*\n", "\n", - "The slope of a neurons input-output transfer function, i.e., $\\frac{d(r(I))}{dI}$, is called the **gain** of the neuron, as it tells how the neuron output will change if the input is changed. In other words, the slope of the transfer function tells us in which range of inputs the neuron output is most sensitive to changes in its input." + "The slope of a neuron input-output transfer function, i.e., $\\frac{d(r(I))}{dI}$, is called the **gain** of the neuron, as it tells how the neuron output will change if the input is changed. In other words, the slope of the transfer function tells us in which range of inputs the neuron output is most sensitive to changes in its input." ] }, { @@ -1064,9 +1058,9 @@ "source": [ "### Interactive Demo 2.3: Calculating the Transfer Function and Gain of a Neuron\n", "\n", - "In the following demo, you can estimate the gain of the following neuron transfer function using numerical differentiaton. We will use our timestep as h. See the cell below for a function that computes the rate via the fomula above and then the gain using numerical differentiation. In the following cell, you can play with the parameters $a$ and $\\theta$ to change the shape of the transfer functon (and see the resulting gain function). You can also set $I_{mean}$ to see how the slope is computed for that value of I. In the left plot, the red vertical lines are the two values of the current being used to compute the slope, while the blue lines point to the corresponding ouput firing rates.\n", + "In the following demo, you can estimate the gain of the following neuron transfer function using numerical differentiation. We will use our timestep as h. See the cell below for a function that computes the rate via the formula above and then the gain using numerical differentiation. In the following cell, you can play with the parameters $a$ and $\\theta$ to change the shape of the transfer function (and see the resulting gain function). You can also set $I_{mean}$ to see how the slope is computed for that value of I. In the left plot, the red vertical lines are the two values of the current being used to calculate the slope, while the blue lines point to the corresponding output firing rates.\n", "\n", - "Change the parameters of the neuron transfer function (i.e., $a$ and $\\theta$) and see if you can predict the value of $I$ for which the neuron has maximal slope and which parameter determines the peak value of the gain.\n", + "Change the parameters of the neuron transfer function (i.e., $a$ and $\\theta$) and see if you can predict the value of $I$ for which the neuron has a maximal slope and which parameter determines the peak value of the gain.\n", "\n", "1. Ensure you understand how the right plot relates to the left!\n", "2. How does $\\theta$ affect the transfer function and gain?\n", @@ -1326,13 +1320,13 @@ "source": [ "### Interactive Demo 3: Visualize partial derivatives\n", "\n", - "In the demo below, you can input any function of x and y and then visualize both the function and partial derivatives.\n", + "In the demo below, you can input any function of $x$ and $y$ and then visualize both the function and partial derivatives.\n", "\n", - "We visualized the 2-dimensional function as a surface plot in which the values of the function are rendered as color. Yellow represents a high value and blue represents a low value. The height of the surface also shows the numerical value of the function. A more complete description of 2D surface plots and why we need them is located in Bonus Section 1.1. The first plot is that of our function. And the two bottom plots are the derivative surfaces with respect to $x$ and $y$ variables.\n", + "We visualized the 2-dimensional function as a surface plot in which the function values are rendered as color. Yellow represents a high value, and blue represents a low value. The height of the surface also shows the numerical value of the function. A complete description of 2D surface plots and why we need them can be found in Bonus Section 1.1. The first plot is that of our function. And the two bottom plots are the derivative surfaces with respect to $x$ and $y$ variables.\n", "\n", "1. Ensure you understand how the plots relate to each other - if not, review the above material\n", - "2. Can you come up with a function where the partial derivative with respect to x will be a linear plane and the derivative with respect to y will be more curvy?\n", - "3. What happens to the partial derivatives if there are no terms involving multiplying $x$ and $y$ together?" + "2. Can you come up with a function where the partial derivative with respect to x will be a linear plane, and the derivative with respect to y will be more curvy?\n", + "3. What happens to the partial derivatives if no terms involve multiplying $x$ and $y$ together?" ] }, { @@ -1621,7 +1615,7 @@ }, "source": [ "There are other methods of numerical integration, such as\n", - "**[Lebesgue integral](https://en.wikipedia.org/wiki/Lebesgue_integral)** and **Runge Kutta**. In the Lebesgue integral, we divide the area under the curve into horizontal stripes. That is, instead of the independent variable, the range of the function $f(t)$ is divided into small intervals. In any case, the Riemann sum is the basis of Euler's method of integration for solving ordinary differential equations - something you will do in a later tutorial today." + "**[Lebesgue integral](https://en.wikipedia.org/wiki/Lebesgue_integral)** and **Runge Kutta**. In the Lebesgue integral, we divide the area under the curve into horizontal stripes. That is, instead of the independent variable, the range of the function $f(t)$ is divided into small intervals. In any case, the Riemann sum is the basis of Euler's integration method for solving ordinary differential equations - something you will do in a later tutorial today." ] }, { @@ -1655,7 +1649,8 @@ }, "source": [ "### Coding Exercise 4.2: Calculating Charge Transfer with Excitatory Input\n", - "An incoming spike elicits a change in the post-synaptic membrane potential (PSP) which can be captured by the following function:\n", + "\n", + "An incoming spike elicits a change in the post-synaptic membrane potential (PSP), which can be captured by the following function:\n", "\n", "\\begin{equation}\n", "PSP(t) = J \\cdot t \\cdot \\text{exp}\\left(-\\frac{t-t_{sp}}{\\tau_{s}}\\right)\n", @@ -1663,7 +1658,7 @@ "\n", "where $J$ is the synaptic amplitude, $t_{sp}$ is the spike time and $\\tau_s$ is the synaptic time constant.\n", "\n", - "Estimate the total charge transfered to the postsynaptic neuron during an PSP with amplitude $J=1.0$, $\\tau_s = 1.0$ and $t_{sp} = 1$ (that is the spike occured at $1$ ms). The total charge will be the integral of the PSP function." + "Estimate the total charge transferred to the postsynaptic neuron during a PSP with amplitude $J=1.0$, $\\tau_s = 1.0$ and $t_{sp} = 1$ (that is the spike occurred at $1$ ms). The total charge will be the integral of the PSP function." ] }, { @@ -1909,9 +1904,7 @@ "execution": {} }, "source": [ - "Notice how the differentiation operation amplifies the fast changes which were contributed by noise. By contrast, the integration operation supresses the fast changing noise. If we perform the same operation of averaging the adjancent samples on the orange trace, we will further smooth the signal. Such sums and subtractions form the basis of digital filters.\n", - "\n", - "\n" + "Notice how the differentiation operation amplifies the fast changes which were contributed by noise. By contrast, the integration operation suppresses the fast-changing noise. We will further smooth the signal if we perform the same operation of averaging the adjacent samples on the orange trace. Such sums and subtractions form the basis of digital filters." ] }, { @@ -1925,13 +1918,13 @@ "\n", "*Estimated timing of tutorial: 80 minutes*\n", "\n", - "* Geometrically, integration is the area under the curve and differentiation is the slope of the function\n", + "* Geometrically, integration is the area under the curve, and differentiation is the slope of the function\n", "* The concepts of slope and area can be easily extended to higher dimensions. We saw this when we took the derivative of a 2-dimensional transfer function of a neuron\n", - "* Numerical estimates of both derivatives and integrals require us to choose a time step $h$. The smaller the $h$, the better the estimate, but for small values of $h$, more computations are needed. So there is always some tradeoff.\n", - "* Partial derivatives are just the estimate of the slope along one of the many dimensions of the function. We can combine the slopes in different directions using vector sum to find the direction of the slope.\n", - "* Because the derivative of a function is zero at the local peak or trough, derivatives are used to solve optimization problems.\n", - "* When thinking of signal, integration operation is equivalent to smoothening the signals (i.e., remove fast changes)\n", - "* Differentiation operations remove slow changes and enhance high frequency content of a signal" + "* Numerical estimates of both derivatives and integrals require us to choose a time step $h$. The smaller the $h$, the better the estimate, but more computations are needed for small values of $h$. So there is always some tradeoff\n", + "* Partial derivatives are just the estimate of the slope along one of the many dimensions of the function. We can combine the slopes in different directions using vector sum to find the direction of the slope\n", + "* Because the derivative of a function is zero at the local peak or trough, derivatives are used to solve optimization problems\n", + "* When thinking of signal, integration operation is equivalent to smoothening the signals (i.e., removing fast changes)\n", + "* Differentiation operations remove slow changes and enhance the high-frequency content of a signal" ] }, { @@ -1954,11 +1947,11 @@ "source": [ "## Bonus Section 1.1: Understanding 2D plots\n", "\n", - "Let's take the example of a neuron driven by excitatory and inhibitory inputs. Because this is for illustrative purposes, we will not go in the details of the numerical range of the input and output variables.\n", + "Let's take the example of a neuron driven by excitatory and inhibitory inputs. Because this is for illustrative purposes, we will not go into the details of the numerical range of the input and output variables.\n", "\n", - "In the function below, we assume that the firing rate of a neuron increases motonotically with an increase in excitation and decreases monotonically with an increase in inhibition. The inhibition is modelled as a subtraction. Like for the 1-dimensional transfer function, here we assume that we can approximate the transfer function as a sigmoid function.\n", + "In the function below, we assume that the firing rate of a neuron increases monotonically with an increase in excitation and decreases monotonically with an increase in inhibition. The inhibition is modeled as a subtraction. As for the 1-dimensional transfer function, we assume that we can approximate the transfer function as a sigmoid function.\n", "\n", - "To evaluate the partial derivatives we can use the same numerical differentiation as before but now we apply it to each row and column separately." + "We can use the same numerical differentiation as before to evaluate the partial derivatives, but now we apply it to each row and column separately." ] }, { @@ -2043,15 +2036,15 @@ "execution": {} }, "source": [ - "In the **Top-Left** plot, we see how the neuron output rate increases as a function of excitatory input (e.g., the blue trace). However, as we increase inhibition, expectedly the neuron output decreases and the curve is shifted downwards. This constant shift in the curve suggests that the effect of inhibition is subtractive, and the amount of subtraction does not depend on the neuron output.\n", + "The **Top-Left** plot shows how the neuron output rate increases as a function of excitatory input (e.g., the blue trace). However, as we increase inhibition, expectedly, the neuron output decreases, and the curve is shifted downwards. This constant shift in the curve suggests that the effect of inhibition is subtractive, and the amount of subtraction does not depend on the neuron output.\n", "\n", "We can alternatively see how the neuron output changes with respect to inhibition and study how excitation affects that. This is visualized in the **Top-Right** plot.\n", "\n", "This type of plotting is very intuitive, but it becomes very tedious to visualize when there are larger numbers of lines to be plotted. A nice solution to this visualization problem is to render the data as color, as surfaces, or both.\n", "\n", - "This is what we have done in the plot on the bottom. The colormap on the right shows the output of the neuron as a function of inhibitory input and excitatory input. The output rate is shown both as height along the z-axis and as the color. Blue means low firing rate and yellow means high firing rate (see the color bar).\n", + "This is what we have done in the plot at the bottom. The color map on the right shows the neuron's output as a function of inhibitory and excitatory input. The output rate is shown both as height along the z-axis and as the color. Blue means a low firing rate, and yellow represents a high firing rate (see the color bar).\n", "\n", - "In the above plot, the output rate of the neuron goes below zero. This is of course not physiological as neurons cannot have negative firing rates. In models, we either choose the operating point such that the output does not go below zero, or else we clamp the neuron output to zero if it goes below zero. You will learn about it more in Week 2." + "In the above plot, the output rate of the neuron goes below zero. This is, of course, not physiological, as neurons cannot have negative firing rates. In models, we either choose the operating point so that the output does not go below zero, or we clamp the neuron output to zero if it goes below zero. You will learn about it more in Week 2." ] }, { @@ -2146,8 +2139,7 @@ "execution": {} }, "source": [ - "Is this what you expected? Change the parameters in the function to generate the 2-d transfer function of the neuron for different excitatory and inhibitory $a$ and $\\theta$ and test your intuitions Can you relate this shape of the partial derivative surface to the gain of the 1-d transfer-function of a neuron (Section 2)?\n", - "\n" + "Is this what you expected? Change the parameters in the function to generate the 2-D transfer function of the neuron for different excitatory and inhibitory $a$ and $\\theta$ and test your intuitions. Can you relate this shape of the partial derivative surface to the gain of the 1-D transfer function of a neuron (Section 2)?" ] }, { diff --git a/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial2.ipynb b/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial2.ipynb index 84c9687..1af93b2 100644 --- a/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial2.ipynb +++ b/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial2.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "74ca2e85", + "id": "bc470a90", "metadata": { "colab_type": "text", "execution": {}, @@ -54,18 +54,18 @@ "\n", "*Estimated timing of tutorial: 45 minutes*\n", "\n", - "A great deal of neuroscience can be modelled using differential equations, from gating channels to single neurons to a network of neurons to blood flow, to behaviour. A simple way to think about differential equations is they are equations that describe how something changes.\n", + "A great deal of neuroscience can be modeled using differential equations, from gating channels to single neurons to a network of neurons to blood flow to behavior. A simple way to think about differential equations is they are equations that describe how something changes.\n", "\n", - "The most famous of these in neuroscience is the Nobel Prize winning Hodgkin Huxley equation, which describes a neuron by modelling the gating of each axon. But we will not start there; we will start a few steps back.\n", + "The most famous of these in neuroscience is the Nobel Prize-winning Hodgkin-Huxley equation, which describes a neuron by modeling the gating of each axon. But we will not start there; we will start a few steps back.\n", "\n", - "Differential Equations are mathematical equations that describe how something like population or a neuron changes over time. The reason why differential equations are so useful is they can generalise a process such that one equation can be used to describe many different outcomes.\n", - "The general form of a first order differential equation is:\n", + "Differential Equations are mathematical equations that describe how something like a population or a neuron changes over time. Differential equations are so useful because they can generalize a process such that one equation can be used to describe many different outcomes.\n", + "The general form of a first-order differential equation is:\n", "\n", "\\begin{equation}\n", "\\frac{d}{dt}y(t) = f\\left( t,y(t) \\right)\n", "\\end{equation}\n", "\n", - "which can be read as \"the change in a process $y$ over time $t$ is a function $f$ of time $t$ and itself $y$\". This might initially seem like a paradox as you are using a process $y$ you want to know about to describe itself, a bit like the MC Escher drawing of two hands painting [each other](https://en.wikipedia.org/wiki/Drawing_Hands). But that is the beauty of mathematics - this can be solved some of time, and when it cannot be solved exactly we can use numerical methods to estimate the answer (as we will see in the next tutorial).\n", + "which can be read as \"the change in a process $y$ over time $t$ is a function $f$ of time $t$ and itself $y$\". This might initially seem like a paradox as you are using a process $y$ you want to know about to describe itself, a bit like the MC Escher drawing of two hands painting [each other](https://en.wikipedia.org/wiki/Drawing_Hands). But that is the beauty of mathematics - this can be solved sometimes, and when it cannot be solved exactly, we can use numerical methods to estimate the answer (as we will see in the next tutorial).\n", "\n", "In this tutorial, we will see how __differential equations are motivated by observations of physical responses__. We will break down the population differential equation, then the integrate and fire model, which leads nicely into raster plots and frequency-current curves to rate models.\n", "\n", @@ -73,7 +73,7 @@ "- Get an intuitive understanding of a linear population differential equation (humans, not neurons)\n", "- Visualize the relationship between the change in population and the population\n", "- Breakdown the Leaky Integrate and Fire (LIF) differential equation\n", - "- Code the exact solution of an LIF for a constant input\n", + "- Code the exact solution of a LIF for a constant input\n", "- Visualize and listen to the response of the LIF for different inputs" ] }, @@ -556,6 +556,7 @@ }, "source": [ "### Think! 1.1: Interpretating the behavior of a linear population equation\n", + "\n", "Using the plot below of change of population $\\frac{d}{dt} p(t) $ as a function of population $p(t)$ with birth-rate $\\alpha=0.3$, discuss the following questions:\n", "\n", "1. Why is the population differential equation known as a linear differential equation?\n", @@ -638,13 +639,14 @@ }, "source": [ "### Section 1.1.1: Initial condition\n", - "The linear population differential equation is known as an initial value differential equation because we need an initial population value to solve it. Here we will set our initial population at time $0$ to $1$:\n", + "\n", + "The linear population differential equation is an initial value differential equation because we need an initial population value to solve it. Here we will set our initial population at time $0$ to $1$:\n", "\n", "\\begin{equation}\n", "p(0) = 1\n", "\\end{equation}\n", "\n", - "Different initial conditions will lead to different answers, but they will not change the differential equation. This is one of the strengths of a differential equation." + "Different initial conditions will lead to different answers but will not change the differential equation. This is one of the strengths of a differential equation." ] }, { @@ -655,7 +657,8 @@ }, "source": [ "### Section 1.1.2: Exact Solution\n", - "To calculate the exact solution of a differential equation, we must integrate both sides. Instead of numerical integration (as you delved into in the last tutorial), we will first try to solve the differential equations using analytical integration. As with derivatives, we can find analytical integrals of simple equations by consulting [a list](https://en.wikipedia.org/wiki/Lists_of_integrals). We can then get integrals for more complex equations using some mathematical tricks - the harder the equation the more obscure the trick.\n", + "\n", + "To calculate the exact solution of a differential equation, we must integrate both sides. Instead of numerical integration (as you delved into in the last tutorial), we will first try to solve the differential equations using analytical integration. As with derivatives, we can find analytical integrals of simple equations by consulting [a list](https://en.wikipedia.org/wiki/Lists_of_integrals). We can then get integrals for more complex equations using some mathematical tricks - the harder the equation, the more obscure the trick.\n", "\n", "The linear population equation\n", "\\begin{equation}\n", @@ -674,9 +677,9 @@ "\\text{\"Population\"} = \\text{\"grows/declines exponentially as a function of time and birth rate\"}.\n", "\\end{equation}\n", "\n", - "Most differential equations do not have a known exact solution, so in the next tutorial on numerical methods we will show how the solution can be estimated.\n", + "Most differential equations do not have a known exact solution, so we will show how the solution can be estimated in the next tutorial on numerical methods.\n", "\n", - "A small aside: a good deal of progress in mathematics was due to mathematicians writing taunting letters to each other saying they had a trick that could solve something better than everyone else. So do not worry too much about the tricks." + "A small aside: a good deal of progress in mathematics was due to mathematicians writing taunting letters to each other, saying they had a trick to solve something better than everyone else. So do not worry too much about the tricks." ] }, { @@ -742,7 +745,7 @@ "\n", "*Estimated timing to here from start of tutorial: 12 min*\n", "\n", - "One of the goals when designing a differential equation is to make it generalisable. Which means that the differential equation will give reasonable solutions for different countries with different birth rates $\\alpha$.\n" + "One of the goals when designing a differential equation is to make it generalizable. This means that the differential equation will give reasonable solutions for different countries with different birth rates $\\alpha$." ] }, { @@ -754,7 +757,7 @@ "source": [ "### Interactive Demo 1.2: Parameter Change\n", "\n", - "Play with the widget to see the relationship between $\\alpha$ and the population differential equation as a function of population (left-hand side), and the population solution as a function of time (right-hand side). Pay close attention to the transition point from positive to negative.\n", + "Play with the widget to see the relationship between $\\alpha$ and the population differential equation as a function of population (left-hand side) and the population solution as a function of time (right-hand side). Pay close attention to the transition point from positive to negative.\n", "\n", "How do changing parameters of the population equation affect the outcome?\n", "\n", @@ -823,17 +826,15 @@ "execution": {} }, "source": [ - "The population differential equation is an over-simplification and has some very obvious limitations:\n", - "1. Population growth is not exponential as there are limited number of resources so the population will level out at some point.\n", - "2. It does not include any external factors on the populations like weather, predators and preys.\n", - "\n", - "These kind of limitations can be addressed by extending the model.\n", - "\n", + "The population differential equation is an over-simplification and has some pronounced limitations:\n", + "1. Population growth is not exponential as there are limited resources, so the population will level out at some point.\n", + "2. It does not include any external factors on the populations, like weather, predators, and prey.\n", "\n", - "While it might not seem that the population equation has direct relevance to neuroscience, a similar equation is used to describe the accumulation of evidence for decision making. This is known as the Drift Diffusion Model and you will see in more detail in the Linear System day in Neuromatch (W2D2).\n", + "These kinds of limitations can be addressed by extending the model.\n", "\n", + "While it might not seem that the population equation has direct relevance to neuroscience, a similar equation is used to describe the accumulation of evidence for decision-making. This is known as the Drift Diffusion Model, and you will see it in more detail in the Linear System Day in Neuromatch (W2D2).\n", "\n", - "Another differential equation that is similar to the population equation is the Leaky Integrate and Fire model which you may have seen in the python pre-course materials on W0D1 and W0D2. It will turn up later in Neuromatch as well. Below we will delve in the motivation of the differential equation." + "Another differential equation similar to the population equation is the Leaky Integrate and Fire model, which you may have seen in the Python pre-course materials on W0D1 and W0D2. It will turn up later in Neuromatch as well. Below we will delve into the motivation of the differential equation." ] }, { @@ -1124,7 +1125,8 @@ }, "source": [ "#### Interactive Demo 2.1.1: Initial Condition $V_{reset}$\n", - "This exercise is to get an intuitive feel of how the different initial conditions $V_{reset}$ impacts the differential equation of the LIF and the exact solution for the equation:\n", + "\n", + "This exercise is to get an intuitive feel of how the different initial conditions $V_{reset}$ impact the differential equation of the LIF and the exact solution for the equation:\n", "\n", "\\begin{equation}\n", "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}\n", @@ -1134,7 +1136,7 @@ "* `E_L = -75,`\n", "* `tau_m = 10.`\n", "\n", - "The panel on the left-hand side plots the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ and right-hand side panel plots the exact solution $V$ as a function of time $t,$ the green dot in both panels is the reset potential $V_{reset}$.\n", + "The panel on the left-hand side plots the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ and the right-hand side panel plots the exact solution $V$ as a function of time $t,$ the green dot in both panels is the reset potential $V_{reset}$.\n", "\n", "Pay close attention to when $V_{reset}=E_L=-75$mV.\n", "\n", @@ -1207,6 +1209,7 @@ }, "source": [ "## Section 2.2: LIF with input\n", + "\n", "*Estimated timing to here from start of tutorial: 24 min*\n", "\n", "We will re-introduce the input $I$ and membrane resistance $R_m$ giving the original equation:\n", @@ -1226,6 +1229,7 @@ }, "source": [ "### Interactive Demo 2.2: The Impact of Input\n", + "\n", "The interactive plot below manipulates $I$ in the differential equation.\n", "\n", "- With increasing input, how does the $\\frac{dV}{dt}$ change? How would this impact the solution?" @@ -1479,7 +1483,8 @@ }, "source": [ "### Interactive Demo 2.3.1: Input on spikes\n", - "This exercise show the relationship between firing rate and the Input for exact solution `V` of the LIF:\n", + "\n", + "This exercise shows the relationship between the firing rate and the Input for the exact solution `V` of the LIF:\n", "\n", "\\begin{equation}\n", "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},\n", @@ -1496,7 +1501,7 @@ "* the middle panel is the membrane potential $V(t)$. To illustrate the spike, $V(t)$ is set to $0$ and then reset to $-75$ mV when there is a spike.\n", "* the bottom panel is the raster plot with each dot indicating a spike.\n", "\n", - "First, as electrophysiologist normally listen to spikes when conducting experiments, listen to the music of the firing rate for a single value of $I$.\n", + "First, as electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the firing rate for a single value of $I$.\n", "\n", "**Note:** The audio doesn't work in some browsers so don't worry about it if you can't hear anything." ] @@ -1598,9 +1603,9 @@ "\n", "*Estimated timing to here from start of tutorial: 38 min*\n", "\n", - "The firing frequency of a neuron plotted as a function of current is called an input-output curve (F–I curve). It is also known as a transfer function, which you came across in the previous tutorial. This function is one of the starting points for the rate model, which extends from modelling single neurons to the firing rate of a collection of neurons.\n", + "The firing frequency of a neuron plotted as a function of current is called an input-output curve (F–I curve). It is also known as a transfer function, which you came across in the previous tutorial. This function is one of the starting points for the rate model, which extends from modeling single neurons to the firing rate of a collection of neurons.\n", "\n", - "By fitting this to a function, we can start to generalise the firing pattern of many neurons, which can be used to build rate models. This will be discussed later in Neuromatch." + "By fitting this to a function, we can start to generalize the firing pattern of many neurons, which can be used to build rate models. This will be discussed later in Neuromatch." ] }, { @@ -1645,8 +1650,7 @@ "__Weaknesses of the LIF model:__\n", "- Spiking is a discontinuity;\n", "- Abstraction from biology;\n", - "- Cannot generate different spiking patterns.\n", - "\n" + "- Cannot generate different spiking patterns." ] }, { @@ -1742,12 +1746,11 @@ "source": [ "In this tutorial, we have seen two differential equations, the population differential equations and the leaky integrate and fire model.\n", "\n", - "\n", - "We learned about:\n", + "We learned about the following:\n", "* The motivation for differential equations.\n", - "* An intuitive relationship between the solution and the form of the differential equation.\n", + "* An intuitive relationship between the solution and the differential equation form.\n", "* How different parameters of the differential equation impact the solution.\n", - "* The strengths and limitations of the simple differential equations.\n" + "* The strengths and limitations of the simple differential equations." ] }, { diff --git a/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial3.ipynb b/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial3.ipynb index 9019c47..ffa529b 100644 --- a/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial3.ipynb +++ b/tutorials/W0D4_Calculus/instructor/W0D4_Tutorial3.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "6c2d4b58", + "id": "dfd443f6", "metadata": { "colab_type": "text", "execution": {}, @@ -61,7 +61,7 @@ "- Investigate the impact of time step on the error of the numerical solution;\n", "- Code the Euler estimate of the Leaky Integrate and Fire model for a constant input;\n", "- Visualize and listen to the response of the integrate for different inputs;\n", - "- Apply the Euler method to estimate the solution of a system of differential equations.\n" + "- Apply the Euler method to estimate the solution of a system of differential equations." ] }, { @@ -477,11 +477,11 @@ }, "source": [ "### Interactive Demo 1.1: Slope of a Line\n", - "The plot below shows a function $y(t)$ in blue, its exact derivative $ \\frac{d}{dt}y(t)$ at $t_0=1$ in black, and the approximate derivative calculated using the slope formula $=\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$ for different time-steps sizes $\\Delta t$ in green.\n", "\n", - "Interact with the widget to see how time-step impacts the accuracy of the slope which is the estimate of the derivative.\n", + "The plot below shows a function $y(t)$ in blue, its exact derivative $ \\frac{d}{dt}y(t)$ at $t_0=1$ in black. The approximate derivative is calculated using the slope formula $=\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$ for different time-steps sizes $\\Delta t$ in green.\n", "\n", - "- How does the size of $\\Delta t$ affect the approximation?\n" + "Interact with the widget to see how time-step impacts the slope's accuracy, which is the derivative's estimate.\n", + "- How does the size of $\\Delta t$ affect the approximation?" ] }, { @@ -610,13 +610,11 @@ }, "source": [ "### Interactive Demo 1.2: Euler error for a single step\n", - "Interact with the widget to see how the time-step $\\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.\n", "\n", + "Interact with the widget to see how the time-step $\\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.\n", "\n", "1. What happens to the estimate $p_1$ as the time-step $\\Delta t$ increases?\n", - "\n", "2. Is there a relationship between the size of $\\Delta t$ and $e_1$?\n", - "\n", "3. How would you improve the error $e_1$?" ] }, @@ -670,7 +668,7 @@ "execution": {} }, "source": [ - "The error $e_1$ from one time-step is known as the __local error__. For the Euler method the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $O(\\Delta t)$." + "The error $e_1$ from one time-step is known as the __local error__. For the Euler method, the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $\\mathcal{O}(\\Delta t)$." ] }, { @@ -988,8 +986,9 @@ "execution": {} }, "source": [ - "The error is smaller for 4 time-steps than taking one large time step from 1 to 5 but do note that the error is increasing for each step. This is known as __global error__ so the futher in time you want to predict, the larger the error.\n", - "You can read the theorems [here.](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)" + "The error is smaller for 4-time steps than taking one large time step from 1 to 5 but does note that the error is increasing for each step. This is known as __global error__ so the further in time you want to predict, the larger the error.\n", + "\n", + "You can read the theorems [here](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)." ] }, { @@ -1739,7 +1738,7 @@ "\n", "1. Which representation is more intuitive (and useful), the time plot or the phase plane plot?\n", "2. Why do we only see one circle?\n", - "3. What do the quadrants represent?\n" + "3. What do the quadrants represent?" ] }, { @@ -2133,8 +2132,7 @@ "execution": {} }, "source": [ - "Using pretty much the formula for the slope of a line, the solution of differential equation can be estimated with reasonable accuracy.\n", - "This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution." + "Using the formula for the slope of a line, the solution of the differential equation can be estimated with reasonable accuracy. This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution." ] }, { @@ -2147,14 +2145,14 @@ "---\n", "## Links to Neuromatch Computational Neuroscience Days\n", "\n", - "Differential equations turn up in a number of different Neuromatch days:\n", - "* The LIF model is discussed in more details in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", - "* Drift Diffusion model which is a differential equation for decision making is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", + "Differential equations turn up on a number of different Neuromatch days:\n", + "* The LIF model is discussed in more detail in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", + "* Drift Diffusion model, which is a differential equation for decision making, is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", "* Phase-plane plots are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", "* The Wilson-Cowan model is discussed in [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", "\n", "## References\n", - "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", + "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", "\n", "\n", @@ -2307,15 +2305,14 @@ "---\n", "## Bonus Section 2: Neural oscillations are a start toward understanding brain activity rather than the end\n", "\n", - "The differential equations we have discussed above are all to simulate neuronal processes, another way differential equations can be used is to motivate experimental findings.\n", + "The differential equations we have discussed above are all to simulate neuronal processes. Another way differential equations can be used is to motivate experimental findings.\n", "\n", - "A great deal of experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrain to a stimulus.\n", - "An issue with these studies is that there is not a consistent definition of what constitutes an oscillation. Right now it is a bit of I know one when I see one problem.\n", + "Many experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrees to a stimulus.\n", + "An issue with these studies is that there is no consistent definition of what constitutes an oscillation. Right now, it is a bit of I know one when I see one problem.\n", "\n", + "In an essay from May 2021 in PLoS Biology, Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally when studying oscillations. The essay proposes that instead of thinking about the brain, we should look at this question from the mathematical side to motivate what can be defined as an oscillation.\n", "\n", - "In an essay from May 2021 in PLOS Biology by Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally when studying oscillations. In the essay they propose that instead of thinking about the brain we should look at this question from the mathematical side, to motivate what can be defined as an oscillation.\n", - "\n", - "To do this they used Stuart–Landau equations, which is a system of differential equations\n", + "To do this, they used Stuart–Landau equations, which is a system of differential equations\n", "\\begin{align}\n", "\\frac{dx}{dt} &= \\lambda x-\\omega y -\\gamma (x^2+y^2)x+s\\\\\n", "\\frac{dy}{dt} &= \\lambda y+\\omega x -\\gamma (x^2+y^2)y\n", @@ -2323,11 +2320,11 @@ "\n", "where $s$ is input to the system, and $\\lambda$, $\\omega$ and $\\gamma$ are parameters.\n", "\n", - "The Stuart–Landau equations are a well described system of non-linear differential equations that generate oscillations. For their purpose, Dowling and Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.\n", + "The Stuart–Landau equations are a well-described system of non-linear differential equations that generate oscillations. For their purpose, Dowling and Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.\n", "In their paper, using the Stuart–Landau equations, they outline\n", "* \"What is an oscillator?\"\n", - "* \"What an oscillator is not\"\n", - "* \"Not all that oscillates is an oscillator\"\n", + "* \"What an oscillator is not.\"\n", + "* \"Not all that oscillates is an oscillator.\"\n", "* \"Not all oscillators are alike.\"\n", "\n", "The Euler form of the Stuart–Landau system of equations is:\n", @@ -2340,7 +2337,7 @@ "y_0&=1,\\\\\n", "\\end{align*}\n", "\n", - "with $ \\Delta t=0.1/1000$ ms.\n" + "with $ \\Delta t=0.1/1000$ ms." ] }, { @@ -2422,10 +2419,14 @@ }, "source": [ "### Bonus 2.1: What is an Oscillator?\n", + "\n", "Doelling & Assaneo (2021), using the Stuart–Landau system, show different possible states of an oscillator by manipulating the $\\lambda$ term in the equation.\n", - "From the paper: \"this qualitative change in behavior takes place at λ = 0: For λ < 0, the system decays to a stable equilibrium, while for λ > 0, it keeps oscillating.\"\n", "\n", - "This illustrates an oscillations does not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\\alpha$ band oscillations in EEG the oscillations come and go." + "From the paper:\n", + "\n", + "> This qualitative change in behavior takes place at $\\lambda = 0$. For $\\lambda < 0$, the system decays to a stable equilibrium, while for $\\lambda > 0$, it keeps oscillating.\n", + "\n", + "This illustrates that oscillations do not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\\alpha$ band oscillations in EEG the oscillations come and go." ] }, { @@ -2500,12 +2501,12 @@ "source": [ "#### Interactive Demo Bonus 2: Stuart-Landau System\n", "\n", - "The plot below shows estimated solution of the Stuart–Landau system with a base frequency of 4Hz by stetting $\\omega$ to $4\\times 2\\pi$, $\\lambda=1$, $\\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\\sin(freq 2\\pi t) $ in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$.\n", + "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency of 4Hz by setting $\\omega$ to $4\\times 2\\pi$, $\\lambda=1$, $\\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\\sin(freq 2\\pi t) $ in the top panel, $x$ as a function of time in the bottom panel and on the right the phase plane plot of $x$ and $y$.\n", "\n", - "You can manipulate the frequency $freq$ of the input to see how the oscillations change and for frequencies $freq$ further and further from the base frequency of 4Hz the oscillations breaks down.\n", + "You can manipulate the frequency $freq$ of the input to see how the oscillations change, and for frequencies $freq$ further and further from the base frequency of 4Hz, the oscillations break down.\n", "\n", - "This shoes that if you have an oscillating input into an oscillator with it does not have to respond by oscillating about could even breakdown. Hence the frequency of the input oscillation is important to the system.\n", - "So if you flash something at 50Hz, for example, the visual system might not follow the signal but that does not mean the visual system is not an oscillator it might just be the wrong frequency." + "This shows that if you have an oscillating input into an oscillator, it does not have to respond by oscillating about could even break down. Hence the frequency of the input oscillation is important to the system.\n", + "So if you flash something at 50Hz, for example, the visual system might not follow the signal, but that does not mean the visual system is not an oscillator. It might just be the wrong frequency." ] }, { @@ -2558,7 +2559,7 @@ "source": [ "**Bonus Reference 2**:\n", "\n", - "Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234. doi: [10.1371/journal.pbio.3001234](https://doi.org/10.1371/journal.pbio.3001234)\n" + "Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234. doi: [10.1371/journal.pbio.3001234](https://doi.org/10.1371/journal.pbio.3001234)" ] } ], diff --git a/tutorials/W0D4_Calculus/solutions/W0D4_Tutorial1_Solution_a0e42694.py b/tutorials/W0D4_Calculus/solutions/W0D4_Tutorial1_Solution_90e4db73.py similarity index 98% rename from tutorials/W0D4_Calculus/solutions/W0D4_Tutorial1_Solution_a0e42694.py rename to tutorials/W0D4_Calculus/solutions/W0D4_Tutorial1_Solution_90e4db73.py index 8836f57..d32511b 100644 --- a/tutorials/W0D4_Calculus/solutions/W0D4_Tutorial1_Solution_a0e42694.py +++ b/tutorials/W0D4_Calculus/solutions/W0D4_Tutorial1_Solution_90e4db73.py @@ -14,4 +14,4 @@ dr/da = dr/dt * dt/da = e^t(4a^3) = 4a^3e^{a^4 + 1} -""" \ No newline at end of file +"""; \ No newline at end of file diff --git a/tutorials/W0D4_Calculus/student/W0D4_Tutorial1.ipynb b/tutorials/W0D4_Calculus/student/W0D4_Tutorial1.ipynb index 647de02..e9e5ce5 100644 --- a/tutorials/W0D4_Calculus/student/W0D4_Tutorial1.ipynb +++ b/tutorials/W0D4_Calculus/student/W0D4_Tutorial1.ipynb @@ -27,7 +27,7 @@ "\n", "**Content reviewers:** Aderogba Bayo, Tessy Tom, Matt McCann\n", "\n", - "**Production editors:** Matthew McCann, Ella Batty" + "**Production editors:** Matthew McCann, Spiros Chavlis, Ella Batty" ] }, { @@ -50,14 +50,12 @@ "\n", "*Estimated timing of tutorial: 80 minutes*\n", "\n", - "In this tutorial, we will cover aspects of calculus that will be frequently used in the main NMA course. We assume that you have some familiarty with calculus, but may be a bit rusty or may not have done much practice. Specifically the objectives of this tutorial are\n", + "In this tutorial, we will cover aspects of calculus that will be frequently used in the main NMA course. We assume that you have some familiarity with calculus but may be a bit rusty or may not have done much practice. Specifically, the objectives of this tutorial are\n", "\n", - "* Get an intuitive understanding of derivative and integration operations\n", - "* Learn to calculate the derivatives of 1- and 2-dimensional functions/signals numerically\n", - "* Familiarize with the concept of neuron transfer function in 1- and 2-dimensions.\n", - "* Familiarize with the idea of numerical integration using Riemann sum\n", - "\n", - "\n" + "* Get an intuitive understanding of derivative and integration operations\n", + "* Learn to calculate the derivatives of 1- and 2-dimensional functions/signals numerically\n", + "* Familiarize with the concept of the neuron transfer function in 1- and 2-dimensions.\n", + "* Familiarize with the idea of numerical integration using the Riemann sum" ] }, { @@ -494,13 +492,13 @@ "source": [ "### Interactive Demo 1: Geometrical understanding\n", "\n", - "In the interactive demo below, you can pick different functions to examine in the drop down menu. You can then choose to show the derivative function and/or the integral function.\n", + "In the interactive demo below, you can pick different functions to examine in the drop-down menu. You can then choose to show the derivative function and/or the integral function.\n", "\n", "For the integral, we have chosen the unknown constant $C$ such that the integral function at the left x-axis limit is $0$, as $f(t = -10) = 0$. So the integral will reflect the area under the curve starting from that position.\n", "\n", "For each function:\n", "\n", - "* Examine just the function first. Discuss and predict what the derivative and integral will look like. Remember that derivative = slope of function, integral = area under curve from $t = -10$ to that t.\n", + "* Examine just the function first. Discuss and predict what the derivative and integral will look like. Remember that _derivative = slope of the function_, _integral = area under the curve from $t = -10$ to that $t$_.\n", "* Check the derivative - does it match your expectations?\n", "* Check the integral - does it match your expectations?" ] @@ -550,9 +548,9 @@ "execution": {} }, "source": [ - "In the demo above you may have noticed that the derivative and integral of the exponential function is same as the exponential function itself.\n", + "In the demo above, you may have noticed that the derivative and integral of the exponential function are the same as the exponential function itself.\n", "\n", - "Some functions like the exponential function, when differentiated or integrated, equal a scalar times the same function. This is a similar idea to eigenvectors of a matrix being those that, when multipled by the matrix, equal a scalar times themselves, as you saw yesterday!\n", + "When differentiated or integrated, some functions, like the exponential function, equal scalar times the same function. This is a similar idea to eigenvectors of a matrix being those that, when multiplied by the matrix, equal scalar times themselves, as you saw yesterday!\n", "\n", "When\n", "\n", @@ -679,15 +677,11 @@ "execution": {} }, "source": [ - "## Section 2.1: Analytical Differentiation\n", - "\n", - "*Estimated timing to here from start of tutorial: 20 min*\n", - "\n", - "When we find the derivative analytically, we are finding the exact formula for the derivative function.\n", + "When we find the derivative analytically, we obtain the exact formula for the derivative function.\n", "\n", - "To do this, instead of having to do some fancy math every time, we can often consult [an online resource](https://en.wikipedia.org/wiki/Differentiation_rules) for a list of common derivatives, in this case our trusty friend Wikipedia.\n", + "To do this, instead of having to do some fancy math every time, we can often consult [an online resource](https://en.wikipedia.org/wiki/Differentiation_rules) for a list of common derivatives, in this case, our trusty friend Wikipedia.\n", "\n", - "If I told you to find the derivative of $f(t) = t^3$, you could consult that site and find in Section 2.1, that if $f(t) = t^n$, then $\\frac{d(f(t))}{dt} = nt^{n-1}$. So you would be able to tell me that the derivative of $f(t) = t^3$ is $\\frac{d(f(t))}{dt} = 3t^{2}$.\n", + "If I told you to find the derivative of $f(t) = t^3$, you could consult that site and find in Section 2.1 that if $f(t) = t^n$, then $\\frac{d(f(t))}{dt} = nt^{n-1}$. So you would be able to tell me that the derivative of $f(t) = t^3$ is $\\frac{d(f(t))}{dt} = 3t^{2}$.\n", "\n", "This list of common derivatives often contains only very simple functions. Luckily, as we'll see in the next two sections, we can often break the derivative of a complex function down into the derivatives of more simple components." ] @@ -717,7 +711,7 @@ "source": [ "#### Coding Exercise 2.1.1: Derivative of the postsynaptic potential alpha function\n", "\n", - "Let's use the product rule to get the derivative of the post-synaptic potential alpha function. As we saw in Video 3, the shape of the postsynaptic potential is given by the so called alpha function:\n", + "Let's use the product rule to get the derivative of the postsynaptic potential alpha function. As we saw in Video 3, the shape of the postsynaptic potential is given by the so-called alpha function:\n", "\n", "\\begin{equation}\n", "f(t) = t \\cdot \\text{exp}\\left( -\\frac{t}{\\tau} \\right)\n", @@ -725,7 +719,7 @@ "\n", "Here $f(t)$ is a product of $t$ and $\\text{exp} \\left(-\\frac{t}{\\tau} \\right)$. So we can have $u(t) = t$ and $v(t) = \\text{exp} \\left( -\\frac{t}{\\tau} \\right)$ and use the product rule!\n", "\n", - "We have defined $u(t)$ and $v(t)$ in the code below, in terms of the variable $t$ which is an array of time steps from 0 to 10. Define $\\frac{du}{dt}$ and $\\frac{dv}{dt}$, the compute the full derivative of the alpha function using the product rule. You can always consult wikipedia to figure out $\\frac{du}{dt}$ and $\\frac{dv}{dt}$!" + "We have defined $u(t)$ and $v(t)$ in the code below for the variable $t$, an array of time steps from 0 to 10. Define $\\frac{du}{dt}$ and $\\frac{dv}{dt}$, then compute the full derivative of the alpha function using the product rule. You can always consult Wikipedia to figure out $\\frac{du}{dt}$ and $\\frac{dv}{dt}$!" ] }, { @@ -800,7 +794,7 @@ "source": [ "### Section 2.1.2: Chain Rule\n", "\n", - "Many times we encounter situations in which the variable $a$ is changing with time ($t$) and affecting another variable $r$. How can we estimate the derivative of $r$ with respect to $a$ i.e. $\\frac{dr}{da} = ?$\n", + "We often encounter situations in which the variable $a$ changes with time ($t$) and affects another variable $r$. How can we estimate the derivative of $r$ with respect to $a$, i.e., $\\frac{dr}{da} = ?$\n", "\n", "To calculate $\\frac{dr}{da}$ we use the [Chain Rule](https://en.wikipedia.org/wiki/Chain_rule).\n", "\n", @@ -808,9 +802,9 @@ "\\frac{dr}{da} = \\frac{dr}{dt}\\cdot\\frac{dt}{da}\n", "\\end{equation}\n", "\n", - "That is, we calculate the derivative of both variables with respect to t and divide that derivative of $r$ by that derivative of $a$.\n", + "We calculate the derivative of both variables with respect to $t$ and divide that derivative of $r$ by that derivative of $a$.\n", "\n", - "We can also use this formula to simplify taking derivatives of complex functions! We can make an arbitrary function t so that we can compute more simple derivatives and multiply, as we will see in this exercise." + "We can also use this formula to simplify taking derivatives of complex functions! We can make an arbitrary function $t$ to compute more simple derivatives and multiply, as seen in this exercise." ] }, { @@ -827,9 +821,9 @@ "r(a) = e^{a^4 + 1}\n", "\\end{equation}\n", "\n", - "What is $\\frac{dr}{da}$? This is a more complex function so we can't simply consult a table of common derivatives. Can you use the chain rule to help?\n", + "What is $\\frac{dr}{da}$? This is a more complex function, so we can't simply consult a table of common derivatives. Can you use the chain rule to help?\n", "\n", - "**Hint:** we didn't define t but you could set t equal to the function in the exponent." + "**Hint:** We didn't define $t$, but you could set $t$ equal to the function in the exponent." ] }, { @@ -839,7 +833,7 @@ "execution": {} }, "source": [ - "[*Click for solution*](https://github.com/NeuromatchAcademy/precourse/tree/main/tutorials/W0D4_Calculus/solutions/W0D4_Tutorial1_Solution_a0e42694.py)\n", + "[*Click for solution*](https://github.com/NeuromatchAcademy/precourse/tree/main/tutorials/W0D4_Calculus/solutions/W0D4_Tutorial1_Solution_90e4db73.py)\n", "\n" ] }, @@ -866,7 +860,7 @@ "\n", "There is a useful Python library for getting the analytical derivatives of functions: SymPy. We actually used this in Interactive Demo 1, under the hood.\n", "\n", - "See the following cell for an example of setting up a sympy function and finding the derivative." + "See the following cell for an example of setting up a SymPy function and finding the derivative." ] }, { @@ -920,9 +914,9 @@ "source": [ "### Interactive Demo 2.2: Numerical Differentiation of the Sine Function\n", "\n", - "Below, we find the numerical derivative of the sine function for different values of $h$, and and compare the result the analytical solution.\n", + "Below, we find the numerical derivative of the sine function for different values of $h$ and compare the result to the analytical solution.\n", "\n", - "- What values of h result in more accurate numerical derivatives?" + "* What values of $h$ result in more accurate numerical derivatives?" ] }, { @@ -1001,7 +995,7 @@ "\n", "*Estimated timing to here from start of tutorial: 34 min*\n", "\n", - "When we inject a constant current (DC) in a neuron, its firing rate changes as a function of strength of the injected current. This is called the **input-output transfer function** or just the *transfer function* or *I/O Curve* of the neuron. For most neurons this can be approximated by a sigmoid function e.g.,:\n", + "When we inject a constant current (DC) into a neuron, its firing rate changes as a function of the strength of the injected current. This is called the **input-output transfer function** or just the *transfer function* or *I/O Curve* of the neuron. For most neurons, this can be approximated by a sigmoid function, e.g.:\n", "\n", "\\begin{equation}\n", "rate(I) = \\frac{1}{1+\\text{exp}(-a \\cdot (I-\\theta))} - \\frac{1}{\\text{exp}(a \\cdot \\theta)} + \\eta\n", @@ -1011,7 +1005,7 @@ "\n", "*You will visit this equation in a different context in Week 3*\n", "\n", - "The slope of a neurons input-output transfer function, i.e., $\\frac{d(r(I))}{dI}$, is called the **gain** of the neuron, as it tells how the neuron output will change if the input is changed. In other words, the slope of the transfer function tells us in which range of inputs the neuron output is most sensitive to changes in its input." + "The slope of a neuron input-output transfer function, i.e., $\\frac{d(r(I))}{dI}$, is called the **gain** of the neuron, as it tells how the neuron output will change if the input is changed. In other words, the slope of the transfer function tells us in which range of inputs the neuron output is most sensitive to changes in its input." ] }, { @@ -1022,9 +1016,9 @@ "source": [ "### Interactive Demo 2.3: Calculating the Transfer Function and Gain of a Neuron\n", "\n", - "In the following demo, you can estimate the gain of the following neuron transfer function using numerical differentiaton. We will use our timestep as h. See the cell below for a function that computes the rate via the fomula above and then the gain using numerical differentiation. In the following cell, you can play with the parameters $a$ and $\\theta$ to change the shape of the transfer functon (and see the resulting gain function). You can also set $I_{mean}$ to see how the slope is computed for that value of I. In the left plot, the red vertical lines are the two values of the current being used to compute the slope, while the blue lines point to the corresponding ouput firing rates.\n", + "In the following demo, you can estimate the gain of the following neuron transfer function using numerical differentiation. We will use our timestep as h. See the cell below for a function that computes the rate via the formula above and then the gain using numerical differentiation. In the following cell, you can play with the parameters $a$ and $\\theta$ to change the shape of the transfer function (and see the resulting gain function). You can also set $I_{mean}$ to see how the slope is computed for that value of I. In the left plot, the red vertical lines are the two values of the current being used to calculate the slope, while the blue lines point to the corresponding output firing rates.\n", "\n", - "Change the parameters of the neuron transfer function (i.e., $a$ and $\\theta$) and see if you can predict the value of $I$ for which the neuron has maximal slope and which parameter determines the peak value of the gain.\n", + "Change the parameters of the neuron transfer function (i.e., $a$ and $\\theta$) and see if you can predict the value of $I$ for which the neuron has a maximal slope and which parameter determines the peak value of the gain.\n", "\n", "1. Ensure you understand how the right plot relates to the left!\n", "2. How does $\\theta$ affect the transfer function and gain?\n", @@ -1273,13 +1267,13 @@ "source": [ "### Interactive Demo 3: Visualize partial derivatives\n", "\n", - "In the demo below, you can input any function of x and y and then visualize both the function and partial derivatives.\n", + "In the demo below, you can input any function of $x$ and $y$ and then visualize both the function and partial derivatives.\n", "\n", - "We visualized the 2-dimensional function as a surface plot in which the values of the function are rendered as color. Yellow represents a high value and blue represents a low value. The height of the surface also shows the numerical value of the function. A more complete description of 2D surface plots and why we need them is located in Bonus Section 1.1. The first plot is that of our function. And the two bottom plots are the derivative surfaces with respect to $x$ and $y$ variables.\n", + "We visualized the 2-dimensional function as a surface plot in which the function values are rendered as color. Yellow represents a high value, and blue represents a low value. The height of the surface also shows the numerical value of the function. A complete description of 2D surface plots and why we need them can be found in Bonus Section 1.1. The first plot is that of our function. And the two bottom plots are the derivative surfaces with respect to $x$ and $y$ variables.\n", "\n", "1. Ensure you understand how the plots relate to each other - if not, review the above material\n", - "2. Can you come up with a function where the partial derivative with respect to x will be a linear plane and the derivative with respect to y will be more curvy?\n", - "3. What happens to the partial derivatives if there are no terms involving multiplying $x$ and $y$ together?" + "2. Can you come up with a function where the partial derivative with respect to x will be a linear plane, and the derivative with respect to y will be more curvy?\n", + "3. What happens to the partial derivatives if no terms involve multiplying $x$ and $y$ together?" ] }, { @@ -1548,7 +1542,7 @@ }, "source": [ "There are other methods of numerical integration, such as\n", - "**[Lebesgue integral](https://en.wikipedia.org/wiki/Lebesgue_integral)** and **Runge Kutta**. In the Lebesgue integral, we divide the area under the curve into horizontal stripes. That is, instead of the independent variable, the range of the function $f(t)$ is divided into small intervals. In any case, the Riemann sum is the basis of Euler's method of integration for solving ordinary differential equations - something you will do in a later tutorial today." + "**[Lebesgue integral](https://en.wikipedia.org/wiki/Lebesgue_integral)** and **Runge Kutta**. In the Lebesgue integral, we divide the area under the curve into horizontal stripes. That is, instead of the independent variable, the range of the function $f(t)$ is divided into small intervals. In any case, the Riemann sum is the basis of Euler's integration method for solving ordinary differential equations - something you will do in a later tutorial today." ] }, { @@ -1582,7 +1576,8 @@ }, "source": [ "### Coding Exercise 4.2: Calculating Charge Transfer with Excitatory Input\n", - "An incoming spike elicits a change in the post-synaptic membrane potential (PSP) which can be captured by the following function:\n", + "\n", + "An incoming spike elicits a change in the post-synaptic membrane potential (PSP), which can be captured by the following function:\n", "\n", "\\begin{equation}\n", "PSP(t) = J \\cdot t \\cdot \\text{exp}\\left(-\\frac{t-t_{sp}}{\\tau_{s}}\\right)\n", @@ -1590,7 +1585,7 @@ "\n", "where $J$ is the synaptic amplitude, $t_{sp}$ is the spike time and $\\tau_s$ is the synaptic time constant.\n", "\n", - "Estimate the total charge transfered to the postsynaptic neuron during an PSP with amplitude $J=1.0$, $\\tau_s = 1.0$ and $t_{sp} = 1$ (that is the spike occured at $1$ ms). The total charge will be the integral of the PSP function." + "Estimate the total charge transferred to the postsynaptic neuron during a PSP with amplitude $J=1.0$, $\\tau_s = 1.0$ and $t_{sp} = 1$ (that is the spike occurred at $1$ ms). The total charge will be the integral of the PSP function." ] }, { @@ -1816,9 +1811,7 @@ "execution": {} }, "source": [ - "Notice how the differentiation operation amplifies the fast changes which were contributed by noise. By contrast, the integration operation supresses the fast changing noise. If we perform the same operation of averaging the adjancent samples on the orange trace, we will further smooth the signal. Such sums and subtractions form the basis of digital filters.\n", - "\n", - "\n" + "Notice how the differentiation operation amplifies the fast changes which were contributed by noise. By contrast, the integration operation suppresses the fast-changing noise. We will further smooth the signal if we perform the same operation of averaging the adjacent samples on the orange trace. Such sums and subtractions form the basis of digital filters." ] }, { @@ -1832,13 +1825,13 @@ "\n", "*Estimated timing of tutorial: 80 minutes*\n", "\n", - "* Geometrically, integration is the area under the curve and differentiation is the slope of the function\n", + "* Geometrically, integration is the area under the curve, and differentiation is the slope of the function\n", "* The concepts of slope and area can be easily extended to higher dimensions. We saw this when we took the derivative of a 2-dimensional transfer function of a neuron\n", - "* Numerical estimates of both derivatives and integrals require us to choose a time step $h$. The smaller the $h$, the better the estimate, but for small values of $h$, more computations are needed. So there is always some tradeoff.\n", - "* Partial derivatives are just the estimate of the slope along one of the many dimensions of the function. We can combine the slopes in different directions using vector sum to find the direction of the slope.\n", - "* Because the derivative of a function is zero at the local peak or trough, derivatives are used to solve optimization problems.\n", - "* When thinking of signal, integration operation is equivalent to smoothening the signals (i.e., remove fast changes)\n", - "* Differentiation operations remove slow changes and enhance high frequency content of a signal" + "* Numerical estimates of both derivatives and integrals require us to choose a time step $h$. The smaller the $h$, the better the estimate, but more computations are needed for small values of $h$. So there is always some tradeoff\n", + "* Partial derivatives are just the estimate of the slope along one of the many dimensions of the function. We can combine the slopes in different directions using vector sum to find the direction of the slope\n", + "* Because the derivative of a function is zero at the local peak or trough, derivatives are used to solve optimization problems\n", + "* When thinking of signal, integration operation is equivalent to smoothening the signals (i.e., removing fast changes)\n", + "* Differentiation operations remove slow changes and enhance the high-frequency content of a signal" ] }, { @@ -1861,11 +1854,11 @@ "source": [ "## Bonus Section 1.1: Understanding 2D plots\n", "\n", - "Let's take the example of a neuron driven by excitatory and inhibitory inputs. Because this is for illustrative purposes, we will not go in the details of the numerical range of the input and output variables.\n", + "Let's take the example of a neuron driven by excitatory and inhibitory inputs. Because this is for illustrative purposes, we will not go into the details of the numerical range of the input and output variables.\n", "\n", - "In the function below, we assume that the firing rate of a neuron increases motonotically with an increase in excitation and decreases monotonically with an increase in inhibition. The inhibition is modelled as a subtraction. Like for the 1-dimensional transfer function, here we assume that we can approximate the transfer function as a sigmoid function.\n", + "In the function below, we assume that the firing rate of a neuron increases monotonically with an increase in excitation and decreases monotonically with an increase in inhibition. The inhibition is modeled as a subtraction. As for the 1-dimensional transfer function, we assume that we can approximate the transfer function as a sigmoid function.\n", "\n", - "To evaluate the partial derivatives we can use the same numerical differentiation as before but now we apply it to each row and column separately." + "We can use the same numerical differentiation as before to evaluate the partial derivatives, but now we apply it to each row and column separately." ] }, { @@ -1950,15 +1943,15 @@ "execution": {} }, "source": [ - "In the **Top-Left** plot, we see how the neuron output rate increases as a function of excitatory input (e.g., the blue trace). However, as we increase inhibition, expectedly the neuron output decreases and the curve is shifted downwards. This constant shift in the curve suggests that the effect of inhibition is subtractive, and the amount of subtraction does not depend on the neuron output.\n", + "The **Top-Left** plot shows how the neuron output rate increases as a function of excitatory input (e.g., the blue trace). However, as we increase inhibition, expectedly, the neuron output decreases, and the curve is shifted downwards. This constant shift in the curve suggests that the effect of inhibition is subtractive, and the amount of subtraction does not depend on the neuron output.\n", "\n", "We can alternatively see how the neuron output changes with respect to inhibition and study how excitation affects that. This is visualized in the **Top-Right** plot.\n", "\n", "This type of plotting is very intuitive, but it becomes very tedious to visualize when there are larger numbers of lines to be plotted. A nice solution to this visualization problem is to render the data as color, as surfaces, or both.\n", "\n", - "This is what we have done in the plot on the bottom. The colormap on the right shows the output of the neuron as a function of inhibitory input and excitatory input. The output rate is shown both as height along the z-axis and as the color. Blue means low firing rate and yellow means high firing rate (see the color bar).\n", + "This is what we have done in the plot at the bottom. The color map on the right shows the neuron's output as a function of inhibitory and excitatory input. The output rate is shown both as height along the z-axis and as the color. Blue means a low firing rate, and yellow represents a high firing rate (see the color bar).\n", "\n", - "In the above plot, the output rate of the neuron goes below zero. This is of course not physiological as neurons cannot have negative firing rates. In models, we either choose the operating point such that the output does not go below zero, or else we clamp the neuron output to zero if it goes below zero. You will learn about it more in Week 2." + "In the above plot, the output rate of the neuron goes below zero. This is, of course, not physiological, as neurons cannot have negative firing rates. In models, we either choose the operating point so that the output does not go below zero, or we clamp the neuron output to zero if it goes below zero. You will learn about it more in Week 2." ] }, { @@ -2053,8 +2046,7 @@ "execution": {} }, "source": [ - "Is this what you expected? Change the parameters in the function to generate the 2-d transfer function of the neuron for different excitatory and inhibitory $a$ and $\\theta$ and test your intuitions Can you relate this shape of the partial derivative surface to the gain of the 1-d transfer-function of a neuron (Section 2)?\n", - "\n" + "Is this what you expected? Change the parameters in the function to generate the 2-D transfer function of the neuron for different excitatory and inhibitory $a$ and $\\theta$ and test your intuitions. Can you relate this shape of the partial derivative surface to the gain of the 1-D transfer function of a neuron (Section 2)?" ] }, { diff --git a/tutorials/W0D4_Calculus/student/W0D4_Tutorial2.ipynb b/tutorials/W0D4_Calculus/student/W0D4_Tutorial2.ipynb index 51dff28..9d604bb 100644 --- a/tutorials/W0D4_Calculus/student/W0D4_Tutorial2.ipynb +++ b/tutorials/W0D4_Calculus/student/W0D4_Tutorial2.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "74ca2e85", + "id": "bc470a90", "metadata": { "colab_type": "text", "execution": {}, @@ -54,18 +54,18 @@ "\n", "*Estimated timing of tutorial: 45 minutes*\n", "\n", - "A great deal of neuroscience can be modelled using differential equations, from gating channels to single neurons to a network of neurons to blood flow, to behaviour. A simple way to think about differential equations is they are equations that describe how something changes.\n", + "A great deal of neuroscience can be modeled using differential equations, from gating channels to single neurons to a network of neurons to blood flow to behavior. A simple way to think about differential equations is they are equations that describe how something changes.\n", "\n", - "The most famous of these in neuroscience is the Nobel Prize winning Hodgkin Huxley equation, which describes a neuron by modelling the gating of each axon. But we will not start there; we will start a few steps back.\n", + "The most famous of these in neuroscience is the Nobel Prize-winning Hodgkin-Huxley equation, which describes a neuron by modeling the gating of each axon. But we will not start there; we will start a few steps back.\n", "\n", - "Differential Equations are mathematical equations that describe how something like population or a neuron changes over time. The reason why differential equations are so useful is they can generalise a process such that one equation can be used to describe many different outcomes.\n", - "The general form of a first order differential equation is:\n", + "Differential Equations are mathematical equations that describe how something like a population or a neuron changes over time. Differential equations are so useful because they can generalize a process such that one equation can be used to describe many different outcomes.\n", + "The general form of a first-order differential equation is:\n", "\n", "\\begin{equation}\n", "\\frac{d}{dt}y(t) = f\\left( t,y(t) \\right)\n", "\\end{equation}\n", "\n", - "which can be read as \"the change in a process $y$ over time $t$ is a function $f$ of time $t$ and itself $y$\". This might initially seem like a paradox as you are using a process $y$ you want to know about to describe itself, a bit like the MC Escher drawing of two hands painting [each other](https://en.wikipedia.org/wiki/Drawing_Hands). But that is the beauty of mathematics - this can be solved some of time, and when it cannot be solved exactly we can use numerical methods to estimate the answer (as we will see in the next tutorial).\n", + "which can be read as \"the change in a process $y$ over time $t$ is a function $f$ of time $t$ and itself $y$\". This might initially seem like a paradox as you are using a process $y$ you want to know about to describe itself, a bit like the MC Escher drawing of two hands painting [each other](https://en.wikipedia.org/wiki/Drawing_Hands). But that is the beauty of mathematics - this can be solved sometimes, and when it cannot be solved exactly, we can use numerical methods to estimate the answer (as we will see in the next tutorial).\n", "\n", "In this tutorial, we will see how __differential equations are motivated by observations of physical responses__. We will break down the population differential equation, then the integrate and fire model, which leads nicely into raster plots and frequency-current curves to rate models.\n", "\n", @@ -73,7 +73,7 @@ "- Get an intuitive understanding of a linear population differential equation (humans, not neurons)\n", "- Visualize the relationship between the change in population and the population\n", "- Breakdown the Leaky Integrate and Fire (LIF) differential equation\n", - "- Code the exact solution of an LIF for a constant input\n", + "- Code the exact solution of a LIF for a constant input\n", "- Visualize and listen to the response of the LIF for different inputs" ] }, @@ -556,6 +556,7 @@ }, "source": [ "### Think! 1.1: Interpretating the behavior of a linear population equation\n", + "\n", "Using the plot below of change of population $\\frac{d}{dt} p(t) $ as a function of population $p(t)$ with birth-rate $\\alpha=0.3$, discuss the following questions:\n", "\n", "1. Why is the population differential equation known as a linear differential equation?\n", @@ -629,13 +630,14 @@ }, "source": [ "### Section 1.1.1: Initial condition\n", - "The linear population differential equation is known as an initial value differential equation because we need an initial population value to solve it. Here we will set our initial population at time $0$ to $1$:\n", + "\n", + "The linear population differential equation is an initial value differential equation because we need an initial population value to solve it. Here we will set our initial population at time $0$ to $1$:\n", "\n", "\\begin{equation}\n", "p(0) = 1\n", "\\end{equation}\n", "\n", - "Different initial conditions will lead to different answers, but they will not change the differential equation. This is one of the strengths of a differential equation." + "Different initial conditions will lead to different answers but will not change the differential equation. This is one of the strengths of a differential equation." ] }, { @@ -646,7 +648,8 @@ }, "source": [ "### Section 1.1.2: Exact Solution\n", - "To calculate the exact solution of a differential equation, we must integrate both sides. Instead of numerical integration (as you delved into in the last tutorial), we will first try to solve the differential equations using analytical integration. As with derivatives, we can find analytical integrals of simple equations by consulting [a list](https://en.wikipedia.org/wiki/Lists_of_integrals). We can then get integrals for more complex equations using some mathematical tricks - the harder the equation the more obscure the trick.\n", + "\n", + "To calculate the exact solution of a differential equation, we must integrate both sides. Instead of numerical integration (as you delved into in the last tutorial), we will first try to solve the differential equations using analytical integration. As with derivatives, we can find analytical integrals of simple equations by consulting [a list](https://en.wikipedia.org/wiki/Lists_of_integrals). We can then get integrals for more complex equations using some mathematical tricks - the harder the equation, the more obscure the trick.\n", "\n", "The linear population equation\n", "\\begin{equation}\n", @@ -665,9 +668,9 @@ "\\text{\"Population\"} = \\text{\"grows/declines exponentially as a function of time and birth rate\"}.\n", "\\end{equation}\n", "\n", - "Most differential equations do not have a known exact solution, so in the next tutorial on numerical methods we will show how the solution can be estimated.\n", + "Most differential equations do not have a known exact solution, so we will show how the solution can be estimated in the next tutorial on numerical methods.\n", "\n", - "A small aside: a good deal of progress in mathematics was due to mathematicians writing taunting letters to each other saying they had a trick that could solve something better than everyone else. So do not worry too much about the tricks." + "A small aside: a good deal of progress in mathematics was due to mathematicians writing taunting letters to each other, saying they had a trick to solve something better than everyone else. So do not worry too much about the tricks." ] }, { @@ -733,7 +736,7 @@ "\n", "*Estimated timing to here from start of tutorial: 12 min*\n", "\n", - "One of the goals when designing a differential equation is to make it generalisable. Which means that the differential equation will give reasonable solutions for different countries with different birth rates $\\alpha$.\n" + "One of the goals when designing a differential equation is to make it generalizable. This means that the differential equation will give reasonable solutions for different countries with different birth rates $\\alpha$." ] }, { @@ -745,7 +748,7 @@ "source": [ "### Interactive Demo 1.2: Parameter Change\n", "\n", - "Play with the widget to see the relationship between $\\alpha$ and the population differential equation as a function of population (left-hand side), and the population solution as a function of time (right-hand side). Pay close attention to the transition point from positive to negative.\n", + "Play with the widget to see the relationship between $\\alpha$ and the population differential equation as a function of population (left-hand side) and the population solution as a function of time (right-hand side). Pay close attention to the transition point from positive to negative.\n", "\n", "How do changing parameters of the population equation affect the outcome?\n", "\n", @@ -807,17 +810,15 @@ "execution": {} }, "source": [ - "The population differential equation is an over-simplification and has some very obvious limitations:\n", - "1. Population growth is not exponential as there are limited number of resources so the population will level out at some point.\n", - "2. It does not include any external factors on the populations like weather, predators and preys.\n", + "The population differential equation is an over-simplification and has some pronounced limitations:\n", + "1. Population growth is not exponential as there are limited resources, so the population will level out at some point.\n", + "2. It does not include any external factors on the populations, like weather, predators, and prey.\n", "\n", - "These kind of limitations can be addressed by extending the model.\n", + "These kinds of limitations can be addressed by extending the model.\n", "\n", + "While it might not seem that the population equation has direct relevance to neuroscience, a similar equation is used to describe the accumulation of evidence for decision-making. This is known as the Drift Diffusion Model, and you will see it in more detail in the Linear System Day in Neuromatch (W2D2).\n", "\n", - "While it might not seem that the population equation has direct relevance to neuroscience, a similar equation is used to describe the accumulation of evidence for decision making. This is known as the Drift Diffusion Model and you will see in more detail in the Linear System day in Neuromatch (W2D2).\n", - "\n", - "\n", - "Another differential equation that is similar to the population equation is the Leaky Integrate and Fire model which you may have seen in the python pre-course materials on W0D1 and W0D2. It will turn up later in Neuromatch as well. Below we will delve in the motivation of the differential equation." + "Another differential equation similar to the population equation is the Leaky Integrate and Fire model, which you may have seen in the Python pre-course materials on W0D1 and W0D2. It will turn up later in Neuromatch as well. Below we will delve into the motivation of the differential equation." ] }, { @@ -1102,7 +1103,8 @@ }, "source": [ "#### Interactive Demo 2.1.1: Initial Condition $V_{reset}$\n", - "This exercise is to get an intuitive feel of how the different initial conditions $V_{reset}$ impacts the differential equation of the LIF and the exact solution for the equation:\n", + "\n", + "This exercise is to get an intuitive feel of how the different initial conditions $V_{reset}$ impact the differential equation of the LIF and the exact solution for the equation:\n", "\n", "\\begin{equation}\n", "\\frac{dV}{dt} = \\frac{-(V-E_L)}{\\tau_m}\n", @@ -1112,7 +1114,7 @@ "* `E_L = -75,`\n", "* `tau_m = 10.`\n", "\n", - "The panel on the left-hand side plots the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ and right-hand side panel plots the exact solution $V$ as a function of time $t,$ the green dot in both panels is the reset potential $V_{reset}$.\n", + "The panel on the left-hand side plots the change in membrane potential $\\frac{dV}{dt}$ as a function of membrane potential $V$ and the right-hand side panel plots the exact solution $V$ as a function of time $t,$ the green dot in both panels is the reset potential $V_{reset}$.\n", "\n", "Pay close attention to when $V_{reset}=E_L=-75$mV.\n", "\n", @@ -1176,6 +1178,7 @@ }, "source": [ "## Section 2.2: LIF with input\n", + "\n", "*Estimated timing to here from start of tutorial: 24 min*\n", "\n", "We will re-introduce the input $I$ and membrane resistance $R_m$ giving the original equation:\n", @@ -1195,6 +1198,7 @@ }, "source": [ "### Interactive Demo 2.2: The Impact of Input\n", + "\n", "The interactive plot below manipulates $I$ in the differential equation.\n", "\n", "- With increasing input, how does the $\\frac{dV}{dt}$ change? How would this impact the solution?" @@ -1443,7 +1447,8 @@ }, "source": [ "### Interactive Demo 2.3.1: Input on spikes\n", - "This exercise show the relationship between firing rate and the Input for exact solution `V` of the LIF:\n", + "\n", + "This exercise shows the relationship between the firing rate and the Input for the exact solution `V` of the LIF:\n", "\n", "\\begin{equation}\n", "V(t) = E_L+R_mI+(V_{reset}-E_L-R_mI)e^{\\frac{-(t-t_{isi})}{\\tau_m}},\n", @@ -1460,7 +1465,7 @@ "* the middle panel is the membrane potential $V(t)$. To illustrate the spike, $V(t)$ is set to $0$ and then reset to $-75$ mV when there is a spike.\n", "* the bottom panel is the raster plot with each dot indicating a spike.\n", "\n", - "First, as electrophysiologist normally listen to spikes when conducting experiments, listen to the music of the firing rate for a single value of $I$.\n", + "First, as electrophysiologists normally listen to spikes when conducting experiments, listen to the music of the firing rate for a single value of $I$.\n", "\n", "**Note:** The audio doesn't work in some browsers so don't worry about it if you can't hear anything." ] @@ -1556,9 +1561,9 @@ "\n", "*Estimated timing to here from start of tutorial: 38 min*\n", "\n", - "The firing frequency of a neuron plotted as a function of current is called an input-output curve (F–I curve). It is also known as a transfer function, which you came across in the previous tutorial. This function is one of the starting points for the rate model, which extends from modelling single neurons to the firing rate of a collection of neurons.\n", + "The firing frequency of a neuron plotted as a function of current is called an input-output curve (F–I curve). It is also known as a transfer function, which you came across in the previous tutorial. This function is one of the starting points for the rate model, which extends from modeling single neurons to the firing rate of a collection of neurons.\n", "\n", - "By fitting this to a function, we can start to generalise the firing pattern of many neurons, which can be used to build rate models. This will be discussed later in Neuromatch." + "By fitting this to a function, we can start to generalize the firing pattern of many neurons, which can be used to build rate models. This will be discussed later in Neuromatch." ] }, { @@ -1603,8 +1608,7 @@ "__Weaknesses of the LIF model:__\n", "- Spiking is a discontinuity;\n", "- Abstraction from biology;\n", - "- Cannot generate different spiking patterns.\n", - "\n" + "- Cannot generate different spiking patterns." ] }, { @@ -1700,12 +1704,11 @@ "source": [ "In this tutorial, we have seen two differential equations, the population differential equations and the leaky integrate and fire model.\n", "\n", - "\n", - "We learned about:\n", + "We learned about the following:\n", "* The motivation for differential equations.\n", - "* An intuitive relationship between the solution and the form of the differential equation.\n", + "* An intuitive relationship between the solution and the differential equation form.\n", "* How different parameters of the differential equation impact the solution.\n", - "* The strengths and limitations of the simple differential equations.\n" + "* The strengths and limitations of the simple differential equations." ] }, { diff --git a/tutorials/W0D4_Calculus/student/W0D4_Tutorial3.ipynb b/tutorials/W0D4_Calculus/student/W0D4_Tutorial3.ipynb index 868ede3..fb9ec1f 100644 --- a/tutorials/W0D4_Calculus/student/W0D4_Tutorial3.ipynb +++ b/tutorials/W0D4_Calculus/student/W0D4_Tutorial3.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "6c2d4b58", + "id": "dfd443f6", "metadata": { "colab_type": "text", "execution": {}, @@ -61,7 +61,7 @@ "- Investigate the impact of time step on the error of the numerical solution;\n", "- Code the Euler estimate of the Leaky Integrate and Fire model for a constant input;\n", "- Visualize and listen to the response of the integrate for different inputs;\n", - "- Apply the Euler method to estimate the solution of a system of differential equations.\n" + "- Apply the Euler method to estimate the solution of a system of differential equations." ] }, { @@ -477,11 +477,11 @@ }, "source": [ "### Interactive Demo 1.1: Slope of a Line\n", - "The plot below shows a function $y(t)$ in blue, its exact derivative $ \\frac{d}{dt}y(t)$ at $t_0=1$ in black, and the approximate derivative calculated using the slope formula $=\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$ for different time-steps sizes $\\Delta t$ in green.\n", "\n", - "Interact with the widget to see how time-step impacts the accuracy of the slope which is the estimate of the derivative.\n", + "The plot below shows a function $y(t)$ in blue, its exact derivative $ \\frac{d}{dt}y(t)$ at $t_0=1$ in black. The approximate derivative is calculated using the slope formula $=\\frac{y(1+\\Delta t)-y(1)}{\\Delta t}$ for different time-steps sizes $\\Delta t$ in green.\n", "\n", - "- How does the size of $\\Delta t$ affect the approximation?\n" + "Interact with the widget to see how time-step impacts the slope's accuracy, which is the derivative's estimate.\n", + "- How does the size of $\\Delta t$ affect the approximation?" ] }, { @@ -605,13 +605,11 @@ }, "source": [ "### Interactive Demo 1.2: Euler error for a single step\n", - "Interact with the widget to see how the time-step $\\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.\n", "\n", + "Interact with the widget to see how the time-step $\\Delta t$, dt in code, impacts the estimate $p_1$ and the error $e_1$ of the Euler method.\n", "\n", "1. What happens to the estimate $p_1$ as the time-step $\\Delta t$ increases?\n", - "\n", "2. Is there a relationship between the size of $\\Delta t$ and $e_1$?\n", - "\n", "3. How would you improve the error $e_1$?" ] }, @@ -656,7 +654,7 @@ "execution": {} }, "source": [ - "The error $e_1$ from one time-step is known as the __local error__. For the Euler method the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $O(\\Delta t)$." + "The error $e_1$ from one time-step is known as the __local error__. For the Euler method, the local error is linear, which means that the error decreases linearly as a function of timestep and is written as $\\mathcal{O}(\\Delta t)$." ] }, { @@ -953,8 +951,9 @@ "execution": {} }, "source": [ - "The error is smaller for 4 time-steps than taking one large time step from 1 to 5 but do note that the error is increasing for each step. This is known as __global error__ so the futher in time you want to predict, the larger the error.\n", - "You can read the theorems [here.](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)" + "The error is smaller for 4-time steps than taking one large time step from 1 to 5 but does note that the error is increasing for each step. This is known as __global error__ so the further in time you want to predict, the larger the error.\n", + "\n", + "You can read the theorems [here](https://colab.research.google.com/github/john-s-butler-dit/Numerical-Analysis-Python/blob/master/Chapter%2001%20-%20Euler%20Methods/102_Euler_method_with_Theorems_nonlinear_Growth_function.ipynb)." ] }, { @@ -1609,7 +1608,7 @@ "\n", "1. Which representation is more intuitive (and useful), the time plot or the phase plane plot?\n", "2. Why do we only see one circle?\n", - "3. What do the quadrants represent?\n" + "3. What do the quadrants represent?" ] }, { @@ -1988,8 +1987,7 @@ "execution": {} }, "source": [ - "Using pretty much the formula for the slope of a line, the solution of differential equation can be estimated with reasonable accuracy.\n", - "This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution." + "Using the formula for the slope of a line, the solution of the differential equation can be estimated with reasonable accuracy. This will be much more relevant when dealing with more complicated (non-linear) differential equations where there is no known exact solution." ] }, { @@ -2002,14 +2000,14 @@ "---\n", "## Links to Neuromatch Computational Neuroscience Days\n", "\n", - "Differential equations turn up in a number of different Neuromatch days:\n", - "* The LIF model is discussed in more details in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", - "* Drift Diffusion model which is a differential equation for decision making is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", + "Differential equations turn up on a number of different Neuromatch days:\n", + "* The LIF model is discussed in more detail in [Model Types](https://compneuro.neuromatch.io/tutorials/W1D1_ModelTypes/chapter_title.html) and [Biological Neuron Models](https://compneuro.neuromatch.io/tutorials/W2D3_BiologicalNeuronModels/chapter_title.html).\n", + "* Drift Diffusion model, which is a differential equation for decision making, is discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html).\n", "* Phase-plane plots are discussed in [Linear Systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html) and [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", "* The Wilson-Cowan model is discussed in [Dynamic Networks](https://compneuro.neuromatch.io/tutorials/W2D4_DynamicNetworks/chapter_title.html).\n", "\n", "## References\n", - "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", + "1. Lotka, A. L, (1920) Analytical note on certain rhythmic relations inorganic systems.Proceedings of the National Academy of Sciences, 6(7):410–415,1920. doi: [10.1073/pnas.6.7.410](https://doi.org/10.1073/pnas.6.7.410)\n", "2. Brunel N, van Rossum MC. Lapicque's 1907 paper: from frogs to integrate-and-fire. Biol Cybern. 2007 Dec;97(5-6):337-9. doi: [10.1007/s00422-007-0190-0](https://doi.org/10.1007/s00422-007-0190-0). Epub 2007 Oct 30. PMID: 17968583.\n", "\n", "\n", @@ -2162,15 +2160,14 @@ "---\n", "## Bonus Section 2: Neural oscillations are a start toward understanding brain activity rather than the end\n", "\n", - "The differential equations we have discussed above are all to simulate neuronal processes, another way differential equations can be used is to motivate experimental findings.\n", + "The differential equations we have discussed above are all to simulate neuronal processes. Another way differential equations can be used is to motivate experimental findings.\n", "\n", - "A great deal of experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrain to a stimulus.\n", - "An issue with these studies is that there is not a consistent definition of what constitutes an oscillation. Right now it is a bit of I know one when I see one problem.\n", + "Many experimental and neurophysiological studies have investigated whether the brain oscillates and/or entrees to a stimulus.\n", + "An issue with these studies is that there is no consistent definition of what constitutes an oscillation. Right now, it is a bit of I know one when I see one problem.\n", "\n", + "In an essay from May 2021 in PLoS Biology, Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally when studying oscillations. The essay proposes that instead of thinking about the brain, we should look at this question from the mathematical side to motivate what can be defined as an oscillation.\n", "\n", - "In an essay from May 2021 in PLOS Biology by Keith Dowling (a past Neuromatch TA) and M. Florencia Assaneo discussed a mathematical way of thinking about what should be expected experimentally when studying oscillations. In the essay they propose that instead of thinking about the brain we should look at this question from the mathematical side, to motivate what can be defined as an oscillation.\n", - "\n", - "To do this they used Stuart–Landau equations, which is a system of differential equations\n", + "To do this, they used Stuart–Landau equations, which is a system of differential equations\n", "\\begin{align}\n", "\\frac{dx}{dt} &= \\lambda x-\\omega y -\\gamma (x^2+y^2)x+s\\\\\n", "\\frac{dy}{dt} &= \\lambda y+\\omega x -\\gamma (x^2+y^2)y\n", @@ -2178,11 +2175,11 @@ "\n", "where $s$ is input to the system, and $\\lambda$, $\\omega$ and $\\gamma$ are parameters.\n", "\n", - "The Stuart–Landau equations are a well described system of non-linear differential equations that generate oscillations. For their purpose, Dowling and Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.\n", + "The Stuart–Landau equations are a well-described system of non-linear differential equations that generate oscillations. For their purpose, Dowling and Assaneo used the equations to motivate what experimenters should expect when conducting experiments looking for oscillations.\n", "In their paper, using the Stuart–Landau equations, they outline\n", "* \"What is an oscillator?\"\n", - "* \"What an oscillator is not\"\n", - "* \"Not all that oscillates is an oscillator\"\n", + "* \"What an oscillator is not.\"\n", + "* \"Not all that oscillates is an oscillator.\"\n", "* \"Not all oscillators are alike.\"\n", "\n", "The Euler form of the Stuart–Landau system of equations is:\n", @@ -2195,7 +2192,7 @@ "y_0&=1,\\\\\n", "\\end{align*}\n", "\n", - "with $ \\Delta t=0.1/1000$ ms.\n" + "with $ \\Delta t=0.1/1000$ ms." ] }, { @@ -2277,10 +2274,14 @@ }, "source": [ "### Bonus 2.1: What is an Oscillator?\n", + "\n", "Doelling & Assaneo (2021), using the Stuart–Landau system, show different possible states of an oscillator by manipulating the $\\lambda$ term in the equation.\n", - "From the paper: \"this qualitative change in behavior takes place at λ = 0: For λ < 0, the system decays to a stable equilibrium, while for λ > 0, it keeps oscillating.\"\n", "\n", - "This illustrates an oscillations does not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\\alpha$ band oscillations in EEG the oscillations come and go." + "From the paper:\n", + "\n", + "> This qualitative change in behavior takes place at $\\lambda = 0$. For $\\lambda < 0$, the system decays to a stable equilibrium, while for $\\lambda > 0$, it keeps oscillating.\n", + "\n", + "This illustrates that oscillations do not have to maintain all the time, so experimentally we should not expect perfectly maintained oscillations. We see this all the time in $\\alpha$ band oscillations in EEG the oscillations come and go." ] }, { @@ -2355,12 +2356,12 @@ "source": [ "#### Interactive Demo Bonus 2: Stuart-Landau System\n", "\n", - "The plot below shows estimated solution of the Stuart–Landau system with a base frequency of 4Hz by stetting $\\omega$ to $4\\times 2\\pi$, $\\lambda=1$, $\\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\\sin(freq 2\\pi t) $ in the top panel, $x$ as a function of time in the the bottom panel and on the right the phase plane plot of $x$ and $y$.\n", + "The plot below shows the estimated solution of the Stuart–Landau system with a base frequency of 4Hz by setting $\\omega$ to $4\\times 2\\pi$, $\\lambda=1$, $\\gamma=1$ and $k=50$ over 3 seconds the input to the system $s(t)=\\sin(freq 2\\pi t) $ in the top panel, $x$ as a function of time in the bottom panel and on the right the phase plane plot of $x$ and $y$.\n", "\n", - "You can manipulate the frequency $freq$ of the input to see how the oscillations change and for frequencies $freq$ further and further from the base frequency of 4Hz the oscillations breaks down.\n", + "You can manipulate the frequency $freq$ of the input to see how the oscillations change, and for frequencies $freq$ further and further from the base frequency of 4Hz, the oscillations break down.\n", "\n", - "This shoes that if you have an oscillating input into an oscillator with it does not have to respond by oscillating about could even breakdown. Hence the frequency of the input oscillation is important to the system.\n", - "So if you flash something at 50Hz, for example, the visual system might not follow the signal but that does not mean the visual system is not an oscillator it might just be the wrong frequency." + "This shows that if you have an oscillating input into an oscillator, it does not have to respond by oscillating about could even break down. Hence the frequency of the input oscillation is important to the system.\n", + "So if you flash something at 50Hz, for example, the visual system might not follow the signal, but that does not mean the visual system is not an oscillator. It might just be the wrong frequency." ] }, { @@ -2413,7 +2414,7 @@ "source": [ "**Bonus Reference 2**:\n", "\n", - "Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234. doi: [10.1371/journal.pbio.3001234](https://doi.org/10.1371/journal.pbio.3001234)\n" + "Doelling, K. B., & Assaneo, M. F. (2021). Neural oscillations are a start toward understanding brain activity rather than the end. PLoS biology, 19(5), e3001234. doi: [10.1371/journal.pbio.3001234](https://doi.org/10.1371/journal.pbio.3001234)" ] } ], diff --git a/tutorials/W0D5_Statistics/W0D5_Tutorial1.ipynb b/tutorials/W0D5_Statistics/W0D5_Tutorial1.ipynb index d2de726..29d4a72 100644 --- a/tutorials/W0D5_Statistics/W0D5_Tutorial1.ipynb +++ b/tutorials/W0D5_Statistics/W0D5_Tutorial1.ipynb @@ -1,2078 +1,1726 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "zBqSZwuYpmC2" - }, - "source": [ - "# Tutorial 1: Probability Distributions\n", - "\n", - "**Week 0, Day 5: Probability & Statistics**\n", - "\n", - "**By Neuromatch Academy**\n", - "\n", - "__Content creators:__ Ulrik Beierholm\n", - "\n", - "__Content reviewers:__ Natalie Schaworonkow, Keith van Antwerp, Anoop Kulkarni, Pooya Pakarian, Hyosub Kim\n", - "\n", - "__Production editors:__ Ethan Cheng, Ella Batty" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "3fx1fKeNpmC4" - }, - "source": [ - "

" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "DF_7FF_QpmC4" - }, - "source": [ - "---\n", - "# Tutorial Objectives\n", - "\n", - "We will cover the basic ideas from probability and statistics, as a reminder of what you have hopefully previously learned. These ideas will be important for almost every one of the following topics covered in the course.\n", - "\n", - "There are many additional topics within probability and statistics that we will not cover as they are not central to the main course. We also do not have time to get into a lot of details, but this should help you recall material you have previously encountered.\n", - "\n", - "\n", - "By completing the exercises in this tutorial, you should:\n", - "* get some intuition about how stochastic randomly generated data can be\n", - "* understand how to model data using simple probability distributions\n", - "* understand the difference between discrete and continuous probability distributions\n", - "* be able to plot a Gaussian distribution" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "KOrfzq_zpmC5" - }, - "source": [ - "---\n", - "# Setup\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "_H1OF1OOpmC5" - }, - "outputs": [], - "source": [ - "# @title Install and import feedback gadget\n", - "\n", - "!pip3 install vibecheck datatops --quiet\n", - "\n", - "from vibecheck import DatatopsContentReviewContainer\n", - "def content_review(notebook_section: str):\n", - " return DatatopsContentReviewContainer(\n", - " \"\", # No text prompt\n", - " notebook_section,\n", - " {\n", - " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", - " \"name\": \"neuromatch-precourse\",\n", - " \"user_key\": \"8zxfvwxw\",\n", - " },\n", - " ).render()\n", - "\n", - "\n", - "feedback_prefix = \"W0D5_T1\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "both", - "execution": {}, - "id": "OcGqwrrDpmC6" - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import scipy as sp\n", - "from scipy.stats import norm # the normal probability distribution" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "S1cSW3-4pmC7" - }, - "outputs": [], - "source": [ - "# @title Figure settings\n", - "import logging\n", - "logging.getLogger('matplotlib.font_manager').disabled = True\n", - "import ipywidgets as widgets # interactive display\n", - "from ipywidgets import interact, fixed, HBox, Layout, VBox, interactive, Label, interact_manual\n", - "%config InlineBackend.figure_format = 'retina'\n", - "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "nuc_0WFEpmC7" - }, - "outputs": [], - "source": [ - "# @title Plotting Functions\n", - "\n", - "def plot_random_sample(x, y, figtitle = None):\n", - " \"\"\" Plot the random sample between 0 and 1 for both the x and y axes.\n", - "\n", - " Args:\n", - " x (ndarray): array of x coordinate values across the random sample\n", - " y (ndarray): array of y coordinate values across the random sample\n", - " figtitle (str): title of histogram plot (default is no title)\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - " fig, ax = plt.subplots()\n", - " ax.set_xlabel('x')\n", - " ax.set_ylabel('y')\n", - " plt.xlim([-0.25, 1.25]) # set x and y axis range to be a bit less than 0 and greater than 1\n", - " plt.ylim([-0.25, 1.25])\n", - " plt.scatter(dataX, dataY)\n", - " if figtitle is not None:\n", - " fig.suptitle(figtitle, size=16)\n", - " plt.show()\n", - "\n", - "\n", - "def plot_random_walk(x, y, figtitle = None):\n", - " \"\"\" Plots the random walk within the range 0 to 1 for both the x and y axes.\n", - "\n", - " Args:\n", - " x (ndarray): array of steps in x direction\n", - " y (ndarray): array of steps in y direction\n", - " figtitle (str): title of histogram plot (default is no title)\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - " fig, ax = plt.subplots()\n", - " plt.plot(x,y,'b-o', alpha = 0.5)\n", - " plt.xlim(-0.1,1.1)\n", - " plt.ylim(-0.1,1.1)\n", - " ax.set_xlabel('x location')\n", - " ax.set_ylabel('y location')\n", - " plt.plot(x[0], y[0], 'go')\n", - " plt.plot(x[-1], y[-1], 'ro')\n", - "\n", - " if figtitle is not None:\n", - " fig.suptitle(figtitle, size=16)\n", - " plt.show()\n", - "\n", - "\n", - "def plot_hist(data, xlabel, figtitle = None, num_bins = None):\n", - " \"\"\" Plot the given data as a histogram.\n", - "\n", - " Args:\n", - " data (ndarray): array with data to plot as histogram\n", - " xlabel (str): label of x-axis\n", - " figtitle (str): title of histogram plot (default is no title)\n", - " num_bins (int): number of bins for histogram (default is 10)\n", - "\n", - " Returns:\n", - " count (ndarray): number of samples in each histogram bin\n", - " bins (ndarray): center of each histogram bin\n", - " \"\"\"\n", - " fig, ax = plt.subplots()\n", - " ax.set_xlabel(xlabel)\n", - " ax.set_ylabel('Count')\n", - " if num_bins is not None:\n", - " count, bins, _ = plt.hist(data, bins = num_bins)\n", - " else:\n", - " count, bins, _ = plt.hist(data, bins = np.arange(np.min(data)-.5, np.max(data)+.6)) # 10 bins default\n", - " if figtitle is not None:\n", - " fig.suptitle(figtitle, size=16)\n", - " plt.show()\n", - " return count, bins\n", - "\n", - "\n", - "def my_plot_single(x, px):\n", - " \"\"\"\n", - " Plots normalized Gaussian distribution\n", - "\n", - " Args:\n", - " x (numpy array of floats): points at which the likelihood has been evaluated\n", - " px (numpy array of floats): normalized probabilities for prior evaluated at each `x`\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - " if px is None:\n", - " px = np.zeros_like(x)\n", - "\n", - " fig, ax = plt.subplots()\n", - " ax.plot(x, px, '-', color='C2', linewidth=2, label='Prior')\n", - " ax.legend()\n", - " ax.set_ylabel('Probability')\n", - " ax.set_xlabel('Orientation (Degrees)')\n", - " plt.show()\n", - "\n", - "\n", - "def plot_gaussian_samples_true(samples, xspace, mu, sigma, xlabel, ylabel):\n", - " \"\"\" Plot a histogram of the data samples on the same plot as the gaussian\n", - " distribution specified by the give mu and sigma values.\n", - "\n", - " Args:\n", - " samples (ndarray): data samples for gaussian distribution\n", - " xspace (ndarray): x values to sample from normal distribution\n", - " mu (scalar): mean parameter of normal distribution\n", - " sigma (scalar): variance parameter of normal distribution\n", - " xlabel (str): the label of the x-axis of the histogram\n", - " ylabel (str): the label of the y-axis of the histogram\n", - "\n", - " Returns:\n", - " Nothing.\n", - " \"\"\"\n", - " fig, ax = plt.subplots()\n", - " ax.set_xlabel(xlabel)\n", - " ax.set_ylabel(ylabel)\n", - " # num_samples = samples.shape[0]\n", - "\n", - " count, bins, _ = plt.hist(samples, density=True)\n", - " plt.plot(xspace, norm.pdf(xspace, mu, sigma),'r-')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "q9o8V-mLpmC7" - }, - "source": [ - "---\n", - "\n", - "# Section 1: Stochasticity and randomness" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "45v8eNKjpmC7" - }, - "source": [ - "## Section 1.1: Intro to Randomness\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "SaDZWnv_pmC8" - }, - "outputs": [], - "source": [ - "# @title Video 1: Stochastic World\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', '-QwTPDp7-a8'), ('Bilibili', 'BV1sU4y1G7Qt')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "Fj033QTNpmC8" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Stochastic_World_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "DXi2V88SpmC8" - }, - "source": [ - "\n", - "Before trying out different probability distributions, let's start with the simple uniform distribution, U(a,b), which assigns equal probability to any value between a and b.\n", - "\n", - "To show that we are drawing a random number $x$ from a uniform distribution with lower and upper bounds $a$ and $b$ we will use this notation:\n", - "$x \\sim \\mathcal{U}(a,b)$. Alternatively, we can say that all the potential values of $x$ are distributed as a uniform distribution between $a$ and $b$. $x$ here is a random variable: a variable whose value depends on the outcome of a random process." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "rj7c7QQbpmC8" - }, - "source": [ - "### Coding Exercise 1.1: Create randomness\n", - "\n", - "Numpy has many functions and capabilities related to randomness. We can draw random numbers from various probability distributions. For example, to draw 5 uniform numbers between 0 and 100, you would use `np.random.uniform(0, 100, size = (5,))`.\n", - "\n", - " We will use `np.random.seed` to set a specific seed for the random number generator. For example, `np.random.seed(0)` sets the seed as 0. By including this, we are actually making the random numbers reproducible, which may seem odd at first. Basically if we do the below code without that 0, we would get different random numbers every time we run it. By setting the seed to 0, we ensure we will get the same random numbers. There are lots of reasons we may want randomness to be reproducible. In NMA-world, it's so your plots will match the solution plots exactly!\n", - "\n", - "```python\n", - "np.random.seed(0)\n", - "random_nums = np.random.uniform(0, 100, size = (5,))\n", - "```\n", - "\n", - "Below, you will complete a function `generate_random_sample` that randomly generates `num_points` $x$ and $y$ coordinate values, all within the range 0 to 1. You will then generate 10 points and visualize." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "bxLLiyzGpmC8" - }, - "outputs": [], - "source": [ - "def generate_random_sample(num_points):\n", - " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", - " in the range [0, 1] using a random number generator object.\n", - "\n", - " Args:\n", - " num_points (int): number of points desired in random sample\n", - "\n", - " Returns:\n", - " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", - " and y coordinates of sampled points\n", - "\n", - " \"\"\"\n", - "\n", - " ###################################################################\n", - " ## TODO for students: Draw the uniform numbers\n", - " ## Fill out the following then remove\n", - " raise NotImplementedError(\"Student exercise: need to complete generate_random_sample\")\n", - " ###################################################################\n", - "\n", - " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", - " # both x and y\n", - " dataX = ...\n", - " dataY = ...\n", - "\n", - " return dataX, dataY\n", - "\n", - "# Set a seed\n", - "np.random.seed(0)\n", - "\n", - "# Set number of points to draw\n", - "num_points = 10\n", - "\n", - "# Draw random points\n", - "dataX, dataY = generate_random_sample(num_points)\n", - "\n", - "# Visualize\n", - "plot_random_sample(dataX, dataY, \"Random sample of 10 points\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "uHcRMx_upmC8" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "def generate_random_sample(num_points):\n", - " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", - " in the range [0, 1] using a random number generator object.\n", - "\n", - " Args:\n", - " num_points (int): number of points desired in random sample\n", - "\n", - " Returns:\n", - " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", - " and y coordinates of sampled points\n", - "\n", - " \"\"\"\n", - "\n", - " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", - " # both x and y\n", - " dataX = np.random.uniform(0, 1, size = (num_points,))\n", - " dataY = np.random.uniform(0, 1, size = (num_points,))\n", - "\n", - " return dataX, dataY\n", - "\n", - "# Set a seed\n", - "np.random.seed(0)\n", - "\n", - "# Set number of points to draw\n", - "num_points = 10\n", - "\n", - "# Draw random points\n", - "dataX, dataY = generate_random_sample(num_points)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_random_sample(dataX, dataY, \"Random sample of 10 points\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "xSI3mnB_pmC9" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Create_Randomness_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "_j2Z_UtppmC9" - }, - "source": [ - "### Interactive Demo 1.1: Random Sample Generation from Uniform Distribution\n", - "In practice this may not look very uniform, although that is of course part of the randomness! Uniform randomness does not mean smoothly uniform. When we have very little data it can be hard to see the distribution.\n", - "\n", - "Below, you can adjust the number of points sampled with a slider. Does it look more uniform now? Try increasingly large numbers of sampled points." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "LIPA1z0XpmC9" - }, - "outputs": [], - "source": [ - "#@markdown Make sure you execute this cell to enable the widget!\n", - "\n", - "def generate_random_sample(num_points):\n", - " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", - " in the range [0, 1] using a random number generator object.\n", - "\n", - " Args:\n", - " num_points (int): number of points desired in random sample\n", - "\n", - " Returns:\n", - " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", - " and y coordinates of sampled points\n", - "\n", - " \"\"\"\n", - "\n", - " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", - " # both x and y\n", - " dataX = np.random.uniform(0, 1, size = (num_points,))\n", - " dataY = np.random.uniform(0, 1, size = (num_points,))\n", - "\n", - " return dataX, dataY\n", - "\n", - "@widgets.interact\n", - "def gen_and_plot_random_sample(num_points = widgets.SelectionSlider(options=[(\"%g\"%i,i) for i in np.arange(0, 500, 10)])):\n", - "\n", - " dataX, dataY = generate_random_sample(num_points)\n", - " fig, ax = plt.subplots()\n", - " ax.set_xlabel('x')\n", - " ax.set_ylabel('y')\n", - " plt.xlim([-0.25, 1.25])\n", - " plt.ylim([-0.25, 1.25])\n", - " plt.scatter(dataX, dataY)\n", - " fig.suptitle(\"Random sample of \" + str(num_points) + \" points\", size=16)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "yroC0BhYpmC9" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Random_Sample_Generation_from_Uniform_Distribution_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "qd47OGFdpmC9" - }, - "source": [ - "## Section 1.2: Random walk\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "EHtLHcPKpmC9" - }, - "outputs": [], - "source": [ - "# @title Video 2: Random walk\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'Tz9gjHcqj5k'), ('Bilibili', 'BV11U4y1G7Bu')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "xCALBgVJpmC9" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Random_Walk_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "mIrkdc8KpmC-" - }, - "source": [ - "Stochastic models can be used to create models of behaviour. As an example, imagine that a rat is placed inside a novel environment, a box. We could try and model its exploration behaviour by assuming that for each time step it takes a random uniformly sampled step in any direction (simultaneous random step in x direction and random step in y direction)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "jHOiAusIpmC-" - }, - "source": [ - "### Coding Exercise 1.2: Modeling a random walk\n", - "\n", - "\n", - "Use the `generate_random_sample` function from above to obtain the random steps the rat takes at each time step and complete the generate_random_walk function below. For plotting, the box will be represented graphically as the unit square enclosed by the points $(0, 0)$ and $(1, 1)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "l9J0EAiDpmC-" - }, - "outputs": [], - "source": [ - "def generate_random_walk(num_steps, step_size):\n", - " \"\"\" Generate the points of a random walk within a 1 X 1 box.\n", - "\n", - " Args:\n", - " num_steps (int): number of steps in the random walk\n", - " step_size (float): how much each random step size is weighted\n", - "\n", - " Returns:\n", - " x, y (ndarray, ndarray): the (x, y) locations reached at each time step of the walk\n", - "\n", - " \"\"\"\n", - " x = np.zeros(num_steps + 1)\n", - " y = np.zeros(num_steps + 1)\n", - "\n", - " ###################################################################\n", - " ## TODO for students: Collect random step values with function from before\n", - " ## Fill out the following then remove\n", - " raise NotImplementedError(\"Student exercise: need to complete generate_random_walk\")\n", - " ###################################################################\n", - "\n", - " # Generate the uniformly random x, y steps for the walk\n", - " random_x_steps, random_y_steps = ...\n", - "\n", - " # Take steps according to the randomly sampled steps above\n", - " for step in range(num_steps):\n", - "\n", - " # take a random step in x and y. We remove 0.5 to make it centered around 0\n", - " x[step + 1] = x[step] + (random_x_steps[step] - 0.5)*step_size\n", - " y[step + 1] = y[step] + (random_y_steps[step] - 0.5)*step_size\n", - "\n", - " # restrict to be within the 1 x 1 unit box\n", - " x[step + 1]= min(max(x[step + 1], 0), 1)\n", - " y[step + 1]= min(max(y[step + 1], 0), 1)\n", - "\n", - " return x, y\n", - "\n", - "# Set a random seed\n", - "np.random.seed(2)\n", - "\n", - "# Select parameters\n", - "num_steps = 100 # number of steps in random walk\n", - "step_size = 0.5 # size of each step\n", - "\n", - "# Generate the random walk\n", - "x, y = generate_random_walk(num_steps, step_size)\n", - "\n", - "# Visualize\n", - "plot_random_walk(x, y, \"Rat's location throughout random walk\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "9xLgLb3vpmC-" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "def generate_random_walk(num_steps, step_size):\n", - " \"\"\" Generate the points of a random walk within a 1 X 1 box.\n", - "\n", - " Args:\n", - " num_steps (int): number of steps in the random walk\n", - " step_size (float): how much each random step size is weighted\n", - "\n", - " Returns:\n", - " x, y (ndarray, ndarray): the (x, y) locations reached at each time step of the walk\n", - "\n", - " \"\"\"\n", - " x = np.zeros(num_steps + 1)\n", - " y = np.zeros(num_steps + 1)\n", - "\n", - " # Generate the uniformly random x, y steps for the walk\n", - " random_x_steps, random_y_steps = generate_random_sample(num_steps)\n", - "\n", - " # Take steps according to the randomly sampled steps above\n", - " for step in range(num_steps):\n", - "\n", - " # take a random step in x and y. We remove 0.5 to make it centered around 0\n", - " x[step + 1] = x[step] + (random_x_steps[step] - 0.5)*step_size\n", - " y[step + 1] = y[step] + (random_y_steps[step] - 0.5)*step_size\n", - "\n", - " # restrict to be within the 1 x 1 unit box\n", - " x[step + 1]= min(max(x[step + 1], 0), 1)\n", - " y[step + 1]= min(max(y[step + 1], 0), 1)\n", - "\n", - " return x, y\n", - "\n", - "# Set a random seed\n", - "np.random.seed(2)\n", - "\n", - "# Select parameters\n", - "num_steps = 100 # number of steps in random walk\n", - "step_size = 0.5 # size of each step\n", - "\n", - "# Generate the random walk\n", - "x, y = generate_random_walk(num_steps, step_size)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " plot_random_walk(x, y, \"Rat's location throughout random walk\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "RcjP6GfupmC-" - }, - "source": [ - "We put a little green dot for the starting point and a red point for the ending point." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "t55kLeRupmC-" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Modeling_a_random_walk_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "LBQcyDdjpmC-" - }, - "source": [ - "### Interactive Demo 1.2: Varying parameters of a random walk\n", - "\n", - "In the interactive demo below, you can examine random walks with different numbers of steps or step sizes, using the sliders.\n", - "\n", - "\n", - "1. What could an increased step size mean for the actual rat's movement we are simulating?\n", - "2. For a given number of steps, is the rat more likely to visit all general areas of the arena with a big step size or small step size?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "5e5nCvG0pmC-" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "\n", - "@widgets.interact(num_steps = widgets.IntSlider(value=100, min=0, max=500, step=1), step_size = widgets.FloatSlider(value=0.1, min=0.1, max=1, step=0.1))\n", - "def gen_and_plot_random_walk(num_steps, step_size):\n", - " x, y = generate_random_walk(num_steps, step_size)\n", - " plot_random_walk(x, y, \"Rat's location throughout random walk\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "L7ElkIzCpmC-" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) A larger step size could mean that the rat is moving faster, or that we sample\n", - " the rats location less often.\n", - "\n", - "2) The rat tends to visit more of the arena with a large step size.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "FZ80yLn2pmC_" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Varying_parameters_of_a_random_walk_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "hDAATt06pmC_" - }, - "source": [ - "In practice a uniform random movement is too simple an assumption. Rats do not move completely randomly; even if you could assume that, you would need to approximate with a more complex probability distribution.\n", - "\n", - "Nevertheless, this example highlights how you can use sampling to approximate behaviour.\n", - "\n", - "**Main course preview:** During [Hidden Dynamics day](https://compneuro.neuromatch.io/tutorials/W3D2_HiddenDynamics/chapter_title.html) we will see how random walk models can be used to also model accumulation of information in decision making." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "ROETCK5GpmC_" - }, - "source": [ - "---\n", - "# Section 2: Discrete distributions" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "n1B1jTSPpmC_" - }, - "source": [ - "## Section 2.1: Binomial distributions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ZT1dYLospmC_" - }, - "outputs": [], - "source": [ - "# @title Video 3: Binomial distribution\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'kOXEQlmzFyw'), ('Bilibili', 'BV1Ev411W7mw')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ZgsgdLKvpmC_" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Binomial_distribution_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "b61_9ZqIpmC_" - }, - "source": [ - "This video covers the Bernoulli and binomial distributions.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "The uniform distribution is very simple, and can only be used in some rare cases. If we only had access to this distribution, our statistical toolbox would be very empty. Thankfully we do have some more advanced distributions!\n", - "\n", - "The uniform distribution that we looked at above is an example of a continuous distribution. The value of $X$ that we draw from this distribution can take **any value** between $a$ and $b$.\n", - "\n", - "However, sometimes we want to be able to look at discrete events. Imagine that the rat from before is now placed in a T-maze, with food placed at the end of both arms. Initially, we would expect the rat to be choosing randomly between the two arms, but after learning it should choose more consistently.\n", - "\n", - "A simple way to model such random behaviour is with a single **Bernoulli trial**, that has two outcomes, {$Left, Right$}, with probability $P(Left)=p$ and $P(Right)=1-p$ as the two mutually exclusive possibilities (whether the rat goes down the left or right arm of the maze).\n", - "
\n", - "\n", - "The binomial distribution simulates $n$ number of binary events, such as the $Left, Right$ choices of the random rat in the T-maze. Imagine that you have done an experiment and found that your rat turned left in 7 out of 10 trials. What is the probability of the rat indeed turning left 7 times ($k = 7$)?\n", - "\n", - "This is given by the binomial probability of $k$, given $n$ trials and probability $p$:\n", - "\n", - "\\begin{align}\n", - "P(k|n,p) &= \\left( \\begin{array} \\\\n \\\\ k\\end{array} \\right) p^k (1-p)^{n-k} \\\\\n", - "\\binom{n}{k} &= {\\frac {n!}{k!(n-k)!}}\n", - "\\end{align}\n", - "\n", - "In this formula, $p$ is the probability of turning left, $n$ is the number of binary events, or trials, and $k$ is the number of times the rat turned left. The term $\\binom {n}{k}$ is the binomial coefficient.\n", - "\n", - "This is an example of a *probability mass function*, which specifies the probability that a discrete random variable is equal to each value. In other words, how large a part of the probability space (mass) is placed at each exact discrete value. We require that all probability adds up to 1, i.e. that\n", - "\n", - "\\begin{equation}\n", - "\\sum_k P(k|n,p)=1.\n", - "\\end{equation}\n", - "\n", - "Essentially, if $k$ can only be one of 10 values, the probabilities of $k$ being equal to each possible value have to sum up to 1 because there is a probability of 1 it will equal one of those 10 values (no other options exist).\n", - "\n", - "If we assume an equal chance of turning left or right, then $p=0.5$. Note that if we only have a single trial $n=1$ this is equivalent to a single Bernoulli trial (feel free to do the math!)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "ChYeI0WCpmDI" - }, - "source": [ - "### Think! 2.1: Binomial distribution sampling\n", - "\n", - "We will draw a desired number of random samples from a binomial distribution, with $n = 10$ and $p = 0.5$. Each sample returns the number of trials, $k$, a rat turns left out of $n$ trials.\n", - "\n", - "We will draw 1000 samples of this (so it is as if we are observing 10 trials of the rat, 1000 different times). We can do this using numpy: `np.random.binomial(n, p, size = (n_samples,))`\n", - "\n", - "See below to visualize a histogram of the different values of $k$, or the number of times the rat turned left in each of the 1000 samples. In a histogram all the data is placed into bins and the contents of each bin is counted, to give a visualisation of the distribution of data. Discuss the following questions.\n", - "\n", - "1. What are the x-axis limits of the histogram and why?\n", - "2. What is the shape of the histogram?\n", - "3. Looking at the histogram, how would you interpret the outcome of the simulation if you didn't know what p was? Would you have guessed p = 0.5?\n", - "3. What do you think the histogram would look like if the probability of turning left is 0.8 ($p = 0.8$)?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "jb9SpLg5pmDI" - }, - "outputs": [], - "source": [ - "# @markdown Execute this cell to see visualization\n", - "\n", - "# Select parameters for conducting binomial trials\n", - "n = 10\n", - "p = 0.5\n", - "n_samples = 1000\n", - "\n", - "# Set random seed\n", - "np.random.seed(1)\n", - "\n", - "# Now draw 1000 samples by calling the function again\n", - "left_turn_samples_1000 = np.random.binomial(n, p, size = (n_samples,))\n", - "\n", - "# Visualize\n", - "count, bins = plot_hist(left_turn_samples_1000, 'Number of left turns in sample')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "M5XsvbkzpmDJ" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) The limits of the histogram at 0 and 10, as these are the minimum and maximum\n", - "number of trials that a rat can turn left out of 10 trials.\n", - "\n", - "2) The shape seems symmetric and is centered around 5.\n", - "\n", - "3) An average/mean around 5 left turns out of 10 trials indicates that the rat\n", - "chose left and right turns in the maze with equal probability (that p = 0.5)\n", - "\n", - "4) With p = 0.8, the center of the histogram would be at x = 8 and it would not be\n", - " as symmetrical (since it would be cut off at max 10). You can go into the code above\n", - " and run it with p = 0.8 to see this.\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "1vMuXxA8pmDJ" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Binomial_distribution_Sampling_Discussion\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "6D_MBuYapmDJ" - }, - "source": [ - "When working with the Bernoulli and binomial distributions, there are only 2 possible outcomes (in this case, turn left or turn right). In the more general case where there are $n$ possible outcomes (our rat is an n-armed maze) each with their own associated probability $p_1, p_2, p_3, p_4, ...$ , we use a **categorical distribution**. Draws from this distribution are a simple extension of the Bernoulli trial: we now have a probability for each outcome and draw based on those probabilities. We have to make sure that the probabilities sum to one:\n", - "\n", - "\\begin{equation}\n", - "\\sum_i P(x=i)=\\sum_i p_i =1\n", - "\\end{equation}\n", - "\n", - "If we sample from this distribution multiple times, we can then describe the distribution of outcomes from each sample as the **multinomial distribution**. Essentially, the categorical distribution is the multiple outcome extension of the Bernoulli, and the multinomial distribution is the multiple outcome extension of the binomial distribution. We'll see a bit more about this in the next tutorial when we look at Markov chains." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "KRhVPtWgpmDJ" - }, - "source": [ - "## Section 2.2: Poisson distribution" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "FObKt46jpmDJ" - }, - "outputs": [], - "source": [ - "# @title Video 4: Poisson distribution\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'E_nvNb596DY'), ('Bilibili', 'BV1wV411x7P6')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "bm8dFIpopmDJ" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Poisson_distribution_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "C0e3pxG-pmDK" - }, - "source": [ - "This video covers the Poisson distribution and how it can be used to describe neural spiking.\n", - "\n", - "
\n", - " Click here for text recap of video \n", - "\n", - "For some phenomena there may not be a natural limit on the maximum number of possible events or outcomes.\n", - "\n", - "The Poisson distribution is a '**point-process**', meaning that it determines the number of discrete 'point', or binary, events that happen within a fixed space or time, allowing for the occurence of a potentially infinite number of events. The Poisson distribution is specified by a single parameter $\\lambda$ that encapsulates the mean number of events that can occur in a single time or space interval (there will be more on this concept of the 'mean' later!).\n", - "\n", - "Relevant to us, we can model the number of times a neuron spikes within a time interval using a Poisson distribution. In fact, neuroscientists often do! As an example, if we are recording from a neuron that tends to fire at an average rate of 4 spikes per second, then the Poisson distribution specifies the distribution of recorded spikes over one second, where $\\lambda=4$.\n", - "\n", - "
\n", - "\n", - "The formula for a Poisson distribution on $x$ is:\n", - "\n", - "\\begin{equation}\n", - "P(x)=\\frac{\\lambda^x e^{-\\lambda}}{x!}\n", - "\\end{equation}\n", - "\n", - "where $\\lambda$ is a parameter corresponding to the average outcome of $x$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "OUZs7IbZpmDK" - }, - "source": [ - "### Coding Exercise 2.2: Poisson distribution sampling\n", - "\n", - "In the exercise below we will draw some samples from the Poisson distribution and see what the histogram looks.\n", - "\n", - "In the code, fill in the missing line so we draw 5 samples from a Poisson distribution with $\\lambda = 4$. Use `np.random.poisson`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "rDZZJvF-pmDK" - }, - "outputs": [], - "source": [ - "# Set random seed\n", - "np.random.seed(0)\n", - "\n", - "# Draw 5 samples from a Poisson distribution with lambda = 4\n", - "sampled_spike_counts = ...\n", - "\n", - "# Print the counts\n", - "print(\"The samples drawn from the Poisson distribution are \" +\n", - " str(sampled_spike_counts))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "FwaAnh5-pmDK" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "\n", - "# Set random seed\n", - "np.random.seed(0)\n", - "\n", - "# Draw 5 samples from a Poisson distribution with lambda = 4\n", - "sampled_spike_counts = np.random.poisson(4, 5)\n", - "\n", - "# Print the counts\n", - "print(\"The samples drawn from the Poisson distribution are \" +\n", - " str(sampled_spike_counts))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "-qWygwpOpmDK" - }, - "source": [ - "You should see that the neuron spiked 6 times, 7 times, 1 time, 8 times, and 4 times in 5 different intervals." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "wItnCjK7pmDK" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Poisson_distribution_sampling_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "ts9l9INxpmDK" - }, - "source": [ - "### Interactive Demo 2.2: Varying parameters of Poisson distribution\n", - "\n", - "Use the interactive demo below to vary $\\lambda$ and the number of samples, and then visualize the resulting histogram.\n", - "\n", - "1. What effect does increasing the number of samples have? \n", - "2. What effect does changing $\\lambda$ have?\n", - "3. With a small lambda, why is the distribution asymmetric?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "TQXqODTbpmDL" - }, - "outputs": [], - "source": [ - "# @markdown Make sure you execute this cell to enable the widget!\n", - "\n", - "@widgets.interact(lambda_value = widgets.FloatSlider(value=4, min=0.1, max=10, step=0.1),\n", - " n_samples = widgets.IntSlider(value=5, min=5, max=500, step=1))\n", - "\n", - "def gen_and_plot_possion_samples(lambda_value, n_samples):\n", - " sampled_spike_counts = np.random.poisson(lambda_value, n_samples)\n", - " count, bins = plot_hist(sampled_spike_counts, 'Recorded spikes per second')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "xFvdOSmupmDL" - }, - "outputs": [], - "source": [ - "# to_remove explanation\n", - "\n", - "\"\"\"\n", - "1) More samples only means that the shape of the distribution becomes clearer to us.\n", - "E.g. with a decent number of samples you may be able to see whether the\n", - "distribution is symmetrical. With a small number of samples we would not be able\n", - "to distinguish different distributions from each other. The more data samples we\n", - "have, the more we can say about the stochastic process that generated the data (obviously).\n", - "\n", - "2) Increasing lambda moves the distribution along the x-axis, essentially changing\n", - "the mean of the distribution. With lambda = 6 for example, we would expect to see\n", - "6 spike counts per interval on average.\n", - "\n", - "3) For small values of lambda the Poisson distribution becomes asymmetrical as\n", - "it is a distribution over non-negative counts\n", - " (you can’t have negative numbers of spikes e.g.).\n", - "\"\"\";" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "ANTEVjNkpmDL" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Varying_parameters_of_Poisson_distribution_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "d2QWTQJYpmDL" - }, - "source": [ - "---\n", - "# Section 3: Continuous distributions" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "0bs1ye2ZpmDM", - "outputId": "aae039b1-875a-4a8f-8c84-6a3aefda39f5", - "colab": { - "base_uri": "https://localhost:8080/", - "height": 582, - "referenced_widgets": [ - "34bf5b356d7b4fbda255665049f8df36", - "44229b29b6e34fd99b9ca14fab650079", - "7f80f5b065704815976c3be30b3ae8cc", - "3f2f18285890479c94aa234de7375ea5", - "0c68ceb7ce1e4840b4ca4061fd958df6", - "083577639f014471b24bf14434020868" - ] - } - }, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "34bf5b356d7b4fbda255665049f8df36" - } - }, - "metadata": {} - } - ], - "source": [ - "# @title Video 5: Continuous distributions\n", - "from ipywidgets import widgets\n", - "from IPython.display import YouTubeVideo\n", - "from IPython.display import IFrame\n", - "from IPython.display import display\n", - "\n", - "\n", - "class PlayVideo(IFrame):\n", - " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", - " self.id = id\n", - " if source == 'Bilibili':\n", - " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", - " elif source == 'Osf':\n", - " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", - " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", - "\n", - "\n", - "def display_videos(video_ids, W=400, H=300, fs=1):\n", - " tab_contents = []\n", - " for i, video_id in enumerate(video_ids):\n", - " out = widgets.Output()\n", - " with out:\n", - " if video_ids[i][0] == 'Youtube':\n", - " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", - " height=H, fs=fs, rel=0)\n", - " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", - " else:\n", - " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", - " height=H, fs=fs, autoplay=False)\n", - " if video_ids[i][0] == 'Bilibili':\n", - " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", - " elif video_ids[i][0] == 'Osf':\n", - " print(f'Video available at https://osf.io/{video.id}')\n", - " display(video)\n", - " tab_contents.append(out)\n", - " return tab_contents\n", - "\n", - "\n", - "video_ids = [('Youtube', 'LJ4Zdokb6lc'), ('Bilibili', 'BV1dq4y1L7eC')]\n", - "tab_contents = display_videos(video_ids, W=854, H=480)\n", - "tabs = widgets.Tab()\n", - "tabs.children = tab_contents\n", - "for i in range(len(tab_contents)):\n", - " tabs.set_title(i, video_ids[i][0])\n", - "display(tabs)" - ] - }, - { - "cell_type": "markdown", - "source": [ - "**Note:** There is a typo in the vido ~3.40, where the product of Gaussian distributions should be $\\mathcal{N}(\\mu_1, \\sigma_1^2) \\cdot \\mathcal{N}(\\mu_2, \\sigma_2^2)$." - ], - "metadata": { - "id": "yJ8OFJ5wqFB3" - } - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "PVQDpWh_pmDM" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Continuous_distributions_Video\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "mzpIhIqVpmDM" - }, - "source": [ - "We do not have to restrict ourselves to only probabilistic models of discrete events. While some events in neuroscience are discrete (e.g., number of spikes by a neuron), many others are continuous (e.g., neuroimaging signals in EEG or fMRI, distance traveled by an animal, human pointing in the direction of a stimulus).\n", - "\n", - "While for discrete outcomes, we can ask about the probability of a specific event (\"What is the probability this neuron will fire 4 times in the next second\"), this is not defined for a continuous distribution (\"What is the probability of the BOLD signal being exactly 4.000120141...\"). Hence we need to focus on intervals when calculating probabilities from a continuous distribution.\n", - "\n", - "If we want to make predictions about possible outcomes (\"I believe the BOLD signal from the area will be in the range $x_1$ to $ x_2 $\"), we can use the integral $\\int_{x_1}^{x_2} P(x)$.\n", - "$P(x)$ is now a **probability density function**, sometimes written as $f(x)$ to distinguish it from the probability mass functions.\n", - "\n", - "With continuous distributions, we have to replace the normalizing sum\n", - "\n", - "\\begin{equation}\n", - "\\sum_i P(x=p_i) = 1\n", - "\\end{equation}\n", - "\n", - "over all possible events, with an integral\n", - "\n", - "\\begin{equation}\n", - "\\int_a^b P(x) = 1\n", - "\\end{equation}\n", - "\n", - "where a and b are the limits of the random variable $x$ (often $-\\infty$ and $\\infty$)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "3pEWyExfpmDM" - }, - "source": [ - "## Section 3.1: Gaussian Distribution\n", - "\n", - "The most widely used continuous distribution is probably the Gaussian (also known as Normal) distribution. It is extremely common across all kinds of statistical analyses. Because of the central limit theorem, many quantities are Gaussian distributed. Gaussians also have some nice mathematical properties that permit simple closed-form solutions to several important problems.\n", - "\n", - "As a working example, imagine that a human participant is asked to point in the direction where they perceived a sound coming from. As an approximation, we can assume that the variability in the direction/orientation they point towards is Gaussian distributed." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "79evZ33spmDM" - }, - "source": [ - "### Coding Exercise 3.1A: Gaussian Distribution\n", - "\n", - "In this exercise, you will implement a Gaussian by filling in the missing portions of code for the function `my_gaussian` below. Gaussians have two parameters. The **mean** $\\mu$, which sets the location of its center, and its \"scale\" or spread is controlled by its **standard deviation** $\\sigma$, or **variance** $\\sigma^2$ (i.e. the square of standard deviation). **Be careful not to use one when the other is required.**\n", - "\n", - "The equation for a Gaussian probability density function is:\n", - "\n", - "\\begin{equation}\n", - "f(x;\\mu,\\sigma^2) = \\mathcal{N}(\\mu,\\sigma^2) = \\frac{1}{\\sqrt{2\\pi\\sigma^2}}\\exp\\left(\\frac{-(x-\\mu)^2}{2\\sigma^2}\\right)\n", - "\\end{equation}\n", - "\n", - "In Python $\\pi$ and $e$ can be written as `np.pi` and `np.exp` respectively.\n", - "\n", - "As a probability distribution this has an integral of one when integrated from $-\\infty$ to $\\infty$, however in the following your numerical Gaussian will only be computed over a finite number of points (for the cell below we will sample from -8 to 9 in step sizes of 0.1). You therefore need to explicitly normalize it to sum to one yourself.\n", - "\n", - "Test out your implementation with a $\\mu = -1$ and $\\sigma = 1$. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "Ba4HT5PZpmDM" - }, - "outputs": [], - "source": [ - "def my_gaussian(x_points, mu, sigma):\n", - " \"\"\" Returns normalized Gaussian estimated at points `x_points`, with\n", - " parameters: mean `mu` and standard deviation `sigma`\n", - "\n", - " Args:\n", - " x_points (ndarray of floats): points at which the gaussian is evaluated\n", - " mu (scalar): mean of the Gaussian\n", - " sigma (scalar): standard deviation of the gaussian\n", - "\n", - " Returns:\n", - " (numpy array of floats) : normalized Gaussian evaluated at `x`\n", - " \"\"\"\n", - "\n", - " ###################################################################\n", - " ## TODO for students: Implement the formula for a Gaussian\n", - " ## Add code to calculate the gaussian px as a function of mu and sigma,\n", - " ## for every x in x_points\n", - " ## Function Hints: exp -> np.exp()\n", - " ## power -> z**2\n", - " ##\n", - " ## Fill out the following then remove\n", - " raise NotImplementedError(\"Student exercise: need to implement Gaussian\")\n", - " ###################################################################\n", - " px = ...\n", - "\n", - " # as we are doing numerical integration we have to remember to normalise\n", - " # taking into account the stepsize (0.1)\n", - " px = px/(0.1*sum(px))\n", - " return px\n", - "\n", - "x = np.arange(-8, 9, 0.1)\n", - "\n", - "# Generate Gaussian\n", - "px = my_gaussian(x, -1, 1)\n", - "\n", - "# Visualize\n", - "my_plot_single(x, px)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "execution": {}, - "id": "Ij82CxIKpmDM" - }, - "outputs": [], - "source": [ - "# to_remove solution\n", - "def my_gaussian(x_points, mu, sigma):\n", - " \"\"\" Returns normalized Gaussian estimated at points `x_points`, with\n", - " parameters: mean `mu` and standard deviation `sigma`\n", - "\n", - " Args:\n", - " x_points (ndarray of floats): points at which the gaussian is evaluated\n", - " mu (scalar): mean of the Gaussian\n", - " sigma (scalar): standard deviation of the gaussian\n", - "\n", - " Returns:\n", - " (numpy array of floats) : normalized Gaussian evaluated at `x`\n", - " \"\"\"\n", - "\n", - " px = 1/(2*np.pi*sigma**2)**1/2 *np.exp(-(x_points-mu)**2/(2*sigma**2))\n", - "\n", - " # as we are doing numerical integration we have to remember to normalise\n", - " # taking into account the stepsize (0.1)\n", - " px = px/(0.1*sum(px))\n", - " return px\n", - "\n", - "x = np.arange(-8, 9, 0.1)\n", - "\n", - "# Generate Gaussian\n", - "px = my_gaussian(x, -1, 1)\n", - "\n", - "# Visualize\n", - "with plt.xkcd():\n", - " my_plot_single(x, px)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "-08eK3E2pmDN" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Gaussian_Distribution_Exercise\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "iAcfqBW7pmDN" - }, - "source": [ - "### Interactive Demo 3.1: Sampling from a Gaussian distribution\n", - "\n", - "Now that we have gained a bit of intuition about the shape of the Gaussian, let's imagine that a human participant is asked to point in the direction of a sound source, which we then measure in horizontal degrees. To simulate that we draw samples from a Normal distribution:\n", - "\n", - "\\begin{equation}\n", - "x \\sim \\mathcal{N}(\\mu,\\sigma)\n", - "\\end{equation}\n", - "\n", - "We can sample from a Gaussian with mean $\\mu$ and standard deviation $\\sigma$ using `np.random.normal(mu, sigma, size = (n_samples,))`.\n", - "\n", - "In the demo below, you can change the mean and standard deviation of the Gaussian, and the number of samples, we can compare the histogram of the samples to the true analytical distribution (in red).\n", - "\n", - "1. With what number of samples would you say that the full distribution (in red) is well approximated by the histogram?\n", - "2. What if you just wanted to approximate the variables that defined the distribution, i.e., mean and variance?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "GRy0DebTpmDN" - }, - "outputs": [], - "source": [ - "#@markdown Make sure you execute this cell to enable the widget!\n", - "\n", - "\n", - "@widgets.interact(mean = widgets.FloatSlider(value=0, min=-5, max=5, step=0.5),\n", - " standard_dev = widgets.FloatSlider(value=0.5, min=0, max=10, step=0.1),\n", - " n_samples = widgets.IntSlider(value=5, min=1, max=300, step=1))\n", - "def gen_and_plot_normal_samples(mean, standard_dev, n_samples):\n", - " x = np.random.normal(mean, standard_dev, size = (n_samples,))\n", - " xspace = np.linspace(-20, 20, 100)\n", - " plot_gaussian_samples_true(x, xspace, mean, standard_dev,\n", - " 'orientation (degrees)', 'probability')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "4_vdStvipmDN" - }, - "source": [ - "**Main course preview:** Gaussian distriutions are everywhere and are critical for filtering, [linear systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html), [optimal control](https://compneuro.neuromatch.io/tutorials/W3D3_OptimalControl/chapter_title.html) and almost any statistical model of continuous data." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "cellView": "form", - "execution": {}, - "id": "uq07GLimpmDN" - }, - "outputs": [], - "source": [ - "# @title Submit your feedback\n", - "content_review(f\"{feedback_prefix}_Sampling_from_a_Gaussian_distribution_Interactive_Demo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "execution": {}, - "id": "mWduI9RqpmDN" - }, - "source": [ - "---\n", - "# Summary\n", - "\n", - "Across the different exercises you should now:\n", - "* have gotten some intuition about how stochastic randomly generated data can be\n", - "* understand how to model data using simple distributions\n", - "* understand the difference between discrete and continuous distributions\n", - "* be able to plot a Gaussian distribution\n", - "\n", - "For more reading on these topics see just about any statistics textbook, or take a look at the [online resources](https://github.com/NeuromatchAcademy/precourse/blob/main/resources.md)." - ] - } - ], - "metadata": { - "colab": { - "name": "W0D5_Tutorial1", - "provenance": [], - "toc_visible": true, - "include_colab_link": true - }, - "kernel": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.17" - }, - "toc-autonumbering": true, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "34bf5b356d7b4fbda255665049f8df36": { - "model_module": "@jupyter-widgets/controls", - "model_name": "TabModel", - "model_module_version": "1.5.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "TabModel", - "_titles": { - "0": "Youtube", - "1": "Bilibili" - }, - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "TabView", - "box_style": "", - "children": [ - "IPY_MODEL_44229b29b6e34fd99b9ca14fab650079", - "IPY_MODEL_7f80f5b065704815976c3be30b3ae8cc" - ], - "layout": "IPY_MODEL_3f2f18285890479c94aa234de7375ea5", - "selected_index": 0 - } - }, - "44229b29b6e34fd99b9ca14fab650079": { - "model_module": "@jupyter-widgets/output", - "model_name": "OutputModel", - "model_module_version": "1.0.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/output", - "_model_module_version": "1.0.0", - "_model_name": "OutputModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/output", - "_view_module_version": "1.0.0", - "_view_name": "OutputView", - "layout": "IPY_MODEL_0c68ceb7ce1e4840b4ca4061fd958df6", - "msg_id": "", - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Video available at https://youtube.com/watch?v=LJ4Zdokb6lc\n" - ] - }, - { - "output_type": "display_data", - "data": { - "text/plain": "", - "text/html": "\n \n ", - "image/jpeg": "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\n" - }, - "metadata": {} - } - ] - } - }, - "7f80f5b065704815976c3be30b3ae8cc": { - "model_module": "@jupyter-widgets/output", - "model_name": "OutputModel", - "model_module_version": "1.0.0", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/output", - "_model_module_version": "1.0.0", - "_model_name": "OutputModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/output", - "_view_module_version": "1.0.0", - "_view_name": "OutputView", - "layout": "IPY_MODEL_083577639f014471b24bf14434020868", - "msg_id": "", - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Video available at https://www.bilibili.com/video/BV1dq4y1L7eC\n" - ] - }, - { - "output_type": "display_data", - "data": { - "text/plain": "<__main__.PlayVideo at 0x7fdbf01c7040>", - "text/html": "\n \n " - }, - "metadata": {} - } - ] - } - }, - "3f2f18285890479c94aa234de7375ea5": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "0c68ceb7ce1e4840b4ca4061fd958df6": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "083577639f014471b24bf14434020868": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "model_module_version": "1.2.0", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - } - } - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "execution": {}, + "id": "view-in-github" + }, + "source": [ + "\"Open   \"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "# Tutorial 1: Probability Distributions\n", + "\n", + "**Week 0, Day 5: Probability & Statistics**\n", + "\n", + "**By Neuromatch Academy**\n", + "\n", + "__Content creators:__ Ulrik Beierholm\n", + "\n", + "__Content reviewers:__ Natalie Schaworonkow, Keith van Antwerp, Anoop Kulkarni, Pooya Pakarian, Hyosub Kim\n", + "\n", + "__Production editors:__ Ethan Cheng, Ella Batty" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "

" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Tutorial Objectives\n", + "\n", + "We will cover the basic ideas from probability and statistics, as a reminder of what you have hopefully previously learned. These ideas will be important for almost every one of the following topics covered in the course.\n", + "\n", + "There are many additional topics within probability and statistics that we will not cover as they are not central to the main course. We also do not have time to get into a lot of details, but this should help you recall material you have previously encountered.\n", + "\n", + "\n", + "By completing the exercises in this tutorial, you should:\n", + "* get some intuition about how stochastic randomly generated data can be\n", + "* understand how to model data using simple probability distributions\n", + "* understand the difference between discrete and continuous probability distributions\n", + "* be able to plot a Gaussian distribution" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Setup\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Install and import feedback gadget\n", + "\n", + "!pip3 install vibecheck datatops --quiet\n", + "\n", + "from vibecheck import DatatopsContentReviewContainer\n", + "def content_review(notebook_section: str):\n", + " return DatatopsContentReviewContainer(\n", + " \"\", # No text prompt\n", + " notebook_section,\n", + " {\n", + " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", + " \"name\": \"neuromatch-precourse\",\n", + " \"user_key\": \"8zxfvwxw\",\n", + " },\n", + " ).render()\n", + "\n", + "\n", + "feedback_prefix = \"W0D5_T1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "both", + "execution": {} + }, + "outputs": [], + "source": [ + "# Imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import scipy as sp\n", + "from scipy.stats import norm # the normal probability distribution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Figure settings\n", + "import logging\n", + "logging.getLogger('matplotlib.font_manager').disabled = True\n", + "import ipywidgets as widgets # interactive display\n", + "from ipywidgets import interact, fixed, HBox, Layout, VBox, interactive, Label, interact_manual\n", + "%config InlineBackend.figure_format = 'retina'\n", + "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Plotting Functions\n", + "\n", + "def plot_random_sample(x, y, figtitle = None):\n", + " \"\"\" Plot the random sample between 0 and 1 for both the x and y axes.\n", + "\n", + " Args:\n", + " x (ndarray): array of x coordinate values across the random sample\n", + " y (ndarray): array of y coordinate values across the random sample\n", + " figtitle (str): title of histogram plot (default is no title)\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + " fig, ax = plt.subplots()\n", + " ax.set_xlabel('x')\n", + " ax.set_ylabel('y')\n", + " plt.xlim([-0.25, 1.25]) # set x and y axis range to be a bit less than 0 and greater than 1\n", + " plt.ylim([-0.25, 1.25])\n", + " plt.scatter(dataX, dataY)\n", + " if figtitle is not None:\n", + " fig.suptitle(figtitle, size=16)\n", + " plt.show()\n", + "\n", + "\n", + "def plot_random_walk(x, y, figtitle = None):\n", + " \"\"\" Plots the random walk within the range 0 to 1 for both the x and y axes.\n", + "\n", + " Args:\n", + " x (ndarray): array of steps in x direction\n", + " y (ndarray): array of steps in y direction\n", + " figtitle (str): title of histogram plot (default is no title)\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + " fig, ax = plt.subplots()\n", + " plt.plot(x,y,'b-o', alpha = 0.5)\n", + " plt.xlim(-0.1,1.1)\n", + " plt.ylim(-0.1,1.1)\n", + " ax.set_xlabel('x location')\n", + " ax.set_ylabel('y location')\n", + " plt.plot(x[0], y[0], 'go')\n", + " plt.plot(x[-1], y[-1], 'ro')\n", + "\n", + " if figtitle is not None:\n", + " fig.suptitle(figtitle, size=16)\n", + " plt.show()\n", + "\n", + "\n", + "def plot_hist(data, xlabel, figtitle = None, num_bins = None):\n", + " \"\"\" Plot the given data as a histogram.\n", + "\n", + " Args:\n", + " data (ndarray): array with data to plot as histogram\n", + " xlabel (str): label of x-axis\n", + " figtitle (str): title of histogram plot (default is no title)\n", + " num_bins (int): number of bins for histogram (default is 10)\n", + "\n", + " Returns:\n", + " count (ndarray): number of samples in each histogram bin\n", + " bins (ndarray): center of each histogram bin\n", + " \"\"\"\n", + " fig, ax = plt.subplots()\n", + " ax.set_xlabel(xlabel)\n", + " ax.set_ylabel('Count')\n", + " if num_bins is not None:\n", + " count, bins, _ = plt.hist(data, bins = num_bins)\n", + " else:\n", + " count, bins, _ = plt.hist(data, bins = np.arange(np.min(data)-.5, np.max(data)+.6)) # 10 bins default\n", + " if figtitle is not None:\n", + " fig.suptitle(figtitle, size=16)\n", + " plt.show()\n", + " return count, bins\n", + "\n", + "\n", + "def my_plot_single(x, px):\n", + " \"\"\"\n", + " Plots normalized Gaussian distribution\n", + "\n", + " Args:\n", + " x (numpy array of floats): points at which the likelihood has been evaluated\n", + " px (numpy array of floats): normalized probabilities for prior evaluated at each `x`\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + " if px is None:\n", + " px = np.zeros_like(x)\n", + "\n", + " fig, ax = plt.subplots()\n", + " ax.plot(x, px, '-', color='C2', linewidth=2, label='Prior')\n", + " ax.legend()\n", + " ax.set_ylabel('Probability')\n", + " ax.set_xlabel('Orientation (Degrees)')\n", + " plt.show()\n", + "\n", + "\n", + "def plot_gaussian_samples_true(samples, xspace, mu, sigma, xlabel, ylabel):\n", + " \"\"\" Plot a histogram of the data samples on the same plot as the gaussian\n", + " distribution specified by the give mu and sigma values.\n", + "\n", + " Args:\n", + " samples (ndarray): data samples for gaussian distribution\n", + " xspace (ndarray): x values to sample from normal distribution\n", + " mu (scalar): mean parameter of normal distribution\n", + " sigma (scalar): variance parameter of normal distribution\n", + " xlabel (str): the label of the x-axis of the histogram\n", + " ylabel (str): the label of the y-axis of the histogram\n", + "\n", + " Returns:\n", + " Nothing.\n", + " \"\"\"\n", + " fig, ax = plt.subplots()\n", + " ax.set_xlabel(xlabel)\n", + " ax.set_ylabel(ylabel)\n", + " # num_samples = samples.shape[0]\n", + "\n", + " count, bins, _ = plt.hist(samples, density=True)\n", + " plt.plot(xspace, norm.pdf(xspace, mu, sigma),'r-')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "\n", + "# Section 1: Stochasticity and randomness" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 1.1: Intro to Randomness\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 1: Stochastic World\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', '-QwTPDp7-a8'), ('Bilibili', 'BV1sU4y1G7Qt')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Stochastic_World_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "\n", + "Before trying out different probability distributions, let's start with the simple uniform distribution, U(a,b), which assigns equal probability to any value between a and b.\n", + "\n", + "To show that we are drawing a random number $x$ from a uniform distribution with lower and upper bounds $a$ and $b$ we will use this notation:\n", + "$x \\sim \\mathcal{U}(a,b)$. Alternatively, we can say that all the potential values of $x$ are distributed as a uniform distribution between $a$ and $b$. $x$ here is a random variable: a variable whose value depends on the outcome of a random process." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Coding Exercise 1.1: Create randomness\n", + "\n", + "Numpy has many functions and capabilities related to randomness. We can draw random numbers from various probability distributions. For example, to draw 5 uniform numbers between 0 and 100, you would use `np.random.uniform(0, 100, size = (5,))`.\n", + "\n", + " We will use `np.random.seed` to set a specific seed for the random number generator. For example, `np.random.seed(0)` sets the seed as 0. By including this, we are actually making the random numbers reproducible, which may seem odd at first. Basically if we do the below code without that 0, we would get different random numbers every time we run it. By setting the seed to 0, we ensure we will get the same random numbers. There are lots of reasons we may want randomness to be reproducible. In NMA-world, it's so your plots will match the solution plots exactly!\n", + "\n", + "```python\n", + "np.random.seed(0)\n", + "random_nums = np.random.uniform(0, 100, size = (5,))\n", + "```\n", + "\n", + "Below, you will complete a function `generate_random_sample` that randomly generates `num_points` $x$ and $y$ coordinate values, all within the range 0 to 1. You will then generate 10 points and visualize." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "def generate_random_sample(num_points):\n", + " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", + " in the range [0, 1] using a random number generator object.\n", + "\n", + " Args:\n", + " num_points (int): number of points desired in random sample\n", + "\n", + " Returns:\n", + " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", + " and y coordinates of sampled points\n", + "\n", + " \"\"\"\n", + "\n", + " ###################################################################\n", + " ## TODO for students: Draw the uniform numbers\n", + " ## Fill out the following then remove\n", + " raise NotImplementedError(\"Student exercise: need to complete generate_random_sample\")\n", + " ###################################################################\n", + "\n", + " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", + " # both x and y\n", + " dataX = ...\n", + " dataY = ...\n", + "\n", + " return dataX, dataY\n", + "\n", + "# Set a seed\n", + "np.random.seed(0)\n", + "\n", + "# Set number of points to draw\n", + "num_points = 10\n", + "\n", + "# Draw random points\n", + "dataX, dataY = generate_random_sample(num_points)\n", + "\n", + "# Visualize\n", + "plot_random_sample(dataX, dataY, \"Random sample of 10 points\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "def generate_random_sample(num_points):\n", + " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", + " in the range [0, 1] using a random number generator object.\n", + "\n", + " Args:\n", + " num_points (int): number of points desired in random sample\n", + "\n", + " Returns:\n", + " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", + " and y coordinates of sampled points\n", + "\n", + " \"\"\"\n", + "\n", + " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", + " # both x and y\n", + " dataX = np.random.uniform(0, 1, size = (num_points,))\n", + " dataY = np.random.uniform(0, 1, size = (num_points,))\n", + "\n", + " return dataX, dataY\n", + "\n", + "# Set a seed\n", + "np.random.seed(0)\n", + "\n", + "# Set number of points to draw\n", + "num_points = 10\n", + "\n", + "# Draw random points\n", + "dataX, dataY = generate_random_sample(num_points)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_random_sample(dataX, dataY, \"Random sample of 10 points\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Create_Randomness_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 1.1: Random Sample Generation from Uniform Distribution\n", + "In practice this may not look very uniform, although that is of course part of the randomness! Uniform randomness does not mean smoothly uniform. When we have very little data it can be hard to see the distribution.\n", + "\n", + "Below, you can adjust the number of points sampled with a slider. Does it look more uniform now? Try increasingly large numbers of sampled points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "#@markdown Make sure you execute this cell to enable the widget!\n", + "\n", + "def generate_random_sample(num_points):\n", + " \"\"\" Generate a random sample containing a desired number of points (num_points)\n", + " in the range [0, 1] using a random number generator object.\n", + "\n", + " Args:\n", + " num_points (int): number of points desired in random sample\n", + "\n", + " Returns:\n", + " dataX, dataY (ndarray, ndarray): arrays of size (num_points,) containing x\n", + " and y coordinates of sampled points\n", + "\n", + " \"\"\"\n", + "\n", + " # Generate desired number of points uniformly between 0 and 1 (using uniform) for\n", + " # both x and y\n", + " dataX = np.random.uniform(0, 1, size = (num_points,))\n", + " dataY = np.random.uniform(0, 1, size = (num_points,))\n", + "\n", + " return dataX, dataY\n", + "\n", + "@widgets.interact\n", + "def gen_and_plot_random_sample(num_points = widgets.SelectionSlider(options=[(\"%g\"%i,i) for i in np.arange(0, 500, 10)])):\n", + "\n", + " dataX, dataY = generate_random_sample(num_points)\n", + " fig, ax = plt.subplots()\n", + " ax.set_xlabel('x')\n", + " ax.set_ylabel('y')\n", + " plt.xlim([-0.25, 1.25])\n", + " plt.ylim([-0.25, 1.25])\n", + " plt.scatter(dataX, dataY)\n", + " fig.suptitle(\"Random sample of \" + str(num_points) + \" points\", size=16)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Random_Sample_Generation_from_Uniform_Distribution_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 1.2: Random walk\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 2: Random walk\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'Tz9gjHcqj5k'), ('Bilibili', 'BV11U4y1G7Bu')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Random_Walk_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "Stochastic models can be used to create models of behaviour. As an example, imagine that a rat is placed inside a novel environment, a box. We could try and model its exploration behaviour by assuming that for each time step it takes a random uniformly sampled step in any direction (simultaneous random step in x direction and random step in y direction)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Coding Exercise 1.2: Modeling a random walk\n", + "\n", + "\n", + "Use the `generate_random_sample` function from above to obtain the random steps the rat takes at each time step and complete the generate_random_walk function below. For plotting, the box will be represented graphically as the unit square enclosed by the points $(0, 0)$ and $(1, 1)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "def generate_random_walk(num_steps, step_size):\n", + " \"\"\" Generate the points of a random walk within a 1 X 1 box.\n", + "\n", + " Args:\n", + " num_steps (int): number of steps in the random walk\n", + " step_size (float): how much each random step size is weighted\n", + "\n", + " Returns:\n", + " x, y (ndarray, ndarray): the (x, y) locations reached at each time step of the walk\n", + "\n", + " \"\"\"\n", + " x = np.zeros(num_steps + 1)\n", + " y = np.zeros(num_steps + 1)\n", + "\n", + " ###################################################################\n", + " ## TODO for students: Collect random step values with function from before\n", + " ## Fill out the following then remove\n", + " raise NotImplementedError(\"Student exercise: need to complete generate_random_walk\")\n", + " ###################################################################\n", + "\n", + " # Generate the uniformly random x, y steps for the walk\n", + " random_x_steps, random_y_steps = ...\n", + "\n", + " # Take steps according to the randomly sampled steps above\n", + " for step in range(num_steps):\n", + "\n", + " # take a random step in x and y. We remove 0.5 to make it centered around 0\n", + " x[step + 1] = x[step] + (random_x_steps[step] - 0.5)*step_size\n", + " y[step + 1] = y[step] + (random_y_steps[step] - 0.5)*step_size\n", + "\n", + " # restrict to be within the 1 x 1 unit box\n", + " x[step + 1]= min(max(x[step + 1], 0), 1)\n", + " y[step + 1]= min(max(y[step + 1], 0), 1)\n", + "\n", + " return x, y\n", + "\n", + "# Set a random seed\n", + "np.random.seed(2)\n", + "\n", + "# Select parameters\n", + "num_steps = 100 # number of steps in random walk\n", + "step_size = 0.5 # size of each step\n", + "\n", + "# Generate the random walk\n", + "x, y = generate_random_walk(num_steps, step_size)\n", + "\n", + "# Visualize\n", + "plot_random_walk(x, y, \"Rat's location throughout random walk\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "def generate_random_walk(num_steps, step_size):\n", + " \"\"\" Generate the points of a random walk within a 1 X 1 box.\n", + "\n", + " Args:\n", + " num_steps (int): number of steps in the random walk\n", + " step_size (float): how much each random step size is weighted\n", + "\n", + " Returns:\n", + " x, y (ndarray, ndarray): the (x, y) locations reached at each time step of the walk\n", + "\n", + " \"\"\"\n", + " x = np.zeros(num_steps + 1)\n", + " y = np.zeros(num_steps + 1)\n", + "\n", + " # Generate the uniformly random x, y steps for the walk\n", + " random_x_steps, random_y_steps = generate_random_sample(num_steps)\n", + "\n", + " # Take steps according to the randomly sampled steps above\n", + " for step in range(num_steps):\n", + "\n", + " # take a random step in x and y. We remove 0.5 to make it centered around 0\n", + " x[step + 1] = x[step] + (random_x_steps[step] - 0.5)*step_size\n", + " y[step + 1] = y[step] + (random_y_steps[step] - 0.5)*step_size\n", + "\n", + " # restrict to be within the 1 x 1 unit box\n", + " x[step + 1]= min(max(x[step + 1], 0), 1)\n", + " y[step + 1]= min(max(y[step + 1], 0), 1)\n", + "\n", + " return x, y\n", + "\n", + "# Set a random seed\n", + "np.random.seed(2)\n", + "\n", + "# Select parameters\n", + "num_steps = 100 # number of steps in random walk\n", + "step_size = 0.5 # size of each step\n", + "\n", + "# Generate the random walk\n", + "x, y = generate_random_walk(num_steps, step_size)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " plot_random_walk(x, y, \"Rat's location throughout random walk\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "We put a little green dot for the starting point and a red point for the ending point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Modeling_a_random_walk_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 1.2: Varying parameters of a random walk\n", + "\n", + "In the interactive demo below, you can examine random walks with different numbers of steps or step sizes, using the sliders.\n", + "\n", + "\n", + "1. What could an increased step size mean for the actual rat's movement we are simulating?\n", + "2. For a given number of steps, is the rat more likely to visit all general areas of the arena with a big step size or small step size?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "\n", + "@widgets.interact(num_steps = widgets.IntSlider(value=100, min=0, max=500, step=1), step_size = widgets.FloatSlider(value=0.1, min=0.1, max=1, step=0.1))\n", + "def gen_and_plot_random_walk(num_steps, step_size):\n", + " x, y = generate_random_walk(num_steps, step_size)\n", + " plot_random_walk(x, y, \"Rat's location throughout random walk\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) A larger step size could mean that the rat is moving faster, or that we sample\n", + " the rats location less often.\n", + "\n", + "2) The rat tends to visit more of the arena with a large step size.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Varying_parameters_of_a_random_walk_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "In practice a uniform random movement is too simple an assumption. Rats do not move completely randomly; even if you could assume that, you would need to approximate with a more complex probability distribution.\n", + "\n", + "Nevertheless, this example highlights how you can use sampling to approximate behaviour.\n", + "\n", + "**Main course preview:** During [Hidden Dynamics day](https://compneuro.neuromatch.io/tutorials/W3D2_HiddenDynamics/chapter_title.html) we will see how random walk models can be used to also model accumulation of information in decision making." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 2: Discrete distributions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 2.1: Binomial distributions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 3: Binomial distribution\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'kOXEQlmzFyw'), ('Bilibili', 'BV1Ev411W7mw')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Binomial_distribution_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "This video covers the Bernoulli and binomial distributions.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "The uniform distribution is very simple, and can only be used in some rare cases. If we only had access to this distribution, our statistical toolbox would be very empty. Thankfully we do have some more advanced distributions!\n", + "\n", + "The uniform distribution that we looked at above is an example of a continuous distribution. The value of $X$ that we draw from this distribution can take **any value** between $a$ and $b$.\n", + "\n", + "However, sometimes we want to be able to look at discrete events. Imagine that the rat from before is now placed in a T-maze, with food placed at the end of both arms. Initially, we would expect the rat to be choosing randomly between the two arms, but after learning it should choose more consistently.\n", + "\n", + "A simple way to model such random behaviour is with a single **Bernoulli trial**, that has two outcomes, {$Left, Right$}, with probability $P(Left)=p$ and $P(Right)=1-p$ as the two mutually exclusive possibilities (whether the rat goes down the left or right arm of the maze).\n", + "
\n", + "\n", + "The binomial distribution simulates $n$ number of binary events, such as the $Left, Right$ choices of the random rat in the T-maze. Imagine that you have done an experiment and found that your rat turned left in 7 out of 10 trials. What is the probability of the rat indeed turning left 7 times ($k = 7$)?\n", + "\n", + "This is given by the binomial probability of $k$, given $n$ trials and probability $p$:\n", + "\n", + "\\begin{align}\n", + "P(k|n,p) &= \\left( \\begin{array} \\\\n \\\\ k\\end{array} \\right) p^k (1-p)^{n-k} \\\\\n", + "\\binom{n}{k} &= {\\frac {n!}{k!(n-k)!}}\n", + "\\end{align}\n", + "\n", + "In this formula, $p$ is the probability of turning left, $n$ is the number of binary events, or trials, and $k$ is the number of times the rat turned left. The term $\\binom {n}{k}$ is the binomial coefficient.\n", + "\n", + "This is an example of a *probability mass function*, which specifies the probability that a discrete random variable is equal to each value. In other words, how large a part of the probability space (mass) is placed at each exact discrete value. We require that all probability adds up to 1, i.e. that\n", + "\n", + "\\begin{equation}\n", + "\\sum_k P(k|n,p)=1.\n", + "\\end{equation}\n", + "\n", + "Essentially, if $k$ can only be one of 10 values, the probabilities of $k$ being equal to each possible value have to sum up to 1 because there is a probability of 1 it will equal one of those 10 values (no other options exist).\n", + "\n", + "If we assume an equal chance of turning left or right, then $p=0.5$. Note that if we only have a single trial $n=1$ this is equivalent to a single Bernoulli trial (feel free to do the math!)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Think! 2.1: Binomial distribution sampling\n", + "\n", + "We will draw a desired number of random samples from a binomial distribution, with $n = 10$ and $p = 0.5$. Each sample returns the number of trials, $k$, a rat turns left out of $n$ trials.\n", + "\n", + "We will draw 1000 samples of this (so it is as if we are observing 10 trials of the rat, 1000 different times). We can do this using numpy: `np.random.binomial(n, p, size = (n_samples,))`\n", + "\n", + "See below to visualize a histogram of the different values of $k$, or the number of times the rat turned left in each of the 1000 samples. In a histogram all the data is placed into bins and the contents of each bin is counted, to give a visualisation of the distribution of data. Discuss the following questions.\n", + "\n", + "1. What are the x-axis limits of the histogram and why?\n", + "2. What is the shape of the histogram?\n", + "3. Looking at the histogram, how would you interpret the outcome of the simulation if you didn't know what p was? Would you have guessed p = 0.5?\n", + "3. What do you think the histogram would look like if the probability of turning left is 0.8 ($p = 0.8$)?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Execute this cell to see visualization\n", + "\n", + "# Select parameters for conducting binomial trials\n", + "n = 10\n", + "p = 0.5\n", + "n_samples = 1000\n", + "\n", + "# Set random seed\n", + "np.random.seed(1)\n", + "\n", + "# Now draw 1000 samples by calling the function again\n", + "left_turn_samples_1000 = np.random.binomial(n, p, size = (n_samples,))\n", + "\n", + "# Visualize\n", + "count, bins = plot_hist(left_turn_samples_1000, 'Number of left turns in sample')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) The limits of the histogram at 0 and 10, as these are the minimum and maximum\n", + "number of trials that a rat can turn left out of 10 trials.\n", + "\n", + "2) The shape seems symmetric and is centered around 5.\n", + "\n", + "3) An average/mean around 5 left turns out of 10 trials indicates that the rat\n", + "chose left and right turns in the maze with equal probability (that p = 0.5)\n", + "\n", + "4) With p = 0.8, the center of the histogram would be at x = 8 and it would not be\n", + " as symmetrical (since it would be cut off at max 10). You can go into the code above\n", + " and run it with p = 0.8 to see this.\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Binomial_distribution_Sampling_Discussion\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "When working with the Bernoulli and binomial distributions, there are only 2 possible outcomes (in this case, turn left or turn right). In the more general case where there are $n$ possible outcomes (our rat is an n-armed maze) each with their own associated probability $p_1, p_2, p_3, p_4, ...$ , we use a **categorical distribution**. Draws from this distribution are a simple extension of the Bernoulli trial: we now have a probability for each outcome and draw based on those probabilities. We have to make sure that the probabilities sum to one:\n", + "\n", + "\\begin{equation}\n", + "\\sum_i P(x=i)=\\sum_i p_i =1\n", + "\\end{equation}\n", + "\n", + "If we sample from this distribution multiple times, we can then describe the distribution of outcomes from each sample as the **multinomial distribution**. Essentially, the categorical distribution is the multiple outcome extension of the Bernoulli, and the multinomial distribution is the multiple outcome extension of the binomial distribution. We'll see a bit more about this in the next tutorial when we look at Markov chains." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 2.2: Poisson distribution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 4: Poisson distribution\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'E_nvNb596DY'), ('Bilibili', 'BV1wV411x7P6')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Poisson_distribution_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "This video covers the Poisson distribution and how it can be used to describe neural spiking.\n", + "\n", + "
\n", + " Click here for text recap of video \n", + "\n", + "For some phenomena there may not be a natural limit on the maximum number of possible events or outcomes.\n", + "\n", + "The Poisson distribution is a '**point-process**', meaning that it determines the number of discrete 'point', or binary, events that happen within a fixed space or time, allowing for the occurence of a potentially infinite number of events. The Poisson distribution is specified by a single parameter $\\lambda$ that encapsulates the mean number of events that can occur in a single time or space interval (there will be more on this concept of the 'mean' later!).\n", + "\n", + "Relevant to us, we can model the number of times a neuron spikes within a time interval using a Poisson distribution. In fact, neuroscientists often do! As an example, if we are recording from a neuron that tends to fire at an average rate of 4 spikes per second, then the Poisson distribution specifies the distribution of recorded spikes over one second, where $\\lambda=4$.\n", + "\n", + "
\n", + "\n", + "The formula for a Poisson distribution on $x$ is:\n", + "\n", + "\\begin{equation}\n", + "P(x)=\\frac{\\lambda^x e^{-\\lambda}}{x!}\n", + "\\end{equation}\n", + "\n", + "where $\\lambda$ is a parameter corresponding to the average outcome of $x$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Coding Exercise 2.2: Poisson distribution sampling\n", + "\n", + "In the exercise below we will draw some samples from the Poisson distribution and see what the histogram looks.\n", + "\n", + "In the code, fill in the missing line so we draw 5 samples from a Poisson distribution with $\\lambda = 4$. Use `np.random.poisson`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# Set random seed\n", + "np.random.seed(0)\n", + "\n", + "# Draw 5 samples from a Poisson distribution with lambda = 4\n", + "sampled_spike_counts = ...\n", + "\n", + "# Print the counts\n", + "print(\"The samples drawn from the Poisson distribution are \" +\n", + " str(sampled_spike_counts))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "\n", + "# Set random seed\n", + "np.random.seed(0)\n", + "\n", + "# Draw 5 samples from a Poisson distribution with lambda = 4\n", + "sampled_spike_counts = np.random.poisson(4, 5)\n", + "\n", + "# Print the counts\n", + "print(\"The samples drawn from the Poisson distribution are \" +\n", + " str(sampled_spike_counts))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "You should see that the neuron spiked 6 times, 7 times, 1 time, 8 times, and 4 times in 5 different intervals." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Poisson_distribution_sampling_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 2.2: Varying parameters of Poisson distribution\n", + "\n", + "Use the interactive demo below to vary $\\lambda$ and the number of samples, and then visualize the resulting histogram.\n", + "\n", + "1. What effect does increasing the number of samples have? \n", + "2. What effect does changing $\\lambda$ have?\n", + "3. With a small lambda, why is the distribution asymmetric?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @markdown Make sure you execute this cell to enable the widget!\n", + "\n", + "@widgets.interact(lambda_value = widgets.FloatSlider(value=4, min=0.1, max=10, step=0.1),\n", + " n_samples = widgets.IntSlider(value=5, min=5, max=500, step=1))\n", + "\n", + "def gen_and_plot_possion_samples(lambda_value, n_samples):\n", + " sampled_spike_counts = np.random.poisson(lambda_value, n_samples)\n", + " count, bins = plot_hist(sampled_spike_counts, 'Recorded spikes per second')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove explanation\n", + "\n", + "\"\"\"\n", + "1) More samples only means that the shape of the distribution becomes clearer to us.\n", + "E.g. with a decent number of samples you may be able to see whether the\n", + "distribution is symmetrical. With a small number of samples we would not be able\n", + "to distinguish different distributions from each other. The more data samples we\n", + "have, the more we can say about the stochastic process that generated the data (obviously).\n", + "\n", + "2) Increasing lambda moves the distribution along the x-axis, essentially changing\n", + "the mean of the distribution. With lambda = 6 for example, we would expect to see\n", + "6 spike counts per interval on average.\n", + "\n", + "3) For small values of lambda the Poisson distribution becomes asymmetrical as\n", + "it is a distribution over non-negative counts\n", + " (you can’t have negative numbers of spikes e.g.).\n", + "\"\"\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Varying_parameters_of_Poisson_distribution_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Section 3: Continuous distributions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Video 5: Continuous distributions\n", + "from ipywidgets import widgets\n", + "from IPython.display import YouTubeVideo\n", + "from IPython.display import IFrame\n", + "from IPython.display import display\n", + "\n", + "\n", + "class PlayVideo(IFrame):\n", + " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", + " self.id = id\n", + " if source == 'Bilibili':\n", + " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", + " elif source == 'Osf':\n", + " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", + " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", + "\n", + "\n", + "def display_videos(video_ids, W=400, H=300, fs=1):\n", + " tab_contents = []\n", + " for i, video_id in enumerate(video_ids):\n", + " out = widgets.Output()\n", + " with out:\n", + " if video_ids[i][0] == 'Youtube':\n", + " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", + " height=H, fs=fs, rel=0)\n", + " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", + " else:\n", + " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", + " height=H, fs=fs, autoplay=False)\n", + " if video_ids[i][0] == 'Bilibili':\n", + " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", + " elif video_ids[i][0] == 'Osf':\n", + " print(f'Video available at https://osf.io/{video.id}')\n", + " display(video)\n", + " tab_contents.append(out)\n", + " return tab_contents\n", + "\n", + "\n", + "video_ids = [('Youtube', 'LJ4Zdokb6lc'), ('Bilibili', 'BV1dq4y1L7eC')]\n", + "tab_contents = display_videos(video_ids, W=854, H=480)\n", + "tabs = widgets.Tab()\n", + "tabs.children = tab_contents\n", + "for i in range(len(tab_contents)):\n", + " tabs.set_title(i, video_ids[i][0])\n", + "display(tabs)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "**Note:** There is a typo in the vido ~3.40, where the product of Gaussian distributions should be $\\mathcal{N}(\\mu_1, \\sigma_1^2) \\cdot \\mathcal{N}(\\mu_2, \\sigma_2^2)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Continuous_distributions_Video\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "We do not have to restrict ourselves to only probabilistic models of discrete events. While some events in neuroscience are discrete (e.g., number of spikes by a neuron), many others are continuous (e.g., neuroimaging signals in EEG or fMRI, distance traveled by an animal, human pointing in the direction of a stimulus).\n", + "\n", + "While for discrete outcomes, we can ask about the probability of a specific event (\"What is the probability this neuron will fire 4 times in the next second\"), this is not defined for a continuous distribution (\"What is the probability of the BOLD signal being exactly 4.000120141...\"). Hence we need to focus on intervals when calculating probabilities from a continuous distribution.\n", + "\n", + "If we want to make predictions about possible outcomes (\"I believe the BOLD signal from the area will be in the range $x_1$ to $ x_2 $\"), we can use the integral $\\int_{x_1}^{x_2} P(x)$.\n", + "$P(x)$ is now a **probability density function**, sometimes written as $f(x)$ to distinguish it from the probability mass functions.\n", + "\n", + "With continuous distributions, we have to replace the normalizing sum\n", + "\n", + "\\begin{equation}\n", + "\\sum_i P(x=p_i) = 1\n", + "\\end{equation}\n", + "\n", + "over all possible events, with an integral\n", + "\n", + "\\begin{equation}\n", + "\\int_a^b P(x) = 1\n", + "\\end{equation}\n", + "\n", + "where a and b are the limits of the random variable $x$ (often $-\\infty$ and $\\infty$)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "## Section 3.1: Gaussian Distribution\n", + "\n", + "The most widely used continuous distribution is probably the Gaussian (also known as Normal) distribution. It is extremely common across all kinds of statistical analyses. Because of the central limit theorem, many quantities are Gaussian distributed. Gaussians also have some nice mathematical properties that permit simple closed-form solutions to several important problems.\n", + "\n", + "As a working example, imagine that a human participant is asked to point in the direction where they perceived a sound coming from. As an approximation, we can assume that the variability in the direction/orientation they point towards is Gaussian distributed." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Coding Exercise 3.1A: Gaussian Distribution\n", + "\n", + "In this exercise, you will implement a Gaussian by filling in the missing portions of code for the function `my_gaussian` below. Gaussians have two parameters. The **mean** $\\mu$, which sets the location of its center, and its \"scale\" or spread is controlled by its **standard deviation** $\\sigma$, or **variance** $\\sigma^2$ (i.e. the square of standard deviation). **Be careful not to use one when the other is required.**\n", + "\n", + "The equation for a Gaussian probability density function is:\n", + "\n", + "\\begin{equation}\n", + "f(x;\\mu,\\sigma^2) = \\mathcal{N}(\\mu,\\sigma^2) = \\frac{1}{\\sqrt{2\\pi\\sigma^2}}\\exp\\left(\\frac{-(x-\\mu)^2}{2\\sigma^2}\\right)\n", + "\\end{equation}\n", + "\n", + "In Python $\\pi$ and $e$ can be written as `np.pi` and `np.exp` respectively.\n", + "\n", + "As a probability distribution this has an integral of one when integrated from $-\\infty$ to $\\infty$, however in the following your numerical Gaussian will only be computed over a finite number of points (for the cell below we will sample from -8 to 9 in step sizes of 0.1). You therefore need to explicitly normalize it to sum to one yourself.\n", + "\n", + "Test out your implementation with a $\\mu = -1$ and $\\sigma = 1$. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "def my_gaussian(x_points, mu, sigma):\n", + " \"\"\" Returns normalized Gaussian estimated at points `x_points`, with\n", + " parameters: mean `mu` and standard deviation `sigma`\n", + "\n", + " Args:\n", + " x_points (ndarray of floats): points at which the gaussian is evaluated\n", + " mu (scalar): mean of the Gaussian\n", + " sigma (scalar): standard deviation of the gaussian\n", + "\n", + " Returns:\n", + " (numpy array of floats) : normalized Gaussian evaluated at `x`\n", + " \"\"\"\n", + "\n", + " ###################################################################\n", + " ## TODO for students: Implement the formula for a Gaussian\n", + " ## Add code to calculate the gaussian px as a function of mu and sigma,\n", + " ## for every x in x_points\n", + " ## Function Hints: exp -> np.exp()\n", + " ## power -> z**2\n", + " ##\n", + " ## Fill out the following then remove\n", + " raise NotImplementedError(\"Student exercise: need to implement Gaussian\")\n", + " ###################################################################\n", + " px = ...\n", + "\n", + " # as we are doing numerical integration we have to remember to normalise\n", + " # taking into account the stepsize (0.1)\n", + " px = px/(0.1*sum(px))\n", + " return px\n", + "\n", + "x = np.arange(-8, 9, 0.1)\n", + "\n", + "# Generate Gaussian\n", + "px = my_gaussian(x, -1, 1)\n", + "\n", + "# Visualize\n", + "my_plot_single(x, px)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": {} + }, + "outputs": [], + "source": [ + "# to_remove solution\n", + "def my_gaussian(x_points, mu, sigma):\n", + " \"\"\" Returns normalized Gaussian estimated at points `x_points`, with\n", + " parameters: mean `mu` and standard deviation `sigma`\n", + "\n", + " Args:\n", + " x_points (ndarray of floats): points at which the gaussian is evaluated\n", + " mu (scalar): mean of the Gaussian\n", + " sigma (scalar): standard deviation of the gaussian\n", + "\n", + " Returns:\n", + " (numpy array of floats) : normalized Gaussian evaluated at `x`\n", + " \"\"\"\n", + "\n", + " px = 1/(2*np.pi*sigma**2)**1/2 *np.exp(-(x_points-mu)**2/(2*sigma**2))\n", + "\n", + " # as we are doing numerical integration we have to remember to normalise\n", + " # taking into account the stepsize (0.1)\n", + " px = px/(0.1*sum(px))\n", + " return px\n", + "\n", + "x = np.arange(-8, 9, 0.1)\n", + "\n", + "# Generate Gaussian\n", + "px = my_gaussian(x, -1, 1)\n", + "\n", + "# Visualize\n", + "with plt.xkcd():\n", + " my_plot_single(x, px)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Gaussian_Distribution_Exercise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "### Interactive Demo 3.1: Sampling from a Gaussian distribution\n", + "\n", + "Now that we have gained a bit of intuition about the shape of the Gaussian, let's imagine that a human participant is asked to point in the direction of a sound source, which we then measure in horizontal degrees. To simulate that we draw samples from a Normal distribution:\n", + "\n", + "\\begin{equation}\n", + "x \\sim \\mathcal{N}(\\mu,\\sigma)\n", + "\\end{equation}\n", + "\n", + "We can sample from a Gaussian with mean $\\mu$ and standard deviation $\\sigma$ using `np.random.normal(mu, sigma, size = (n_samples,))`.\n", + "\n", + "In the demo below, you can change the mean and standard deviation of the Gaussian, and the number of samples, we can compare the histogram of the samples to the true analytical distribution (in red).\n", + "\n", + "1. With what number of samples would you say that the full distribution (in red) is well approximated by the histogram?\n", + "2. What if you just wanted to approximate the variables that defined the distribution, i.e., mean and variance?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "#@markdown Make sure you execute this cell to enable the widget!\n", + "\n", + "\n", + "@widgets.interact(mean = widgets.FloatSlider(value=0, min=-5, max=5, step=0.5),\n", + " standard_dev = widgets.FloatSlider(value=0.5, min=0, max=10, step=0.1),\n", + " n_samples = widgets.IntSlider(value=5, min=1, max=300, step=1))\n", + "def gen_and_plot_normal_samples(mean, standard_dev, n_samples):\n", + " x = np.random.normal(mean, standard_dev, size = (n_samples,))\n", + " xspace = np.linspace(-20, 20, 100)\n", + " plot_gaussian_samples_true(x, xspace, mean, standard_dev,\n", + " 'orientation (degrees)', 'probability')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "**Main course preview:** Gaussian distriutions are everywhere and are critical for filtering, [linear systems](https://compneuro.neuromatch.io/tutorials/W2D2_LinearSystems/chapter_title.html), [optimal control](https://compneuro.neuromatch.io/tutorials/W3D3_OptimalControl/chapter_title.html) and almost any statistical model of continuous data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "execution": {} + }, + "outputs": [], + "source": [ + "# @title Submit your feedback\n", + "content_review(f\"{feedback_prefix}_Sampling_from_a_Gaussian_distribution_Interactive_Demo\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "---\n", + "# Summary\n", + "\n", + "Across the different exercises you should now:\n", + "* have gotten some intuition about how stochastic randomly generated data can be\n", + "* understand how to model data using simple distributions\n", + "* understand the difference between discrete and continuous distributions\n", + "* be able to plot a Gaussian distribution\n", + "\n", + "For more reading on these topics see just about any statistics textbook, or take a look at the [online resources](https://github.com/NeuromatchAcademy/precourse/blob/main/resources.md)." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "W0D5_Tutorial1", + "provenance": [], + "toc_visible": true + }, + "kernel": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + }, + "toc-autonumbering": true + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/tutorials/W0D5_Statistics/instructor/W0D5_Tutorial1.ipynb b/tutorials/W0D5_Statistics/instructor/W0D5_Tutorial1.ipynb index 48b401d..7e19bc3 100644 --- a/tutorials/W0D5_Statistics/instructor/W0D5_Tutorial1.ipynb +++ b/tutorials/W0D5_Statistics/instructor/W0D5_Tutorial1.ipynb @@ -1425,6 +1425,15 @@ "display(tabs)" ] }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "**Note:** There is a typo in the vido ~3.40, where the product of Gaussian distributions should be $\\mathcal{N}(\\mu_1, \\sigma_1^2) \\cdot \\mathcal{N}(\\mu_2, \\sigma_2^2)$." + ] + }, { "cell_type": "code", "execution_count": null, @@ -1444,14 +1453,14 @@ "execution": {} }, "source": [ - "We do not have to restrict ourselves to only probabilistic models of discrete events. While some events in neuroscience are discrete (e.g. number of spikes by a neuron), many others are continuous (e.g. neuroimaging signals in EEG or fMRI, distance traveled by an animal, human pointing in a direction of a stimulus).\n", + "We do not have to restrict ourselves to only probabilistic models of discrete events. While some events in neuroscience are discrete (e.g., number of spikes by a neuron), many others are continuous (e.g., neuroimaging signals in EEG or fMRI, distance traveled by an animal, human pointing in the direction of a stimulus).\n", "\n", - "While for discrete outcomes we can ask about the probability of an specific event (\"what is the probability this neuron will fire 4 times in the next second\"), this is not defined for a continuous distribution (\"what is the probability of the BOLD signal being exactly 4.000120141...\"). Hence we need to focus on intervals when calculating probabilities from a continuous distribution.\n", + "While for discrete outcomes, we can ask about the probability of a specific event (\"What is the probability this neuron will fire 4 times in the next second\"), this is not defined for a continuous distribution (\"What is the probability of the BOLD signal being exactly 4.000120141...\"). Hence we need to focus on intervals when calculating probabilities from a continuous distribution.\n", "\n", - "If we want to make predictions about possible outcomes (\"I believe the BOLD signal from the area will be in the range $x_1$ to $ x_2 $\") we can use the integral $\\int_{x_1}^{x_2} P(x)$.\n", + "If we want to make predictions about possible outcomes (\"I believe the BOLD signal from the area will be in the range $x_1$ to $ x_2 $\"), we can use the integral $\\int_{x_1}^{x_2} P(x)$.\n", "$P(x)$ is now a **probability density function**, sometimes written as $f(x)$ to distinguish it from the probability mass functions.\n", "\n", - "With continuous distributions we have to replace the normalising sum\n", + "With continuous distributions, we have to replace the normalizing sum\n", "\n", "\\begin{equation}\n", "\\sum_i P(x=p_i) = 1\n", @@ -1476,7 +1485,7 @@ "\n", "The most widely used continuous distribution is probably the Gaussian (also known as Normal) distribution. It is extremely common across all kinds of statistical analyses. Because of the central limit theorem, many quantities are Gaussian distributed. Gaussians also have some nice mathematical properties that permit simple closed-form solutions to several important problems.\n", "\n", - "As a working example, imagine that a human participant is asked to point in the direction where they perceived a sound coming from. As an approximation, we can assume that the variability in the direction/orientation they point towards is Gaussian distributed.\n" + "As a working example, imagine that a human participant is asked to point in the direction where they perceived a sound coming from. As an approximation, we can assume that the variability in the direction/orientation they point towards is Gaussian distributed." ] }, { diff --git a/tutorials/W0D5_Statistics/student/W0D5_Tutorial1.ipynb b/tutorials/W0D5_Statistics/student/W0D5_Tutorial1.ipynb index e301a5f..3be060c 100644 --- a/tutorials/W0D5_Statistics/student/W0D5_Tutorial1.ipynb +++ b/tutorials/W0D5_Statistics/student/W0D5_Tutorial1.ipynb @@ -1305,6 +1305,15 @@ "display(tabs)" ] }, + { + "cell_type": "markdown", + "metadata": { + "execution": {} + }, + "source": [ + "**Note:** There is a typo in the vido ~3.40, where the product of Gaussian distributions should be $\\mathcal{N}(\\mu_1, \\sigma_1^2) \\cdot \\mathcal{N}(\\mu_2, \\sigma_2^2)$." + ] + }, { "cell_type": "code", "execution_count": null, @@ -1324,14 +1333,14 @@ "execution": {} }, "source": [ - "We do not have to restrict ourselves to only probabilistic models of discrete events. While some events in neuroscience are discrete (e.g. number of spikes by a neuron), many others are continuous (e.g. neuroimaging signals in EEG or fMRI, distance traveled by an animal, human pointing in a direction of a stimulus).\n", + "We do not have to restrict ourselves to only probabilistic models of discrete events. While some events in neuroscience are discrete (e.g., number of spikes by a neuron), many others are continuous (e.g., neuroimaging signals in EEG or fMRI, distance traveled by an animal, human pointing in the direction of a stimulus).\n", "\n", - "While for discrete outcomes we can ask about the probability of an specific event (\"what is the probability this neuron will fire 4 times in the next second\"), this is not defined for a continuous distribution (\"what is the probability of the BOLD signal being exactly 4.000120141...\"). Hence we need to focus on intervals when calculating probabilities from a continuous distribution.\n", + "While for discrete outcomes, we can ask about the probability of a specific event (\"What is the probability this neuron will fire 4 times in the next second\"), this is not defined for a continuous distribution (\"What is the probability of the BOLD signal being exactly 4.000120141...\"). Hence we need to focus on intervals when calculating probabilities from a continuous distribution.\n", "\n", - "If we want to make predictions about possible outcomes (\"I believe the BOLD signal from the area will be in the range $x_1$ to $ x_2 $\") we can use the integral $\\int_{x_1}^{x_2} P(x)$.\n", + "If we want to make predictions about possible outcomes (\"I believe the BOLD signal from the area will be in the range $x_1$ to $ x_2 $\"), we can use the integral $\\int_{x_1}^{x_2} P(x)$.\n", "$P(x)$ is now a **probability density function**, sometimes written as $f(x)$ to distinguish it from the probability mass functions.\n", "\n", - "With continuous distributions we have to replace the normalising sum\n", + "With continuous distributions, we have to replace the normalizing sum\n", "\n", "\\begin{equation}\n", "\\sum_i P(x=p_i) = 1\n", @@ -1356,7 +1365,7 @@ "\n", "The most widely used continuous distribution is probably the Gaussian (also known as Normal) distribution. It is extremely common across all kinds of statistical analyses. Because of the central limit theorem, many quantities are Gaussian distributed. Gaussians also have some nice mathematical properties that permit simple closed-form solutions to several important problems.\n", "\n", - "As a working example, imagine that a human participant is asked to point in the direction where they perceived a sound coming from. As an approximation, we can assume that the variability in the direction/orientation they point towards is Gaussian distributed.\n" + "As a working example, imagine that a human participant is asked to point in the direction where they perceived a sound coming from. As an approximation, we can assume that the variability in the direction/orientation they point towards is Gaussian distributed." ] }, {

svBpd!?=87sI~&m?p(dVB(NIShl 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