Benchmarks#
How lyapax’s Lyapunov-exponent estimates and wall-clock performance
compare against other tools that compute the same quantity, on the same
benchmark systems lyapax’s own test suite is validated against (see
Validation: how lyapax’s results are checked).
The tables on this page are generated, not hand-written, and are refreshed periodically rather than on every commit or doc build - treat any specific number below as a snapshot, not a live guarantee. To reproduce or refresh them yourself:
python benchmarks/collect_results.py # runs every tool, writes benchmarks/results.json
python benchmarks/report_tables.py --write # regenerates the tables below from that file
collect_results.py needs optional dependencies this doc build doesn’t
carry (a Julia install with ChaosTools.jl, jitcode/jitcdde, and,
for the GPU column, an actual GPU) - see
benchmarks/collect_results.py’s module docstring for the full
requirements. That’s also why these tables are committed as a static
snapshot (benchmarks/results.json) instead of being executed as part
of building this documentation.
Comparison targets#
jitcode - the established Python reference for ODE Lyapunov spectra (symbolic → compiled C).
jitcdde - the DDE counterpart, and the closest existing precedent for
lyapax’s own delayed-system engine.-
the most widely used dynamical-systems toolbox in Julia; covers both continuous and discrete (map) systems.
Published/analytic references - exact eigenvalues, Lambert-W roots, or literature values, wherever a benchmark system has one.
None of these share lyapax’s exact integration scheme or default
tolerances, so this is a cross-validation of the computed exponents
against independent implementations, not a claim that every tool is
configured identically. Step size, transient length, total run length,
and QR-renormalization interval are matched as closely as each tool’s
API allows; where a tool’s own defaults differ meaningfully (e.g.
adaptive step control), that’s left as-is rather than silently
normalized away.
Accuracy#
Each row compares the estimated Lyapunov exponent(s) (or, for chaotic
systems without a closed-form spectrum, a structural invariant like
sum(λ)) against an exact or published reference, across whichever
tools support that system class (map-only systems have no jitcode
column since it’s ODE-only; DDE systems have no ChaosTools.jl column
since it has no delay-equation support).
ODE systems#
System |
lyapax |
lyapax (RK6) |
jitcode |
ChaosTools.jl |
ChaosTools.jl (RK4) |
ChaosTools.jl (Vern6) |
Reference |
Notes |
|---|---|---|---|---|---|---|---|---|
Linear ODE (Tier 0.1) |
|
|
|
|
|
|
exact |
max abs diff from reference – lyapax: |
Lorenz λ1 (Tier 1.1/2) |
|
|
|
|
|
|
published |
max abs diff from reference – lyapax: |
Lorenz sum(λ) |
|
|
|
|
|
|
exact |
max abs diff from reference – lyapax: |
Rössler λ1 (Tier 1.2/2) |
|
|
|
|
|
|
qualitative |
max abs diff from reference – lyapax: |
4-node linear network (Tier 3.1) |
|
|
|
|
|
|
exact |
max abs diff from reference – lyapax: |
Map systems#
System |
lyapax |
ChaosTools.jl |
Exact |
Notes |
|---|---|---|---|---|
Logistic map |
|
|
|
max abs diff from reference – lyapax: |
Tent map (Tier 0.2) |
|
|
|
max abs diff from reference – lyapax: |
Hénon map, sum(λ) (Tier 0.3) |
|
|
|
max abs diff from reference – lyapax: |
DDE systems#
System |
lyapax |
lyapax (RK6) |
jitcdde |
Reference |
Notes |
|---|---|---|---|---|---|
Linear scalar DDE (Tier 4.2) |
|
|
|
Lambert W root |
max abs diff from reference – lyapax: |
Mackey-Glass λ1 (Tier 4.1) |
|
|
|
qualitative |
lyapax inside band; lyapax (RK6) inside band; jitcdde inside band |
Mackey-Glass KY dimension |
|
|
|
|
lyapax inside band; lyapax (RK6) inside band; jitcdde inside band |
2-node delayed linear network (Tier 4.3) |
not yet run |
not yet run |
not yet run |
Lambert W root (2x2) |
deferred – not yet run against jitcdde |
Performance#
Steady-state per-call wall-clock time (each tool’s JIT/compile step has
already run by the time these numbers are measured). The GPU columns
re-run the same lyapax scripts with a CUDA backend rather than CPU - see
benchmarks/collect_results.py for how that pass is skipped (with a
warning, not a failure) when no working GPU is found.
The default ChaosTools.jl column uses its own idiomatic choice
(Tsit5, adaptive step control) - a different algorithm from lyapax’s
fixed-step rk4/rk6, so that timing isn’t a same-method comparison
(see the “Comparison targets” fairness note above). The ChaosTools.jl (RK4) and ChaosTools.jl (Vern6) columns close that gap: they run
OrdinaryDiffEq.jl’s RK4() and Vern6() (the exact tableau lyapax’s
rk6_step implements - see lyapax.integrators.rk6_combine’s
docstring) with adaptive=false at lyapax’s own fixed dt, so the two
sides are running the literal same algorithm and step size. The gap
that remains at that point is a genuine implementation/runtime
difference (Julia/LLVM vs. JAX/XLA at these small state sizes), not an
algorithm mismatch.
System |
lyapax |
lyapax (RK6) |
lyapax (GPU) |
lyapax (RK6, GPU) |
jitcode |
jitcdde |
ChaosTools.jl |
ChaosTools.jl (RK4) |
ChaosTools.jl (Vern6) |
|---|---|---|---|---|---|---|---|---|---|
Linear ODE (Tier 0.1) |
|
|
|
|
|
– |
|
|
|
Lorenz |
|
|
|
|
|
– |
|
|
|
Rössler |
|
|
|
|
|
– |
|
|
|
4-node network (Tier 3.1) |
|
|
|
|
|
– |
|
|
|
Linear scalar DDE (Tier 4.2) |
|
|
|
|
– |
|
– |
– |
– |
Mackey-Glass |
|
|
|
|
– |
|
– |
– |
– |
The plot below is generated from the same benchmarks/results.json
snapshot as the tables above; regenerate it with
python benchmarks/report_plots.py. It’s every tool’s steady-state
wall-clock time per system, log-scaled since the range spans several
orders of magnitude – jitcode/jitcdde are the tools closest to
lyapax’s own execution model (a one-time trace/compile, then a tight
numerical loop for every call after); ChaosTools.jl has near-zero
per-call overhead on these small toy systems and is the fastest tool
for most of them regardless.

Network-size scaling#
Every system above is small (3–4 state dimensions) - deliberately, so
each has an exact or published reference to validate against. But it
also means none of them exercise the part of lyapax’s design meant for
bigger problems. This section uses one further system, a dense
(all-to-all) Kuramoto network, at increasing size d, tracking a fixed
k=5 partial spectrum - the same system as
10_matrix_free_scaling.py /
14_gpu_acceleration.py, run
here across tools instead of just lyapax CPU vs. GPU. It’s a
performance-only comparison - a short, no-transient run, not validated
against a reference spectrum the way the tables above are - so read the
timings, not the exponent values.
lyapax’s cost here is dominated by k, not d. jax.jvp computes
only the k=5 tangent directions actually requested, regardless of how
large the network is (see lyapax.core.lyapunov_spectrum’s docstring).
That’s why ChaosTools.jl and jitcode - both of which stop well short
of lyapax’s full d=50/200/1000/2000 sweep - aren’t a same-algorithm
comparison being run partway; they’re at (or past) their own scaling
limit:
ChaosTools.jlstops atd=200(using the fixed-stepRK4()established above as the fair algorithm to compare). Its tangent propagation forms the full densed x dJacobian viaForwardDiffevery step, regardless ofk- the opposite oflyapax’s matrix-free approach. The table below shows this cost growing far worse than quadratically betweend=50andd=200; a direct test atd=1000was abandoned after running well past ten minutes without finishing a single call. This is a genuine per-call cost, not a one-time compile tax - a direct comparison of that samed=200run’s first and warm calls came out within a few percent of each other, unlike a case dominated by JIT/compile overhead.jitcodestops atd=50for a different reason: it differentiates the right-hand side symbolically before compiling to C, and that compile step alone took on the order of a minute for a densed=50network (~2,450 nonzero coupling terms) in a direct test, before a single integration step ran - a cost the table below doesn’t show, since it (like the rest of this page) reports steady-state, post-compile time only. UnlikeChaosTools.jl’s bottleneck, this one is front-loaded - once compiled,jitcode’s own steady-state call is fast - but the compile cost itself scales with network density and would only be worse atd=200+.
lyapax pays neither cost at any of these sizes: no symbolic
differentiation step to compile, and no dense Jacobian to form at
run time.
Network size (d) |
lyapax |
lyapax (GPU) |
ChaosTools.jl (RK4) |
jitcode |
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
not attempted |
|
|
|
not attempted |
not attempted |
|
|
|
not attempted |
not attempted |
The d=1000/d=2000 rows are also where lyapax’s GPU backend earns
its keep - unlike the small 3–4-dimensional systems above, where GPU
lost outright to CPU (see the performance table’s GPU columns), a dense
d=2000 network’s per-step arithmetic is large enough to amortize GPU
dispatch overhead.
