Skip to content

Visualization

The gallery

This page is the answer to "what plots can this library actually draw?" — and it is generated from the registry at build time, not written by hand. Every figure below was produced by the code printed beside it, on this machine, during this documentation build.

That matters more than it sounds. A hand-written gallery drifts: a transform is renamed, an option changes, a picture stays. Here the page is built by walking registry.plot_transforms, and each snippet string is executed to make the figure it is shown next to. A transform that stopped drawing would fail the docs build; a transform added tomorrow appears here tomorrow with no edit to this page.

How to read it

Three ideas structure the whole visualization layer, and this page is laid out along them.

A transform turns a subject — a Trajectory, a system, an analysis result, a bare array — into plottable geometry: coordinates, channels, labels, a frame. It is named, registered and documented, and it owns no new mathematics (it adapts an estimator from tsdynamics.analysis).

A primitive is how that geometry is drawn — a line, a point cloud, an image, a quiver field, a 3-D surface. "Primitive" does not mean "simple".

The compatibility matrix says which primitives may draw which transform. It is declared, one row per transform, at the transform's definition site — so an invalid pair raises, naming the valid set, instead of quietly drawing something that looks plausible and is wrong. The tabs under each entry below are exactly that row.

Transforms come in exactly two source categories, and the page is split by them:

  • data — computable from a series or a point set.

    A Trajectory, a NumPy array, an analysis result. It also accepts a system, because a model gives you data for free.

  • model — must evaluate or integrate the right-hand side.

    At points that are not in the input: a lattice of initial conditions, a Jacobian at an equilibrium. A bare array cannot serve, and it says so.

The one-liner

Everything on this page is reachable through one function:

import tsdynamics as ts

# skip-doctest — the calling pattern; `subject` and the transform name are yours
ts.plot(subject, "transform_name", **options)                 # draw it
ts.plot(subject, "transform_name", primitive="contour")       # draw it differently
ts.plot(subject, "flow_speed", "nullclines", "streamlines")   # overlay several

ts.plot always returns a Plot, which renders itself — .show(), .save("figure.pdf"), .render("plotly") — so a gallery snippet is also a working first line of your own figure. Two rungs below it are public when you want the numbers rather than the picture:

# skip-doctest — the escape-hatch pattern; `subject` is yours
g = ts.viz.geometry(subject, "ftle", grid=201)   # the arrays, and stop there
ts.viz.draw(g, "contour")                        # hand them back to the library

And ts.viz.compatibility() prints the same matrix this page draws.

There are 39 plot transforms and 101 declared (transform, primitive) pairs below. Every figure on this page was produced by the code printed beside it, and the page itself is generated from registry.plot_transforms at build time — so a transform that stopped drawing would take the docs build down with it.

transform source space primitives (bold = default) what it draws
time_series data time line, points, steps One curve per selected component against time.
phase_portrait data state2/state3 density, line, line3d, points, points3d An orbit in state space over an arbitrary component pair or triple.
delay_embedding data state2 density, line, points The x(t) vs x(t - delay) reconstruction of a scalar observable.
phase_portrait_field model state2 quiver A direction field with a host orbit drawn on it.
cobweb data state2 line, points The 1-D staircase x_{n+1} vs x_n with the y = x diagonal.
spacetime data grid2 contour, image, surface3d Component index versus time as a colour-mapped lattice.
spatial_field data grid2 contour, image, line, points, surface3d A spatially-extended system's field, with the per-time stack for a movie.
ftle model state2 contour, image, surface3d The finite-time Lyapunov exponent field; its ridges are the LCS.
escape_time model state2 contour, image, surface3d How long each initial condition takes to leave the region.
transient_time model state2 contour, image, surface3d How long each initial condition takes to settle onto its attractor.
invariant_density data scaling/state2 contour, histogram, image, line, steps, surface3d The natural measure of an orbit: a density curve, or a 2-D measure image.
psd data scaling line, points Power spectral density S(f) of one trajectory component.
autocorrelation data scaling line, points, steps The autocorrelation C(tau) with its 1/e and first-zero crossings marked.
mutual_information data scaling line, points, steps Time-delayed mutual information I(tau) with the chosen delay marked.
fnn data scaling line, points, steps Kennel's false-nearest-neighbour fraction against embedding dimension.
cao data scaling line, points, steps Cao's averaged-false-neighbour E1(d) / E2(d) against embedding dimension.
line_lengths data scaling line, points, steps P(l) and P(v) — the recurrence line-length distributions DET/LAM are read from.
return_time data scaling line, points, steps The first-return / first-passage time distribution of a level set.
hilbert data grid2 image, surface3d A 1-D observable laid on a space-filling curve as a 2-D image.
hilbert_fourier data grid2 contour, image, surface3d The centred 2-D power spectrum of a Hilbert plot — periodicity as symmetry.
hilbert_difference data grid2 contour, image, surface3d The locality-loss field — where the curve's 2-D neighbourhood is not a 1-D one.
hilbert_labels data grid2 image The curve's own path — each cell coloured by the step at which it is visited.
nullclines model state2 line, points The curves f_i = 0 on a 2-D slice; their crossings are the equilibria.
vector_field model state2 quiver The right-hand side as arrows on a lattice; unit-length by default.
flow_speed model state2 contour, image, surface3d The speed
streamlines model state2 line, points Integral curves of the sliced field, seeded on a lattice.
trace_determinant model param2 points A system's equilibria placed on the (tr J, det J) stability plane.
recurrence data grid2 contour, image The recurrence plot R(i, j) — where the orbit revisits its own neighbourhood.
poincare_section model state2 density, points Where an orbit pierces a plane — the flow's discrete portrait.
orbit_diagram model param1 density, points The asymptotic orbit against a swept parameter — the bifurcation cascade.
basins model state2 boundary, contour, image Which attractor each initial condition reaches — the basin label image.
ensemble_fan data time band An ensemble's median with its spread as a shaded envelope.
lyapunov_spectrum data index bars, line, points The ordered exponents as a stem plot against the lambda = 0 line.
lyapunov_convergence data scaling line, points Each exponent's running Benettin estimate against time — has it settled?
gali_curves data scaling line, points GALI_k against time on log axes, with the analytic power-law reference.
zero_one_pq_plane data state2 density, line, points The (p, q) translation plane of the 0-1 test: bounded blob or Brownian sprawl.
scaling_fit data scaling line, points A log-log scaling curve with the fitted window delimited (+ residual views).
eigenvalue_plane model complex line, points The Jacobian spectrum of an equilibrium against its stability boundary.
floquet_multipliers model complex line, points A periodic orbit's monodromy spectrum on the unit circle, trivial one marked.

Data-capable transforms

Computable from a series or a point set — a Trajectory, a bare NumPy array, an analysis result. They also accept a system, because a model gives you data for free (it is integrated once, and the choice is recorded in meta).

time_series

data · time · 3 primitives

One curve per selected component against time.

One curve per selected component against time, drawn with the line primitive

The three Lorenz coordinates against time — one layer per component, coloured from the theme palette. x and y swap sign together at every lobe change; z never goes negative.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=25.0, dt=0.01, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "time_series")

One curve per selected component against time, drawn with the points primitive

The same series as a scatter — the sampling grid made visible.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=8.0, dt=0.05, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "time_series", primitive="points")

One curve per selected component against time, drawn with the steps primitive

A piecewise-constant staircase — the right mark for a sampled or discrete-time record.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=8.0, dt=0.05, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "time_series", primitive="steps")

phase_portrait

data · state2/state3 · 5 primitives

An orbit in state space over an arbitrary component pair or triple.

An orbit in state space over an arbitrary component pair or triple, drawn with the line3d primitive

The Lorenz butterfly. With three components selected the geometry is 3-D, so the geometry's own default (line3d) wins over the transform's declared default — which is why asking for flat line here raises instead of silently projecting.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "phase_portrait")

An orbit in state space over an arbitrary component pair or triple, drawn with the density primitive

The same projection as a 2-D histogram — where the orbit spends its time, which a line plot hides once the curve overdraws itself. The rims of both wings are where it lingers.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "phase_portrait", components=("x", "z"), primitive="density", bins=160)

An orbit in state space over an arbitrary component pair or triple, drawn with the line primitive

The classic (x, z) projection of the butterfly.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "phase_portrait", components=("x", "z"), primitive="line")

An orbit in state space over an arbitrary component pair or triple, drawn with the points primitive

The Lorenz butterfly. With three components selected the geometry is 3-D, so the geometry's own default (line3d) wins over the transform's declared default — which is why asking for flat line here raises instead of silently projecting.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "phase_portrait", components=("x", "z"), primitive="points", markersize=0.8, alpha=0.4)

An orbit in state space over an arbitrary component pair or triple, drawn with the points3d primitive

The same orbit as a point cloud — sample density, not connectivity. The style keywords are split out of the same call and applied to this transform's layers.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "phase_portrait", primitive="points3d", markersize=0.8, alpha=0.4)

delay_embedding

data · state2 · 3 primitives

The x(t) vs x(t - delay) reconstruction of a scalar observable.

The x(t) vs x(t - delay) reconstruction of a scalar observable, drawn with the line primitive

Takens' reconstruction from the single observable x(t): plot it against x(t - τ) and the butterfly reappears, without ever using y or z.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "delay_embedding", delay=17)

The x(t) vs x(t - delay) reconstruction of a scalar observable, drawn with the density primitive

The reconstruction as an occupancy density.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "delay_embedding", delay=17, primitive="density")

The x(t) vs x(t - delay) reconstruction of a scalar observable, drawn with the points primitive

Takens' reconstruction from the single observable x(t): plot it against x(t - τ) and the butterfly reappears, without ever using y or z.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "delay_embedding", delay=17, primitive="points")

cobweb

data · state2 · 2 primitives

The 1-D staircase x_{n+1} vs x_n with the y = x diagonal.

The 1-D staircase x_{n+1} vs x_n with the y = x diagonal, drawn with the line primitive

The logistic map at r = 4, iterated as a staircase between the map's graph and the diagonal y = x. Every vertical segment is an application of f, every horizontal one is feeding the result back in. Forty-eight iterates is a cobweb; twenty thousand is a filled square, which is why this one is short on purpose.

import tsdynamics as ts

orbit = ts.systems.Logistic(r=4.0).run(steps=48, ic=[0.2])

ts.plot(orbit, "cobweb")

The 1-D staircase x_{n+1} vs x_n with the y = x diagonal, drawn with the points primitive

Only the visited (x_n, x_{n+1}) pairs — the staircase's corners.

import tsdynamics as ts

orbit = ts.systems.Logistic(r=4.0).run(steps=48, ic=[0.2])

ts.plot(orbit, "cobweb", primitive="points")

spacetime

data · grid2 · 3 primitives

Component index versus time as a colour-mapped lattice.

Component index versus time as a colour-mapped lattice, drawn with the image primitive

Lorenz-96 as component index versus time: 20 coupled sites, with the westward-drifting waves that a 20-line time series cannot show.

import tsdynamics as ts

traj = ts.systems.Lorenz96().run(final_time=30.0, dt=0.05)

ts.plot(traj, "spacetime")

Component index versus time as a colour-mapped lattice, drawn with the contour primitive

The same lattice as level sets, coloured by level from the transform's colormap rather than by layer index.

import tsdynamics as ts

traj = ts.systems.Lorenz96().run(final_time=30.0, dt=0.05)

ts.plot(traj, "spacetime", primitive="contour", levels=6)

Component index versus time as a colour-mapped lattice, drawn with the surface3d primitive

The lattice lifted into a surface — amplitude as height.

import tsdynamics as ts

traj = ts.systems.Lorenz96().run(final_time=30.0, dt=0.05)

ts.plot(traj, "spacetime", primitive="surface3d")

spatial_field

data · grid2 · 5 primitives

A spatially-extended system's field, with the per-time stack for a movie.

A spatially-extended system's field, with the per-time stack for a movie, drawn with the image primitive

The Swift–Hohenberg field on its own 32×32 grid — the state vector un-flattened by the system's _field_shape, which is why no shape= argument is needed. The stripes are the pattern-forming instability.

import tsdynamics as ts

traj = ts.systems.SwiftHohenberg().run(final_time=40.0, dt=0.2)

ts.plot(traj, "spatial_field")

A spatially-extended system's field, with the per-time stack for a movie, drawn with the contour primitive

The same field as contours.

import tsdynamics as ts

traj = ts.systems.SwiftHohenberg().run(final_time=40.0, dt=0.2)

ts.plot(traj, "spatial_field", primitive="contour")

A spatially-extended system's field, with the per-time stack for a movie, drawn with the line primitive

A 1-D field is a profile, not an image: the final Kuramoto–Sivashinsky wave u(x). One transform, two spatial dimensionalities, no guessing.

import tsdynamics as ts

traj = ts.systems.KuramotoSivashinsky().run(final_time=200.0, dt=0.5)

ts.plot(traj, "spatial_field", primitive="line")

A spatially-extended system's field, with the per-time stack for a movie, drawn with the points primitive

The same profile as its sampled lattice points.

import tsdynamics as ts

traj = ts.systems.KuramotoSivashinsky().run(final_time=200.0, dt=0.5)

ts.plot(traj, "spatial_field", primitive="points")

A spatially-extended system's field, with the per-time stack for a movie, drawn with the surface3d primitive

The field as a height surface.

import tsdynamics as ts

traj = ts.systems.SwiftHohenberg().run(final_time=40.0, dt=0.2)

ts.plot(traj, "spatial_field", primitive="surface3d")

invariant_density

data · scaling/state2 · 6 primitives

The natural measure of an orbit: a density curve, or a 2-D measure image.

Adapts tsdynamics.analysis.planar.invariant_density — the transform itself owns no new math.

The natural measure of an orbit: a density curve, or a 2-D measure image, drawn with the histogram primitive

The natural measure of the logistic map at r = 4 — the analytic arcsine law 1/(π√(x(1-x))), with its two integrable spikes at the interval ends, recovered from 20 000 iterates.

import tsdynamics as ts

orbit = ts.systems.Logistic(r=4.0).run(steps=20_000, ic=[0.2])

ts.plot(orbit, "invariant_density", bins=120)

The natural measure of an orbit: a density curve, or a 2-D measure image, drawn with the contour primitive

The same 2-D measure as level sets. A histogram needs coarse bins before its level sets mean anything — at 200 bins the contours trace shot noise.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "invariant_density", components=("x", "z"), bins=48, primitive="contour", levels=6)

The natural measure of an orbit: a density curve, or a 2-D measure image, drawn with the image primitive

In two components the measure is an image: the Lorenz attractor's occupancy, brightest along the slow spiralling rims.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "invariant_density", components=("x", "z"), bins=200, primitive="image")

The natural measure of an orbit: a density curve, or a 2-D measure image, drawn with the line primitive

The same density as a curve.

import tsdynamics as ts

orbit = ts.systems.Logistic(r=4.0).run(steps=20_000, ic=[0.2])

ts.plot(orbit, "invariant_density", bins=120, primitive="line")

The natural measure of an orbit: a density curve, or a 2-D measure image, drawn with the steps primitive

The same density as a step histogram outline.

import tsdynamics as ts

orbit = ts.systems.Logistic(r=4.0).run(steps=20_000, ic=[0.2])

ts.plot(orbit, "invariant_density", bins=120, primitive="steps")

The natural measure of an orbit: a density curve, or a 2-D measure image, drawn with the surface3d primitive

The measure as a landscape.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "invariant_density", components=("x", "z"), bins=80, primitive="surface3d")

psd

data · scaling · 2 primitives

Power spectral density S(f) of one trajectory component.

Power spectral density S(f) of one trajectory component, drawn with the line primitive

The power spectrum of the Lorenz x coordinate: broadband, a continuum with no line standing out of it — the spectral signature of chaos. The geometry declares log-log axes, because on linear axes every spectrum is a spike at f = 0 and a flat line.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "psd")

Power spectral density S(f) of one trajectory component, drawn with the points primitive

The same spectrum as its Welch bins. Compare with a periodic orbit's spectrum, which is a comb of discrete lines rather than a continuum.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "psd", primitive="points")

autocorrelation

data · scaling · 3 primitives

The autocorrelation C(tau) with its 1/e and first-zero crossings marked.

Adapts tsdynamics.analysis.embedding.delay.autocorrelation — the transform itself owns no new math.

The autocorrelation C(tau) with its 1/e and first-zero crossings marked, drawn with the line primitive

C(τ) for Lorenz x, with the 1/e level and the first zero crossing marked — the two classical rules of thumb for choosing an embedding delay.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "autocorrelation", max_delay=400)

The autocorrelation C(tau) with its 1/e and first-zero crossings marked, drawn with the points primitive

C(τ) for Lorenz x, with the 1/e level and the first zero crossing marked — the two classical rules of thumb for choosing an embedding delay.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "autocorrelation", max_delay=400, primitive="points")

The autocorrelation C(tau) with its 1/e and first-zero crossings marked, drawn with the steps primitive

C(τ) for Lorenz x, with the 1/e level and the first zero crossing marked — the two classical rules of thumb for choosing an embedding delay.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "autocorrelation", max_delay=400, primitive="steps")

mutual_information

data · scaling · 3 primitives

Time-delayed mutual information I(tau) with the chosen delay marked.

Adapts tsdynamics.analysis.embedding.delay.mutual_information — the transform itself owns no new math.

Time-delayed mutual information I(tau) with the chosen delay marked, drawn with the line primitive

Time-delayed mutual information. Fraser & Swinney's rule takes the first minimum — marked — which is the nonlinear answer to the same question the autocorrelation answers linearly.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "mutual_information", max_delay=300)

Time-delayed mutual information I(tau) with the chosen delay marked, drawn with the points primitive

Time-delayed mutual information. Fraser & Swinney's rule takes the first minimum — marked — which is the nonlinear answer to the same question the autocorrelation answers linearly.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "mutual_information", max_delay=300, primitive="points")

Time-delayed mutual information I(tau) with the chosen delay marked, drawn with the steps primitive

Time-delayed mutual information. Fraser & Swinney's rule takes the first minimum — marked — which is the nonlinear answer to the same question the autocorrelation answers linearly.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "mutual_information", max_delay=300, primitive="steps")

fnn

data · scaling · 3 primitives

Kennel's false-nearest-neighbour fraction against embedding dimension.

Adapts tsdynamics.analysis.embedding.dimension.false_nearest_neighbors — the transform itself owns no new math.

Kennel's false-nearest-neighbour fraction against embedding dimension, drawn with the line primitive

Kennel's false-nearest-neighbour fraction against embedding dimension. It collapses to zero at m = 3 — the true dimension of the Lorenz system, recovered from one scalar observable.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "fnn", delay=17, max_dim=8)

Kennel's false-nearest-neighbour fraction against embedding dimension, drawn with the points primitive

Kennel's false-nearest-neighbour fraction against embedding dimension. It collapses to zero at m = 3 — the true dimension of the Lorenz system, recovered from one scalar observable.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "fnn", delay=17, max_dim=8, primitive="points")

Kennel's false-nearest-neighbour fraction against embedding dimension, drawn with the steps primitive

Kennel's false-nearest-neighbour fraction against embedding dimension. It collapses to zero at m = 3 — the true dimension of the Lorenz system, recovered from one scalar observable.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "fnn", delay=17, max_dim=8, primitive="steps")

cao

data · scaling · 3 primitives

Cao's averaged-false-neighbour E1(d) / E2(d) against embedding dimension.

Adapts tsdynamics.analysis.embedding.dimension.cao_dimension — the transform itself owns no new math.

Cao's averaged-false-neighbour E1(d) / E2(d) against embedding dimension, drawn with the line primitive

Cao's E1(d) saturates at 1 once the embedding unfolds the attractor, while E2(d) stays away from 1 — which is how the method separates deterministic data from noise, not just picks a dimension.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "cao", delay=17, max_dim=8)

Cao's averaged-false-neighbour E1(d) / E2(d) against embedding dimension, drawn with the points primitive

Cao's E1(d) saturates at 1 once the embedding unfolds the attractor, while E2(d) stays away from 1 — which is how the method separates deterministic data from noise, not just picks a dimension.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "cao", delay=17, max_dim=8, primitive="points")

Cao's averaged-false-neighbour E1(d) / E2(d) against embedding dimension, drawn with the steps primitive

Cao's E1(d) saturates at 1 once the embedding unfolds the attractor, while E2(d) stays away from 1 — which is how the method separates deterministic data from noise, not just picks a dimension.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "cao", delay=17, max_dim=8, primitive="steps")

line_lengths

data · scaling · 3 primitives

P(l) and P(v) — the recurrence line-length distributions DET/LAM are read from.

Adapts tsdynamics.analysis.recurrence.rqa.rqa — the transform itself owns no new math.

P(l) and P(v) — the recurrence line-length distributions DET/LAM are read from, drawn with the line primitive

The diagonal and vertical line-length distributions of the recurrence matrix — the histograms that DET, LAM, L_max and TT are read off. Long diagonals mean deterministic recurrence; long verticals mean laminar trapping. Both are heavy-tailed, so they are read on log axes; ts.plot returns the spec, so the fluent tweak chains straight onto the call.

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "line_lengths", recurrence_rate=0.05).rescale(x="log", y="log")

P(l) and P(v) — the recurrence line-length distributions DET/LAM are read from, drawn with the points primitive

The diagonal and vertical line-length distributions of the recurrence matrix — the histograms that DET, LAM, L_max and TT are read off. Long diagonals mean deterministic recurrence; long verticals mean laminar trapping. Both are heavy-tailed, so they are read on log axes; ts.plot returns the spec, so the fluent tweak chains straight onto the call.

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "line_lengths", recurrence_rate=0.05, primitive="points").rescale(x="log", y="log")

P(l) and P(v) — the recurrence line-length distributions DET/LAM are read from, drawn with the steps primitive

The diagonal and vertical line-length distributions of the recurrence matrix — the histograms that DET, LAM, L_max and TT are read off. Long diagonals mean deterministic recurrence; long verticals mean laminar trapping. Both are heavy-tailed, so they are read on log axes; ts.plot returns the spec, so the fluent tweak chains straight onto the call.

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "line_lengths", recurrence_rate=0.05, primitive="steps").rescale(x="log", y="log")

return_time

data · scaling · 3 primitives

The first-return / first-passage time distribution of a level set.

The first-return / first-passage time distribution of a level set, drawn with the line primitive

How long the Lorenz orbit takes to come back to a level set of z — one circuit of a wing. The distribution is sharply peaked at T ≈ 0.69 with a right tail: most loops are the same size, and the long ones are the wide swings that follow a lobe change. A periodic orbit would put every count in a single bin; the width of this peak is the chaos.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=400.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "return_time", components="z", n_bins=30)

The first-return / first-passage time distribution of a level set, drawn with the points primitive

How long the Lorenz orbit takes to come back to a level set of z — one circuit of a wing. The distribution is sharply peaked at T ≈ 0.69 with a right tail: most loops are the same size, and the long ones are the wide swings that follow a lobe change. A periodic orbit would put every count in a single bin; the width of this peak is the chaos.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=400.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "return_time", components="z", n_bins=30, primitive="points")

The first-return / first-passage time distribution of a level set, drawn with the steps primitive

How long the Lorenz orbit takes to come back to a level set of z — one circuit of a wing. The distribution is sharply peaked at T ≈ 0.69 with a right tail: most loops are the same size, and the long ones are the wide swings that follow a lobe change. A periodic orbit would put every count in a single bin; the width of this peak is the chaos.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=400.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "return_time", components="z", n_bins=30, primitive="steps")

hilbert

data · grid2 · 2 primitives

A 1-D observable laid on a space-filling curve as a 2-D image.

A 1-D observable laid on a space-filling curve as a 2-D image, drawn with the image primitive

The same record on the dependency-free snake ordering. The Hilbert-type curves need the optional hilbertplot package, which this build does not have; asking for one raises rather than substituting, so what you see is what was asked for.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "hilbert", curve="snake")

A 1-D observable laid on a space-filling curve as a 2-D image, drawn with the surface3d primitive

The same record on the dependency-free snake ordering. The Hilbert-type curves need the optional hilbertplot package, which this build does not have; asking for one raises rather than substituting, so what you see is what was asked for.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "hilbert", curve="snake", primitive="surface3d")

hilbert_fourier

data · grid2 · 3 primitives

The centred 2-D power spectrum of a Hilbert plot — periodicity as symmetry.

The centred 2-D power spectrum of a Hilbert plot — periodicity as symmetry, drawn with the image primitive

The snake-ordering spectrum — the hilbertplot extra is not installed on this build.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "hilbert_fourier", curve="snake")

The centred 2-D power spectrum of a Hilbert plot — periodicity as symmetry, drawn with the contour primitive

The snake-ordering spectrum — the hilbertplot extra is not installed on this build.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "hilbert_fourier", curve="snake", primitive="contour")

The centred 2-D power spectrum of a Hilbert plot — periodicity as symmetry, drawn with the surface3d primitive

The snake-ordering spectrum — the hilbertplot extra is not installed on this build.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "hilbert_fourier", curve="snake", primitive="surface3d")

hilbert_difference

data · grid2 · 3 primitives

The locality-loss field — where the curve's 2-D neighbourhood is not a 1-D one.

The locality-loss field — where the curve's 2-D neighbourhood is not a 1-D one, drawn with the image primitive

Locality loss for the snake ordering, and it is loss almost everywhere: two vertically adjacent cells are a whole row apart in the record, so the mean neighbour gap is uniformly large — which is exactly the argument for the Hilbert curve, and why this plot is worth having. The hilbertplot extra is not installed on this build, and the transform raises rather than substituting a curve you did not ask for.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "hilbert_difference", curve="snake")

The locality-loss field — where the curve's 2-D neighbourhood is not a 1-D one, drawn with the contour primitive

Locality loss for the snake ordering, and it is loss almost everywhere: two vertically adjacent cells are a whole row apart in the record, so the mean neighbour gap is uniformly large — which is exactly the argument for the Hilbert curve, and why this plot is worth having. The hilbertplot extra is not installed on this build, and the transform raises rather than substituting a curve you did not ask for.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "hilbert_difference", curve="snake", primitive="contour")

The locality-loss field — where the curve's 2-D neighbourhood is not a 1-D one, drawn with the surface3d primitive

Locality loss for the snake ordering, and it is loss almost everywhere: two vertically adjacent cells are a whole row apart in the record, so the mean neighbour gap is uniformly large — which is exactly the argument for the Hilbert curve, and why this plot is worth having. The hilbertplot extra is not installed on this build, and the transform raises rather than substituting a curve you did not ask for.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=200.0, dt=0.02, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "hilbert_difference", curve="snake", primitive="surface3d")

hilbert_labels

data · grid2 · 1 primitive

The curve's own path — each cell coloured by the step at which it is visited.

The curve's own path — each cell coloured by the step at which it is visited, drawn with the image primitive

The snake ordering's path: left to right, then right to left. Compare it with the Hilbert curve's recursive quadrant walk to see why locality differs. The hilbertplot extra is not installed here.

optional dependency missing — fallback example

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])

ts.plot(traj, "hilbert_labels", curve="snake")

recurrence

data · grid2 · 2 primitives

The recurrence plot R(i, j) — where the orbit revisits its own neighbourhood.

Adapts tsdynamics.analysis.recurrence_matrix — the transform itself owns no new math.

The recurrence plot R(i, j) — where the orbit revisits its own neighbourhood, drawn with the image primitive

The recurrence plot R(i, j): a dot wherever the orbit returns to within epsilon of an earlier state. The diagonal stripes are near-repetitions of the Rössler cycle; their lengths are what rqa turns into DET and L_max.

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=150.0, dt=0.25, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "recurrence", recurrence_rate=0.05)

The recurrence plot R(i, j) — where the orbit revisits its own neighbourhood, drawn with the contour primitive

The same matrix as the boundary of its recurrent set.

import tsdynamics as ts

traj = ts.systems.Rossler().run(final_time=150.0, dt=0.25, ic=[1.0, 1.0, 0.1])

ts.plot(traj, "recurrence", recurrence_rate=0.05, primitive="contour")

ensemble_fan

data · time · 1 primitive

An ensemble's median with its spread as a shaded envelope.

An ensemble's median with its spread as a shaded envelope, drawn with the band primitive

Forty Lorenz starts within 0.05 of each other, drawn as their median and spread. The band is invisible at first and then opens out — sensitive dependence, as a picture of an ensemble rather than of two orbits.

import tsdynamics as ts

import numpy as np

band = ts.systems.Lorenz().ensemble(
    np.random.default_rng(0).normal(1.0, 0.05, size=(40, 3)))
batch = band.run(final_time=12.0, dt=0.01)

ts.plot(batch, "ensemble_fan")

lyapunov_spectrum

data · index · 3 primitives

The ordered exponents as a stem plot against the lambda = 0 line.

Adapts tsdynamics.analysis.lyapunov.lyapunov_spectrum — the transform itself owns no new math.

The ordered exponents as a stem plot against the lambda = 0 line, drawn with the points primitive

The Lorenz spectrum as a stem plot against the λ = 0 line: one positive exponent (chaos), one zero (the flow direction), one strongly negative (volume contraction). Their sum is the divergence, -(σ + 1 + β).

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "lyapunov_spectrum", final_time=400.0)

The ordered exponents as a stem plot against the lambda = 0 line, drawn with the bars primitive

The Lorenz spectrum as a stem plot against the λ = 0 line: one positive exponent (chaos), one zero (the flow direction), one strongly negative (volume contraction). Their sum is the divergence, -(σ + 1 + β).

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "lyapunov_spectrum", final_time=400.0, primitive="bars")

The ordered exponents as a stem plot against the lambda = 0 line, drawn with the line primitive

The same exponents joined as a line.

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "lyapunov_spectrum", final_time=400.0, primitive="line")

lyapunov_convergence

data · scaling · 2 primitives

Each exponent's running Benettin estimate against time — has it settled?

Adapts tsdynamics.derived.tangent.TangentSystem.convergence — the transform itself owns no new math.

Each exponent's running Benettin estimate against time — has it settled?, drawn with the line primitive

Each exponent's running Benettin estimate against time — the plot that answers has it converged?, which a single returned number never can.

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "lyapunov_convergence", steps=4000)

Each exponent's running Benettin estimate against time — has it settled?, drawn with the points primitive

Each exponent's running Benettin estimate against time — the plot that answers has it converged?, which a single returned number never can.

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "lyapunov_convergence", steps=4000, primitive="points")

gali_curves

data · scaling · 2 primitives

GALI_k against time on log axes, with the analytic power-law reference.

Adapts tsdynamics.analysis.chaos.gali — the transform itself owns no new math.

GALI_k against time on log axes, with the analytic power-law reference, drawn with the line primitive

GALI₂ and GALI₃ on log axes for a chaotic Hénon–Heiles orbit, against the analytic reference slope -(k-1)λ₁. Exponential decay is the chaos verdict; a regular orbit would leave GALI₂ flat.

import tsdynamics as ts

hh = ts.systems.HenonHeiles()

ts.plot(hh, "gali_curves", k=(2, 3), ic=[0.0, -0.1, 0.49, 0.0], final_time=1000.0)

GALI_k against time on log axes, with the analytic power-law reference, drawn with the points primitive

GALI₂ and GALI₃ on log axes for a chaotic Hénon–Heiles orbit, against the analytic reference slope -(k-1)λ₁. Exponential decay is the chaos verdict; a regular orbit would leave GALI₂ flat.

import tsdynamics as ts

hh = ts.systems.HenonHeiles()

ts.plot(hh, "gali_curves", k=(2, 3), ic=[0.0, -0.1, 0.49, 0.0], final_time=1000.0, primitive="points")

zero_one_pq_plane

data · state2 · 3 primitives

The (p, q) translation plane of the 0-1 test: bounded blob or Brownian sprawl.

Adapts tsdynamics.analysis.chaos.zero_one_test — the transform itself owns no new math.

The (p, q) translation plane of the 0-1 test: bounded blob or Brownian sprawl, drawn with the line primitive

The (p, q) translation variables of the 0–1 test. Chaotic dynamics drives them on a Brownian sprawl whose extent grows without bound; a regular orbit keeps them on a bounded torus-like blob.

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "zero_one_pq_plane", final_time=2000.0, dt=0.1)

The (p, q) translation plane of the 0-1 test: bounded blob or Brownian sprawl, drawn with the density primitive

The same walk as an occupancy density.

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "zero_one_pq_plane", final_time=2000.0, dt=0.1, primitive="density")

The (p, q) translation plane of the 0-1 test: bounded blob or Brownian sprawl, drawn with the points primitive

The same walk as its sampled points.

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "zero_one_pq_plane", final_time=2000.0, dt=0.1, primitive="points")

scaling_fit

data · scaling · 2 primitives

A log-log scaling curve with the fitted window delimited (+ residual views).

Adapts tsdynamics.analysis._result_scaling.ScalingResult — the transform itself owns no new math.

A log-log scaling curve with the fitted window delimited (+ residual views), drawn with the points primitive

The log-log correlation sum with the fitted scaling window delimited. The slope inside the window is the correlation dimension; the point of drawing it is that you can see whether the window was the right one.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])
result = ts.analysis.correlation_dimension(traj)

ts.plot(result, "scaling_fit")

A log-log scaling curve with the fitted window delimited (+ residual views), drawn with the line primitive

The same scaling curve as a line.

import tsdynamics as ts

traj = ts.systems.Lorenz().run(final_time=60.0, dt=0.005, ic=[1.0, 1.0, 20.0])
result = ts.analysis.correlation_dimension(traj)

ts.plot(result, "scaling_fit", primitive="line")

Model-only transforms

These must evaluate or integrate the right-hand side at points that are not in the input, so they need the system itself. Handing one a bare array cannot work, and it says so rather than guessing.

phase_portrait_field

model · state2 · 1 primitive

A direction field with a host orbit drawn on it.

A direction field with a host orbit drawn on it, drawn with the quiver primitive

A direction field with its host orbit drawn on it — the orbit is pinned to a line even though the transform's primitive is quiver, because a part may pin its own primitive.

import tsdynamics as ts

import numpy as np

def rhs(u):
    return np.stack([u[..., 1], 2.0 * (1 - u[..., 0] ** 2) * u[..., 1] - u[..., 0]], axis=-1)

traj = ts.systems.VanDerPol(mu=2.0).run(final_time=30.0, dt=0.01, ic=[0.1, 0.0])

ts.plot(rhs, "phase_portrait_field", source=traj, xlim=(-3.0, 3.0), ylim=(-6.0, 6.0), grid=18)

ftle

model · state2 · 3 primitives

The finite-time Lyapunov exponent field; its ridges are the LCS.

Adapts tsdynamics.analysis.planar.ftle_field — the transform itself owns no new math.

The finite-time Lyapunov exponent field; its ridges are the LCS, drawn with the image primitive

The finite-time Lyapunov exponent field of the unforced two-well Duffing oscillator. The bright ridge is the stable manifold of the saddle at the origin — the separatrix between the two wells, found without ever computing a basin.

import tsdynamics as ts

duff = ts.systems.Duffing(gamma=0.0)

ts.plot(duff, "ftle", xlim=(-2.0, 2.0), ylim=(-1.5, 1.5), grid=201, final_time=8.0)

The finite-time Lyapunov exponent field; its ridges are the LCS, drawn with the contour primitive

The same field as level sets — the ridge as a contour crowd.

import tsdynamics as ts

duff = ts.systems.Duffing(gamma=0.0)

ts.plot(duff, "ftle", xlim=(-2.0, 2.0), ylim=(-1.5, 1.5), grid=121, final_time=8.0, primitive="contour")

The finite-time Lyapunov exponent field; its ridges are the LCS, drawn with the surface3d primitive

The FTLE field as a landscape; the ridge is a literal ridge.

import tsdynamics as ts

duff = ts.systems.Duffing(gamma=0.0)

ts.plot(duff, "ftle", xlim=(-2.0, 2.0), ylim=(-1.5, 1.5), grid=81, final_time=8.0, primitive="surface3d")

escape_time

model · state2 · 3 primitives

How long each initial condition takes to leave the region.

Adapts tsdynamics.analysis.planar.escape_time_field — the transform itself owns no new math.

How long each initial condition takes to leave the region, drawn with the image primitive

Transient chaos, drawn. Hénon–Heiles above its escape energy: the triangular bound region in the middle (white — never escaped within the horizon, so NaN, a hole rather than a fake number), the three exit channels, and between them the fractal filaments of starts that bounce around for tens of time units before committing to an exit. A two-colour which exit diagram cannot resolve that structure; the level sets of when can. Note at= — the system is 4-D and this is the (x, y) slice at p_y = 0.4, which is what puts it above the escape energy.

import tsdynamics as ts

hh = ts.systems.HenonHeiles()

ts.plot(hh, "escape_time", plane=("x", "y"), at=[0.0, 0.0, 0.0, 0.40],
        xlim=(-1.2, 1.2), ylim=(-1.0, 1.4), grid=201,
        final_time=60.0, chunks=120, escape=2.0)

How long each initial condition takes to leave the region, drawn with the contour primitive

The same field as escape isochrones — a level set is the set of starts that leave the window at the same time.

import tsdynamics as ts

hh = ts.systems.HenonHeiles()

ts.plot(hh, "escape_time", plane=("x", "y"), at=[0.0, 0.0, 0.0, 0.40],
        xlim=(-1.2, 1.2), ylim=(-1.0, 1.4), grid=121,
        final_time=60.0, chunks=120, primitive="contour")

How long each initial condition takes to leave the region, drawn with the surface3d primitive

Escape time as a landscape: the spikes are the long-lived orbits on the boundary between exits.

import tsdynamics as ts

hh = ts.systems.HenonHeiles()

ts.plot(hh, "escape_time", plane=("x", "y"), at=[0.0, 0.0, 0.0, 0.40],
        xlim=(-1.2, 1.2), ylim=(-1.0, 1.4), grid=81,
        final_time=60.0, chunks=120, escape=2.0, primitive="surface3d")

transient_time

model · state2 · 3 primitives

How long each initial condition takes to settle onto its attractor.

Adapts tsdynamics.analysis.planar.transient_time_field — the transform itself owns no new math.

How long each initial condition takes to settle onto its attractor, drawn with the image primitive

The complement of escape time: how long each start takes to arrive at the Brusselator's stable focus, below its Hopf threshold. The arrival threshold is read off the field itself (1% of the median lattice speed) and recorded in meta, rather than being a fixed number in units the flow has never heard of.

import tsdynamics as ts

bru = ts.systems.Brusselator(b=1.5)

ts.plot(bru, "transient_time", xlim=(0.0, 4.0), ylim=(0.0, 4.0), grid=161, final_time=40.0, chunks=80)

How long each initial condition takes to settle onto its attractor, drawn with the contour primitive

The same field as level sets — each contour is an arrival isochrone.

import tsdynamics as ts

bru = ts.systems.Brusselator(b=1.5)

ts.plot(bru, "transient_time", xlim=(0.0, 4.0), ylim=(0.0, 4.0), grid=121, final_time=40.0, chunks=80, primitive="contour")

How long each initial condition takes to settle onto its attractor, drawn with the surface3d primitive

Arrival time as a landscape.

import tsdynamics as ts

bru = ts.systems.Brusselator(b=1.5)

ts.plot(bru, "transient_time", xlim=(0.0, 4.0), ylim=(0.0, 4.0), grid=81, final_time=40.0, chunks=80, primitive="surface3d")

nullclines

model · state2 · 2 primitives

The curves f_i = 0 on a 2-D slice; their crossings are the equilibria.

Adapts tsdynamics.analysis.planar.nullclines — the transform itself owns no new math.

The curves f_i = 0 on a 2-D slice; their crossings are the equilibria, drawn with the line primitive

The FitzHugh–Nagumo nullclines: the cubic v' = 0 curve and the straight w' = 0 line. Their crossing is the equilibrium — the textbook picture, computed by marching squares on the real right-hand side.

import tsdynamics as ts

fhn = ts.systems.FitzHughNagumo()

ts.plot(fhn, "nullclines", xlim=(-2.5, 2.5), ylim=(-1.0, 2.0))

The curves f_i = 0 on a 2-D slice; their crossings are the equilibria, drawn with the points primitive

The marching-squares vertices themselves.

import tsdynamics as ts

fhn = ts.systems.FitzHughNagumo()

ts.plot(fhn, "nullclines", xlim=(-2.5, 2.5), ylim=(-1.0, 2.0), primitive="points")

vector_field

model · state2 · 1 primitive

The right-hand side as arrows on a lattice; unit-length by default.

Adapts tsdynamics.analysis.planar.flow_field — the transform itself owns no new math.

The right-hand side as arrows on a lattice; unit-length by default, drawn with the quiver primitive

The right-hand side as unit arrows on a lattice — the direction field. Handed the system it infers its own window, exactly as flow_speed does; normalize=False keeps the true magnitudes. direction_field is an alias for this transform, not a second row.

import tsdynamics as ts

vdp = ts.systems.VanDerPol(params={"mu": 1.0})

ts.plot(vdp, "vector_field", grid=22)

flow_speed

model · state2 · 3 primitives

The speed |f| of the flow as a scalar backdrop.

Adapts tsdynamics.analysis.planar.flow_field — the transform itself owns no new math.

The speed |f| of the flow as a scalar backdrop, drawn with the image primitive

|f| as a backdrop for the Van der Pol relaxation oscillator, on a log scale: the two fast jumps at the ends of the cubic are bright, the two slow crawls along its branches are dark, and the equilibrium at the origin is a hole — log=True maps |f| = 0 to NaN rather than faking a minimum. Speed is exactly what a direction field's unit arrows throw away.

import tsdynamics as ts

vdp = ts.systems.VanDerPol(mu=2.0)

ts.plot(vdp, "flow_speed", xlim=(-3.0, 3.0), ylim=(-6.0, 6.0), log=True)

The speed |f| of the flow as a scalar backdrop, drawn with the contour primitive

The same speed field as level sets, coloured by level.

import tsdynamics as ts

vdp = ts.systems.VanDerPol(mu=2.0)

ts.plot(vdp, "flow_speed", xlim=(-3.0, 3.0), ylim=(-6.0, 6.0), log=True, primitive="contour")

The speed |f| of the flow as a scalar backdrop, drawn with the surface3d primitive

Speed as a height surface (linear, so the ridges keep their scale).

import tsdynamics as ts

vdp = ts.systems.VanDerPol(mu=2.0)

ts.plot(vdp, "flow_speed", xlim=(-3.0, 3.0), ylim=(-6.0, 6.0), primitive="surface3d")

streamlines

model · state2 · 2 primitives

Integral curves of the sliced field, seeded on a lattice.

Adapts tsdynamics.analysis.planar.streamlines — the transform itself owns no new math.

Integral curves of the sliced field, seeded on a lattice, drawn with the line primitive

Arc-length-parametrised integral curves of the field, integrated both ways from a lattice of seeds through the system's own right-hand side — not a plotting library's interpolation of a pre-sampled lattice, which is what matters near a separatrix. Every curve is swept onto the same limit cycle, which is the picture of an attractor.

import tsdynamics as ts

vdp = ts.systems.VanDerPol(mu=2.0)

ts.plot(vdp, "streamlines", xlim=(-3.0, 3.0), ylim=(-6.0, 6.0), seeds=6, steps=120, alpha=0.75)

Integral curves of the sliced field, seeded on a lattice, drawn with the points primitive

The streamline vertices: evenly spaced by arc length, not by time — so their spacing carries no speed information, deliberately.

import tsdynamics as ts

vdp = ts.systems.VanDerPol(mu=2.0)

ts.plot(vdp, "streamlines", xlim=(-3.0, 3.0), ylim=(-6.0, 6.0), seeds=4, steps=60, primitive="points", markersize=2.0, alpha=0.7)

trace_determinant

model · param2 · 1 primitive

A system's equilibria placed on the (tr J, det J) stability plane.

Adapts tsdynamics.analysis.planar.trace_determinant — the transform itself owns no new math.

A system's equilibria placed on the (tr J, det J) stability plane, drawn with the points primitive

Every equilibrium of the system placed on the (tr J, det J) plane, with the axes and the discriminant parabola that partition it into node / spiral / saddle / centre. Lotka–Volterra's two equilibria land in two different regions: the extinction state below the det = 0 axis is a saddle, the coexistence state on the tr = 0 axis is a centre — which is why its orbits are closed. The frame is param2, so this correctly refuses to overlay on a state space.

import tsdynamics as ts

lv = ts.systems.LotkaVolterra()

ts.plot(lv, "trace_determinant")

poincare_section

model · state2 · 2 primitives

Where an orbit pierces a plane — the flow's discrete portrait.

Adapts tsdynamics.analysis.poincare_section — the transform itself owns no new math.

Where an orbit pierces a plane — the flow's discrete portrait, drawn with the points primitive

(No curated example yet — drawn on the small fixture poincare_section registers for the compatibility gate. Add one to docs/_tooling/gallery.py.)

import tsdynamics as ts

# subject and options from the transform's own example factory
subject, options = ts.viz.transforms.get('poincare_section').example('points')

ts.plot(subject, 'poincare_section', **options)

Where an orbit pierces a plane — the flow's discrete portrait, drawn with the density primitive

(No curated example yet — drawn on the small fixture poincare_section registers for the compatibility gate. Add one to docs/_tooling/gallery.py.)

import tsdynamics as ts

# subject and options from the transform's own example factory
subject, options = ts.viz.transforms.get('poincare_section').example('points')

ts.plot(subject, 'poincare_section', **options, primitive="density")

orbit_diagram

model · param1 · 2 primitives

The asymptotic orbit against a swept parameter — the bifurcation cascade.

The asymptotic orbit against a swept parameter — the bifurcation cascade, drawn with the points primitive

The period-doubling cascade, drawn as a transform rather than read off a result — so it overlays, grids and styles like any other layer. One fixed point doubles at r = 3, again at 1 + sqrt(6), and smears into chaos near 3.57, threaded with periodic windows.

import tsdynamics as ts

import numpy as np

log = ts.systems.Logistic()

ts.plot(log, "orbit_diagram", param="r", values=np.linspace(2.8, 4.0, 600),
        points=120, transient=400)

The asymptotic orbit against a swept parameter — the bifurcation cascade, drawn with the density primitive

The same sweep as an occupancy density — where the orbit spends its time, not merely where it goes.

import tsdynamics as ts

import numpy as np

log = ts.systems.Logistic()

ts.plot(log, "orbit_diagram", param="r", values=np.linspace(2.8, 4.0, 600),
        points=120, transient=400, primitive="density")

basins

model · state2 · 3 primitives

Which attractor each initial condition reaches — the basin label image.

Adapts tsdynamics.analysis.basins — the transform itself owns no new math.

Which attractor each initial condition reaches — the basin label image, drawn with the image primitive

Which fate each initial condition meets: the Hénon attractor's basin against the starts that escape to infinity. The boundary is the stable manifold of the saddle at infinity — smooth here, fractal for many other systems, which is what basin_entropy measures.

import tsdynamics as ts

hen = ts.systems.Henon()

ts.plot(hen, "basins", region=[(-2.0, 2.0, 160), (-2.0, 2.0, 160)])

Which attractor each initial condition reaches — the basin label image, drawn with the boundary primitive

Only the cells on the basin boundary — the set whose fate a small perturbation can change.

import tsdynamics as ts

hen = ts.systems.Henon()

ts.plot(hen, "basins", region=[(-2.0, 2.0, 160), (-2.0, 2.0, 160)],
        primitive="boundary")

Which attractor each initial condition reaches — the basin label image, drawn with the contour primitive

The label field as level sets, for overlaying on a portrait.

import tsdynamics as ts

hen = ts.systems.Henon()

ts.plot(hen, "basins", region=[(-2.0, 2.0, 160), (-2.0, 2.0, 160)],
        primitive="contour")

eigenvalue_plane

model · complex · 2 primitives

The Jacobian spectrum of an equilibrium against its stability boundary.

Adapts tsdynamics.analysis.fixedpoints.fixed_points — the transform itself owns no new math.

The Jacobian spectrum of an equilibrium against its stability boundary, drawn with the points primitive

The Jacobian spectrum of the Lorenz origin against the imaginary axis: {+11.83, -2.67, -22.83} — one eigenvalue to the right of the boundary, so the origin is the saddle that separates the two wings. The boundary is real geometry in its own layer, so it survives JSON and three.js export.

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "eigenvalue_plane", at=[0.0, 0.0, 0.0])

The Jacobian spectrum of an equilibrium against its stability boundary, drawn with the line primitive

The spectrum joined as a line.

import tsdynamics as ts

lorenz = ts.systems.Lorenz()

ts.plot(lorenz, "eigenvalue_plane", at=[0.0, 0.0, 0.0], primitive="line")

floquet_multipliers

model · complex · 2 primitives

A periodic orbit's monodromy spectrum on the unit circle, trivial one marked.

Adapts tsdynamics.analysis.fixedpoints.periodic_orbit — the transform itself owns no new math.

A periodic orbit's monodromy spectrum on the unit circle, trivial one marked, drawn with the points primitive

The monodromy spectrum of the Van der Pol limit cycle (period T = 6.663) against the unit circle. One multiplier sits at +1 — the trivial one, along the flow, found by aligning the eigenvector with f(x₀) and marked as such. The other is at the origin to plotting accuracy (|μ| ~ 1e-3), which is what strongly attracting means.

import tsdynamics as ts

vdp = ts.systems.VanDerPol()

ts.plot(vdp, "floquet_multipliers")

A periodic orbit's monodromy spectrum on the unit circle, trivial one marked, drawn with the line primitive

The multipliers joined as a line.

import tsdynamics as ts

vdp = ts.systems.VanDerPol()

ts.plot(vdp, "floquet_multipliers", primitive="line")

Compositions

Nothing above is a special case: a transform's geometry declares the coordinate space it lives in, and any two that agree can share one set of axes. That is what makes the overlays below one call rather than a figure-assembly exercise — and it is also why an (x, z) portrait refuses (x, y) equilibria, and why the (tr J, det J) plane refuses a state space:

InvalidParameterError: cannot overlay frame 'param2' (diagnostic_curve) on frame 'state2'
(phase_portrait_2d): they are drawings of different spaces. Use layout='stack' / 'row' / 'grid'
to give each its own panel.

The phase-plane payoff

The phase-plane payoff

Four things on one axes, from three different transform families: the direction field, both nullclines, and an integrated orbit. The equilibrium is where the nullclines cross, the orbit is tangent to the arrows everywhere, and the relaxation loop rides the outer branches of the cubic. Draw order is by role — field under curve — so the arguments could be given in any order.

import tsdynamics as ts

fhn = ts.systems.FitzHughNagumo()
traj = fhn.run(final_time=120.0, dt=0.05, ic=[-1.0, -0.5])

ts.viz.plot(
    ts.plot(fhn, "direction_field", "nullclines",
            xlim=(-2.5, 2.5), ylim=(-1.0, 2.0), grid=21),
    ts.plot(traj, "phase_portrait"),
)

Speed under streamlines

Speed under streamlines

One call, two model transforms, one shared window: the speed field as the backdrop and its integral curves on top. A shared keyword is routed only to the transforms that accept it — log= reaches flow_speed, seeds= reaches streamlines, and neither gets the other's.

import tsdynamics as ts

vdp = ts.systems.VanDerPol(mu=2.0)

ts.plot(vdp, "flow_speed", "streamlines",
        xlim=(-3.0, 3.0), ylim=(-6.0, 6.0), log=True, seeds=5, steps=120)

A separatrix, twice

A separatrix, twice

An orbit drawn on the FTLE field of its own system. The orbit spirals inside one well and never crosses the bright ridge — which is the claim the ridge makes: it is the separatrix, computed from the variational dynamics, not from watching where orbits end up.

import tsdynamics as ts

duff = ts.systems.Duffing(gamma=0.0)
traj = duff.run(final_time=60.0, dt=0.01, ic=[1.4, 0.0, 0.0])

ts.viz.plot(
    ts.plot(duff, "ftle", xlim=(-2.0, 2.0), ylim=(-1.5, 1.5),
            grid=161, final_time=8.0),
    ts.plot(traj, "phase_portrait", components=("x", "y"), color="w"),
)

Adding a plot

A new transform is one registration and nothing else — no renderer edit, no PlotKind edit, no compose edit, no test edit:

Four declarations and a function that returns a mapping. Nothing else in the library is edited:

# skip-doctest — a template: `measure` is yours, and registering a placeholder
# transform here would put it in every listing for the rest of the session
import tsdynamics as ts

@ts.viz.transforms.register(source="data",              # or "model"
                            frame="scaling",
                            kind="scaling_curve",
                            primitives=("line", "points"))
def my_diagnostic(subject, *, option=1.0):
    """One line — it becomes the summary in the matrix above."""
    r, c = measure(subject, option)
    return {"x": r, "y": c}              # a plain channel mapping — no IR type

Everything else is derived: the name from the function, the summary from the docstring, the axis count from the frame's arity, the default primitive from the first one listed, the accepted subjects from source. The optional keywords are name=, default_primitive=, aliases=, labels=, role=, analysis= (the estimator this adapts) and example= (a small subject the gallery can draw).

The registration carries the compatibility row, so tests/test_viz_compatibility.py starts rendering every cell of it, and this gallery starts showing it — on the small fixture the example= factory returns, until someone curates a nicer subject in docs/_tooling/gallery.py. The generator prints any uncurated transform at the end of a run, so a new one is visible the moment it lands.

Out of tree, declare the same function against the tsdynamics.plot_transforms entry-point group and it joins the registry — and this page — on import.