{ "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "A Spirograph Animation with matplotlib and iPython" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*This notebook is inspired by the*\n", "[*blog post*](http://jakevdp.github.io/blog/2013/05/12/embedding-matplotlib-animations/)\n", "*on*\n", "[*Pythonic Perambulations*](http://jakevdp.github.io).\n", "\n", "*License:* [*BSD*](http://opensource.org/licenses/BSD-3-Clause)\n", "*(C) 2014, Kyle Cranmer.*\n", "*Feel free to use, distribute, and modify with the above attribution.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When teaching Physics I at NYU I gave a challenging problem to my students to use change of coordinates with rotations and translations to derive the parametric equation for a spirograph. They also had to solve for the velocity and acceleration. It was hard, but a lot of students liked it. In the process, I made some [pretty plots](http://www.flickr.com/photos/hoonynoo/2159185452/in/photostream/) and posted them to Flickr. When I saw Jake's awesome animation examples, I had to try it out with something novel." ] }, { "cell_type": "code", "collapsed": false, "input": [ "%pylab inline" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Populating the interactive namespace from numpy and matplotlib\n" ] } ], "prompt_number": 1 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we'll create a function that will save an animation and embed it in\n", "an html string. Note that this will require ffmpeg or mencoder to be\n", "installed on your system. For reasons entirely beyond my limited understanding\n", "of video encoding details, this also requires using the libx264 encoding\n", "for the resulting mp4 to be properly embedded into HTML5. " ] }, { "cell_type": "code", "collapsed": false, "input": [ "from tempfile import NamedTemporaryFile\n", "\n", "VIDEO_TAG = \"\"\"\"\"\"\n", "\n", "def anim_to_html(anim):\n", " if not hasattr(anim, '_encoded_video'):\n", " with NamedTemporaryFile(suffix='.mp4') as f:\n", " anim.save(f.name, fps=20, dpi=70,extra_args=['-vcodec', 'libx264', '-pix_fmt', 'yuv420p'])\n", " video = open(f.name, \"rb\").read()\n", " anim._encoded_video = video.encode(\"base64\")\n", " \n", " return VIDEO_TAG.format(anim._encoded_video)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "With this HTML function in place, we can use IPython's HTML display tools\n", "to create a function which will show the video inline:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from IPython.display import HTML\n", "\n", "def display_animation(anim):\n", " plt.close(anim._fig)\n", " return HTML(anim_to_html(anim))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now define a class for the spirograph." ] }, { "cell_type": "code", "collapsed": false, "input": [ "class spirograph():\n", " def __init__(self,a,b,f,noZ=False):\n", " self._a=a #number of teeth on small gear\n", " self._b = b #number of teeth on outer wheel\n", " self._rho = f*a #radius of pen from center of small wheel (in units of tooth spacing) [redundant with below]\n", " self._f = f #fraction of the inner wheel's radius for where the pen goes.\n", " self._noZ = noZ #a switch so that the z-component of the spirograph traces time.\n", " def inspect(self):\n", " print self._a, self._b, self._f\n", " def graph(self,t0):\n", " #if t0==0: self.inspect()\n", " a=self._a\n", " b=self._b\n", " rho=self._rho\n", " f=self._f\n", " lengthscale=5.*2*np.pi/b #scale the spirograph so outer ring is ~5 in graphing coordinates\n", " timescale=min(a,b)/gcd(a,b) #scale timing so that when t0=2\u03c0 the spirograph is a closed curve\n", " return (lengthscale*((b-a)*cos(timescale*t0)+rho*cos(-(1.*b/a -1.)*timescale*t0)),\n", " lengthscale*((b-a)*sin(timescale*t0)+rho*sin(-(1.*b/a -1.)*timescale*t0)),\n", " 0 if self._noZ else 5+5*t0 )" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 4 }, { "cell_type": "code", "collapsed": false, "input": [ "import numpy as np\n", "from numpy import sin, cos\n", "\n", "from matplotlib import pyplot as plt\n", "from mpl_toolkits.mplot3d import Axes3D\n", "from matplotlib.colors import cnames\n", "from matplotlib import animation\n", "from fractions import gcd\n", "\n", "# Solve for the trajectories (if t->2pi the spirograph will complete)\n", "t = np.linspace(0, 2.1*np.pi, 500)\n", "\n", "noZswitch=True\n", "myspiros = [spirograph(a=63,b=96,f=0.6,noZ=True),spirograph(a=63,b=96,f=0.8,noZ=True),\n", " spirograph(a=51,b=96,f=0.6,noZ=True),spirograph(a=51,b=96,f=0.8,noZ=True)]\n", "N_trajectories = len(myspiros)\n", "\n", "\n", "x_t = np.asarray([[myspiro.graph(t0) for t0 in t] for myspiro in myspiros])\n" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 5 }, { "cell_type": "code", "collapsed": false, "input": [ "#use the right sum http://stackoverflow.com/questions/17313853/unexpected-typeerror-with-ipython\n", "#maybe there's a better way?\n", "import __builtin__\n", "sum = __builtin__.sum" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 6 }, { "cell_type": "code", "collapsed": false, "input": [ "#3-d plotting with minimal modifications from Jake's lorentz system example\n", "# Set up figure & 3D axis for animation\n", "fig = plt.figure()\n", "ax = fig.add_axes([0, 0, 1, 1], projection='3d')\n", "ax.axis('off')\n", "\n", "# choose a different color for each trajectory\n", "colors = plt.cm.jet(np.linspace(0, 1, N_trajectories))\n", "for c in colors:\n", " print c\n", "\n", "# set up lines and points\n", "lines = sum([ax.plot([], [], [], '-', c=c)\n", " for c in colors], [])\n", "pts = sum([ax.plot([], [], [], 'o', c=c)\n", " for c in colors], [])\n", "\n", "# prepare the axes limits\n", "ax.set_xlim((-25, 25))\n", "ax.set_ylim((-35, 35))\n", "ax.set_zlim((5, 55))\n", "\n", "# set point-of-view: specified by (altitude degrees, azimuth degrees)\n", "ax.view_init(30, 0)\n", "\n", "# initialization function: plot the background of each frame\n", "def init():\n", " for line, pt in zip(lines, pts):\n", " line.set_data([], [])\n", " line.set_3d_properties([])\n", "\n", " pt.set_data([], [])\n", " pt.set_3d_properties([])\n", " return lines + pts\n", "\n", "# animation function. This will be called sequentially with the frame number\n", "def animate(i):\n", " # we'll step two time-steps per frame. This leads to nice results.\n", " i = (2 * i) % x_t.shape[1]\n", "\n", " for line, pt, xi in zip(lines, pts, x_t):\n", " x, y, z = xi[:i].T\n", " line.set_data(x, y)\n", " line.set_3d_properties(z)\n", "\n", " pt.set_data(x[-1:], y[-1:])\n", " pt.set_3d_properties(z[-1:])\n", "\n", " ax.view_init(90*cos(np.pi*i/500.), 0.3 * i)\n", " fig.canvas.draw()\n", " return lines + pts" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "[ 0. 0. 0.5 1. ]\n", "[ 0. 0.83333333 1. 1. ]\n", "[ 1. 0.90123457 0. 1. ]\n", "[ 0.5 0. 0. 1. ]\n" ] }, { "metadata": {}, "output_type": "display_data", "png": "iVBORw0KGgoAAAANSUhEUgAAAb4AAAEuCAYAAADx63eqAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAABIpJREFUeJzt1TEBACAMwDDAv+fhAo4mCvp1z8wsAIg4vwMA4CXjAyDF\n+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTj\nAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowP\ngBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4A\nUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABI\nMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF\n+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTj\nAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowP\ngBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4A\nUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABI\nMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF\n+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTj\nAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowP\ngBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4A\nUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABI\nMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF\n+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTj\nAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowP\ngBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4A\nUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABI\nMT4AUowPgBTjAyDF+ABIMT4AUowPgBTjAyDF+ABIMT4AUowPgJQL3LAGWFlkmdwAAAAASUVORK5C\nYII=\n", "text": [ "" ] } ], "prompt_number": 7 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now make the animation and display it" ] }, { "cell_type": "code", "collapsed": false, "input": [ "anim = animation.FuncAnimation(fig, animate, init_func=init,\n", " frames=500, interval=30,blit=True)\n", "display_animation(anim)\n" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "" ], "metadata": {}, "output_type": "pyout", "prompt_number": 8, "text": [ "" ] } ], "prompt_number": 8 }, { "cell_type": "markdown", "metadata": {}, "source": [ "*This post was created entirely in IPython notebook. Download the raw notebook*\n", "[*here*](https://github.com/cranmer/play/raw/master/SpyrographAnimation/Spirograph3d.ipynb), *or see a static view on*\n", "[*nbviewer*](http://nbviewer.ipython.org/url/github.com/cranmer/play/raw/master/SpyrographAnimation/Spirograph3d.ipynb).\n" ] }, { "cell_type": "code", "collapsed": false, "input": [ "#an alternate way to display the animation\n", "#animation.Animation._repr_html_ = anim_to_html\n", "#animation.FuncAnimation(fig, animate, init_func=init,\n", "# frames=100, interval=20, blit=True)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 9 } ], "metadata": {} } ] }