{ "cells": [ { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {}, "id": "view-in-github" }, "source": [ "\"Open   \"Open" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Tutorial 1: Linear dynamical systems\n", "\n", "**Week 2, Day 2: Linear Systems**\n", "\n", "**By Neuromatch Academy**\n", "\n", "**Content Creators**: Bing Wen Brunton, Alice Schwarze\n", "\n", "**Content Reviewers**: Norma Kuhn, Karolina Stosio, John Butler, Matthew Krause, Ella Batty, Richard Gao, Michael Waskom\n", "\n", "**Production editors:** Gagana B, Spiros Chavlis" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Tutorial Objectives\n", "\n", "*Estimated timing of tutorial: 1 hour*\n", "\n", "In this tutorial, we will be learning about behavior of dynamical systems -- systems that evolve in time -- where the rules by which they evolve in time are described precisely by a differential equation.\n", "\n", "Differential equations are equations that express the **rate of change** of the state variable $x$. One typically describes this rate of change using the derivative of $x$ with respect to time ($dx/dt$) on the left hand side of the differential equation:\n", "\n", "\\begin{equation}\n", "\\frac{dx}{dt} = f(x)\n", "\\end{equation}\n", "\n", "A common notational short-hand is to write $\\dot{x}$ for $\\frac{dx}{dt}$. The dot means \"the derivative with respect to time\".\n", "\n", "Today, the focus will be on **linear dynamics**, where $f(x)$ is a linear function of $x$. In Tutorial 1, we will:\n", "\n", "* Explore and understand the behavior of such systems where $x$ is a single variable\n", "* Consider cases where $\\mathbf{x}$ is a state vector representing two variables." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @markdown\n", "from IPython.display import IFrame\n", "from ipywidgets import widgets\n", "out = widgets.Output()\n", "with out:\n", " print(f\"If you want to download the slides: https://osf.io/download/snv4m/\")\n", " display(IFrame(src=f\"https://mfr.ca-1.osf.io/render?url=https://osf.io/snv4m/?direct%26mode=render%26action=download%26mode=render\", width=730, height=410))\n", "display(out)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Setup" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Install and import feedback gadget\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "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_cn\",\n", " \"user_key\": \"y1x3mpx5\",\n", " },\n", " ).render()\n", "\n", "\n", "feedback_prefix = \"W2D2_T1\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# Imports\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from scipy.integrate import solve_ivp # numerical integration solver" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Figure settings\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Figure settings\n", "import logging\n", "logging.getLogger('matplotlib.font_manager').disabled = True\n", "\n", "import ipywidgets as widgets # interactive display\n", "%config InlineBackend.figure_format = 'retina'\n", "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/course-content/main/nma.mplstyle\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Plotting Functions\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Plotting Functions\n", "def plot_trajectory(system, params, initial_condition, dt=0.1, T=6,\n", " figtitle=None):\n", "\n", " \"\"\"\n", " Shows the solution of a linear system with two variables in 3 plots.\n", " The first plot shows x1 over time. The second plot shows x2 over time.\n", " The third plot shows x1 and x2 in a phase portrait.\n", "\n", " Args:\n", " system (function): a function f(x) that computes a derivative from\n", " inputs (t, [x1, x2], *params)\n", " params (list or tuple): list of parameters for function \"system\"\n", " initial_condition (list or array): initial condition x0\n", " dt (float): time step of simulation\n", " T (float): end time of simulation\n", " figtitlte (string): title for the figure\n", "\n", " Returns:\n", " nothing, but it shows a figure\n", " \"\"\"\n", "\n", " # time points for which we want to evaluate solutions\n", " t = np.arange(0, T, dt)\n", "\n", " # Integrate\n", " # use built-in ode solver\n", " solution = solve_ivp(system,\n", " t_span=(0, T),\n", " y0=initial_condition, t_eval=t,\n", " args=(params),\n", " dense_output=True)\n", " x = solution.y\n", "\n", " # make a color map to visualize time\n", " timecolors = np.array([(1 , 0 , 0, i) for i in t / t[-1]])\n", "\n", " # make a large figure\n", " fig, (ah1, ah2, ah3) = plt.subplots(1, 3)\n", " fig.set_size_inches(10, 3)\n", "\n", " # plot x1 as a function of time\n", " ah1.scatter(t, x[0,], color=timecolors)\n", " ah1.set_xlabel('time')\n", " ah1.set_ylabel('x1', labelpad=-5)\n", "\n", " # plot x2 as a function of time\n", " ah2.scatter(t, x[1], color=timecolors)\n", " ah2.set_xlabel('time')\n", " ah2.set_ylabel('x2', labelpad=-5)\n", "\n", " # plot x1 and x2 in a phase portrait\n", " ah3.scatter(x[0,], x[1,], color=timecolors)\n", " ah3.set_xlabel('x1')\n", " ah3.set_ylabel('x2', labelpad=-5)\n", " #include initial condition is a blue cross\n", " ah3.plot(x[0,0], x[1,0], 'bx')\n", "\n", " # adjust spacing between subplots\n", " plt.subplots_adjust(wspace=0.5)\n", "\n", " # add figure title\n", " if figtitle is not None:\n", " fig.suptitle(figtitle, size=16)\n", " plt.show()\n", "\n", "\n", "def plot_streamplot(A, ax, figtitle=None, show=True):\n", " \"\"\"\n", " Show a stream plot for a linear ordinary differential equation with\n", " state vector x=[x1,x2] in axis ax.\n", "\n", " Args:\n", " A (numpy array): 2x2 matrix specifying the dynamical system\n", " ax (matplotlib.axes): axis to plot\n", " figtitle (string): title for the figure\n", " show (boolean): enable plt.show()\n", "\n", " Returns:\n", " nothing, but shows a figure\n", " \"\"\"\n", "\n", " # sample 20 x 20 grid uniformly to get x1 and x2\n", " grid = np.arange(-20, 21, 1)\n", " x1, x2 = np.meshgrid(grid, grid)\n", "\n", " # calculate x1dot and x2dot at each grid point\n", " x1dot = A[0,0] * x1 + A[0,1] * x2\n", " x2dot = A[1,0] * x1 + A[1,1] * x2\n", "\n", " # make a colormap\n", " magnitude = np.sqrt(x1dot ** 2 + x2dot ** 2)\n", " color = 2 * np.log1p(magnitude) #Avoid taking log of zero\n", "\n", " # plot\n", " plt.sca(ax)\n", " plt.streamplot(x1, x2, x1dot, x2dot, color=color,\n", " linewidth=1, cmap=plt.cm.cividis,\n", " density=2, arrowstyle='->', arrowsize=1.5)\n", " plt.xlabel(r'$x1$')\n", " plt.ylabel(r'$x2$')\n", "\n", " # figure title\n", " if figtitle is not None:\n", " plt.title(figtitle, size=16)\n", "\n", " # include eigenvectors\n", " if True:\n", " # get eigenvalues and eigenvectors of A\n", " lam, v = np.linalg.eig(A)\n", "\n", " # get eigenvectors of A\n", " eigenvector1 = v[:,0].real\n", " eigenvector2 = v[:,1].real\n", "\n", " # plot eigenvectors\n", " plt.arrow(0, 0, 20*eigenvector1[0], 20*eigenvector1[1],\n", " width=0.5, color='r', head_width=2,\n", " length_includes_head=True)\n", " plt.arrow(0, 0, 20*eigenvector2[0], 20*eigenvector2[1],\n", " width=0.5, color='b', head_width=2,\n", " length_includes_head=True)\n", " if show:\n", " plt.show()\n", "\n", "\n", "def plot_specific_example_stream_plots(A_options):\n", " \"\"\"\n", " Show a stream plot for each A in A_options\n", "\n", " Args:\n", " A (list): a list of numpy arrays (each element is A)\n", "\n", " Returns:\n", " nothing, but shows a figure\n", " \"\"\"\n", " # get stream plots for the four different systems\n", " plt.figure(figsize=(10, 10))\n", "\n", " for i, A in enumerate(A_options):\n", "\n", " ax = plt.subplot(2, 2, 1+i)\n", " # get eigenvalues and eigenvectors\n", " lam, v = np.linalg.eig(A)\n", "\n", " # plot eigenvalues as title\n", " # (two spaces looks better than one)\n", " eigstr = \", \".join([f\"{x:.2f}\" for x in lam])\n", " figtitle =f\"A with eigenvalues\\n\"+ '[' + eigstr + ']'\n", " plot_streamplot(A, ax, figtitle=figtitle, show=False)\n", "\n", " # Remove y_labels on righthand plots\n", " if i % 2:\n", " ax.set_ylabel(None)\n", " if i < 2:\n", " ax.set_xlabel(None)\n", "\n", " plt.subplots_adjust(wspace=0.3, hspace=0.3)\n", " plt.show()" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Section 1: One-dimensional Differential Equations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Video 1: Linear Dynamical Systems\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 1: Linear Dynamical Systems\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', '87z6OR7-DBI'), ('Bilibili', 'BV1up4y1S7wj')]\n", "tab_contents = display_videos(video_ids, W=730, H=410)\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": {}, "source": [ "## Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Linear_Dynamical_Systems_Video\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "This video serves as an introduction to dynamical systems as the mathematics of things that change in time, including examples of relevant timescales relevant for neuroscience. It covers the definition of a linear system and why we are spending a whole day on linear dynamical systems, and walks through solutions to one-dimensional, deterministic dynamical systems, their behaviors, and stability criteria.\n", "\n", "Note that this section is a recap of [Tutorials 2](https://compneuro.neuromatch.io/tutorials/W0D4_Calculus/student/W0D4_Tutorial2.html) and [3](https://compneuro.neuromatch.io/tutorials/W0D4_Calculus/student/W0D4_Tutorial3.html) of our pre-course calculus day.\n", "\n", "
\n", " Click here for text recap of video \n", "\n", "Let's start by reminding ourselves of a one-dimensional differential equation in $x$ of the form\n", "\n", "$$\\dot{x} = a x$$\n", "\n", "where $a$ is a scalar.\n", "\n", "Solutions for how $x$ evolves in time when its dynamics are governed by such a differential equation take the form\n", "\n", "\\begin{equation}\n", "x(t) = x_0\\exp(a t)\n", "\\end{equation}\n", "\n", "where $x_0$ is the **initial condition** of the equation -- that is, the value of $x$ at time $0$.\n", "
\n", "\n", "To gain further intuition, let's explore the behavior of such systems with a simple simulation. We can simulate an ordinary differential equation by approximating or modeling time as a discrete list of time steps $t_0, t_1, t_2, \\dots$, such that $t_{i+1}=t_i+dt$. We can get the small change $dx$ over a small duration $dt$ of time from the definition of the differential:\n", "\n", "\\begin{eqnarray}\n", "\\dot x &=& \\frac{dx}{dt} \\\\\n", "dx &=& \\dot x\\, dt\n", "\\end{eqnarray}\n", "\n", "So, at each time step $t_i$, we compute a value of $x$, $x(t_i)$, as the sum of the value of $x$ at the previous time step, $x(t_{i-1})$ and a small change $dx=\\dot x\\,dt$:\n", "\n", "\\begin{equation}\n", "x(t_i)=x(t_{i-1})+\\dot x(t_{i-1}) dt\n", "\\end{equation}\n", "\n", "This very simple integration scheme, known as **forward Euler integration**, works well if $dt$ is small and the ordinary differential equation is simple. It can run into issues when the ordinary differential equation is very noisy or when the dynamics include sudden big changes of $x$. Such big jumps can occur, for example, in models of excitable neurons. In such cases, one needs to choose an integration scheme carefully. However, for our simple system, the simple integration scheme should work just fine!" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Coding Exercise 1: Forward Euler Integration\n", "\n", "*Referred to as Exercise 1B in video*\n", "\n", "In this exercise, we will complete a function, ``integrate_exponential``, to compute the solution of the differential equation $\\dot{x} = a x$ using forward Euler integration. We will then plot this solution over time.\n", "\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "code", "execution": {} }, "outputs": [], "source": [ "def integrate_exponential(a, x0, dt, T):\n", " \"\"\"Compute solution of the differential equation xdot=a*x with\n", " initial condition x0 for a duration T. Use time step dt for numerical\n", " solution.\n", "\n", " Args:\n", " a (scalar): parameter of xdot (xdot=a*x)\n", " x0 (scalar): initial condition (x at time 0)\n", " dt (scalar): timestep of the simulation\n", " T (scalar): total duration of the simulation\n", "\n", " Returns:\n", " ndarray, ndarray: `x` for all simulation steps and the time `t` at each step\n", " \"\"\"\n", "\n", " # Initialize variables\n", " t = np.arange(0, T, dt)\n", " x = np.zeros_like(t, dtype=complex)\n", " x[0] = x0 # This is x at time t_0\n", "\n", " # Step through system and integrate in time\n", " for k in range(1, len(t)):\n", "\n", " ###################################################################\n", " ## Fill out the following then remove\n", " raise NotImplementedError(\"Student exercise: need to implement simulation\")\n", " ###################################################################\n", "\n", " # for each point in time, compute xdot from x[k-1]\n", " xdot = ...\n", "\n", " # Update x based on x[k-1] and xdot\n", " x[k] = ...\n", "\n", " return x, t\n", "\n", "\n", "# Choose parameters\n", "a = -0.5 # parameter in f(x)\n", "T = 10 # total Time duration\n", "dt = 0.001 # timestep of our simulation\n", "x0 = 1. # initial condition of x at time 0\n", "\n", "# Use Euler's method\n", "x, t = integrate_exponential(a, x0, dt, T)\n", "\n", "# Visualize\n", "plt.plot(t, x.real)\n", "plt.xlabel('Time (s)')\n", "plt.ylabel('x')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main/tutorials/W2D2_LinearSystems/solutions/W2D2_Tutorial1_Solution_acf4095a.py)\n", "\n", "*Example output:*\n", "\n", "Solution hint\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Forward_Euler_Integration_Exercise\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Interactive Demo 1: Forward Euler Integration\n", "\n", "1. What happens when you change $a$? Try values where $a<0$ and $a>0$.\n", "\n", "2. The $dt$ is the step size of the forward Euler integration. Try $a = -1.5$ and increase $dt$. What happens to the numerical solution when you increase $dt$?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Make sure you execute this cell to enable the widget!\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @markdown Make sure you execute this cell to enable the widget!\n", "\n", "T = 10 # total Time duration\n", "x0 = 1. # initial condition of x at time 0\n", "\n", "@widgets.interact(\n", " a=widgets.FloatSlider(value=-0.5, min=-2.5, max=1.5, step=0.25,\n", " description=\"α\", readout_format='.2f'),\n", " dt=widgets.FloatSlider(value=0.001, min=0.001, max=1.0, step=0.001,\n", " description=\"dt\", readout_format='.3f')\n", " )\n", "\n", "def plot_euler_integration(a, dt):\n", " # Have to do this clunky word around to show small values in slider accurately\n", " # (from https://github.com/jupyter-widgets/ipywidgets/issues/259)\n", "\n", " x, t = integrate_exponential(a, x0, dt, T)\n", " plt.plot(t, x.real) # integrate_exponential returns complex\n", " plt.xlabel('Time (s)')\n", " plt.ylabel('x')\n", " plt.show()" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main/tutorials/W2D2_LinearSystems/solutions/W2D2_Tutorial1_Solution_c65d3e49.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_What_models_Video\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Section 2: Oscillatory Dynamics\n", "\n", "*Estimated timing to here from start of tutorial: 20 min*\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Video 2: Oscillatory Solutions\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 2: Oscillatory Solutions\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', 'vPYQPI4nKT8'), ('Bilibili', 'BV1gZ4y1u7PK')]\n", "tab_contents = display_videos(video_ids, W=730, H=410)\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": {}, "source": [ "## Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Forward_Euler_Integration_Interactive_Demo_Discussion\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "We will now explore what happens when $a$ is a complex number and has a non-zero imaginary component." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Interactive Demo 2: Oscillatory Dynamics\n", "\n", "*Referred to as exercise 1B in video*\n", "\n", "In the following demo, you can change the real part and imaginary part of $a$ (so a = real + imaginary i)\n", "\n", "1. What values of $a$ produce dynamics that both ***oscillate*** and ***grow***?\n", "2. What value of $a$ is needed to produce a stable oscillation of 0.5 Hertz (cycles/time units)?\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Make sure you execute this cell to enable the widget!\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @markdown Make sure you execute this cell to enable the widget!\n", "\n", "# parameters\n", "T = 5 # total Time duration\n", "dt = 0.0001 # timestep of our simulation\n", "x0 = 1. # initial condition of x at time 0\n", "\n", "@widgets.interact\n", "def plot_euler_integration(real=(-2, 2, .2), imaginary=(-4, 7, .1)):\n", "\n", " a = complex(real, imaginary)\n", " x, t = integrate_exponential(a, x0, dt, T)\n", " plt.plot(t, x.real) # integrate exponential returns complex\n", " plt.grid(True)\n", " plt.xlabel('Time (s)')\n", " plt.ylabel('x')\n", " plt.show()" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main/tutorials/W2D2_LinearSystems/solutions/W2D2_Tutorial1_Solution_2888a5d5.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Oscillatory_Dynamics_Interactive_Demo_Discussion\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Section 3: Deterministic Linear Dynamics in Two Dimensions\n", "\n", "*Estimated timing to here from start of tutorial: 33 min*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Video 3: Multi-Dimensional Dynamics\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 3: Multi-Dimensional Dynamics\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', 'c_GdNS3YH_M'), ('Bilibili', 'BV1pf4y1R7uy')]\n", "tab_contents = display_videos(video_ids, W=730, H=410)\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": {}, "source": [ "## Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_MultiDimensional_Dynamics_Video\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "This video serves as an introduction to two-dimensional, deterministic dynamical systems written as a vector-matrix equation. It covers stream plots and how to connect phase portraits with the eigenvalues and eigenvectors of the transition matrix A.\n", "\n", "\n", "
\n", " Click here for text recap of relevant part of video \n", "\n", "Adding one additional variable (or _dimension_) adds more variety of behaviors. Additional variables are useful in modeling the dynamics of more complex systems with richer behaviors, such as systems of multiple neurons. We can write such a system using two linear ordinary differential equations:\n", "\n", "\\begin{eqnarray}\n", " \\dot{x}_1 &=& {a}_{11} x_1 \\\\\n", " \\dot{x}_2 &=& {a}_{22} x_2 \\\\\n", "\\end{eqnarray}\n", "\n", "So far, this system consists of two variables (e.g. neurons) in isolation. To make things interesting, we can add interaction terms:\n", "\n", "\\begin{eqnarray}\n", " \\dot{x}_1 &=& {a}_{11} x_1 + {a}_{12} x_2 \\\\\n", " \\dot{x}_2 &=& {a}_{21} x_1 + {a}_{22} x_2 \\\\\n", "\\end{eqnarray}\n", "\n", "We can write the two equations that describe our system as one (vector-valued) linear ordinary differential equation:\n", "\n", "$$\\dot{\\mathbf{x}} = \\mathbf{A} \\mathbf{x}$$\n", "\n", "For two-dimensional systems, $\\mathbf{x}$ is a vector with 2 elements ($x_1$ and $x_2$) and $\\mathbf{A}$ is a $2 \\times 2$ matrix with $\\mathbf{A}=\\bigg[\\begin{array} & a_{11} & a_{12} \\\\ a_{21} & a_{22} \\end{array} \\bigg]$.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Coding Exercise 3: Sample trajectories in 2 dimensions\n", "\n", "*Referred to in video as step 1 and 2 of exercise 1C*\n", "\n", "We want to simulate some **trajectories** of a given system and plot how 𝑥1 and 𝑥2 evolve in time. We will begin with this example system:\n", "\n", "\\begin{equation}\n", "\\dot{\\mathbf{x}} = \\bigg[\\begin{array} & 2 & -5 \\\\ 1 & -2 \\end{array} \\bigg] \\mathbf{x}\n", "\\end{equation}\n", "\n", "We will use an integrator from scipy, so we won't have to solve the system ourselves. We have a helper function, ``plot_trajectory``, that plots these trajectories given a system function. In this exercise, we will write the system function for a linear system with two variables." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "def system(t, x, a00, a01, a10, a11):\n", " '''\n", " Compute the derivative of the state x at time t for a linear\n", " differential equation with A matrix [[a00, a01], [a10, a11]].\n", "\n", " Args:\n", " t (float): time\n", " x (ndarray): state variable\n", " a00, a01, a10, a11 (float): parameters of the system\n", "\n", " Returns:\n", " ndarray: derivative xdot of state variable x at time t\n", " '''\n", " #################################################\n", " ## TODO for students: Compute xdot1 and xdot2 ##\n", " ## Fill out the following then remove\n", " raise NotImplementedError(\"Student exercise: say what they should have done\")\n", " #################################################\n", "\n", " # compute x1dot and x2dot\n", " x1dot = ...\n", " x2dot = ...\n", "\n", " return np.array([x1dot, x2dot])\n", "\n", "\n", "# Set parameters\n", "T = 6 # total time duration\n", "dt = 0.1 # timestep of our simulation\n", "A = np.array([[2, -5],\n", " [1, -2]])\n", "x0 = [-0.1, 0.2]\n", "\n", "# Simulate and plot trajectories\n", "plot_trajectory(system, [A[0, 0], A[0, 1], A[1, 0], A[1, 1]], x0, dt=dt, T=T)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main/tutorials/W2D2_LinearSystems/solutions/W2D2_Tutorial1_Solution_4de3f1d3.py)\n", "\n", "*Example output:*\n", "\n", "Solution hint\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Sample_trajectories_in_2_dimensions_Exercise\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Interactive Demo 3A: Varying A\n", "\n", "We will now use the function we created in the last exercise to plot trajectories with different options for A. What kinds of qualitatively different dynamics do you observe?\n", "\n", "**Hint:** Keep an eye on the x-axis and y-axis!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Make sure you execute this cell to enable the widget!\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @markdown Make sure you execute this cell to enable the widget!\n", "\n", "# parameters\n", "T = 6 # total Time duration\n", "dt = 0.1 # timestep of our simulation\n", "x0 = np.asarray([-0.1, 0.2]) # initial condition of x at time 0\n", "\n", "A_option_1 = [[2, -5],[1, -2]]\n", "A_option_2 = [[3,4], [1, 2]]\n", "A_option_3 = [[-1, -1], [0, -0.25]]\n", "A_option_4 = [[3, -2],[2, -2]]\n", "\n", "@widgets.interact\n", "def plot_euler_integration(A=widgets.Dropdown(options=[A_option_1,\n", " A_option_2,\n", " A_option_3,\n", " A_option_4,\n", " None],\n", " value=A_option_1)):\n", " if A:\n", " plot_trajectory(system, [A[0][0],A[0][1],A[1][0],A[1][1]], x0, dt=dt, T=T)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main/tutorials/W2D2_LinearSystems/solutions/W2D2_Tutorial1_Solution_7a6d90d7.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Varying_A_Interactive_Demo_Discussion\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Interactive Demo 3B: Varying Initial Conditions\n", "\n", "We will now vary the initial conditions for a given $\\mathbf{A}$:\n", "\n", "\\begin{equation}\n", "\\dot{\\mathbf{x}} = \\bigg[\\begin{array} & 2 & -5 \\\\ 1 & -2 \\end{array} \\bigg] \\mathbf{x}\n", "\\end{equation}\n", "\n", "What kinds of qualitatively different dynamics do you observe? Hint: Keep an eye on the x-axis and y-axis!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Make sure you execute this cell to enable the widget!\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @markdown Make sure you execute this cell to enable the widget!\n", "\n", "# parameters\n", "T = 6 # total Time duration\n", "dt = 0.1 # timestep of our simulation\n", "x0 = np.asarray([-0.1, 0.2]) # initial condition of x at time 0\n", "A = [[2, -5],[1, -2]]\n", "\n", "x0_option_1 = [-.1, 0.2]\n", "x0_option_2 = [10, 10]\n", "x0_option_3 = [-4, 3]\n", "\n", "@widgets.interact\n", "def plot_euler_integration(x0 = widgets.Dropdown(options=[x0_option_1,\n", " x0_option_2,\n", " x0_option_3,\n", " None],\n", " value=x0_option_1)):\n", " if x0:\n", " plot_trajectory(system, [A[0][0], A[0][1], A[1][0], A[1][1]], x0, dt=dt, T=T)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main/tutorials/W2D2_LinearSystems/solutions/W2D2_Tutorial1_Solution_e4e72f38.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Varying_Initial_Conditions_Interactive_Demo_Discussion\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Section 4: Stream Plots\n", "\n", "*Estimated timing to here from start of tutorial: 45 min*\n", "\n", "It's a bit tedious to plot trajectories one initial condition at a time!\n", "\n", "Fortunately, to get an overview of how a grid of initial conditions affect trajectories of a system, we can use a _stream plot_.\n", "\n", "We can think of a initial condition ${\\bf x}_0=(x_{1_0},x_{2_0})$ as coordinates for a position in a space. For a 2x2 matrix $\\bf A$, a stream plot computes at each position $\\bf x$ a small arrow that indicates $\\bf Ax$ and then connects the small arrows to form _stream lines_. Remember from the beginning of this tutorial that $\\dot {\\bf x} = \\bf Ax$ is the rate of change of $\\bf x$. So the stream lines indicate how a system changes. If you are interested in a particular initial condition ${\\bf x}_0$, just find the corresponding position in the stream plot. The stream line that goes through that point in the stream plot indicates ${\\bf x}(t)$." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Think! 4: Interpreting Eigenvalues and Eigenvectors\n", "\n", "Using some helper functions, we show the stream plots for each option of A that you examined in the earlier interactive demo. We included the eigenvectors of $\\bf A$ as a red line (1st eigenvalue) and a blue line (2nd eigenvalue) in the stream plots.\n", "\n", "What is special about the direction in which the principal eigenvector points? And how does the stability of the system relate to the corresponding eigenvalues? (Hint: Remember from your [introduction to linear algebra](https://www.youtube.com/watch?v=PFDu9oVAE-g&list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab&index=15&t=0s) that, for matrices with real eigenvalues, the eigenvectors indicate the lines on which $\\bf Ax$ is parallel to $\\bf x$ and real eigenvalues indicate the factor by which $\\bf Ax$ is stretched or shrunk compared to $\\bf x$.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Execute this cell to see stream plots\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @markdown Execute this cell to see stream plots\n", "\n", "A_option_1 = np.array([[2, -5], [1, -2]])\n", "A_option_2 = np.array([[3,4], [1, 2]])\n", "A_option_3 = np.array([[-1, -1], [0, -0.25]])\n", "A_option_4 = np.array([[3, -2], [2, -2]])\n", "\n", "A_options = [A_option_1, A_option_2, A_option_3, A_option_4]\n", "plot_specific_example_stream_plots(A_options)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main/tutorials/W2D2_LinearSystems/solutions/W2D2_Tutorial1_Solution_215cdc86.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Submit your feedback\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Interpreting_Eigenvalues_and_Eigenvectors_Discussion\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Summary\n", "\n", "*Estimated timing of tutorial: 1 hour*\n", "\n", "In this tutorial, we learned:\n", "\n", "* How to simulate the trajectory of a dynamical system specified by a differential equation $\\dot{x} = f(x)$ using a forward Euler integration scheme.\n", "* The behavior of a one-dimensional linear dynamical system $\\dot{x} = a x$ is determined by $a$, which may be a complex valued number. Knowing $a$, we know about the stability and oscillatory dynamics of the system.\n", "* The dynamics of high-dimensional linear dynamical systems $\\dot{\\mathbf{x}} = \\mathbf{A} \\mathbf{x}$ can be understood using the same intuitions, where we can summarize the behavior of the trajectories using the eigenvalues and eigenvectors of $\\mathbf{A}$." ] } ], "metadata": { "colab": { "collapsed_sections": [], "include_colab_link": true, "name": "W2D2_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 }