codex/sensitive-dependence.py

sensitive-dependence.py
"""Compare contraction and sensitive logistic evolution.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Writes sensitive-dependence.png to the caller's working directory.
Uses only the dependencies in the repository's pyproject.toml.
"""

from pathlib import Path

import matplotlib
import numpy as np

matplotlib.use("Agg")
import matplotlib.pyplot as plt


def orbit(r, initial, steps):
    values = np.empty(steps + 1)
    values[0] = initial
    for n in range(steps):
        values[n + 1] = r * values[n] * (1 - values[n])
    return values


def main():
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 12})
    fig, axes = plt.subplots(2, 2, figsize=(11, 6.5), layout="constrained", facecolor="white")
    steps = np.arange(56)
    initial = 0.2
    epsilon = 1e-8
    for col, r in enumerate((2.8, 4.0)):
        first = orbit(r, initial, 55)
        second = orbit(r, initial + epsilon, 55)
        upper, lower = axes[:, col]
        upper.plot(steps, first, color="#2166ac", lw=1.4, label=r"$x_0=0.2$")
        upper.plot(steps, second, color="#cf651a", lw=1.2, ls="--",
                   label=r"$y_0=0.2+10^{-8}$")
        upper.set(xlim=(0, 55), ylim=(0, 1), xlabel="Iteration n", ylabel="State",
                  title=f"r = {r:g}: " + ("nearby orbits converge" if col == 0 else "nearby orbits separate"))
        upper.legend(loc="lower left", fontsize=10, framealpha=0.95)
        separation = np.abs(first - second)
        # Mask exact zeros rather than assigning them a fictitious log distance.
        lower.semilogy(steps, np.where(separation > 0, separation, np.nan),
                       ".-", ms=4, color="#2166ac", lw=1.4, label="Computed separation")
        if col == 0:
            lower.semilogy(steps, epsilon * 0.8 ** steps, ls="--", color="#666666", lw=1,
                           label=r"Reference slope: $(0.8)^n$")
        else:
            growth_steps = steps[steps <= 26]
            lower.semilogy(growth_steps, epsilon * 2.0 ** growth_steps,
                           ls="--", color="#666666", lw=1,
                           label=r"Reference slope: $2^n$")
            lower.axhline(0.1, color="#9e2929", lw=0.9, ls=":")
            lower.text(54, 0.13, "separation = 0.1", color="#9e2929", ha="right", fontsize=9)
        lower.set(xlim=(0, 55), ylim=(1e-15, 2), xlabel="Iteration n",
                  ylabel=r"Separation $|x_n-y_n|$",
                  title="Log scale reveals shrinking errors" if col == 0 else "Growth fluctuates, then is bounded by 1")
        lower.legend(loc="lower left", fontsize=9, framealpha=0.95)
        for ax in (upper, lower):
            ax.set_facecolor("white")
            ax.grid(alpha=0.18)
    fig.savefig(Path.cwd() / "sensitive-dependence.png", dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()