2023-10-26

Chaos: Bifurcation Diagrams

I'm currently reading "Chaos" by James Gleick and wanted to play with the ideas in code and recreate some of the images, specifically the bifurcation diagrams. These diagrams are used in the study of dynamical systems and help reveal complex system behaviors that emerge as a single control parameter varies.

To create these diagrams we iterate a function, meaning \(x_{n+1} = f(x_{n})\), and record the long-term pattern of behavior for different values of the control parameter. This means for each control parameter value we record a series of state values after iterating a bunch and letting the system reach a steady rhythm. When we plot all this data together we can see how the systems long-term behavior evolves throughout parameter space. By mapping out the entire parameter space, bifurcation diagrams illuminate the borderlines between order and chaos and the tipping points where a small tweak in the parameter can lead to a dramatic transformation in the system.

This type of sensitivity to initial conditions and parameter settings is a cornerstone of chaotic systems. In exploring these diagrams, we gain a deeper understanding of the underlying patterns inside many chaotic non-linear systems.

Bifurcation Diagrams in Python

from typing import List, Callable
import math

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

def logistic_map(x, r):
    return r * (x - x**2)

def sine_map(x, r):
    return r * math.sin(math.pi * x)

def tent_map(x, r):
    if x < 0.5:
        return r * x
    else:
        return r * (1 - x)

def iterate_f(
    f: Callable,
    n: int,
    x_init: float,
    control_param: float,
    store_vals: bool = True,
    **kwargs
):
    """
    Iterate given function and return either all intermediate vals
    or just the last val.
    """
    x = x_init
    vals = []
    for _ in range(n):
        x = f(x, control_param, **kwargs)
        if store_vals:
            vals.append(x)
    return vals if store_vals else x

def f_steady_vals(
    f: Callable,
    steady_n: int,
    transient_n: int,
    x_init: float,
    control_param: float,
    **kwargs
) -> List[float]:
    """
    Get 'steady_n' state values after iterating 'transient_n'
    steps for given function for some constant control param value.
    """
    x = iterate_f(f, transient_n, x_init, control_param, store_vals=False, **kwargs)
    return iterate_f(f, steady_n, x, control_param, store_vals=True, **kwargs)

def get_bifurcation_data(
    f: Callable,
    steady_n: int,
    transient_n: int,
    x_init: float,
    control_param_vals: List[float],
    **kwargs
) -> {str: List[float], str: List[float]}:
    cp_vals = []
    state_vals = []
    for cp in control_param_vals:
        steady_vals = f_steady_vals(f, steady_n, transient_n, x_init, cp, **kwargs)
        cp_vals += [cp] * steady_n
        state_vals += steady_vals

    return {"cp_vals": cp_vals, "state_vals": state_vals}

def get_bifurcation_fig(
    title: str,
    f: Callable,
    steady_n: int,
    transient_n: int,
    x_init: float,
    control_param_vals: List[float],
    **kwargs
):
    data = get_bifurcation_data(
        f, steady_n, transient_n, x_init, control_param_vals, **kwargs
    )
    xs = data["cp_vals"]
    ys = data["state_vals"]

    fig, ax = plt.subplots()
    ax.plot(xs, ys, ",k")
    ax.set_title(title)
    ax.set_xlabel("Control Parameter")
    ax.set_ylabel("State")

    return fig

Logistic Map

f = logistic_map
steady_n = 100
transient_n = 1000
x_init = 0.4
control_param_vals = np.linspace(-2, 4, 1000)
title = 'Logistic Map Bifurcation: $x_{n+1}=rx_{n}(1-x_{n})$'
fig = get_bifurcation_fig(title, f, steady_n, transient_n, x_init, control_param_vals)
logistic_map.png

Sine Map

f = sine_map
steady_n = 100
transient_n = 10000
x_init = 0.4
control_param_vals = np.linspace(-4, 4, 1000)
title = 'Sine Map Bifurcation: $x_{n+1}=r \sin(\pi x_{n})$'
fig = get_bifurcation_fig(title, f, steady_n, transient_n, x_init, control_param_vals)
sine_mape.png

Tent Map

f = tent_map
steady_n = 100
transient_n = 10000
x_init = 0.4
control_param_vals = np.linspace(-2, 2, 1000)
title = 'Tent Map Bifurcation: $x_{n+1}=r(1-2|x_{n}-0.5|)$'
fig = get_bifurcation_fig(title, f, steady_n, transient_n, x_init, control_param_vals)
tent_map.png