Citation info: Boeing, G. 2016. "Visual Analysis of Nonlinear Dynamical Systems: Chaos, Fractals, Self-Similarity and the Limits of Prediction." Systems, 4 (4), 37. doi:10.3390/systems4040037.
Pynamical documentation: http://pynamical.readthedocs.org
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.
In [1]:
import pynamical
from pynamical import simulate, phase_diagram_3d
import pandas as pd, numpy as np, matplotlib.pyplot as plt, random, glob, os, IPython.display as IPdisplay
from PIL import Image
%matplotlib inline
In [2]:
title_font = pynamical.get_title_font()
label_font = pynamical.get_label_font()
In [3]:
save_folder = 'images/phase-animate'
In [4]:
# set a filename, run the logistic model, and create the plot
gif_filename = '01-pan-rotate-zoom-demo'
working_folder = '{}/{}'.format(save_folder, gif_filename)
if not os.path.exists(working_folder):
os.makedirs(working_folder)
pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)
fig, ax = phase_diagram_3d(pops, remove_ticks=False, show=False, save=False)
# create 36 frames for the animated gif
steps = 36
# a viewing perspective is composed of an elevation, distance, and azimuth
# define the range of values we'll cycle through for the distance of the viewing perspective
min_dist = 7.
max_dist = 10.
dist_range = np.arange(min_dist, max_dist, (max_dist-min_dist)/steps)
# define the range of values we'll cycle through for the elevation of the viewing perspective
min_elev = 10.
max_elev = 60.
elev_range = np.arange(max_elev, min_elev, (min_elev-max_elev)/steps)
# now create the individual frames that will be combined later into the animation
for azimuth in range(0, 360, int(360/steps)):
# pan down, rotate around, and zoom out
ax.azim = float(azimuth/3.)
ax.elev = elev_range[int(azimuth/(360./steps))]
ax.dist = dist_range[int(azimuth/(360./steps))]
# set the figure title to the viewing perspective, and save each figure as a .png
fig.suptitle('elev={:.1f}, azim={:.1f}, dist={:.1f}'.format(ax.elev, ax.azim, ax.dist))
plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, azimuth))
# don't display the static plot...
plt.close()
# load all the static images into a list then save as an animated gif
gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)
images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]
gif = images[0]
gif.info['duration'] = 75 #milliseconds per frame
gif.info['loop'] = 0 #how many times to loop (0=infinite)
gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])
IPdisplay.Image(url=gif_filepath)
Out[4]:
In [5]:
# set a filename, run the logistic model, and create the plot
gif_filename = '02-pan-rotate-logistic-phase-diagram'
working_folder = '{}/{}'.format(save_folder, gif_filename)
if not os.path.exists(working_folder):
os.makedirs(working_folder)
pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)
fig, ax = phase_diagram_3d(pops, color='#003399', xlabel='Population (t)', ylabel='Population (t + 1)', zlabel='',
show=False, save=False)
# look straight down at the x-y plane to start off
ax.elev = 89.9
ax.azim = 270.1
ax.dist = 11.0
# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor
for n in range(0, 100):
if n > 19 and n < 23:
ax.set_xlabel('')
ax.set_ylabel('') #don't show axis labels while we move around, it looks weird
ax.elev = ax.elev-0.5 #start by panning down slowly
if n > 22 and n < 37:
ax.elev = ax.elev-1.0 #pan down faster
if n > 36 and n < 61:
ax.elev = ax.elev-1.5
ax.azim = ax.azim+1.1 #pan down faster and start to rotate
if n > 60 and n < 65:
ax.elev = ax.elev-1.0
ax.azim = ax.azim+1.1 #pan down slower and rotate same speed
if n > 64 and n < 74:
ax.elev = ax.elev-0.5
ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed
if n > 73 and n < 77:
ax.elev = ax.elev-0.2
ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position
if n > 76: #add axis labels at the end, when the plot isn't moving around
ax.set_xlabel('Population (t)')
ax.set_ylabel('Population (t + 1)')
ax.set_zlabel('Population (t + 2)')
# add a figure title to each plot then save the figure to the disk
fig.suptitle('Logistic Map, r=3.99', fontsize=16, x=0.5, y=0.85)
plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')
# don't display the static plot
plt.close()
# load all the static images into a list then save as an animated gif
gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)
images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]
gif = images[0]
gif.info['duration'] = 10 #milliseconds per frame
gif.info['loop'] = 0 #how many times to loop (0=infinite)
gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])
IPdisplay.Image(url=gif_filepath)
Out[5]:
In [6]:
# run the logistic model and create random noise
chaos_pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)
random_pops = pd.DataFrame([random.random() for _ in range(0, 1000)], columns=['value'])
pops = pd.concat([chaos_pops, random_pops], axis=1)
pops.columns = ['chaos', 'random']
pops.tail()
Out[6]:
In [7]:
# set a filename and then create the plot
gif_filename = '03-pan-rotate-logistic-random'
working_folder = '{}/{}'.format(save_folder, gif_filename)
if not os.path.exists(working_folder):
os.makedirs(working_folder)
fig, ax = phase_diagram_3d(pops, color=['#003399','#cc0000'], xlabel='Population (t)', ylabel='Population (t + 1)',
zlabel='', legend=True, legend_bbox_to_anchor=(0.94, 0.96), show=False, save=False)
# configure the initial viewing perspective to look straight down at the x-y plane
ax.elev = 89.9
ax.azim = 270.1
ax.dist = 11.0
# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor
for n in range(0, 100):
if n >= 20 and n <= 22:
ax.set_xlabel('')
ax.set_ylabel('') #don't show axis labels while we move around, it looks weird
ax.elev = ax.elev-0.5 #start by panning down slowly
if n >= 23 and n <= 36:
ax.elev = ax.elev-1.0 #pan down faster
if n >= 37 and n <= 60:
ax.elev = ax.elev-1.5
ax.azim = ax.azim+1.1 #pan down faster and start to rotate
if n >= 61 and n <= 64:
ax.elev = ax.elev-1.0
ax.azim = ax.azim+1.1 #pan down slower and rotate same speed
if n >= 65 and n <= 73:
ax.elev = ax.elev-0.5
ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed
if n >= 74 and n <= 76:
ax.elev = ax.elev-0.2
ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position
if n == 77: #add axis labels at the end, when the plot isn't moving around
ax.set_xlabel('Population (t)')
ax.set_ylabel('Population (t + 1)')
ax.set_zlabel('Population (t + 2)')
# add a figure title to each plot then save the figure to the disk
fig.suptitle(u'3-D phase diagram, chaos vs random', fontsize=16, x=0.5, y=0.85)
plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')
# don't display the static plot
plt.close()
# load all the static images into a list then save as an animated gif
gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)
images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]
gif = images[0]
gif.info['duration'] = 10 #milliseconds per frame
gif.info['loop'] = 0 #how many times to loop (0=infinite)
gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])
IPdisplay.Image(url=gif_filepath)
Out[7]:
In [8]:
# run the model for 2,000 generations for 50 growth rate parameters between 3.6 and 4.0
pops = simulate(num_gens=2000, rate_min=3.6, rate_max=4.0, num_rates=50)
In [9]:
# set a filename and create the plot
gif_filename = '04-pan-rotate-chaotic-regime'
working_folder = '{}/{}'.format(save_folder, gif_filename)
if not os.path.exists(working_folder):
os.makedirs(working_folder)
fig, ax = phase_diagram_3d(pops, color='viridis', color_reverse=False,
xlabel='Population (t)', ylabel='Population (t + 1)', zlabel='',
show=False, save=False)
# configure the initial viewing perspective to look straight down at the x-y plane
ax.elev = 89.9
ax.azim = 270.1
ax.dist = 11.0
# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor
for n in range(0, 100):
if n > 19 and n < 23:
ax.set_xlabel('')
ax.set_ylabel('') #don't show axis labels while we move around, it looks weird
ax.elev = ax.elev-0.5 #start by panning down slowly
if n > 22 and n < 37:
ax.elev = ax.elev-1.0 #pan down faster
if n > 36 and n < 61:
ax.elev = ax.elev-1.5
ax.azim = ax.azim+1.1 #pan down faster and start to rotate
if n > 60 and n < 65:
ax.elev = ax.elev-1.0
ax.azim = ax.azim+1.1 #pan down slower and rotate same speed
if n > 64 and n < 74:
ax.elev = ax.elev-0.5
ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed
if n > 73 and n < 77:
ax.elev = ax.elev-0.2
ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position
if n > 76: #add axis labels at the end, when the plot isn't moving around
ax.set_xlabel('Population (t)')
ax.set_ylabel('Population (t + 1)')
ax.set_zlabel('Population (t + 2)')
# add a figure title to each plot then save the figure to the disk
fig.suptitle('Logistic Map, r=3.6 to r=4.0', fontsize=16, x=0.5, y=0.85)
plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')
# don't display the static plot
plt.close()
# load all the static images into a list then save as an animated gif
gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)
images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]
gif = images[0]
gif.info['duration'] = 10 #milliseconds per frame
gif.info['loop'] = 0 #how many times to loop (0=infinite)
gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])
IPdisplay.Image(url=gif_filepath)
Out[9]:
In [10]:
# run the model for 4,000 generations for 50 growth rate parameters between 3.6 and 4.0
pops = simulate(num_gens=4000, rate_min=3.6, rate_max=4.0, num_rates=50)
In [11]:
# set a filename and create the plot
gif_filename = '05-logistic-3d-phase-diagram-chaotic-regime'
working_folder = '{}/{}'.format(save_folder, gif_filename)
if not os.path.exists(working_folder):
os.makedirs(working_folder)
fig, ax = phase_diagram_3d(pops, color='viridis', color_reverse=False, show=False, save=False)
# configure the initial viewing perspective
ax.elev = 25.
ax.azim = 321.
ax.dist = 11.0
# zoom in to reveal the 3-D structure of the strange attractor
for n in range(0, 100):
if n <= 18:
ax.azim = ax.azim-0.2 #begin by rotating very slowly
if n >= 19 and n <= 29:
ax.azim = ax.azim-10
ax.dist = ax.dist-0.05
ax.elev = ax.elev-2 #quickly whip around to the other side
if n >= 33 and n <= 49:
ax.azim = ax.azim+3
ax.dist = ax.dist-0.55
ax.elev = ax.elev+1.4 #zoom into the center
if n >= 61 and n <= 79:
ax.azim = ax.azim-2
ax.elev = ax.elev-2
ax.dist = ax.dist+0.2 #pull back and pan up
if n >= 80:
ax.azim = ax.azim-0.2 #end by rotating very slowly
# add a figure title to each plot then save the figure to the disk
fig.suptitle('Logistic Map, r=3.6 to r=4.0', fontsize=16, x=0.5, y=0.85)
plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')
# don't display the static plot
plt.close()
# load all the static images into a list then save as an animated gif
gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)
images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]
gif = images[0]
gif.info['duration'] = 10 #milliseconds per frame
gif.info['loop'] = 0 #how many times to loop (0=infinite)
gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])
IPdisplay.Image(url=gif_filepath)
Out[11]: