Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Lorenz '96 — High-Dimensional Chaos

Authors
Affiliations
University of Valencia

The Lorenz '96 system is a periodic 1D lattice of NN coupled variables driven by a constant forcing FF. It is a workhorse model for testing data assimilation and ensemble forecasting methods.

What you’ll learn:

  1. How to set up and run a high-dimensional chaotic ODE in somax

  2. How the Hovmoller diagram reveals wave-like propagation

  3. How forcing strength FF controls the transition to chaos

  4. How to compute gradients and run ensembles at scale

Background

The system consists of NN variables XkX_k on a periodic ring:

dXkdt=(Xk+1−Xk−2) Xk−1⏟advection−Xk+Fk=1,…,N\frac{dX_k}{dt} = \underbrace{(X_{k+1} - X_{k-2})\, X_{k-1}}_{\text{advection}} - X_k + F \qquad k = 1, \ldots, N

with periodic boundary conditions Xk+N=XkX_{k+N} = X_k.

  • The quadratic term mimics nonlinear advection (energy-conserving).

  • The linear damping −Xk-X_k dissipates energy.

  • The constant forcing FF injects energy.

At F=8F = 8 the system is fully chaotic with a doubling time of approximately 0.42 time units (about 2.1 days if one time unit equals 5 days).

1. Create the model

We use N=360N = 360 variables for a smooth Hovmoller diagram. Lorenz’s original choice was N=40N = 40; the higher resolution reveals finer-scale wave structure on the periodic ring.

Lorenz96(params=L96Params(F=weak_f32[]))

2. Forward simulation

The initial condition is a small perturbation of the FF-equilibrium state Xk=FX_k = F for all kk. This is an unstable fixed point of the system — the perturbation grows and the trajectory settles onto the chaotic attractor.

Trajectory shape: (4000, 360)

3. Hovmoller diagram

A Hovmoller plot shows the state as a function of position (x-axis) and time (y-axis). The tilted bands reveal wave-like structures propagating around the periodic ring.

<Figure size 1000x600 with 2 Axes>

4. Time series and statistics

Individual variables look like noisy oscillations. The climatological mean is close to FF and the variance grows with forcing strength.

<Figure size 1200x600 with 2 Axes>

5. Diagnostics

model.diagnose() returns the total energy E=12∑kXk2E = \tfrac{1}{2} \sum_k X_k^2 and the spatial mean.

<Figure size 1200x350 with 2 Axes>

6. Forcing regimes

The Lorenz '96 system transitions from steady state to periodic to chaotic as FF increases. We compare three regimes.

<Figure size 1500x400 with 3 Axes>

7. Adjoint methods and differentiation

diffrax supports several adjoint methods for backpropagating through diffeqsolve. Each trades off memory, accuracy, and speed:

AdjointMemoryGradientsUse case
RecursiveCheckpointAdjointO(N)O(\sqrt{N})ExactDefault
DirectAdjointO(N)O(N)ExactShort windows
BacksolveAdjointO(1)O(1)ApproxLong windows
ImplicitAdjointO(1)O(1)ExactSteady states
  • RecursiveCheckpointAdjoint (the default) uses optimal checkpointing: re-computes forward steps from saved checkpoints, giving exact gradients with O(N)O(\sqrt{N}) memory.

  • DirectAdjoint stores the entire forward trajectory — exact but O(N)O(N) memory. Also supports forward-mode AD.

  • BacksolveAdjoint solves the continuous adjoint ODE backwards with O(1)O(1) memory. Gradients are approximate (optimize-then-discretize).

  • ImplicitAdjoint uses the implicit function theorem: du∗/dθ=−(dg/du)−1 dg/dθdu^*/d\theta = -(dg/du)^{-1}\, dg/d\theta. Exact and O(1)O(1) memory, but only for steady-state / fixed-point solves.

7a. Gradient w.r.t. parameters

Use case: parameter estimation. Find the forcing FF that minimizes a cost function over the trajectory:

∂L∂F=∫0Tλ(t)⊤∂f∂F dt\frac{\partial \mathcal{L}}{\partial F} = \int_0^T \lambda(t)^\top \frac{\partial f}{\partial F}\, dt
--- Gradient w.r.t. parameters ---
  dL/dF = 3630.4766

7b. Gradient w.r.t. initial state

Use case: 4D-Var data assimilation. Find the initial condition X0\mathbf{X}_0 that best fits observations. The gradient is the adjoint state at t=0t = 0:

∂L∂X0=λ(0)\frac{\partial \mathcal{L}}{\partial \mathbf{X}_0} = \lambda(0)
--- Gradient w.r.t. initial state ---
  dL/d(X0) shape: (360,)
  dL/d(X0) norm:  4228.1538
  dL/d(X0)[:5]:   [-1638.6827   -413.43512  1382.2256   1065.4902   -724.1401 ]

7c. Joint gradient — parameters and state

Use case: weak-constraint 4D-Var / bi-level optimization. Simultaneously optimize the initial condition and model parameters.

--- Joint gradient ---
  dL/dF         = 3630.4766
  dL/d(X0) norm = 4228.1538

7d. Comparing adjoint methods

We time RecursiveCheckpointAdjoint vs DirectAdjoint. BacksolveAdjoint requires a different API pattern (explicit args to diffeqsolve, not via closure) — see the diffrax docs.

--- Adjoint method comparison ---
  RecursiveCheckpoint        dL/dF=3630.48  (3.6390s)
  Direct                     dL/dF=3630.47  (2.5357s)

8. Ensemble forecast divergence

We launch 30 ensemble members from perturbed initial conditions and measure the ensemble spread over time — the hallmark of deterministic chaos.

<Figure size 1300x400 with 2 Axes>

Summary

Conceptsomax API
Create a modelLorenz96.create(F=8.0)
Initial conditionL96State.init_state(ndim=40, F=8.0)
Forward simulationmodel.integrate(state0, t0, t1, dt, saveat=...)
Diagnosticsmodel.diagnose(state) — energy, mean
Grad w.r.t. paramseqx.filter_grad(loss)(model) — dL/dF
Grad w.r.t. statejax.grad(loss)(state0) — dL/dX0
Joint gradjax.grad(loss, argnums=(0, 1))(state0, model)
Ensembleeqx.filter_vmap(integrate_one)(batch_states)

Key takeaways:

  • F=8F = 8 produces fully developed chaos with a doubling time of ~0.42 time units

  • The Hovmoller diagram reveals eastward-propagating wave packets

  • Ensemble spread saturates after ~4 time units (the predictability horizon)