{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Pynamical: animated 3D phase diagrams of the logistic map\n", "\n", "**Citation info**: Boeing, G. 2016. \"[Visual Analysis of Nonlinear Dynamical Systems: Chaos, Fractals, Self-Similarity and the Limits of Prediction](http://geoffboeing.com/publications/nonlinear-chaos-fractals-prediction/).\" *Systems*, 4 (4), 37. doi:10.3390/systems4040037.\n", "\n", "Pynamical documentation: http://pynamical.readthedocs.org\n", "\n", "This notebook demonstrates how to make animated GIFs that pan and zoom around 3-D phase diagrams to visualize fractal data sets, strange attractors, and chaos." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import glob\n", "import IPython.display as IPdisplay\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import os\n", "import pandas as pd\n", "import pynamical\n", "import random \n", "from PIL import Image\n", "from pynamical import simulate, phase_diagram_3d\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "title_font = pynamical.get_title_font()\n", "label_font = pynamical.get_label_font()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "save_folder = 'images/phase-animate'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a 3-D phase diagram as an animated gif that pans, rotates, and zooms. This demonstrates how the viewing perspective is composed of an elevation, a distance, and an azimuth." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# set a filename, run the logistic model, and create the plot\n", "gif_filename = '01-pan-rotate-zoom-demo'\n", "working_folder = '{}/{}'.format(save_folder, gif_filename)\n", "if not os.path.exists(working_folder):\n", " os.makedirs(working_folder)\n", " \n", "pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)\n", "fig, ax = phase_diagram_3d(pops, remove_ticks=False, show=False, save=False)\n", "\n", "# create 36 frames for the animated gif\n", "steps = 36\n", "\n", "# a viewing perspective is composed of an elevation, distance, and azimuth\n", "# define the range of values we'll cycle through for the distance of the viewing perspective\n", "min_dist = 7.\n", "max_dist = 10.\n", "dist_range = np.arange(min_dist, max_dist, (max_dist-min_dist)/steps)\n", "\n", "# define the range of values we'll cycle through for the elevation of the viewing perspective\n", "min_elev = 10.\n", "max_elev = 60.\n", "elev_range = np.arange(max_elev, min_elev, (min_elev-max_elev)/steps)\n", "\n", "# now create the individual frames that will be combined later into the animation\n", "for azimuth in range(0, 360, int(360/steps)):\n", " \n", " # pan down, rotate around, and zoom out\n", " ax.azim = float(azimuth/3.)\n", " ax.elev = elev_range[int(azimuth/(360./steps))]\n", " ax.dist = dist_range[int(azimuth/(360./steps))]\n", " \n", " # set the figure title to the viewing perspective, and save each figure as a .png\n", " fig.suptitle('elev={:.1f}, azim={:.1f}, dist={:.1f}'.format(ax.elev, ax.azim, ax.dist))\n", " plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, azimuth))\n", " \n", "# don't display the static plot...\n", "plt.close()\n", "\n", "# load all the static images into a list then save as an animated gif\n", "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n", "images = [Image.open(image) for image in sorted(glob.glob('{}/*.png'.format(working_folder)))]\n", "gif = images[0]\n", "gif.info['duration'] = 75 #milliseconds per frame\n", "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n", "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n", "IPdisplay.Image(url=gif_filepath)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a 3-D phase diagram as an animated gif starts by looking straight down at the x-y plane (this is what a 2-D plot would look like), then panning and rotating around to show the 3-D structure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# set a filename, run the logistic model, and create the plot\n", "gif_filename = '02-pan-rotate-logistic-phase-diagram'\n", "working_folder = '{}/{}'.format(save_folder, gif_filename)\n", "if not os.path.exists(working_folder):\n", " os.makedirs(working_folder)\n", " \n", "pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)\n", "fig, ax = phase_diagram_3d(pops, color='#003399', xlabel='Population (t)', ylabel='Population (t + 1)', zlabel='', \n", " show=False, save=False)\n", "\n", "# look straight down at the x-y plane to start off\n", "ax.elev = 89.9\n", "ax.azim = 270.1\n", "ax.dist = 11.0\n", "\n", "# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor\n", "for n in range(0, 100):\n", " if n > 19 and n < 23:\n", " ax.set_xlabel('')\n", " ax.set_ylabel('') #don't show axis labels while we move around, it looks weird\n", " ax.elev = ax.elev-0.5 #start by panning down slowly\n", " if n > 22 and n < 37:\n", " ax.elev = ax.elev-1.0 #pan down faster\n", " if n > 36 and n < 61:\n", " ax.elev = ax.elev-1.5\n", " ax.azim = ax.azim+1.1 #pan down faster and start to rotate\n", " if n > 60 and n < 65:\n", " ax.elev = ax.elev-1.0\n", " ax.azim = ax.azim+1.1 #pan down slower and rotate same speed\n", " if n > 64 and n < 74:\n", " ax.elev = ax.elev-0.5\n", " ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed\n", " if n > 73 and n < 77:\n", " ax.elev = ax.elev-0.2\n", " ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position \n", " if n > 76: #add axis labels at the end, when the plot isn't moving around\n", " ax.set_xlabel('Population (t)')\n", " ax.set_ylabel('Population (t + 1)')\n", " ax.set_zlabel('Population (t + 2)')\n", " \n", " # add a figure title to each plot then save the figure to the disk\n", " fig.suptitle('Logistic Map, r=3.99', fontsize=16, x=0.5, y=0.85)\n", " plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')\n", "\n", "# don't display the static plot\n", "plt.close()\n", "\n", "# load all the static images into a list then save as an animated gif\n", "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n", "images = [Image.open(image) for image in sorted(glob.glob('{}/*.png'.format(working_folder)))]\n", "gif = images[0]\n", "gif.info['duration'] = 10 #milliseconds per frame\n", "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n", "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n", "IPdisplay.Image(url=gif_filepath)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Do the same thing again, but this time plot both the chaotic logistic model output and random noise" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# run the logistic model and create random noise\n", "chaos_pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)\n", "random_pops = pd.DataFrame([random.random() for _ in range(0, 1000)], columns=['value'])\n", "pops = pd.concat([chaos_pops, random_pops], axis=1)\n", "pops.columns = ['chaos', 'random']\n", "pops.tail()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# set a filename and then create the plot\n", "gif_filename = '03-pan-rotate-logistic-random'\n", "working_folder = '{}/{}'.format(save_folder, gif_filename)\n", "if not os.path.exists(working_folder):\n", " os.makedirs(working_folder)\n", " \n", "fig, ax = phase_diagram_3d(pops, color=['#003399','#cc0000'], xlabel='Population (t)', ylabel='Population (t + 1)', \n", " zlabel='', legend=True, legend_bbox_to_anchor=(0.94, 0.96), show=False, save=False)\n", "\n", "# configure the initial viewing perspective to look straight down at the x-y plane\n", "ax.elev = 89.9\n", "ax.azim = 270.1\n", "ax.dist = 11.0\n", "\n", "# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor\n", "for n in range(0, 100):\n", " if n >= 20 and n <= 22:\n", " ax.set_xlabel('')\n", " ax.set_ylabel('') #don't show axis labels while we move around, it looks weird\n", " ax.elev = ax.elev-0.5 #start by panning down slowly\n", " if n >= 23 and n <= 36:\n", " ax.elev = ax.elev-1.0 #pan down faster\n", " if n >= 37 and n <= 60:\n", " ax.elev = ax.elev-1.5\n", " ax.azim = ax.azim+1.1 #pan down faster and start to rotate\n", " if n >= 61 and n <= 64:\n", " ax.elev = ax.elev-1.0\n", " ax.azim = ax.azim+1.1 #pan down slower and rotate same speed\n", " if n >= 65 and n <= 73:\n", " ax.elev = ax.elev-0.5\n", " ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed\n", " if n >= 74 and n <= 76:\n", " ax.elev = ax.elev-0.2\n", " ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position \n", " if n == 77: #add axis labels at the end, when the plot isn't moving around\n", " ax.set_xlabel('Population (t)')\n", " ax.set_ylabel('Population (t + 1)')\n", " ax.set_zlabel('Population (t + 2)')\n", " \n", " # add a figure title to each plot then save the figure to the disk\n", " fig.suptitle(u'3-D phase diagram, chaos vs random', fontsize=16, x=0.5, y=0.85)\n", " plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')\n", "\n", "# don't display the static plot\n", "plt.close()\n", "\n", "# load all the static images into a list then save as an animated gif\n", "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n", "images = [Image.open(image) for image in sorted(glob.glob('{}/*.png'.format(working_folder)))]\n", "gif = images[0]\n", "gif.info['duration'] = 10 #milliseconds per frame\n", "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n", "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n", "IPdisplay.Image(url=gif_filepath)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a 3-D phase diagram to show the logistic map's strange attractors across the chaotic regime (from r=3.6 to r=4.0), twisting and curling around their state space in three dimensions. Animated it by panning and rotating to reveal the structure and its odd folds." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# run the model for 2,000 generations for 50 growth rate parameters between 3.6 and 4.0\n", "pops = simulate(num_gens=2000, rate_min=3.6, rate_max=4.0, num_rates=50)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# set a filename and create the plot\n", "gif_filename = '04-pan-rotate-chaotic-regime'\n", "working_folder = '{}/{}'.format(save_folder, gif_filename)\n", "if not os.path.exists(working_folder):\n", " os.makedirs(working_folder)\n", " \n", "fig, ax = phase_diagram_3d(pops, color='viridis', color_reverse=False,\n", " xlabel='Population (t)', ylabel='Population (t + 1)', zlabel='', \n", " show=False, save=False)\n", "\n", "# configure the initial viewing perspective to look straight down at the x-y plane\n", "ax.elev = 89.9\n", "ax.azim = 270.1\n", "ax.dist = 11.0\n", "\n", "# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor\n", "for n in range(0, 100):\n", " if n > 19 and n < 23:\n", " ax.set_xlabel('')\n", " ax.set_ylabel('') #don't show axis labels while we move around, it looks weird\n", " ax.elev = ax.elev-0.5 #start by panning down slowly\n", " if n > 22 and n < 37:\n", " ax.elev = ax.elev-1.0 #pan down faster\n", " if n > 36 and n < 61:\n", " ax.elev = ax.elev-1.5\n", " ax.azim = ax.azim+1.1 #pan down faster and start to rotate\n", " if n > 60 and n < 65:\n", " ax.elev = ax.elev-1.0\n", " ax.azim = ax.azim+1.1 #pan down slower and rotate same speed\n", " if n > 64 and n < 74:\n", " ax.elev = ax.elev-0.5\n", " ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed\n", " if n > 73 and n < 77:\n", " ax.elev = ax.elev-0.2\n", " ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position\n", " \n", " if n > 76: #add axis labels at the end, when the plot isn't moving around\n", " ax.set_xlabel('Population (t)')\n", " ax.set_ylabel('Population (t + 1)')\n", " ax.set_zlabel('Population (t + 2)')\n", " \n", " # add a figure title to each plot then save the figure to the disk\n", " fig.suptitle('Logistic Map, r=3.6 to r=4.0', fontsize=16, x=0.5, y=0.85)\n", " plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')\n", "\n", "# don't display the static plot\n", "plt.close()\n", "\n", "# load all the static images into a list then save as an animated gif\n", "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n", "images = [Image.open(image) for image in sorted(glob.glob('{}/*.png'.format(working_folder)))]\n", "gif = images[0]\n", "gif.info['duration'] = 10 #milliseconds per frame\n", "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n", "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n", "IPdisplay.Image(url=gif_filepath)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Now zoom into the 3D plot" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# run the model for 4,000 generations for 50 growth rate parameters between 3.6 and 4.0\n", "pops = simulate(num_gens=4000, rate_min=3.6, rate_max=4.0, num_rates=50)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "# set a filename and create the plot\n", "gif_filename = '05-logistic-3d-phase-diagram-chaotic-regime'\n", "working_folder = '{}/{}'.format(save_folder, gif_filename)\n", "if not os.path.exists(working_folder):\n", " os.makedirs(working_folder)\n", " \n", "fig, ax = phase_diagram_3d(pops, color='viridis', color_reverse=False, show=False, save=False)\n", "\n", "# configure the initial viewing perspective\n", "ax.elev = 25.\n", "ax.azim = 321.\n", "ax.dist = 11.0\n", "\n", "# zoom in to reveal the 3-D structure of the strange attractor\n", "for n in range(0, 100):\n", " if n <= 18:\n", " ax.azim = ax.azim-0.2 #begin by rotating very slowly\n", " if n >= 19 and n <= 29:\n", " ax.azim = ax.azim-10\n", " ax.dist = ax.dist-0.05\n", " ax.elev = ax.elev-2 #quickly whip around to the other side\n", " if n >= 33 and n <= 49:\n", " ax.azim = ax.azim+3\n", " ax.dist = ax.dist-0.55\n", " ax.elev = ax.elev+1.4 #zoom into the center\n", " if n >= 61 and n <= 79:\n", " ax.azim = ax.azim-2\n", " ax.elev = ax.elev-2\n", " ax.dist = ax.dist+0.2 #pull back and pan up\n", " if n >= 80:\n", " ax.azim = ax.azim-0.2 #end by rotating very slowly\n", " \n", " # add a figure title to each plot then save the figure to the disk\n", " fig.suptitle('Logistic Map, r=3.6 to r=4.0', fontsize=16, x=0.5, y=0.85)\n", " plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')\n", "\n", "# don't display the static plot\n", "plt.close()\n", "\n", "# load all the static images into a list then save as an animated gif\n", "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n", "images = [Image.open(image) for image in sorted(glob.glob('{}/*.png'.format(working_folder)))]\n", "gif = images[0]\n", "gif.info['duration'] = 10 #milliseconds per frame\n", "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n", "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n", "IPdisplay.Image(url=gif_filepath)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## For more info:\n", " - [Read the journal article](http://geoffboeing.com/publications/nonlinear-chaos-fractals-prediction/)\n", " - [Pynamical documentation](http://pynamical.readthedocs.org)\n", " - [Chaos Theory and the Logistic Map](http://geoffboeing.com/2015/03/chaos-theory-logistic-map/)\n", " - [Visualizing Chaos and Randomness with Phase Diagrams](http://geoffboeing.com/2015/04/visualizing-chaos-and-randomness/)\n", " - [Animated 3D Plots in Python](http://geoffboeing.com/2015/04/animated-3d-plots-python/)" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python (pynamical)", "language": "python", "name": "pynamical" }, "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.10.4" } }, "nbformat": 4, "nbformat_minor": 4 }