Source code for lyapax.sweep

"""Batched parameter/initial-condition sweeps via ``jax.vmap``.

Mirrors vbi's ``JaxSweeper`` pattern (batch a simulation over a grid of
parameter values) without reusing its code, per the vendoring decision.
The surface needed here is much smaller than a
general sweeper: ``lyapax.simulator.make_step_fn``'s carry already threads
``params`` through as data rather than closing over it (see that function's
docstring), so a parameter
sweep is just ``jax.vmap`` applied to a function that calls
``lyapax.core.lyapunov_spectrum`` once, with ``params`` (and optionally
``state0``) as the vmapped argument. No new tangent-propagation or
QR machinery -- this is a batching wrapper around the existing engine, not
a third one.

The one piece of the network path that still closed over ``params`` at
construction time was ``lyapax.network.make_network_step_fn`` (a thin
adapter, not the tangent engine); ``make_parametrized_network_step_fn``
(in that module) is the call-time-``params`` sibling this needs.
"""
from __future__ import annotations

from typing import Callable

import jax
import jax.numpy as jnp

from .core import LyapunovResult, lyapunov_spectrum

StepFlatParametrized = Callable[[jnp.ndarray, dict], jnp.ndarray]


[docs] def sweep_lyapunov_spectrum( step_fn: StepFlatParametrized, state0: jnp.ndarray, params_batch: dict, dt: float, n_steps: int, k: int | None = None, renorm_every: int = 1, t_transient: float = 0.0, seed: int = 0, state0_batch: jnp.ndarray | None = None, ) -> LyapunovResult: """ Batched ``lyapunov_spectrum`` over a grid of parameter values (and, optionally, a matching grid of initial conditions) via one ``jax.vmap`` call, rather than a Python loop making one call per grid point (as in e.g. ``examples/05_kuramoto_sync.py``/``09_kuramoto_delayed_network.py``). step_fn : ``(state, params) -> new_state`` -- ``params`` must be a call-time argument, *not* closed over. See ``lyapax.network.make_parametrized_network_step_fn`` for the network-coupling case; a hand-written function with this signature works too (same "plain callable, no registry" extension point as ``lyapax.coupling``). state0 : (d,) initial state, shared across the whole sweep, unless ``state0_batch`` is given. params_batch : pytree matching one ``params`` dict's structure, but every leaf carries an extra leading batch axis of size ``n_sweep`` -- e.g. ``{"G": jnp.linspace(0, 4, 13), "omega": jnp.broadcast_to(omega, (13, n_nodes))}`` to sweep ``G`` at fixed ``omega``. dt, n_steps, k, renorm_every, t_transient, seed : see ``lyapax.core.lyapunov_spectrum`` - shared (not swept) across the whole grid. state0_batch : optional ``(n_sweep, d)`` initial conditions, one row per grid point, if the initial condition should vary alongside ``params_batch`` rather than staying fixed at ``state0``. Returns ------- LyapunovResult, every field with an extra leading batch axis of size ``n_sweep`` -- e.g. ``result.exponents.shape == (n_sweep, k)``. Examples -------- Sweep the coupling strength ``G`` of a Kuramoto network in one ``jax.vmap`` call instead of a Python loop over ``lyapunov_spectrum``: >>> import jax >>> jax.config.update("jax_enable_x64", True) >>> import jax.numpy as jnp >>> from lyapax.coupling import kuramoto_coupling >>> from lyapax.network import Network, network_step_parametrized >>> from lyapax.simulator import ModelSpec, StateVar, Parameter, build_jax_dfun >>> from lyapax.sweep import sweep_lyapunov_spectrum >>> n_nodes = 6 >>> omega = jnp.linspace(-1.0, 1.0, n_nodes) >>> weights = jnp.ones((n_nodes, n_nodes)) - jnp.eye(n_nodes) >>> model = ModelSpec( ... name="kuramoto", ... state_variables=(StateVar("theta", default_init=0.0),), ... parameters=(Parameter("omega", 0.0),), ... cvar=("theta",), ... dfun_str={"theta": "omega + c"}, ... ) >>> dfun = build_jax_dfun(model) >>> network = Network(weights=weights, cvar_indices=model.cvar_indices) >>> dt = 1e-2 >>> state0 = jnp.linspace(0.0, 2 * jnp.pi, n_nodes, endpoint=False) >>> G_values = jnp.linspace(0.0, 4.0, 13) >>> step_p = network_step_parametrized(dfun, network, kuramoto_coupling(alpha=0.0), dt) >>> params_batch = {"omega": jnp.broadcast_to(omega, (13, n_nodes)), "G": G_values} >>> result = sweep_lyapunov_spectrum( ... step_p, state0, params_batch, dt=dt, n_steps=5_000, ... renorm_every=10, k=2, t_transient=50.0, ... ) >>> result.exponents.shape # doctest: +SKIP (13, 2) """ def _run_one(params, state0_i): return lyapunov_spectrum( lambda state: step_fn(state, params), state0_i, dt, n_steps, k=k, renorm_every=renorm_every, t_transient=t_transient, seed=seed, ) if state0_batch is None: return jax.vmap(_run_one, in_axes=(0, None))(params_batch, state0) return jax.vmap(_run_one, in_axes=(0, 0))(params_batch, state0_batch)