{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Анимация в matplotlib\n", "\n", "## Пример из руководства\n", "\n", "Коротко разберем, как можно осуществлять анимацию средствами matplotlib. Начнем с примера, приведенного в [руководстве](https://matplotlib.org/stable/api/animation_api.html).\n", "\n", "```python\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from matplotlib.animation import FuncAnimation\n", "\n", "fig, ax = plt.subplots()\n", "xdata, ydata = [], []\n", "ln, = plt.plot([], [], 'ro')\n", "\n", "def init():\n", " ax.set_xlim(0, 2*np.pi)\n", " ax.set_ylim(-1, 1)\n", " return ln,\n", "\n", "def update(frame):\n", " xdata.append(frame)\n", " ydata.append(np.sin(frame))\n", " ln.set_data(xdata, ydata)\n", " return ln,\n", "\n", "ani = FuncAnimation(fig, update, frames=np.linspace(0, 2*np.pi, 128),\n", " init_func=init, blit=True)\n", "plt.show()\n", "```\n", "\n", "Запуск скрипта с таким содержимым создаст окно, на котором постепенно построится график синуса. Есть как минимум два подхода заставить этот пример работать в `jupyter notebook`:\n", "\n", "a) применить команду `%matplotlib notebook`, т.е. сделать что-то такое\n", "```python\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from matplotlib.animation import FuncAnimation\n", "%matplotlib notebook\n", "```\n", "Эта команда встраивает окна, созданные `matplotlib`, в выход ячейки, сохраняя интерактивность.\n", "\n", "b) использовать специализированные средства `jupyter` ноутбуков для отображения анимации. Для этого надо заменить `plt.show()` на набор следующих команд\n", "```python\n", "from IPython.display import HTML, display\n", "display(HTML(animation.to_jshtml()))\n", "plt.close(fig)\n", "```\n", "Метод `to_jshtml` генерирует `html` код, соответствующий анимации, функция `HTML` из модуля `IPython.display` обрабатывает `html`, а функция `display` отображает его. Метод `plt.close` используется чтобы скрыть созданную фигуру, т.к. она отображается в `jupyter notebook` по умолчанию, несмотря на то, что метод `plt.show` не вызывается.\n", " \n", " \n", "Т.к. первый способ не отображается на этом ресурсе (приблизительно по той же причине, что графики `plotly`), то везде ниже будет использоваться второй способ." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from matplotlib.animation import FuncAnimation\n", "\n", "\n", "fig, ax = plt.subplots(figsize=(8, 6))\n", "xdata, ydata = [], []\n", "ln, = plt.plot([], [], 'ro')\n", "\n", "def init():\n", " ax.set_xlim(0, 2*np.pi)\n", " ax.set_ylim(-1, 1)\n", " return ln,\n", "\n", "def update(frame):\n", " xdata.append(frame)\n", " ydata.append(np.sin(frame))\n", " ln.set_data(xdata, ydata)\n", " return ln,\n", "\n", "animation = FuncAnimation(fig, update, frames=np.linspace(0, 2*np.pi, 128),\n", " init_func=init, blit=True)\n", "\n", "\n", "from IPython.display import HTML, display\n", "display(HTML(animation.to_jshtml()))\n", "plt.close(fig)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вернемся к разбору этого примера. \n", "\n", "Для создания анимации используется функция `FuncAnimation` из подмодуля `matplotlib.animation`, основными аргументами которой являются `fig`, `blit`, `func`, `init_func`, `frames`. \n", "\n", "`fig` --- фигура, которая будет использоваться для анимации. Функция `FuncAnimation` будет периодически обновлять содержимое этой фигуры, при этом она способна делать это экономно. Если передать в качестве параметра `blit` значение `True`, то содержимое фигуры перерисовывается не полностью, а обновляются лишь те компоненты (линия синуса в данном примере), которые изменяются между кадрами. За перерисовку кадра отвечает второй параметр `func` функции `FuncAnimation`, в качестве которого передается функция `update` в этом примере. Также есть опциональный параметр `init_func` (функция `init` в примере), который используется для подготовки фигуры перед началом анимации. \n", "\n", "Рассмотрим подробнее последние два параметра `func` и `init_func`. \n", "\n", "- Если `blit==False`, то действие функции `FuncAnimation` грубо можно описать в виде \n", "```python\n", "...\n", "init_func()\n", "for f in frames:\n", " func(f, *fargs)\n", " ...\n", "...\n", "\n", "```\n", "Т.е. вначале вызывается функция `init_func`, после чего функция `func` вызывается с каждым значением из аргумента `frames` (`*fargs` можно передать в функцию `FuncAnimation` в качестве дополнительного параметра).\n", "\n", "- Если `blit==True`, действие функции `FuncAnimation` грубо можно описать в виде \n", "```python\n", "...\n", "artists = init_func()\n", "for f in frames:\n", " artists = func(f, *fargs)\n", " update_blit(artists)\n", " ...\n", "...\n", "```\n", "В данном случае функции `init_func` и `func` должны возвращать `artists` --- список (или любой другой итерируемый объект) элементов на графике, которые необходимо обновить. \n", "\n", "```python\n", "fig, ax = plt.subplots(figsize=(8, 6))\n", "xdata, ydata = [], []\n", "ln, = plt.plot([], [], 'ro')\n", "\n", "def init():\n", " ax.set_xlim(0, 2*np.pi)\n", " ax.set_ylim(-1, 1)\n", " return ln,\n", "\n", "def update(frame):\n", " xdata.append(frame)\n", " ydata.append(np.sin(frame))\n", " ln.set_data(xdata, ydata)\n", " return ln,\n", "```\n", "\n", "В рассматриваемом примере функция `init` задаёт диапазоны для горизонтальной и вертикальной осей графика, а функция `update` принимает на вход значение `x`, обновляет списки `xdata` и `ydata`, а затем обновляет и содержимое нарисованной линии `ln` графика синуса. При этом обе этих функции возвращают объект линии `ln`, чтобы функция `FuncAnimation` перерисовала этот объект. \n", "\n", "Недостатком данного примера является тот факт, что функции `init` и `update` используют объекты из глобального пространства имен. Самый удобный способ избежать этого --- создать пользовательский класс, чтобы инкапсулировать всё необходимое в один объект. Ниже приведен пример того, как это можно осуществить." ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from matplotlib.animation import FuncAnimation\n", "\n", "class SinAnimation:\n", " def __init__(self):\n", " self.fig, self.ax = plt.subplots(figsize=(8, 6))\n", " self.xdata = []\n", " self.ydata = []\n", " self.ln, = plt.plot([], [], 'ro')\n", " \n", " def init_func(self):\n", " self.ax.set_xlim(0, 2*np.pi)\n", " self.ax.set_ylim(-1, 1)\n", " return self.ln,\n", " \n", " def update_func(self, frame):\n", " self.xdata.append(frame)\n", " self.ydata.append(np.sin(frame))\n", " self.ln.set_data(self.xdata, self.ydata)\n", " return self.ln,\n", "\n", "\n", "painter = SinAnimation()\n", " \n", "animation = FuncAnimation(\n", " painter.fig, \n", " painter.update_func, \n", " frames=np.linspace(0, 2*np.pi, 128),\n", " init_func=painter.init_func, blit=True)\n", "\n", "\n", "from IPython.display import HTML, display\n", "display(HTML(animation.to_jshtml()))\n", "plt.close(painter.fig)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Объектно ориентированное программирование в `python` будет обсуждаться позже, а пока лишь коротко отметим основные детали.\n", "\n", "- `class SinAnimation:` заголовок объявления пользовательского класса с названием `SinAnimation`. Далее следует объявление этого класса с отступом вправо;\n", "- далее объявлены методы этого класса. У всех из них обязательным первым аргументом идёт имя `self` --- ссылка на текущий объект (аналог `this` из `C++`);\n", "- метод `__init__` --- конструктор класса, который вызывается при создании экземпляра класса. Создание экземпляра осуществляется в строке `painter = SinAnimation()`;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Пример с математическим маятником\n", "\n", "Рассмотрим гармонические колебания [математического маятника](https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D0%BC%D0%B0%D1%8F%D1%82%D0%BD%D0%B8%D0%BA). Пусть $\\theta$ --- отклонение маятника (угол) от положения равновесия, $L$ --- длина подвеса. \n", "\n", "```{figure} /_static/lecture_specific/matplotlib_animation/pendulum.png\n", ":scale: 70%\n", "```\n", "\n", "Тогда в случае малых колебаний \n", "\n", "$$\n", "\\theta(t) = A\\sin(\\omega_0 t + \\alpha),\n", "$$\n", "где $A$ --- амплитуда колебаний, $\\alpha$ --- начальная фаза колебаний, а $\\omega_0$ --- собственная частота колебаний.\n", "\n", "Анимируем колебания такого маятника." ] }, { "cell_type": "code", "execution_count": 97, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from matplotlib.animation import FuncAnimation\n", "from scipy.constants import g\n", "\n", "\n", "# Параметры маятника\n", "L = 1 # длина (m)\n", "M = 1 # масса (kg)\n", "omega_0 = np.sqrt(g / L) # собственная частота\n", "T = 2 * np.pi / omega_0 # период колебаний\n", "A = np.pi / 4 # амплитуда\n", "alpha = -A # начальная фаза\n", "\n", "# параметры для отрисовки\n", "frames_per_period = 36 # количество кадров на один период\n", "delta = 0.1 # отступ\n", "N_periods = 4 # Количество периодов\n", "t_final = N_periods * T \n", "N_frames = frames_per_period * N_periods # итоговое количество кадров\n", "\n", "\n", "def angle(t):\n", " return A * np.sin(omega_0 * t + alpha)\n", "\n", "def x_and_y(theta):\n", " x = np.sin(theta)\n", " y = L - L * np.cos(theta)\n", " return x, y\n", "\n", "\n", "class PendulumAnimation:\n", " def __init__(self):\n", " self.t = np.linspace(0, t_final, N_frames)\n", " self.frames = np.arange(N_frames)\n", " self.theta = angle(self.t)\n", " self.x, self.y = x_and_y(self.theta)\n", " self.fig, self.ax = plt.subplots(figsize=(8, 8))\n", " self.delta = 0.05\n", " self.trace, = plt.plot([self.x[0]], [self.y[0]], 'bo-', lw=1, ms=2)\n", " self.l, = plt.plot([0], [1], 'bo-', lw=4, ms=15)\n", " \n", " \n", " def init(self):\n", " # limits\n", " x_min, x_max = self.x.min(), self.x.max()\n", " y_min, y_max = self.y.min(), self.y.max()\n", " self.ax.set_xlim([-L - delta, L + delta])\n", " self.ax.set_ylim([0 - delta, 2 * L + delta])\n", " self.ax.set_xlabel(\"x\")\n", " self.ax.set_ylabel(\"y\")\n", " return self.l, self.trace\n", " \n", " def update(self, frame):\n", " xs = [0, self.x[frame]]\n", " ys = [1, self.y[frame]]\n", " self.trace.set_data(self.x[:frame], self.y[:frame])\n", " self.l.set_data(xs, ys)\n", " return self.l, self.trace\n", " \n", "painter = PendulumAnimation()\n", "\n", "animation = FuncAnimation(\n", " painter.fig, \n", " painter.update, \n", " frames=painter.frames,\n", " init_func=painter.init, \n", " blit=False,\n", " interval=T / frames_per_period * 1000\n", ")\n", "\n", "\n", "from IPython.display import HTML, display\n", "display(HTML(animation.to_jshtml()))\n", "plt.close(painter.fig)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Функция `angle(t)` задаёт зависимость угла $\\theta$ от времени $t$, а функция `x_and_y(theta)` определяет положение маятника в декартовых координатах $xOy$ при заданном угле $\\theta$.\n", "\n", "Анимирование маятника осуществляется с помощью обычного графика с увеличенными маркерами (параметр `ms`, сокращение от `marker size`). Отрисовка в целом не сильно отличается от предыдущих примеров за исключением того, что необходимо корректно подобрать частоту кадров (`fps`) в анимации (или длину интервала между ними), т.к. визуализируется реальная физическая система. " ] } ], "metadata": { "interpreter": { "hash": "196ade6f477ee0922199b767e69f530090abd1652d0b07faa3fef0670dc28e2c" }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.8.10" } }, "nbformat": 4, "nbformat_minor": 2 }