Search code examples
pythonmatplotlibslider

Reconstruct Veritasium's plots from one the videos


Inspired by one of Veritasium Youtube video where he explained chaotic bifurcation map (logistic map). the mathematical equation is simply: X[i+1]=R*X[i](1-X[i])

the first graph he plots: this X[i] values (y-axis values, in the range of 0 to 1) versus iteration time i (x-axis values, number of calculation) with a certain value of R (say, R_init=2, and I wanted to incorporate a Slider in my code for changing the value of R)

the second graph (the bifurcation graph) he plots: the value R (x-axis values) versus equilibrium population of X[i], that is: the number of X[i] values that it oscillates between (depending on the R value, after certain/many iteration i, X[i] can oscillate between finite numbers and also infinite numbers -- chaos!)

Eventually, in the code, I want two side by side subplot, left one is the "first graph" with the Slider for those two variables, right side is the "second graph". Below I pasted the my code for a starter, where I only try to achieve plotting "first graph" with a Slider for R and initial value of X[0] (but, of course, it failed to show anything so far..) I'd appreciate if anyone can help me finishing up this project, or giving me some advice on my problematic code.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider

R = 1.5
x = np.linspace(0, 100, 1)
k_init = 0.4       # initial y value

fig = plt.figure(figsize=(8, 8))
ax = plt.axes([0.125, 0.15, 0.775, 0.80])

myplot, = plt.plot(0, 0, c="royalblue")     # not really ploting anything first
plt.xlim(0, 100)
plt.ylim(0, 1)

# create slider panel & values sets
slider_r = plt.axes([0.125, 0.03, 0.775, 0.03])
slider_k = plt.axes([0.125, 0.07, 0.775, 0.03])
r_slider = Slider(slider_r, "R", 1, 20, valinit=R, valstep=0.1)
k_slider = Slider(slider_k, "k_init", 0, 1, valinit=k_init, valstep=0.05)

k = []     # y-values starts with List items
def update(*args):
    k.append(k_init)
    for i in np.arange(1,100,1):
        k_new = R*(k[i-1])*(1-(k[i-1]))    
        k.append(k_new)
    y = np.array(k)
    myplot.set_xdata(x)
    myplot.set_ydata(y)

r_slider.on_changed(update)
k_slider.on_changed(update)

update()
plt.show()

Solution

  • Short answer

    • replace x = np.linspace(0, 100, 1) by x = np.arange(100)
    • remove k = []
    • add k0, R = k_slider.val, r_slider.val at the beginning of update function
    • replace k.append(k_init) by k = [k0]

    Long answer

    1. Get a simulation that runs (without animation)

    First, let's try to obtain a working plot without sliders. Here are some tips and some things to fix:

    • x = np.linspace(0, 100, 1) only contains one element, while y will contain 100 values!
    • As the final size of y is already known, initialize it as an empty array, instead of adding items to the list k. This will make the code more readable and faster.
    • Use variables to store constant values such as the number of epochs.
    • In the description of the problem you use the variable x whereas in the implementation it is y ans k: Define notations that make sense, and stick to them.
    • It is rather confusing to define such similar names as r_slider and slider_r , especially since these two variables store completely different objects.
    ## define constants
    R = 2.6
    X0 = 0.5  # initial value
    N = 75  # number of epochs
    
    ## logistic map iterates
    def get_logistic_map(x0, R, N):
        x = np.empty(N)
        x[0] = x0
        for i in range(1, N):
            x[i] = R * x[i-1] * (1 - x[i-1])
        return x
    
    x = get_logistic_map(X0, R, N)
    
    ## plot
    plt.figure(figsize=(7, 4))
    plt.plot(x, 'o-', c="royalblue", ms=2)
    plt.ylim(0, 1)
    plt.margins(x=.01)
    plt.grid(c="lightgray")
    plt.xlabel(r"$n$")
    plt.ylabel(r"$x_n$")
    plt.show()
    

    2. Let's bring in interaction with sliders

    In function update:

    • You should update the data displayed using the new values of the sliders. Usually when a slider is updated, it gives its new value as an argument to the callback function (here update function). Since here two sliders are linked to the same update function, you cannot use this input parameter. However you can access these values using attribute val of each slider. (And since such callback functions require a parameter, you use an *args appropriately ;).
    • I'm not sure if it is intentional or not, but k is a global list. So each time update is called 100 new values are added.
    ## Prepare figure layout
    fig = plt.figure(figsize=(7, 5))
    ax = plt.axes([0.125, 0.15, 0.775, 0.80])
    line, = plt.plot(range(N), np.zeros(N), 'o-', c="royalblue", ms=2)
    # set the layout of the main axes before defining the axes of the sliders
    plt.ylim(0, 1)
    plt.xlim(0, N-1)
    
    ## Create sliders
    ax_slider_x0 = plt.axes([0.125, 0.03, 0.775, 0.03])
    ax_slider_r = plt.axes([0.125, 0.07, 0.775, 0.03])
    
    slider_x0 = Slider(ax_slider_x0, r"$x_0$", 0, 1, valinit=X0)
    slider_r = Slider(ax_slider_r, r"$R$", 0, 4, valinit=R)
    # plt.sca(ax) # uncomment to set the main axes as the current one
    
    def update(*args):
        x0_val, r_val = slider_x0.val, slider_r.val
        x = get_logistic_map(x0_val, r_val, N)
        line.set_ydata(x)
        # Set the title on the main axes (plt.title would have added a
        # title on the current axes (by default the last one to be defined)
        ax.set_title(rf"Logistic map with $R$={r_val:.3f} and $x_0={x0_val:.3f}$")
    
    slider_x0.on_changed(update)
    slider_r.on_changed(update)
    
    update() # initialize the plot
    plt.show()
    

    A personal tip – To debug a callback function with sliders, it is tempting to scatter some print. But as this turns out to be fruitless, you can instead display the debugging text inside the title of the plot!

    results

    3. What's next?

    Now the first plot is interactive. For the bifurcation graph, I don't see the interactivity that could be added.