Three Visualizations of Chaos

In this blog post, we explore some computationally simple systems with complex behaviours that I came across while reading James Gleick's excellent book Chaos: Making a New Science.

The logistic map: a simple population model

The first system we'll compute is the logistic map. As described in the book, this system comes from a 1976 paper by the biologist Robert May and can be described as a discrete-time model for how a population grows.

Given a population $x_n$, you can compute its next state $x_{n+1}$ with the following function:

In [1]:
def logistic_iter(x, r):
    "One iteration of the logistic map."
    return r * x - r * x**2

The above expression is made of two parts: $r x$ is analogous to a growth term as a function of previous population while $-r x^2$ can be described as a death rate when the population gets high. Critically, both are multiplied by $r$, which is a parameter of this model.

Let's build some understanding of this simple system by iterating it for 50 times, with a given start population between 0 and 1.

In [2]:
%matplotlib inline
from ipywidgets import interact, interactive, fixed
from matplotlib import pyplot as plt
import numpy as np
In [3]:
@interact
def plot_logistic_iterated(x0=(0.1, 1, 0.01), r=(0, 4, 0.01)):
    "Plots 50 iterations of a population, given the r parameter."
    vals = [x0]
    for _ in range(50):
        vals.append(logistic_iter(vals[-1], r))
    plt.plot(vals)

Interestingly, different things can happen. For low $r$ values, the population will just decrease:

In [4]:
plot_logistic_iterated(x0=0.4, r=0.3)

For intermediate values, below 4, the population will reach equilibrium.

In [5]:
plot_logistic_iterated(x0=0.4, r=2.3)

And then for values closer but below 4, strange things happen:

In [6]:
plt.subplot(2, 2, 1)
plot_logistic_iterated(x0=0.4, r=3.5)
plt.subplot(2, 2, 2)
plot_logistic_iterated(x0=0.4, r=3.6)
plt.subplot(2, 2, 3)
plot_logistic_iterated(x0=0.4, r=3.7)
plt.subplot(2, 2, 4)
plot_logistic_iterated(x0=0.4, r=3.8)

We can visualize this in another way, by showing the iterated values on a two dimensional graph:

In [7]:
@interact
def plot_logistic_iterated_2D(x0=(0.1, 1, 0.01), r=(0.01, 4, 0.01), ax=fixed(None)):
    "Plots logistic map for 50 iterations on 2D plot."
    # computes iterated values
    vals = [x0]
    for _ in range(50):
        vals.append(logistic_iter(vals[-1], r))
    # plottingf
    if ax is None:
        fig, ax = plt.subplots()

    for x_start, x_end in zip(vals[:-1], vals[1:]):
        ax.plot([x_start, x_start, x_end], [x_start, x_end, x_end], '-ko')
    xx = np.linspace(0, 1)
    ax.plot(xx, logistic_iter(xx, r))
    ax.plot(xx, xx)

We can display what we observed before in this visualization:

In [8]:
plt.figure(figsize=(10, 8))
ax = plt.subplot(2, 2, 1)
plot_logistic_iterated_2D(0.3, 1, ax=ax) # decrease to 0
ax = plt.subplot(2, 2, 2)
plot_logistic_iterated_2D(0.9, 2, ax=ax) # stable
ax = plt.subplot(2, 2, 3)
plot_logistic_iterated_2D(0.9, 3, ax=ax) # 2 cycle
ax = plt.subplot(2, 2, 4)
plot_logistic_iterated_2D(0.9, 3.9, ax=ax) # ? cycles

Finally, let's make an animation of this.

In [9]:
import matplotlib.pyplot as plt
import numpy as np
from moviepy.editor import VideoClip
from moviepy.video.io.bindings import mplfig_to_npimage

x = np.linspace(-2, 2, 200)

duration = 10

fig, ax = plt.subplots(dpi=100)
def make_frame(t):
    r = t / duration * 3.99
    ax.clear()
    plot_logistic_iterated_2D(0.3, r, ax=ax)
    return mplfig_to_npimage(fig)

animation = VideoClip(make_frame, duration=duration)
plt.close(fig)
animation.ipython_display(fps=25, loop=True, autoplay=True)
100%|███████████████████████████████████████▊| 250/251 [00:43<00:00,  5.71it/s]
Out[9]: