Utilities & Diagnostics¶
Supporting cast: a reference dynamical system for experiments, the statistical gates that decide whether an inference setup can be trusted, and the plotting helpers used throughout the end-to-end examples.
Dynamical systems¶
Lorenz-63 and Lorenz-96 are the standard chaotic testbeds for
assimilation experiments; the simulate_* helpers generate trajectories
for the Lorenz examples and the test suite,
and time_patches produces the consecutive time pairs consumed by the
one-step increment losses.
vardax — Modular variational data assimilation with learned components.
All public symbols are re-exported from the private _src subpackage so
that user code imports from the top-level namespace:
Lorenz63
¶
Bases: Module
Lorenz-63 vector field as an Equinox module.
Implements the system
The module is a diffrax-compatible right-hand side f(t, y, args).
Attributes:
| Name | Type | Description |
|---|---|---|
sigma |
float
|
Prandtl number \(\sigma\) (classically 10). |
rho |
float
|
Rayleigh number \(\rho\) (classically 28). |
beta |
float
|
Geometric factor \(\beta\) (classically 8/3). |
Examples:
The vector field at \((1, 1, 1)\) for the classical parameters:
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> f = vdx.Lorenz63(sigma=10.0, rho=28.0, beta=8.0 / 3.0)
>>> dydt = f(0.0, jnp.ones(3), None)
>>> (
... dydt.shape,
... bool(jnp.allclose(dydt, jnp.array([0.0, 26.0, 1.0 - 8.0 / 3.0]))),
... )
((3,), True)
The origin is a fixed point:
It drives a DynTrajectory prior:
Source code in src/vardax/_src/utils/dynamical_systems.py
Lorenz96
¶
Bases: Module
Lorenz-96 vector field as an Equinox module.
Implements the system (with periodic boundary conditions)
The module is a diffrax-compatible right-hand side f(t, y, args).
Attributes:
| Name | Type | Description |
|---|---|---|
forcing |
float
|
Forcing constant \(F\) (classically 8). |
Examples:
The uniform state \(x_k = F\) is a fixed point, and at the origin the field equals the forcing:
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> f = vdx.Lorenz96(forcing=8.0)
>>> bool(jnp.allclose(f(0.0, jnp.full(6, 8.0), None), 0.0))
True
>>> bool(jnp.allclose(f(0.0, jnp.zeros(6), None), 8.0))
True
It maps an \(N\)-vector to an \(N\)-vector, for any \(N\):
It drives a DynTrajectory prior:
>>> ts = jnp.linspace(0.0, 0.05, 6)
>>> vdx.DynTrajectory(model=f)(jnp.arange(5.0), ts).shape
(6, 5)
Source code in src/vardax/_src/utils/dynamical_systems.py
simulate_lorenz63
¶
simulate_lorenz63(
key: Array,
*,
sigma: float = 10.0,
rho: float = 28.0,
beta: float = 8.0 / 3.0,
dt: float = 0.01,
n_steps: int = 5000,
n_burn_in: int = 1000,
x0: Float[Array, " 3"] | None = None,
) -> tuple[Float[Array, " T"], Float[Array, "T 3"]]
Simulate the Lorenz-63 system and return state trajectory.
Integrates \(\dot x = \sigma (y - x)\), \(\dot y = x (\rho - z) - y\), \(\dot z = x y - \beta z\) with diffrax and discards a burn-in so the trajectory lies on the attractor.
Algorithm
- Build the initial condition, \((1, 1, 1)\) perturbed by
key, orx0. - Integrate with
Tsit5overn_burn_in + n_stepssteps of sizedt, saving every step. - Drop the first
n_burn_instates and shift time to start at 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX PRNG key used to perturb the initial condition when
|
required |
sigma
|
float
|
Prandtl number \(\sigma\). |
10.0
|
rho
|
float
|
Rayleigh number \(\rho\). |
28.0
|
beta
|
float
|
Geometric factor \(\beta\). |
8.0 / 3.0
|
dt
|
float
|
Integration time step. |
0.01
|
n_steps
|
int
|
Total number of integration steps after burn-in. |
5000
|
n_burn_in
|
int
|
Number of initial steps to discard (burn-in). |
1000
|
x0
|
Float[Array, ' 3'] | None
|
Optional explicit initial condition. When
|
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' T']
|
|
Float[Array, 'T 3']
|
Lorenz-63 state trajectory, with |
Examples:
A short run without burn-in:
>>> import jax
>>> import vardax as vdx
>>> t, states = vdx.simulate_lorenz63(
... jax.random.PRNGKey(0), n_steps=10, n_burn_in=0
... )
>>> t.shape, states.shape
((11,), (11, 3))
>>> float(t[0])
0.0
An explicit initial condition is used as given:
>>> import jax.numpy as jnp
>>> x0 = jnp.array([1.0, 2.0, 3.0])
>>> _, states = vdx.simulate_lorenz63(
... jax.random.PRNGKey(0), n_steps=5, n_burn_in=0, x0=x0
... )
>>> bool(jnp.allclose(states[0], x0))
True
A burn-in is dropped from the output but the time axis restarts at 0:
>>> t, states = vdx.simulate_lorenz63(
... jax.random.PRNGKey(0), n_steps=5, n_burn_in=5
... )
>>> states.shape, float(t[0])
((6, 3), 0.0)
Source code in src/vardax/_src/utils/dynamical_systems.py
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
simulate_lorenz96
¶
simulate_lorenz96(
key: Array,
*,
n_sites: int = 40,
forcing: float = 8.0,
dt: float = 0.01,
n_steps: int = 5000,
n_burn_in: int = 1000,
) -> tuple[Float[Array, " T"], Float[Array, "T N"]]
Simulate the Lorenz-96 system and return state trajectory.
Integrates \(\dot x_k = (x_{k+1} - x_{k-2})\, x_{k-1} - x_k + F\) on a periodic ring with diffrax and discards a burn-in.
Algorithm
- Start from the uniform state \(x_k = F\) plus a small perturbation
drawn from
key. - Integrate with
Tsit5overn_burn_in + n_stepssteps of sizedt, saving every step. - Drop the first
n_burn_instates and shift time to start at 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array
|
JAX PRNG key used to perturb the initial condition. |
required |
n_sites
|
int
|
Number of variables (spatial dimension). |
40
|
forcing
|
float
|
Forcing constant. |
8.0
|
dt
|
float
|
Integration time step. |
0.01
|
n_steps
|
int
|
Total number of integration steps after burn-in. |
5000
|
n_burn_in
|
int
|
Number of initial steps to discard (burn-in). |
1000
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' T']
|
|
Float[Array, 'T N']
|
state trajectory, with |
Examples:
>>> import jax
>>> import vardax as vdx
>>> t, states = vdx.simulate_lorenz96(
... jax.random.PRNGKey(0), n_sites=8, n_steps=10, n_burn_in=0
... )
>>> t.shape, states.shape
((11,), (11, 8))
The perturbation of the initial state depends on the key:
>>> import jax.numpy as jnp
>>> _, other = vdx.simulate_lorenz96(
... jax.random.PRNGKey(1), n_sites=8, n_steps=10, n_burn_in=0
... )
>>> bool(jnp.allclose(states[0], other[0]))
False
The result feeds a plotting helper directly:
>>> import matplotlib
>>> matplotlib.use("Agg")
>>> fig, ax = vdx.plot_l96_grid(states, t)
>>> type(ax).__name__
'Axes'
Source code in src/vardax/_src/utils/dynamical_systems.py
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | |
time_patches
¶
Overlapping consecutive time pairs for one-step increment losses.
Reimplements the legacy kernex.kmap(kernel_size=(2,), relative=True)
sliding window without the kernex dependency: for a 1-D array of
times [t_0, t_1, ..., t_{T-1}] it returns the T - 1 overlapping
pairs [[t_0, t_1], [t_1, t_2], ..., [t_{T-2}, t_{T-1}]].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ts
|
Float[Array, ' T']
|
Monotonic time coordinates \(t_0, \dots, t_{T-1}\). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'T-1 2']
|
The consecutive pairs \((t_i, t_{i+1})\), one row per pair. |
Examples:
>>> import jax.numpy as jnp
>>> from vardax import time_patches
>>> time_patches(jnp.arange(4.0))
Array([[0., 1.],
[1., 2.],
[2., 3.]], dtype=float32)
A single time yields no pairs:
The pairs drive the one-step solves of
DynIncrements:
Source code in src/vardax/_src/utils/patches.py
Forward-model adapters¶
SoftBoundedForward wraps any pipekit_cycle.ForwardModel so that the
line search of a variational solver cannot drive an explicit ODE step
off the attractor into overflow: inside a box the adapter is the
identity, outside it saturates smoothly. Notebooks
13 and
14 use it around
Lorenz-96 and Lorenz-63 forwards.
vardax — Modular variational data assimilation with learned components.
All public symbols are re-exported from the private _src subpackage so
that user code imports from the top-level namespace:
SoftBoundedForward
¶
Bases: Module
ForwardModel adapter that softly confines states to a box.
Explicit Runge–Kutta steps of dynamics with quadratic terms (Lorenz-63,
Lorenz-96, …) overflow, or drive an adaptive step-size controller past
its step cap, when the state is far outside the attractor. A
line-search minimiser such as the BFGS inside
StrongFourDVar will try such states, and one
non-finite cost is enough to poison the whole solve. This adapter maps
the state through
on the way into and out of every step, so that
- states inside the box \(|x_i| \le b\) are untouched (choose
boundso the attractor sits well inside it — the adapter is then exactly the wrapped model wherever the analysis can end up); - states outside are confined to \(|x_i| < 2b\), keeping every trial cost finite;
- the map is continuously differentiable (slope one at the box edge
from both sides) and its gradient outside, \(1/(1+u)^2\), stays
representable in single precision for trial states up to about
\(10^{19}\) times the bound, unlike a hard clip (zero gradient) or a
tanhsaturation (gradient underflows to zero a few bounds out); a rejected trial therefore still carries curvature information if the minimiser asks for it.
Algorithm
- Confine the incoming state with \(s\).
- Advance it with the wrapped model's
step. - Confine the result with \(s\) again.
Attributes:
| Name | Type | Description |
|---|---|---|
forward |
Any
|
The wrapped |
bound |
float
|
Half-width \(b\) of the box on which the adapter is the identity. |
Examples:
>>> import jax.numpy as jnp
>>> import vardax
>>> class Double:
... dt = 1.0
... state_signature = None
...
... def step(self, x, dt):
... return 2.0 * x
>>> fwd = vardax.SoftBoundedForward(Double(), bound=10.0)
>>> fwd.step(jnp.array([1.0, -3.0]), 1.0) # inside the box: untouched
Array([ 2., -6.], dtype=float32)
>>> bool(jnp.all(jnp.abs(fwd.step(jnp.array([1e6, -1e6]), 1.0)) < 20.0))
True
The step length and signature are forwarded from the wrapped model:
Wrapped around a diffrax-backed model it keeps a far-off state finite:
>>> def decay(t, y, args):
... return -y
>>> inner = vardax.DynTrajectory(model=decay).as_forward_model(0.1)
>>> safe = vardax.SoftBoundedForward(inner, bound=5.0)
>>> bool(jnp.all(jnp.isfinite(safe.step(jnp.array([1e9]), 0.1))))
True
Source code in src/vardax/_src/utils/forward.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |
step
¶
Advance state by dt with confinement before and after.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
Array
|
State \(x\) to advance. |
required |
dt
|
float
|
Step length \(\Delta t\). |
required |
Returns:
| Type | Description |
|---|---|
Array
|
\(s(\Phi_{\Delta t}(s(x)))\), with \(\Phi\) the wrapped model's step. |
Source code in src/vardax/_src/utils/forward.py
Validation gates¶
The six-step methodology's go/no-go checks (Decision D12):
simulation-based calibration ranks the truth within posterior samples and
must be uniform; assert_posterior_agreement cross-checks two
posterior adapters against each other; and
assert_adjoint_calibrated verifies that a cheap
adjoint tracks the exact gradient before it is used for
training. Run these before believing any uncertainty estimate.
vardax — Modular variational data assimilation with learned components.
All public symbols are re-exported from the private _src subpackage so
that user code imports from the top-level namespace:
simulation_based_calibration
¶
simulation_based_calibration(
sample_posterior: Callable[
[Array, PRNGKeyArray, int], Array
],
sample_prior: Callable[[PRNGKeyArray], Array],
simulate_obs: Callable[[Array, PRNGKeyArray], Array],
*,
key: PRNGKeyArray,
n_runs: int = 200,
n_samples: int = 200,
) -> Int[Array, " n_runs"]
Per Talts et al. 2018: rank histogram of true draws within posterior samples.
Procedure for each of n_runs independent draws:
- Sample \(x^{(j)} \sim p(x)\) from the prior.
- Simulate \(y^{(j)} = \mathrm{simulate\_obs}(x^{(j)})\).
- Draw
n_samplesposterior samples from \(q_\phi(\cdot \mid y^{(j)})\). - Compute the rank of (a flattened scalar reduction of) \(x^{(j)}\) in the sample set.
A well-calibrated posterior produces uniformly distributed ranks
over [0, n_samples]. Bumps near the edges → over-confident;
centre-mass bump → under-confident.
Algorithm
- For each run, split the key into prior, observation and posterior keys.
- Draw \(x\), simulate \(y\) and draw the posterior samples.
- Reduce \(x\) and every sample to its \(\ell_2\) norm.
- Record how many sample norms fall below the norm of \(x\).
References
Talts, Betancourt, Simpson, Vehtari & Gelman (2018). "Validating Bayesian inference algorithms with simulation-based calibration."
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sample_posterior
|
Callable[[Array, PRNGKeyArray, int], Array]
|
|
required |
sample_prior
|
Callable[[PRNGKeyArray], Array]
|
|
required |
simulate_obs
|
Callable[[Array, PRNGKeyArray], Array]
|
|
required |
key
|
PRNGKeyArray
|
Top-level PRNG key. |
required |
n_runs
|
int
|
Number of (prior, obs, posterior) triples. |
200
|
n_samples
|
int
|
Posterior samples per run (defines the rank histogram resolution). |
200
|
Returns:
| Type | Description |
|---|---|
Int[Array, ' n_runs']
|
Integer ranks in |
Examples:
Ranks of the truth among samples drawn around the observation lie in
[0, n_samples]:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> prior = lambda k: jax.random.normal(k, (2,))
>>> observe = lambda x, k: x + 0.1 * jax.random.normal(k, (2,))
>>> posterior = lambda y, k, n: y + 0.1 * jax.random.normal(k, (n, 2))
>>> ranks = vdx.simulation_based_calibration(
... posterior,
... prior,
... observe,
... key=jax.random.PRNGKey(0),
... n_runs=6,
... n_samples=10,
... )
>>> ranks.shape, bool(jnp.all((ranks >= 0) & (ranks <= 10)))
((6,), True)
A posterior that ignores the data puts the truth below every sample, so every rank is zero:
>>> wrong = lambda y, k, n: 100.0 + jax.random.normal(k, (n, 2))
>>> bad = vdx.simulation_based_calibration(
... wrong,
... prior,
... observe,
... key=jax.random.PRNGKey(0),
... n_runs=6,
... n_samples=10,
... )
>>> bool(jnp.all(bad == 0))
True
An AmortizedPosterior plugs in through a
one-element batch:
>>> key = jax.random.PRNGKey(1)
>>> amortized = vdx.AmortizedPosterior(
... encoder=vdx.MLPObsEncoder(input_size=6, context_dim=4, key=key),
... head=vdx.RegressionHead(context_dim=4, state_shape=(2, 3), key=key),
... config=vdx.AmortizedConfig(),
... )
>>> def sample_posterior(y, k, n):
... batch = vdx.Batch1D(input=y[None], mask=jnp.ones_like(y)[None])
... return amortized.sample(batch, k, n)[0]
>>> ranks = vdx.simulation_based_calibration(
... sample_posterior,
... lambda k: jax.random.normal(k, (2, 3)),
... lambda x, k: x,
... key=jax.random.PRNGKey(2),
... n_runs=4,
... n_samples=5,
... )
>>> ranks.shape
(4,)
Source code in src/vardax/_src/utils/validation.py
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
assert_posterior_agreement
¶
assert_posterior_agreement(
p_fast: Posterior,
p_oracle: Posterior,
*,
tolerance_sigma: float = 1.0,
) -> None
Check that p_fast.mean lies within tolerance_sigma standard
deviations of p_oracle.mean.
Marginal-only test: each component of the mean must satisfy
The oracle marginal \(\sigma_i\) is extracted from
p_oracle.cov by probing \(e_i^T \Sigma e_i\) via one matvec per
component. Cheap for moderate state size; for very large state
sizes use Hutchinson estimation upstream and supply the diagonal
directly via Posterior.cov materialised as a diagonal operator.
Algorithm
- Extract the oracle marginal standard deviations \(\sigma_i\) from
p_oracle.cov. - Form the mean difference \(\Delta x^* = x^*_\text{fast} - x^*_\text{oracle}\).
- Compute the z-scores \(z_i = |\Delta x^*_i| / \sigma_i\).
- Fail if any \(z_i\) exceeds
tolerance_sigma.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p_fast
|
Posterior
|
Posterior produced by the amortized / fast model. |
required |
p_oracle
|
Posterior
|
Posterior produced by the oracle (e.g. |
required |
tolerance_sigma
|
float
|
Allowed deviation in units of \(\sigma\). |
1.0
|
Raises:
| Type | Description |
|---|---|
MissingInputError
|
If |
ShapeError
|
If the two means have different shapes. |
AssertionError
|
If any component exceeds the tolerance. |
Examples:
A fast mean within one oracle standard deviation passes silently:
>>> import jax
>>> import jax.numpy as jnp
>>> import lineax as lx
>>> import vardax as vdx
>>> cov = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((3,), jnp.float32))
>>> oracle = vdx.Posterior(mean=jnp.zeros(3), cov=cov)
>>> fast = vdx.Posterior(mean=jnp.array([0.5, -0.5, 0.0]))
>>> vdx.assert_posterior_agreement(fast, oracle, tolerance_sigma=1.0)
A deviation of three standard deviations fails:
>>> far = vdx.Posterior(mean=jnp.array([3.0, 0.0, 0.0]))
>>> vdx.assert_posterior_agreement(
... far, oracle
... )
Traceback (most recent call last):
...
AssertionError: posterior agreement failed: max z = 3.000 > 1.000.
An oracle without a covariance cannot supply the scale:
>>> vdx.assert_posterior_agreement(
... fast, fast
... )
Traceback (most recent call last):
...
MissingInputError: assert_posterior_agreement requires p_oracle.cov ...
Source code in src/vardax/_src/utils/validation.py
assert_adjoint_calibrated
¶
assert_adjoint_calibrated(
fn_fast: Callable[[Array], Array],
fn_oracle: Callable[[Array], Array],
y: Float[Array, ...],
*,
key: PRNGKeyArray,
threshold: float = 0.05,
n_probes: int = 10,
) -> None
Random-vector probe of the Jacobian agreement at y.
Tests
for n_probes random unit vectors \(v\). Avoids
materialising either Jacobian — uses jax.jvp to apply each as
needed. Cheaper than dense Jacobian comparison and is the
operational test used by the six-step cycle.
Algorithm
- Draw a random unit vector \(v\) of the shape of
y. - Apply both Jacobians to it with
jax.jvp. - Compute the relative error of the two products.
- Repeat for
n_probesvectors and fail if the worst error exceedsthreshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn_fast
|
Callable[[Array], Array]
|
Callable |
required |
fn_oracle
|
Callable[[Array], Array]
|
Same signature, but the oracle. |
required |
y
|
Float[Array, ...]
|
Observation tensor at which the Jacobians are probed. |
required |
key
|
PRNGKeyArray
|
PRNG key for the probe vectors. |
required |
threshold
|
float
|
Maximum allowed relative error. |
0.05
|
n_probes
|
int
|
Number of probe vectors. |
10
|
Raises:
| Type | Description |
|---|---|
AssertionError
|
If any probe exceeds the threshold. |
Examples:
Two maps with the same Jacobian agree, even if their values differ:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> y = jnp.arange(4.0)
>>> vdx.assert_adjoint_calibrated(
... lambda v: 2.0 * v + 1.0,
... lambda v: 2.0 * v,
... y,
... key=jax.random.PRNGKey(0),
... )
A fast model whose sensitivity is off by a third fails:
>>> vdx.assert_adjoint_calibrated(
... lambda v: 2.0 * v, lambda v: 3.0 * v, y, key=jax.random.PRNGKey(0)
... )
Traceback (most recent call last):
...
AssertionError: adjoint calibration failed: ...
Source code in src/vardax/_src/utils/validation.py
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | |
Visualization¶
vardax — Modular variational data assimilation with learned components.
All public symbols are re-exported from the private _src subpackage so
that user code imports from the top-level namespace:
plot_l96_trajectories
¶
plot_l96_trajectories(
states: ndarray,
time_coords: ndarray,
*,
n_vars: int = 5,
ax: Axes | None = None,
) -> tuple[Figure, Axes]
Line plot of selected Lorenz-96 variables over time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
states
|
ndarray
|
State trajectory, indexed |
required |
time_coords
|
ndarray
|
Time coordinate of each row of |
required |
n_vars
|
int
|
Number of evenly-spaced variables to plot (capped at the number of variables). |
5
|
ax
|
Axes | None
|
Optional existing |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Figure, Axes]
|
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If |
Examples:
>>> import matplotlib
>>> matplotlib.use("Agg")
>>> import numpy as np
>>> import vardax as vdx
>>> states = np.random.default_rng(0).normal(size=(20, 8))
>>> time = np.linspace(0.0, 1.0, 20)
>>> fig, ax = vdx.plot_l96_trajectories(states, time, n_vars=3)
>>> len(ax.lines)
3
Asking for more variables than exist plots all of them:
A non-positive count is rejected:
>>> vdx.plot_l96_trajectories(
... states, time, n_vars=0
... )
Traceback (most recent call last):
...
ConfigurationError: n_vars must be a positive integer, got 0.
Source code in src/vardax/_src/utils/viz.py
plot_l96_grid
¶
plot_l96_grid(
states: ndarray,
time_coords: ndarray,
*,
ax: Axes | None = None,
) -> tuple[Figure, Axes]
Hovmöller-style image of Lorenz-96 states over time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
states
|
ndarray
|
State trajectory, indexed |
required |
time_coords
|
ndarray
|
Time coordinate of each row of |
required |
ax
|
Axes | None
|
Optional existing |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Figure, Axes]
|
|
Examples:
>>> import matplotlib
>>> matplotlib.use("Agg")
>>> import numpy as np
>>> import vardax as vdx
>>> states = np.random.default_rng(0).normal(size=(20, 8))
>>> fig, ax = vdx.plot_l96_grid(states, np.linspace(0.0, 1.0, 20))
>>> type(ax).__name__, ax.get_xlabel()
('Axes', 'time')
Drawing onto an existing axes returns that axes:
>>> import matplotlib.pyplot as plt
>>> _, mine = plt.subplots()
>>> _, same = vdx.plot_l96_grid(states, np.linspace(0.0, 1.0, 20), ax=mine)
>>> same is mine
True
Source code in src/vardax/_src/utils/viz.py
plot_reconstruction_comparison
¶
plot_reconstruction_comparison(
target: ndarray,
masked_input: ndarray,
reconstruction: ndarray,
*,
sample_idx: int = 0,
) -> tuple[Figure, ndarray]
Side-by-side comparison of target, masked input, and reconstruction.
Draws three panels (time on the x-axis, feature on the y-axis) for one batch element.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
ndarray
|
Ground-truth states, indexed |
required |
masked_input
|
ndarray
|
Masked / noisy observations, same layout. |
required |
reconstruction
|
ndarray
|
Model reconstruction, same layout. |
required |
sample_idx
|
int
|
Which batch element to visualize. |
0
|
Returns:
| Type | Description |
|---|---|
tuple[Figure, ndarray]
|
|
Examples:
>>> import matplotlib
>>> matplotlib.use("Agg")
>>> import numpy as np
>>> import vardax as vdx
>>> truth = np.random.default_rng(0).normal(size=(2, 6, 4))
>>> fig, axes = vdx.plot_reconstruction_comparison(truth, truth, truth)
>>> axes.shape, type(axes[0]).__name__
((3,), 'Axes')
Another batch element can be selected:
>>> fig, axes = vdx.plot_reconstruction_comparison(
... truth, truth, truth, sample_idx=1
... )
>>> axes[1].get_title()
'Masked input'
Source code in src/vardax/_src/utils/viz.py
Errors¶
Every error vardax raises on purpose derives from VardaxError, and each subclass names what failed. Each one also inherits the builtin it replaced (ValueError, or TypeError for InputTypeError), so existing except ValueError handlers keep working.
vardax — Modular variational data assimilation with learned components.
All public symbols are re-exported from the private _src subpackage so
that user code imports from the top-level namespace:
VardaxError
¶
Bases: Exception
Base class for every error vardax raises on purpose.
Examples:
Catch any vardax error with the base class:
>>> import vardax as vdx
>>> try:
... vdx.OptimistixMinimiser(max_steps=0)
... except vdx.VardaxError as error:
... type(error).__name__
'ConfigurationError'
Every subclass is still the builtin it replaced:
Source code in src/vardax/_src/errors.py
ConfigurationError
¶
Bases: VardaxError, ValueError
A hyperparameter or constructor argument has an invalid value.
ShapeError
¶
Bases: VardaxError, ValueError
An array or batch has a shape the operation cannot use.
GridError
¶
Bases: VardaxError, ValueError
Grid axes or coordinates are malformed (too few points, not increasing).
ObservationError
¶
Bases: VardaxError, ValueError
Observations are inconsistent with the model or the assimilation window.
DataError
¶
Bases: VardaxError, ValueError
A dataset cannot be processed (no observed values, too few samples).
MissingInputError
¶
Bases: VardaxError, ValueError
A required input (a key, a target, a covariance) was not given.
UnsupportedInputError
¶
Bases: VardaxError, ValueError
The input is valid in general but this operation does not support it.
InputTypeError
¶
Bases: VardaxError, TypeError
An argument has the wrong type.