Regime classification
A \(k\)-torus carries exactly \(k\) neutral exponents (\(|\lambda| <\) neutral_tol)
and no positive one; chaos replaces one neutral direction with a positive
exponent. Together with \(D_2\) on the attractor (\(\approx k\)) and on the
section (\(\approx k-1\)) this fixes the torus dimension; the Lorenz96
Ruelle–Takens–Newhouse route is the worked example.
Decision tree
- Fixed point — signal variance collapses (no sustained oscillation), or the full Lyapunov spectrum is entirely negative (decisive when integrator noise keeps the tail wiggling at a stable focus, e.g. Lorenz-63 at \(\rho\)=10: \(\lambda\) = [-0.6, -0.6, -12.5]).
- Limit cycle, period-k — return-map local maxima form k \(\leq\)
k_maxdistinct tight clusters. Checked before the Lyapunov step: k tight maxima levels over a long record are incompatible with chaos, whereas a perturbation-growth \(\lambda_1\) can read large and positive on a stable orbit through non-normal transient amplification (Rijke limit cycles read \(\lambda_1\) ~ 200 otherwise;leading_lyapunovadditionally rejects fits whose saturation level sits far below the attractor diameter). - Chaotic — \(\lambda_1\) positive beyond the trust tolerance.
characterize()classifies with the Benettin spectrum's \(\lambda_1\) whenever a spectrum was computed (it is convergence-controlled, std \(\approx\) 0), and with the perturbation-growth \(\lambda_1\) otherwise (whose member spread sets the 3\(\sigma\) part of the tolerance). - Otherwise: if a full spectrum is available, \(\geq\) 2 neutral exponents (\(|\lambda|\) < 2e-3) \(\to\)
quasiperiodic (a 2-torus has two zero exponents; a locked periodic orbit exactly
one — more robust than the PSD ratio test, whose
limit_denominator(10)rationals are dense enough to "match" incommensurate ratios, e.g. L96 F=4.4: \(f_2/f_1\) = 0.552 \(\approx\) 5/9). Else dominant PSD frequencies rationally related \(\to\) frequency-locked; incommensurate \(\to\) quasiperiodic.
Caveat: the period-k count is the number of distinct maxima levels per cycle. A period-1 orbit whose waveform has several humps (e.g. the Rijke tube near its Hopf point: one large and one small maximum per acoustic period) is reported as period-k of the hump count. Disambiguate with the PSD in the panel text box: a genuine period-doubling shows a new subharmonic at f1/2; a multi-humped period-1 orbit shows only the harmonic comb of f1 (see the Rijke verification scripts in the romda repo, beta=0.4).
Bifurcation diagram: Lorenz-96 F-route
Local extrema of the observables against the swept parameter summarize the whole route the
decision tree walks — bifurcation_sweep runs the sweep as one ensemble, plot_bifurcation
draws it:
import numpy as np
from dynamodels.physical import Lorenz96
from ntsa.bifurcation import bifurcation_sweep, plot_bifurcation
model = Lorenz96(Nx=10)
values, peaks = bifurcation_sweep(model, 'F', np.linspace(0.5, 8., 120), extrema=('max', 'min'))
plot_bifurcation(values, peaks, model.alpha_labels['F'], model.obs_labels)

Read with the decision tree: maxima and minima coincide on the stable fixed point (\(F \lesssim 0.9\)); the Hopf bifurcation opens one tight level per branch (period-1 limit cycle); a second minima level appears near \(F \approx 3.6\) (a multi-humped waveform — the period-k caveat above); the levels smear into bands on the torus (\(F \approx 4.3\)) and fill out into broad scatter in chaos (\(F \gtrsim 4.6\)). The torus page walks this same route case by case. The three panels repeat the story at \(x_0\), \(x_5\), \(x_9\): the route is site-independent by the model's cyclic symmetry.
Measured \(\lambda_1\) tables in the model files
The dominant Lyapunov exponents measured here are stored in the dynamodels
physical-model files (dynamodels/physical/{lorenz63,lorenz96,rijke}.py): a per-model
_LAM1_MEASURED dict of chaotic parameter points, from which __init__ sets the
instance's t_lyap = 1/$\lambda_1$ via Model.t_lyap_from_table (log-interpolated at the
constructed sweep-parameter value). Class attributes keep the historical constants, so downstream users of the
class constants (e.g. romda's scripts/mains) are unaffected; off-table configurations (limit cycles, tori, L96 with
Nx != 10) keep them too. Values set once at construction — parameters change by
re-instantiation. Extend the tables with {param: $\lambda_1$} pairs from converged runs;
covered by test_t_lyap_tables in the dynamodels test suite.
Defaults
| Quantity | Default |
|---|---|
| Delay \(\zeta\) | First local minimum of the average mutual information |
| Embedding dimension d | Kennel FNN with Rtol=10, Atol=2, threshold 1% |
| Recurrence threshold \(\varepsilon\) | 10% of the max pairwise distance; the panel retargets to a fixed 10% recurrence rate (rr=0.10 quantile) when the density degenerates |
| Zoom inset / recurrence window | ~5 / ~10 mean inter-maximum intervals (recurrence window clipped to [500, 2000] samples) |
| MDS / recurrence subsample | 2000 points max |
| \(\lambda_1\) trust tolerance | \(\max(3\sigma,\ 10/T)\) (\(\lambda_1\) must exceed this to call chaos) |
| Spectrum integrator | RK4, dt = min(model.dt, 0.01) |
| Spectrum horizon | floor 200 * t_ref, extended (up to t_max = 20 * floor) until \(\lvert\lambda(T) - \lambda(T/2)\rvert < \max(\text{atol}, \text{rtol}\cdot\lvert\lambda\rvert)\) (atol = 2e-3, rtol = 5%) at two consecutive checks; warns if capped unconverged |
| Spectrum / leading-\(\lambda_1\) warmup | _settled_state: transient doubled (up to \(4\times\)) while stationary_start still sees amplitude drift in a 20*t_ref probe |
Run horizon (characterize) |
100 * model.t_CR, after discarding model.t_transient; residual drift trimmed by stationary_start |
Notes
- The spectrum's neutral (along-flow) exponent converges to 0 only as O(1/T), so
lyapunov_spectrumextends its horizon until the halving test passes: on a limit cycle the reported \(\lambda_1\) lands within ~atol–2atolof zero (VdP: +0.005 at the auto-chosen T\(\approx\)31 with the defaultatol=2e-3; +8e-4 withatol=5e-4, which runs to thet_maxcap). Tightenatol(and raiset_max) for a cleaner zero; classification is unaffected (the \(\lambda_1\) trust floor \(\max(3\sigma,\ 10/T)\) sits far above it). - The same O(1/T) bias is why a still-settling initial condition used to inflate \(\lambda_1\) on
limit cycles: both
lyapunov_spectrumandleading_lyapunovnow warm up with_settled_state, which doubles the discarded transient until a probe run shows no amplitude drift (critical near Hopf points, where the decay rate vanishes and no fixed transient multiple is safe). covariant_lyapunov_vectorsis a Ginelli (2007) TODO stub.lyapunov_spectrumneedstime_derivative(continuous models);leading_lyapunovworks for any Model (including discrete maps such as ESN or KS).characterize(..., spectrum=False)still estimates the dominant exponent automatically: the independentlyapunovswitch (default'auto') runsleading_lyapunov, which is based on perturbation growth alone — no Jacobian or tangent operator required.spectrumonly controls the full Benettin QR pass; the spectrum's \(\lambda_1\) is used by the classification only as a fallback when the growth fit is rejected.- Tutorial notebook:
tutorial_ntsa.ipynb(full walkthrough on Lorenz63 + Lorenz96, executed). - All randomness is seeded (
np.random.default_rng(seed)).
References are collected on the theory overview.