Skip to content

Analysis · 04

Poincaré sections

A Poincaré section reduces a continuous flow to the discrete sequence of its crossings through a hyperplane — periodic orbits become finite point sets, chaotic attractors become fractal dust. poincare_section accepts either a live system or an already-computed trajectory.

From a system (accurate)

import tsdynamics as ts

section = ts.poincare_section(ts.Rossler(), plane=(1, 0.0), steps=500)
section.t      # crossing times
section.y      # full-dimensional crossing states, shape (500, 3)

The system path marches the flow with a detection step dt, brackets each sign change of the plane function, and refines the crossing with cubic Hermite interpolation — endpoint derivatives come from the system's numeric right-hand side, giving \(O(\Delta t^4)\) accuracy in the crossing point. dt only needs to be small enough not to skip crossings; the refinement supplies the precision. (Systems without a numeric RHS, such as DDEs, fall back to linear interpolation, \(O(\Delta t^2)\).)

From trajectory data (no system needed)

traj = ts.Lorenz().integrate(final_time=500.0, dt=0.01)
section = ts.poincare_section(traj, plane=(2, 25.0))    # plane z = 25

The data path finds sign changes between consecutive samples and locates crossings by linear interpolation. It needs nothing but the arrays — useful for archived or experimental data — but its accuracy is limited by the trajectory's sampling interval. When you hold the system, prefer the system path.

Specifying the plane

Spec Meaning
(i, c) — int + float The axis-aligned section \(y_i = c\), e.g. (1, 0.0) for \(y = 0\)
(normal, offset) — vector + float The general section \(\mathbf{n} \cdot \mathbf{y} = \text{offset}\)

The direction argument filters crossings by orientation: +1 (default) keeps crossings where \(\mathbf{n} \cdot \mathbf{y}\) increases, -1 where it decreases, 0 keeps both. One-sided sections are usually what you want — two-sided sections superimpose the two halves of the attractor.

The PoincareMap wrapper

poincare_section(system, ...) is a convenience over the real machinery, the PoincareMap derived system:

from tsdynamics import PoincareMap

pmap = PoincareMap(ts.Rossler(), plane=(1, 0.0), direction=+1, dt=0.01)
u1 = pmap.step()              # advance the flow to the next crossing
sec = pmap.trajectory(500)    # collect 500 crossings
pmap.crossing_count           # bookkeeping

Because PoincareMap is a discrete System, it slots into anything written for maps — most importantly orbit diagrams, where it turns a parameter sweep of a flow into a bifurcation diagram. A RuntimeError is raised if no crossing occurs within max_time (the plane may miss the attractor, or direction is wrong).

See also