Analysis · 03
Orbit & bifurcation diagrams¶
orbit_diagram sweeps one parameter and records the asymptotic orbit at
each value — the classic picture of period-doubling cascades and chaotic
bands.
Maps: the logistic diagram¶
import numpy as np
import tsdynamics as ts
od = ts.orbit_diagram(
ts.Logistic(), "r", np.linspace(2.5, 4.0, 600),
n=120, # points recorded per parameter value
transient=500, # steps discarded first, at every value
)
x, y = od.flat() # scatter-ready arrays
# plt.plot(x, y, ",k", markersize=0.5)
The result is an OrbitDiagram: iterate it for (value, points) pairs,
or call flat() for plotting. Counting distinct branches at a value shows
the period directly:
for r, pts in od:
n_branches = len(np.unique(np.round(pts[:, 0], 6)))
# r < 3 → 1 branch (fixed point)
# 3 < r < 3.449 → 2 branches (period 2)
# ...period-doubling cascade → chaos near r ≈ 3.57
carry_state — following the attractor¶
By default (carry_state=True) each parameter value starts from the
previous value's final state. This follows the attractor branch
continuously and produces clean diagrams without re-converging through a
transient basin at every step. Set carry_state=False to restart every
value from the same ic — useful when probing coexisting attractors,
where following a branch would hide the others.
The swept system is never mutated: each value gets a fresh
with_params copy.
Flows: the composition¶
orbit_diagram requires a discrete-time view — and that is exactly what
the derived wrappers
provide. Wrap a flow in a PoincareMap and the orbit diagram is the
bifurcation diagram of the flow:
from tsdynamics import PoincareMap
od = ts.orbit_diagram(
PoincareMap(ts.Rossler(), (1, 0.0)), # section y = 0
"c", np.linspace(2.0, 6.0, 80),
n=60, transient=30,
)
x, c_vals = od.flat() # x-coordinate of section crossings vs. c
Each "iteration" is one section crossing; with_params re-parametrizes
the inner Rössler system and rebuilds the wrapper, so the sweep
composes transparently.
For periodically forced oscillators, sample once per forcing period with a
StroboscopicMap instead:
from tsdynamics import StroboscopicMap
duf = ts.Duffing() # forcing frequency omega=1.4
od = ts.orbit_diagram(
StroboscopicMap(duf, period=2 * np.pi / 1.4),
"gamma", np.linspace(0.2, 0.65, 150),
n=40, transient=60,
)
Cost notes¶
- Maps — each value costs one quick Numba re-JIT plus the iterations; sweeps of hundreds of values are routine.
- ODE-backed wrappers — parameter changes reuse the compiled module (control parameters), so the per-value cost is just the integration.
- DDE-backed sweeps — each parameter value compiles a fresh module (DDE structure depends on all parameters). Budget accordingly, or sweep coarsely first.
See also¶
- Poincaré sections — the section machinery behind
PoincareMap - Reference · Analysis —
orbit_diagram/OrbitDiagramsignatures