Source code for lyapax.systems
"""Benchmark systems for validating the Lyapunov engine.
Plain flat-vector JAX functions, deliberately independent of the vendored
ModelSpec/coupling machinery in ``lyapax.simulator`` - these are single-node,
uncoupled textbook systems, so routing them through the string-expression
codegen built for networked models would only add indirection. Each entry
here corresponds to a tier of the validation suite; see
:doc:`/background/validation` for the reference Lyapunov values and
citations.
Tier 4 (DDE) entries have a different shape than the rest: ``rhs_delayed
(state_now, state_delayed) -> dstate`` instead of ``rhs(state) -> dstate``,
since they need both the current and the delayed state (see
``lyapax.dde.DelayedRHS``). Pair with
``lyapax.dde.make_scalar_delayed_step_fn``, not the plain integrators in
``lyapax.integrators`` -- mirrors this module's relationship to
``lyapax.integrators.rk4_step`` for the ODE case: plain math here, a
step-function builder elsewhere.
"""
from __future__ import annotations
from typing import Callable
import jax.numpy as jnp
RHS = Callable[[jnp.ndarray], jnp.ndarray]
Step = Callable[[jnp.ndarray], jnp.ndarray]
# ---------------------------------------------------------------------------
# Tier 0 - exact analytic references
# ---------------------------------------------------------------------------
[docs]
def linear_system(A: jnp.ndarray) -> RHS:
"""rhs(x) = A @ x. Exact Lyapunov spectrum = Re(eigvals(A)) (Tier 0.1)."""
A = jnp.asarray(A)
def rhs(x: jnp.ndarray) -> jnp.ndarray:
return A @ x
return rhs
[docs]
def logistic_map(r: float) -> Step:
"""x_{n+1} = r x (1-x). At r=4, exact LE = ln(2) (Tier 0.2)."""
def step(x: jnp.ndarray) -> jnp.ndarray:
return r * x * (1.0 - x)
return step
[docs]
def tent_map() -> Step:
"""Exact LE = ln(2) (Tier 0.2)."""
def step(x: jnp.ndarray) -> jnp.ndarray:
return jnp.where(x < 0.5, 2.0 * x, 2.0 * (1.0 - x))
return step
[docs]
def henon_map(a: float = 1.4, b: float = 0.3) -> Step:
"""Constant Jacobian determinant -b => sum(LE) = ln|b| exactly (Tier 0.3)."""
def step(state: jnp.ndarray) -> jnp.ndarray:
x, y = state[0], state[1]
return jnp.array([1.0 - a * x * x + y, b * x])
return step
# ---------------------------------------------------------------------------
# Tier 1/2 - chaotic flows (structural invariants + published values)
# ---------------------------------------------------------------------------
[docs]
def lorenz(sigma: float = 10.0, rho: float = 28.0, beta: float = 8.0 / 3.0) -> RHS:
"""Constant trace(J) = -(sigma+1+beta) => sum(LE) exactly known (Tier 1.1);
lambda1 has a well-documented published value (Tier 2)."""
def rhs(state: jnp.ndarray) -> jnp.ndarray:
x, y, z = state[0], state[1], state[2]
dx = sigma * (y - x)
dy = x * (rho - z) - y
dz = x * y - beta * z
return jnp.array([dx, dy, dz])
return rhs
[docs]
def rossler(a: float = 0.2, b: float = 0.2, c: float = 5.7) -> RHS:
"""trace(J) = a + x - c (state-dependent) => sum(LE) = a - c + <x>_t
(Tier 1.2); lambda1 has a published order-of-magnitude value (Tier 2)."""
def rhs(state: jnp.ndarray) -> jnp.ndarray:
x, y, z = state[0], state[1], state[2]
dx = -y - z
dy = x + a * y
dz = b + z * (x - c)
return jnp.array([dx, dy, dz])
return rhs
# ---------------------------------------------------------------------------
# Tier 4 - DDE benchmarks: rhs_delayed(state_now, state_delayed) -> dstate,
# both shape (1,). Pair with lyapax.dde.make_scalar_delayed_step_fn.
# ---------------------------------------------------------------------------
[docs]
def mackey_glass(beta: float = 0.2, gamma: float = 0.1, n: float = 10.0):
"""x'(t) = beta*x(t-tau)/(1+x(t-tau)^n) - gamma*x(t). The classic
beta=0.2, gamma=0.1, n=10, tau=17 parameter set is chaotic -- the
system Farmer (1982) used to introduce the discretized-map method for
DDE Lyapunov spectra (Tier 4.1)."""
def rhs(x_now: jnp.ndarray, x_delayed: jnp.ndarray) -> jnp.ndarray:
return beta * x_delayed / (1.0 + x_delayed ** n) - gamma * x_now
return rhs
[docs]
def linear_scalar_dde(a: float = 1.0):
"""x'(t) = -a*x(t-tau). Dominant Lyapunov exponent solves the
transcendental characteristic equation lambda = -a*exp(-lambda*tau)
(Lambert W: lambda = W(-a*tau)/tau) -- a closer-to-analytic reference
for isolating DDE tangent-propagation bugs from Mackey-Glass's
nonlinear chaos (Tier 4.2)."""
def rhs(x_now: jnp.ndarray, x_delayed: jnp.ndarray) -> jnp.ndarray:
return -a * x_delayed
return rhs