From Smooth Chaos to Fractal Geometry#

mathematicskit.ode_dynamics and mathematicskit.fractals_chaos study two sides of the same coin: the first asks when a simple deterministic system starts behaving unpredictably, and the second asks how to quantify that unpredictability, and what geometric objects (fractals) it tends to leave behind.

The logistic map’s period-doubling cascade#

LogisticMap iterates \(x_{n+1} = rx_n(1-x_n)\). At \(r=3.2\), past the map’s first period-doubling bifurcation, the orbit settles into a stable 2-cycle rather than a single fixed point:

import numpy as np
from mathematicskit.ode_dynamics.systems.logistic_map import LogisticMap, estimate_feigenbaum_delta

m = LogisticMap(r=3.2)
orbit = m.iterate(x0=0.4, n_transient=500, n_keep=10)
print(np.round(orbit, 4))
# [0.7995 0.513  0.7995 0.513  0.7995 0.513  0.7995 0.513  0.7995 0.513 ]

As \(r\) increases further, the orbit doubles its period again and again, at parameter intervals that shrink by Feigenbaum’s universal ratio \(\delta \approx 4.6692\):

delta = estimate_feigenbaum_delta()
print(round(delta, 3))
# 4.745 -- using only the first three bifurcation points; converges to 4.6692 with more

Quantifying the chaos: the Lyapunov exponent#

Once \(r\) passes the accumulation point of that cascade (\(r_\infty \approx 3.56995\)), the map becomes chaotic. At \(r=4\) the logistic map is exactly conjugate to a tent map with a known Lyapunov exponent, \(\ln 2\), which mathematicskit.fractals_chaos.lyapunov_exponent_1d_map() recovers numerically:

from mathematicskit.fractals_chaos import lyapunov_exponent_1d_map

f = lambda x: 4.0 * x * (1.0 - x)
fprime = lambda x: 4.0 - 8.0 * x
lam = lyapunov_exponent_1d_map(f, fprime, x0=0.4, n_iterations=100000)
print(round(lam, 4))
# 0.6932 -- matches ln(2) = 0.6931 to 3 decimal places

A positive Lyapunov exponent is the quantitative signature of chaos: two nearby orbits separate exponentially fast, at exactly this average rate.

The fractal left behind: box-counting dimension#

Chaotic and self-similar dynamics both tend to leave behind sets with a non-integer dimension. The Sierpinski triangle – generated in mathematicskit.fractals_chaos via the “chaos game” rather than any literally chaotic map, but self-similar in exactly the same spirit – has box-counting dimension \(\log 3/\log 2 \approx 1.585\):

from mathematicskit.fractals_chaos import SierpinskiTriangle, box_counting_dimension

points = SierpinskiTriangle().generate(60000, seed=1)
result = box_counting_dimension(points)
print(round(result.dimension, 4))
# 1.551 -- close to log(3)/log(2) = 1.585, the known closed-form value

The estimate is a bit rough because box_counting_dimension() is measuring a genuinely finite point sample against an idealized infinite fractal; mathematicskit.ode_dynamics’s own bifurcation diagram, plotted at successively higher resolution near \(r_\infty\), is visually self-similar in exactly the same way, for exactly the same underlying reason.

See Also#