Distributions & Exponential Family¶
Layer 2: Gaussian distributions over structured covariance operators, the sugar operations that probabilistic code actually calls, and the exponential-family (natural-parameter) view used by variational and EP-style inference.
Multivariate normal distributions¶
NumPyro-compatible distributions whose covariance (or precision) is a lineax
operator, so sample / log_prob inherit every structured fast path.
MultivariateNormalPrecision carries \(\Lambda = \Sigma^{-1}\) directly — the
natural home for natural-parameter guides, where materializing \(\Sigma\) would
be wasted work. Both require numpyro to be installed.
Both subclass AbstractMultivariateNormal, so code written against the base
works for either parameterisation. It provides covariance_operator and
precision_operator (one native, the other a lazy inv), numpyro's dense
covariance_matrix / precision_matrix / scale_tril, and p.kl(q) for any
mix of the two classes:
When q is precision-parameterised, \(\Lambda_q\) is its native operator, so
the trace and quadratic terms need no solve. MultivariateNormalPrecision.variance
takes diag_inv's exact structured paths (for example, the \(O(N d^3)\) selected
inverse of a BlockTriDiag precision) and densifies only an unstructured
precision.
With a stochastic solver (e.g. CGSolver()), log_prob and entropy
estimate the log-determinant from the strategy's fixed seed, so every call
reuses the same probes. That is fine for SVI gradients, but averaging calls
reduces no variance and MCMC targets a fixed pseudo-likelihood. To vary the
probes, pass solver=KeyedSolver(CGSolver(), key) with a fresh key per step
(see Solvers). The functional gaussian_log_prob,
gaussian_entropy and kl_standard_normal take key= directly.
Structured linear algebra and Gaussian primitives for JAX.
AbstractMultivariateNormal
¶
Bases: Distribution
Common surface of MultivariateNormal and MultivariateNormalPrecision.
A subclass stores one native operator: the covariance \(\Sigma\)
(MultivariateNormal) or the precision \(\Lambda = \Sigma^{-1}\)
(MultivariateNormalPrecision). The other one is the lazy
gaussx.inv of it. Code written against this base works for either
parameterisation:
covariance_operator/precision_operator: lineax operators, one native and one a lazy inverse.covariance_matrix/precision_matrix/scale_tril: dense arrays with numpyro's names and batch broadcasting. They materialise \(N \times N\) matrices, so they are for small problems and interop.mean,log_prob,kl.
Requires the numpyro optional extra
(pip install "gaussx[numpyro]").
Source code in src/gaussx/_distributions/_mvn_base.py
covariance_operator: lx.AbstractLinearOperator
property
¶
The covariance \(\Sigma\) as a lineax operator.
precision_operator: lx.AbstractLinearOperator
property
¶
The precision \(\Lambda = \Sigma^{-1}\) as a lineax operator.
covariance_matrix() -> Float[Array, '*batch N N']
¶
Dense \(\Sigma\), broadcast over the batch shape.
precision_matrix() -> Float[Array, '*batch N N']
¶
Dense \(\Lambda = \Sigma^{-1}\), broadcast over the batch shape.
scale_tril() -> Float[Array, '*batch N N']
¶
Dense lower Cholesky factor \(L\) of \(\Sigma = L L^\top\) (numpyro's name).
kl(other: AbstractMultivariateNormal) -> Float[Array, '*batch']
¶
\(\mathrm{KL}(\text{self} \,\|\, \text{other})\) in closed form.
Any mix of covariance and precision parameterisations works. A
precision-parameterised other needs no solve for
\(\operatorname{tr}(\Sigma_q^{-1}\Sigma_p)\) or the quadratic term,
because the inverse of its lazy covariance is its precision. Batch
shapes broadcast as in numpyro's kl_divergence. Unlike
numpyro.distributions.kl_divergence, it uses the closed form
whatever the solver strategies are.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
AbstractMultivariateNormal
|
The second Gaussian, with the same event shape. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*batch']
|
The KL divergence, with the broadcast batch shape. |
Source code in src/gaussx/_distributions/_mvn_base.py
MultivariateNormal
¶
Bases: AbstractMultivariateNormal
Multivariate normal parameterized by a lineax linear operator.
Covariance-parameterised member of AbstractMultivariateNormal, which
supplies the shared accessors (covariance_operator,
precision_operator, covariance_matrix, scale_tril, kl, ...).
Unlike numpyro.distributions.MultivariateNormal which requires
dense arrays, this distribution accepts any
lineax.AbstractLinearOperator as its covariance. This enables
efficient log-prob, sampling, and entropy for structured covariances
(Kronecker, block-diagonal, low-rank, diagonal, etc.) via gaussx
structural dispatch.
Requires the numpyro optional extra
(pip install "gaussx[numpyro]").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loc
|
Float[Array, '*batch N']
|
Mean vector of shape |
required |
cov_operator
|
AbstractLinearOperator
|
Covariance as a lineax linear operator of shape
|
required |
solver
|
AbstractSolverStrategy | None
|
Solver strategy for |
None
|
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Examples:
>>> import jax.numpy as jnp
>>> import lineax as lx
>>> from gaussx._distributions import MultivariateNormal
>>> Sigma = lx.MatrixLinearOperator(
... jnp.eye(3), lx.positive_semidefinite_tag
... )
>>> d = MultivariateNormal(jnp.zeros(3), Sigma)
>>> round(float(d.log_prob(jnp.ones(3))), 4)
-4.2568
Source code in src/gaussx/_distributions/_mvn.py
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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
MultivariateNormalPrecision
¶
Bases: AbstractMultivariateNormal
Multivariate normal parameterized by a precision (inverse covariance) operator.
This is the natural parameterization for many inference algorithms
(e.g. message passing, variational inference in natural coordinates).
The precision operator Lambda satisfies Lambda = Sigma^{-1}.
Precision-parameterised member of AbstractMultivariateNormal, which
supplies the shared accessors (covariance_operator,
precision_operator, covariance_matrix, scale_tril, kl, ...).
Requires the numpyro optional extra
(pip install "gaussx[numpyro]").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loc
|
Float[Array, '*batch N']
|
Mean vector of shape |
required |
prec_operator
|
AbstractLinearOperator
|
Precision matrix as a lineax linear operator of
shape |
required |
solver
|
AbstractSolverStrategy | None
|
Solver strategy for |
None
|
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Examples:
>>> import jax.numpy as jnp
>>> import lineax as lx
>>> from gaussx._distributions import MultivariateNormalPrecision
>>> Lambda = lx.MatrixLinearOperator(
... 2.0 * jnp.eye(3), lx.positive_semidefinite_tag
... )
>>> d = MultivariateNormalPrecision(jnp.zeros(3), Lambda)
>>> round(float(d.log_prob(jnp.ones(3))), 4)
-4.7171
Source code in src/gaussx/_distributions/_mvn_prec.py
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 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 | |
Gaussian Markov random fields¶
GaussianMRF is \(\mathcal N(\mu, Q^{-1})\) with a structured precision, and
IntrinsicGMRF the improper \(\mathcal N(\mu, (\tau R)^+)\) of RW1 / RW2 /
Besag (ICAR) priors, with a structure matrix \(R\) and its null space. Every
operation dispatches on the precision's structure
(GMRF precision builders return the right one):
- sample, in order: a factorisable precision (dense,
BlockTriDiag, or aSparseOperatorthrough its sparse Cholesky factor) gives \(\mu + L^{-\top}z\); a Kronecker-structured one (Kronecker,KroneckerSum,SpectralFunction) the symmetric root in its factor eigenvectors; anything else withprecision_factors\(Q = \sum_k F_k^\top F_k\) is sampled by perturbation-optimisation: \(x = Q^{-1}\sum_k F_k^\top z_k\) has covariance \(Q^{-1}QQ^{-1} = Q^{-1}\), one CG solve per draw and no factorisation (graph precisions come factored, \(B^\top\operatorname{diag}(w)B\) from an incidence matrix). - log_prob: \(\tfrac12\log|Q| - \tfrac12(x-\mu)^\top Q(x-\mu) - \tfrac N2\log 2\pi\),
with \(\log|Q|\) from
log_det_precisionwhen known in closed form (no stochastic estimate inside an MCMC density), elselogdet_strategy,solveror the structurallogdet. The intrinsic density is \(\tfrac{N-c}{2}\log\tau - \tfrac\tau2(x-\mu)^\top R(x-\mu)\), plus the \(\tau\)-free \(\tfrac12\log|R|_+\) (pseudo_logdet) withinclude_normalizer. - marginal variances:
diag_inv(Takahashi on a sparse or banded factor, factor eigenvectors on a grid). - observations \(y = Ax + \varepsilon\):
condition_on_observationsreturns the posterior GMRF, \(Q + A^\top\Lambda A\) (sparse on the union pattern for a sparse prior andA, matrix-free for a grid prior) with its mean from one solve. - constraints \(A_cx = e\):
condition_on_constraintsreturns aConstrainedGMRF, conditioning by kriging (Rue & Held, 2005, §2.3.3): \(x^\ast = x - Q^{-1}A_c^\top(A_cQ^{-1}A_c^\top)^{-1}(A_cx - e)\) is an exact draw from \(x \mid A_cx = e\), with corrected marginal variances and the constrained density.IntrinsicGMRFdefaults to the hard constraintnull_spaceᵀx = 0;constraint="soft"puts a tight Gaussian onnull_spaceᵀxinstead, for NUTS.
An odd-length rw2_structure(n) has one decoupled padding node: build the
IntrinsicGMRF on all n + 1 nodes with null_space zero on it, and drop
it from draws. All four require numpyro to be installed.
BYM2. Use BYM2GMRF(R_star, tau, phi, null_space) for the BYM2 pair
\((b, u^\ast)\), not an IntrinsicGMRF on bym2_precision. The joint precision
\(Q(\tau, \phi)\) has the null vector \((\sqrt{\phi/\tau}\,\mathbf 1, \mathbf 1)\),
which depends on \(\theta\). So the bracket IntrinsicGMRF drops by default is
not constant (it holds \(\tfrac n2\log\tfrac{\tau}{1-\phi}\)), and projecting
draws along null_space does not remove the singular direction.
BYM2GMRF uses the exact density on \(\{V^\top u^\ast = 0\}\),
\(-\tfrac12 x^\top Q x + \tfrac n2\log\tfrac{\tau}{1-\phi}\) (plus the constant
\(\tfrac12\log|R^\ast|_+ - \tfrac{2n-c}{2}\log 2\pi\) with include_normalizer),
and draws \(u^\ast\) from the ICAR on \(R^\ast\), then \(b\) from its definition.
It is an IntrinsicGMRF, so laplace_mode takes it as a prior, and its
log-marginal is then correct in \((\tau, \phi)\):
scale = gx.generalized_variance_scale(R, V) # R: the Besag structure, n areas
R_star = scale * R
# theta-free, so once: log|s R|_+ = log|R|_+ + (n - c) log s, with c = 1 here
log_pdet = gx.pseudo_logdet(R, structure="laplacian") + (n - 1) * jnp.log(scale)
def log_marginal(tau, phi):
prior = gx.BYM2GMRF(R_star, tau, phi, V, include_normalizer=True, log_pdet=log_pdet)
return gx.laplace_mode(
prior, gx.PoissonLikelihood(counts), projector=A
).log_marginal
import einx
import jax
import jax.numpy as jnp
import numpy as np
import gaussx as gx
# Gap-fill a cloudy sea-surface-temperature snapshot on an H × W grid
H, W = 30, 40
kappa, tau, alpha = gx.matern_spde_params(range=8.0, sigma=1.0, nu=1.0, d=2)
prior = gx.GaussianMRF(
jnp.zeros(H * W), gx.spde_precision_grid((H, W), kappa, tau, alpha=alpha)
)
clear_idx = np.flatnonzero(np.random.default_rng(0).random(H * W) > 0.6)
n_clear = clear_idx.size
sst = jnp.sin(jnp.arange(H * W) / 50.0) # stand-in for the measured field
A = gx.SparseOperator.from_coo(
np.arange(n_clear), clear_idx, jnp.ones(n_clear), (n_clear, H * W)
)
post = prior.condition_on_observations(A, noise_precision=1 / 0.2**2, y=sst[clear_idx])
filled = einx.id("(h w) -> h w", post.loc, h=H)
sd = einx.id("(h w) -> h w", jnp.sqrt(post.marginal_variances()), h=H)
draws = post.sample(jax.random.PRNGKey(0), (20,)) # 20 plausible gap-filled fields
# An ICAR field with a hard sum-to-zero constraint per island group: the
# mainland path 0 - 1 - 2 - 3 and the island pair 4 - 5
senders, receivers = np.array([1, 2, 3, 5]), np.array([0, 1, 2, 4])
degree = np.bincount(np.r_[senders, receivers], minlength=6).astype(float)
R = gx.SparseOperator.from_coo(
np.r_[np.arange(6), senders],
np.r_[np.arange(6), receivers],
jnp.asarray(np.r_[degree, -np.ones(4)]),
(6, 6),
symmetric=True,
)
null_space = jnp.zeros((6, 2)).at[:4, 0].set(0.5).at[4:, 1].set(2**-0.5)
icar = gx.IntrinsicGMRF(jnp.zeros(6), 2.0, R, null_space) # kernellib: graph_null_space
u = icar.sample(jax.random.PRNGKey(1)) # null_spaceᵀ u == 0 to machine precision
lp = icar.log_prob(u) # (N − c)/2 · log τ − τ/2 uᵀRu, with N − c = 4
Structured linear algebra and Gaussian primitives for JAX.
GaussianMRF
¶
Bases: Distribution
Gaussian Markov random field \(x \sim \mathcal N(\mu, Q^{-1})\).
The precision is a structured lineax operator, and log_prob,
sample and marginal_variances take its cheapest exact path:
SparseOperator: sparse Cholesky,μ + Pᵀ L⁻ᵀ z; Takahashi for the marginal variances.BlockTriDiag: block Cholesky and the block selected inverse.Kronecker,KroneckerSum,SpectralFunction: the factor eigenvectors,V⁻¹ Λ^{-1/2} V z, and the factor eigenvalues.- dense: Cholesky.
- anything else, with
precision_factors: perturbation-optimisation, one solve ofQ x = Σ_k F_kᵀ z_kper draw;solver/logdet_strategyforlog|Q|.
A SparseOperator defaults to SparseCholeskySolver for every solve and
log-determinant; pass precision_factors (and no sparse Cholesky
solver) to sample matrix-free by CG instead (solver, if given, does
that solve). Graph precisions come factored — Bᵀ diag(w) B from an
incidence matrix, proper CAR, Leroux — so a 10⁶-node field samples with
no factorisation at all.
Requires the numpyro optional extra. loc is a single field
(batch_shape == ()); log_prob takes any leading sample axes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loc
|
Float[ArrayLike, ' N']
|
Mean \(\mu\), shape |
required |
precision
|
AbstractLinearOperator
|
\(Q\), a symmetric positive-definite operator of size |
required |
precision_factors
|
tuple[AbstractLinearOperator, ...] | None
|
Operators \(F_k\) with \(Q = \sum_k F_k^\top F_k\), for the perturbation-optimisation sampler. |
None
|
solver
|
AbstractSolverStrategy | None
|
Solve (and log-determinant) strategy, e.g.
|
None
|
logdet_strategy
|
AbstractLogdetStrategy | None
|
Log-determinant strategy; takes precedence over
|
None
|
log_det_precision
|
Float[ArrayLike, ''] | None
|
A known \(\log|Q|\), which overrides both (callers with a closed form, such as a proper CAR, skip a stochastic estimate inside an MCMC log-density). |
None
|
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Examples:
import jax
import jax.numpy as jnp
import numpy as np
import gaussx as gx
# A proper field on the path graph 0 - 1 - 2 - 3 - 4: Q = R + I
n = 5
Q = gx.SparseOperator.from_coo(
np.r_[np.arange(n), np.arange(1, n)],
np.r_[np.arange(n), np.arange(n - 1)],
jnp.r_[jnp.array([2.0, 3.0, 3.0, 3.0, 2.0]), -jnp.ones(n - 1)],
(n, n),
symmetric=True,
)
prior = gx.GaussianMRF(jnp.zeros(n), Q) # sparse Cholesky throughout
x = prior.sample(jax.random.PRNGKey(0), (3,)) # (3, 5)
lp = prior.log_prob(x) # (3,)
sd = jnp.sqrt(prior.marginal_variances()) # Takahashi
# Observe nodes 0 and 3 with noise sd 0.1: the posterior is a GMRF
A = gx.SparseOperator.from_coo([0, 1], [0, 3], jnp.ones(2), (2, n))
post = prior.condition_on_observations(A, 1 / 0.1**2, jnp.array([1.0, -1.0]))
# A sum-to-zero field, by kriging
zero_sum = prior.condition_on_constraints(jnp.ones((1, n)), jnp.zeros(1))
u = zero_sum.sample(jax.random.PRNGKey(1)) # u.sum() == 0
Source code in src/gaussx/_distributions/_gmrf.py
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 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 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 | |
log_prob(value: Float[Array, '*sample N']) -> Float[Array, '*sample']
¶
\(\tfrac12\log|Q| - \tfrac12(x-\mu)^\top Q(x-\mu) - \tfrac N2\log 2\pi\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Float[Array, '*sample N']
|
Points, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*sample']
|
Log-densities, shape |
Source code in src/gaussx/_distributions/_gmrf.py
sample(key: jax.Array | None, sample_shape: tuple[int, ...] = ()) -> Float[Array, '*sample N']
¶
Exact draws through the structural sampling dispatch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array | None
|
PRNG key. |
required |
sample_shape
|
tuple[int, ...]
|
Leading sample shape. |
()
|
Returns:
| Type | Description |
|---|---|
Float[Array, '*sample N']
|
Draws, shape |
Source code in src/gaussx/_distributions/_gmrf.py
marginal_variances() -> Float[Array, ' N']
¶
diag(Q⁻¹) through diag_inv's structural dispatch.
Returns:
| Type | Description |
|---|---|
Float[Array, ' N']
|
Marginal variances, shape |
Source code in src/gaussx/_distributions/_gmrf.py
condition_on_observations(A: lx.AbstractLinearOperator | Float[ArrayLike, 'M N'], noise_precision: Float[ArrayLike, ''] | Float[ArrayLike, ' M'], y: Float[ArrayLike, ' M']) -> GaussianMRF
¶
The posterior given \(y = Ax + \varepsilon\) with noise precision \(\Lambda\).
\(Q_\text{post} = Q + A^\top\Lambda A\) and \(\mu_\text{post} = \mu + Q_\text{post}^{-1}A^\top\Lambda(y - A\mu)\), one solve. The posterior precision keeps what structure it can:
- a
SparseOperatorprior observed through aSparseOperatorAstays sparse, on the union of the two patterns (SparseOperator.congruence, planned once per pair of patterns), so it keeps the sparse Cholesky path; - a dense prior stays dense;
- anything else (a grid
SpectralFunction, aKroneckerSum, aBlockTriDiagobserved at scattered points) becomes the matrix-free sumQ + (Λ^{1/2}A)ᵀ(Λ^{1/2}A): solves bysolve(CG when large), and draws by perturbation-optimisation with the factors(Q^{1/2}, Λ^{1/2}A),Q^{1/2}the symmetric root of aSpectralFunctionorKroneckerSumprior.
precision_factors gain Λ^{1/2}A; a known log_det_precision
is dropped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
AbstractLinearOperator | Float[ArrayLike, 'M N']
|
Observation operator, shape |
required |
noise_precision
|
Float[ArrayLike, ''] | Float[ArrayLike, ' M']
|
\(\Lambda\)'s diagonal, a scalar or shape |
required |
y
|
Float[ArrayLike, ' M']
|
Observations, shape |
required |
Returns:
| Type | Description |
|---|---|
GaussianMRF
|
The posterior |
GaussianMRF
|
|
Source code in src/gaussx/_distributions/_gmrf.py
condition_on_constraints(A_c: Float[ArrayLike, 'c N'] | Float[ArrayLike, ' N'], e: Float[ArrayLike, ' c'] | Float[ArrayLike, '']) -> ConstrainedGMRF
¶
Hard linear constraints A_c x = e, by conditioning by kriging.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A_c
|
Float[ArrayLike, 'c N'] | Float[ArrayLike, ' N']
|
Constraint matrix, shape |
required |
e
|
Float[ArrayLike, ' c'] | Float[ArrayLike, '']
|
Constraint values, shape |
required |
Returns:
| Type | Description |
|---|---|
ConstrainedGMRF
|
The |
Source code in src/gaussx/_distributions/_gmrf.py
IntrinsicGMRF
¶
Bases: Distribution
Improper \(\mathcal N(\mu, (\tau R)^+)\) on the complement of \(\ker R\).
\(R\) is a positive-semidefinite structure matrix (RW1, RW2, Besag / ICAR,
a grid Laplacian) with null space spanned by the columns of
null_space (c of them), and \(\tau\) is the precision scale. The
log-density is
the bracket only with include_normalizer (it is \(\tau\)-free, so an
MCMC target can skip it; pseudo_logdet computes it per call). The
\(\tau\) term is always included, which is what makes \(\tau\) identifiable
under a hyperprior. N − c must be rank(R). The bracket is
constant only when \(R\) itself does not depend on the hyperparameters:
for BYM2, whose structure holds \((\tau, \phi)\), use BYM2GMRF.
constraint chooses what happens on \(\ker R\):
"hard"(default, what INLA needs): the field lives on \(\{V^\top x = 0\}\), \(V\) =null_space. The log-density above (with the normaliser) is then exactly the constrained density on that subspace. Samples are kriged onto it: R-INLA's recipe of factoring \(\tau R + \varepsilon I\) (with \(\varepsilon = \sqrt{\text{eps}}\) times the mean diagonal) and kriging against \(V^\top x = 0\) reduces, since \((\tau R + \varepsilon I)^{-1}V = V/\varepsilon\), to the orthogonal projection \(x - V(V^\top V)^{-1}V^\top x\), which is what is applied (exactly, with no extra solves). Marginal variances are kriging- corrected (ConstrainedGMRF) on the shifted factor, or exact from the pseudo-inverse on a Kronecker-structured \(R\)."soft"(for NUTS, which cannot sample a hard constraint): adds a tight Gaussian \(V^\top x \sim \mathcal N(0, s^2 I)\), \(s\) =soft_constraint_scale, to the density and to the samples."none": only $\mu + $ a draw from \(\mathcal N(0, (\tau R)^+)\) — the density is improper and invariant along \(\ker R\).
The draw from \(\mathcal N(0, (\tau R)^+)\) dispatches like GaussianMRF:
Cholesky of \(\tau R + \varepsilon I\) (sparse, banded or dense), the
pseudo-inverse root in the factored eigenbasis (exact, no shift), or
perturbation-optimisation with precision_factors
(\(R = \sum_k F_k^\top F_k\), so the right-hand side lies in
\(\operatorname{range}(R)\) and CG converges on the singular system); each
is then projected orthogonally to null_space.
Odd-n RW2. rw2_structure with odd n returns n + 1 rows,
the last a decoupled unit-precision node. Build the field on all
N = n + 1 nodes with null_space zero on the padding row
(c = 2): then N − c = n − 1 = rank(R), and the padding node is an
independent \(\mathcal N(0, 1/\tau)\) coordinate whose own ½ log τ is the
one extra in (N − c)/2 · log τ. Sample it with the rest and drop it
(x[:n]).
Requires the numpyro optional extra.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loc
|
Float[ArrayLike, ' N']
|
\(\mu\), shape |
required |
precision_scale
|
Float[ArrayLike, '']
|
\(\tau > 0\). |
required |
structure
|
AbstractLinearOperator
|
\(R\), symmetric positive semidefinite, size |
required |
null_space
|
Float[ArrayLike, 'N c'] | Float[ArrayLike, ' N']
|
A basis of \(\ker R\), shape |
required |
precision_factors
|
tuple[AbstractLinearOperator, ...] | None
|
\(F_k\) with \(R = \sum_k F_k^\top F_k\) (e.g. the weighted incidence matrix), for the CG sampler. |
None
|
constraint
|
Literal['none', 'soft', 'hard']
|
|
'hard'
|
soft_constraint_scale
|
float
|
\(s\) for |
0.001
|
include_normalizer
|
bool
|
Add the \(\tau\)-free constant (exact for an
orthonormal |
False
|
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown |
Examples:
import jax
import jax.numpy as jnp
import gaussx as gx
# An RW1 trend with a hard sum-to-zero constraint
n = 50
R = gx.rw1_structure(n)
trend = gx.IntrinsicGMRF(jnp.zeros(n), 4.0, R, jnp.ones((n, 1)) / jnp.sqrt(n))
u = trend.sample(jax.random.PRNGKey(0), (5,)) # rows sum to 0
lp = trend.log_prob(u) # includes (n - 1)/2 · log 4
# RW2 with odd n: one padding node, null space {1, t} zero on it
R2 = gx.rw2_structure(7) # 8 x 8
t = jnp.arange(7.0)
V = jnp.zeros((8, 2)).at[:7, 0].set(1.0).at[:7, 1].set(t - t.mean())
smooth = gx.IntrinsicGMRF(jnp.zeros(8), 10.0, R2, V)
x = smooth.sample(jax.random.PRNGKey(1))[:7] # drop the padding node
Source code in src/gaussx/_distributions/_gmrf.py
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 | |
log_prob(value: Float[Array, '*sample N']) -> Float[Array, '*sample']
¶
\(\tfrac{N-c}{2}\log\tau - \tfrac\tau2(x-\mu)^\top R(x-\mu)\), and extras.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Float[Array, '*sample N']
|
Points, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*sample']
|
Log-densities, shape |
Source code in src/gaussx/_distributions/_gmrf.py
sample(key: jax.Array | None, sample_shape: tuple[int, ...] = ()) -> Float[Array, '*sample N']
¶
Draws orthogonal to null_space (plus the soft or "none" part).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array | None
|
PRNG key. |
required |
sample_shape
|
tuple[int, ...]
|
Leading sample shape. |
()
|
Returns:
| Type | Description |
|---|---|
Float[Array, '*sample N']
|
Draws, shape |
Float[Array, '*sample N']
|
|
Source code in src/gaussx/_distributions/_gmrf.py
marginal_variances() -> Float[Array, ' N']
¶
diag((τR)⁺), plus s² diag(V (VᵀV)⁻² Vᵀ) for "soft".
The pseudo-inverse diagonal is exact from the factor eigenvalues on a
Kronecker structure, and otherwise the kriging-corrected
diag((τR + εI)⁻¹) (R-INLA's constrained marginal variances).
Returns:
| Type | Description |
|---|---|
Float[Array, ' N']
|
Marginal variances of the samples, shape |
Source code in src/gaussx/_distributions/_gmrf.py
ConstrainedGMRF
¶
Bases: Distribution
A GaussianMRF conditioned on hard linear constraints \(A_cx = e\).
Conditioning by kriging (Rue & Held, 2005, §2.3.3): with
\(W = Q^{-1}A_c^\top\) (c solves with one factorisation) and
\(S = A_cW\), an unconstrained draw \(x\) maps to the exact constrained draw
\(x^\ast = x - WS^{-1}(A_cx - e)\), so \(A_cx^\ast = e\) to machine
precision. The marginal variances are
\(\operatorname{diag}(Q^{-1}) - \operatorname{diag}(WS^{-1}W^\top)\), and
the density (Rue & Held, eq. 2.30) is
a density on the affine subspace \(\{A_cx = e\}\) (w.r.t. its Lebesgue measure), evaluated at points assumed to satisfy the constraint.
Built by GaussianMRF.condition_on_constraints. Requires numpyro.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base
|
GaussianMRF
|
The unconstrained field. |
required |
constraint_matrix
|
Float[ArrayLike, 'c N'] | Float[ArrayLike, ' N']
|
\(A_c\), shape |
required |
constraint_value
|
Float[ArrayLike, ' c'] | Float[ArrayLike, '']
|
\(e\), shape |
required |
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Examples:
import jax
import jax.numpy as jnp
import lineax as lx
import gaussx as gx
Q = lx.MatrixLinearOperator(2.0 * jnp.eye(4), lx.positive_semidefinite_tag)
prior = gx.GaussianMRF(jnp.zeros(4), Q)
field = prior.condition_on_constraints(jnp.ones(4), 1.0) # Σ x = 1
x = field.sample(jax.random.PRNGKey(0), (10,)) # each row sums to 1
var = field.marginal_variances() # 0.5 - 0.5 / 4
Source code in src/gaussx/_distributions/_gmrf.py
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 | |
log_prob(value: Float[Array, '*sample N']) -> Float[Array, '*sample']
¶
The constrained density (Rue & Held, eq. 2.30) on A_c x = e.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Float[Array, '*sample N']
|
Points satisfying the constraint, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*sample']
|
Log-densities, shape |
Source code in src/gaussx/_distributions/_gmrf.py
sample(key: jax.Array | None, sample_shape: tuple[int, ...] = ()) -> Float[Array, '*sample N']
¶
Base draws, kriged onto A_c x = e.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array | None
|
PRNG key. |
required |
sample_shape
|
tuple[int, ...]
|
Leading sample shape. |
()
|
Returns:
| Type | Description |
|---|---|
Float[Array, '*sample N']
|
Draws, shape |
Source code in src/gaussx/_distributions/_gmrf.py
marginal_variances() -> Float[Array, ' N']
¶
diag(Q⁻¹) − diag(W S⁻¹ Wᵀ).
Returns:
| Type | Description |
|---|---|
Float[Array, ' N']
|
Constrained marginal variances, shape |
Source code in src/gaussx/_distributions/_gmrf.py
BYM2GMRF
¶
Bases: IntrinsicGMRF
The BYM2 pair \((b, u^\ast)\) (Riebler et al., 2016), with its exact density.
\(b = (\sqrt{1-\phi}\,v + \sqrt\phi\,u^\ast)/\sqrt\tau\) with
\(v \sim \mathcal N(0, I)\) and \(u^\ast\) the scaled ICAR field (structure
\(R^\ast\), generalized_variance_scale) under one sum-to-zero constraint
per connected component. The joint precision \(Q(\tau, \phi)\) is
bym2_precision, and the field lives on \(\{V^\top u^\ast = 0\}\), where
its log-density (Rue & Held, eq. 2.30) is
because the Schur complement of \(Q\)'s \(b\)-block is exactly \(R^\ast\).
Why not IntrinsicGMRF on \(Q\): \(Q\)'s null vector is
\((\sqrt{\phi/\tau}\,\mathbf 1, \mathbf 1)\), which depends on \(\theta\).
So the bracket IntrinsicGMRF drops by default is not \(\theta\)-free (it
holds the \(\tfrac n2\log\tfrac{\tau}{1-\phi}\) above), its normaliser
takes a dense determinant of size \(2n\) at every \(\theta\), and its
sampler's projection along null_space leaves the \(b\)-part of a
shifted-Cholesky draw unbounded. Here the \(\theta\)-dependent term is
always included, the bracket is truly constant (from \(R^\ast\) alone:
the matrix-tree theorem, one sparse Cholesky, or log_pdet), and
draws are exact: \(u^\ast\) from the constrained ICAR on \(R^\ast\), then
\(b\) from its definition.
It is an IntrinsicGMRF whose structure is \(Q\), precision_scale
is 1 and null_space is \(V\) padded with zeros on the \(b\) rows, so
laplace_mode and vb_mean_correction take it as a prior unchanged
(their log-marginal then has the correct \(\theta\)-dependence).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure_scaled
|
AbstractLinearOperator
|
\(R^\ast = sR\), a (scaled) |
required |
tau
|
Float[ArrayLike, '']
|
Precision \(\tau > 0\) of \(b\) (may be traced). |
required |
phi
|
Float[ArrayLike, '']
|
Mixing \(0 \le \phi < 1\) (may be traced). |
required |
null_space
|
Float[ArrayLike, 'n c'] | Float[ArrayLike, ' n']
|
Basis of \(\ker R^\ast\), shape |
required |
loc
|
Float[ArrayLike, ' N'] | None
|
Mean of the stacked |
None
|
constraint
|
Literal['none', 'soft', 'hard']
|
|
'hard'
|
soft_constraint_scale
|
float
|
\(s\) for |
0.001
|
include_normalizer
|
bool
|
Add the \(\theta\)-free constant in brackets. |
False
|
log_pdet
|
Float[ArrayLike, ''] | None
|
\(\log|R^\ast|_+\), if already known: it is
\(\theta\)-free, so compute it once outside a \(\theta\) loop.
|
None
|
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Examples:
import jax
import jax.numpy as jnp
import numpy as np
import gaussx as gx
# A path of 3 areas: R = D - W
R = gx.SparseOperator.from_coo(
np.array([0, 1, 2, 1, 2]),
np.array([0, 1, 2, 0, 1]),
jnp.array([1.0, 2.0, 1.0, -1.0, -1.0]),
(3, 3),
symmetric=True,
)
V = jnp.ones(3) / jnp.sqrt(3.0)
R_star = gx.generalized_variance_scale(R, V) * R
prior = gx.BYM2GMRF(R_star, tau=2.0, phi=0.6, null_space=V)
x = prior.sample(jax.random.PRNGKey(0), (100,)) # (100, 6): (b, u*)
lp = prior.log_prob(x) # correct in (tau, phi) for a marginal likelihood
Source code in src/gaussx/_distributions/_gmrf.py
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 | |
log_prob(value: Float[Array, '*sample N']) -> Float[Array, '*sample']
¶
\(-\tfrac12 x^\top Q x + \tfrac n2\log\tfrac{\tau}{1-\phi}\), and extras.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Float[Array, '*sample N']
|
Points |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*sample']
|
Log-densities, shape |
Source code in src/gaussx/_distributions/_gmrf.py
sample(key: jax.Array | None, sample_shape: tuple[int, ...] = ()) -> Float[Array, '*sample N']
¶
Exact draws: u* from the ICAR on R*, then b from u*.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
Array | None
|
PRNG key. |
required |
sample_shape
|
tuple[int, ...]
|
Leading sample shape. |
()
|
Returns:
| Type | Description |
|---|---|
Float[Array, '*sample N']
|
Draws |
Source code in src/gaussx/_distributions/_gmrf.py
marginal_variances() -> Float[Array, ' N']
¶
var(u*) from the ICAR on R*, var(b) = ((1−φ) + φ var(u*))/τ.
Returns:
| Type | Description |
|---|---|
Float[Array, ' N']
|
Marginal variances of |
Source code in src/gaussx/_distributions/_gmrf.py
Sequential distributions¶
The linear-Gaussian state-space model as a density rather than a set of
functions: log_prob is the Kalman marginal likelihood \(\log p(y_{1:T})\)
(delegated to kalman_filter), sample is ancestral forward
simulation, and event_shape is \((T, M)\) so log_prob returns a scalar with
no .to_event() wrapping. That makes them usable directly as a NumPyro
likelihood site.
A, H, Q and R each take a dense array or a lineax operator, exactly
matching kalman_filter's contract — a structured \(Q\) / \(R\) keeps its
structure through the Cholesky in sample, and \(H\) through the sandwich in
variance.
MaskedLGSSM carries a \((T, M)\) observation mask and returns the exact
marginal \(\log p(y_{\mathrm{obs}})\) — not a bound — because
\(p(y_{\mathrm{miss}} \mid y_{\mathrm{obs}})\) is closed-form Gaussian.
LGSSMFactory is the mask -> MaskedLGSSM callable for conditional use; it
is an equinox.Module rather than a closure so the state-space parameters
stay visible to equinox.filter_grad.
To use one as a normalizing-flow base, wrap it with gauss_flows.NumpyroBase,
which already adapts any numpyro distribution — no bespoke adapter class is
needed on either side. All three require numpyro to be installed.
Structured linear algebra and Gaussian primitives for JAX.
LGSSM
¶
Bases: Distribution
Linear-Gaussian state-space model as a density over (T, M).
Models
with \(x_0 \sim N(m_0, P_0)\) and \(t = 1, \dots, T\). log_prob is
the Kalman marginal likelihood \(\log p(y_{1:T})\), delegated to
gaussx.kalman_filter; sample is ancestral forward
simulation. event_shape is (T, M), so log_prob returns a
scalar and no .to_event() wrapping is needed.
Requires the numpyro optional extra
(pip install "gaussx[numpyro]").
A, H, Q and R each accept a dense array or a
lineax.AbstractLinearOperator, exactly matching
gaussx.kalman_filter's contract. Structured Q / R keep
their structure through the Cholesky in sample, H through
the sandwich in variance, and A / H apply via
structural matvec rather than a dense matmul. m0 and P0 are
dense, as they are for gaussx.kalman_filter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
_Transition
|
Transition matrix or operator, shape |
required |
H
|
_Emission
|
Emission matrix or operator, shape |
required |
Q
|
_StateCov
|
Process noise covariance or operator, shape |
required |
R
|
_ObsCov
|
Observation noise covariance or operator, shape |
required |
m0
|
Float[Array, ' N']
|
Initial state mean, shape |
required |
P0
|
Float[Array, 'N N']
|
Initial state covariance, shape |
required |
n_steps
|
int
|
Sequence length |
required |
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Examples:
>>> import jax.numpy as jnp, jax.random as jr
>>> from gaussx import LGSSM
>>> d = LGSSM(0.9 * jnp.eye(2), jnp.eye(2), 0.1 * jnp.eye(2),
... 0.2 * jnp.eye(2), jnp.zeros(2), jnp.eye(2), n_steps=10)
>>> y = d.sample(jr.key(0))
>>> y.shape
(10, 2)
>>> d.log_prob(y).shape
()
Notes
To use as a normalizing-flow base, wrap with
gauss_flows.NumpyroBase; no bespoke adapter is required.
Source code in src/gaussx/_distributions/_lgssm.py
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 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 205 206 207 208 209 210 211 212 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 | |
n_steps: int
property
¶
Sequence length T.
mean() -> Float[Array, 'T M']
¶
Marginal mean \(H A^t m_0\) of each observation, shape (T, M).
Source code in src/gaussx/_distributions/_lgssm.py
variance() -> Float[Array, 'T M']
¶
Marginal variance \(\mathrm{diag}(H P_t H^\top + R)\), shape (T, M).
The state recursion runs on dense P_t — it is the evolving
quantity, so no input structure survives it — but the emission
step avoids forming H P H^T: operators go through
gaussx.sandwich + gaussx.diag, arrays contract straight
to the diagonal.
Source code in src/gaussx/_distributions/_lgssm.py
MaskedLGSSM
¶
Bases: LGSSM
LGSSM whose log_prob marginalises unobserved channels exactly.
Takes a (T, M) boolean mask at construction. log_prob returns
the exact marginal \(\log p(y_{\mathrm{obs}})\), not a bound: the
conditional \(p(y_{\mathrm{miss}} \mid y_{\mathrm{obs}})\) is
closed-form Gaussian, so dropping dimensions costs nothing in
exactness. Masked entries of value are never read and may be
NaN.
sample is inherited unchanged and simulates the complete
(T, M) series — the mask governs which entries the density
scores, not which are generated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
_Transition
|
Transition matrix or operator, shape |
required |
H
|
_Emission
|
Emission matrix or operator, shape |
required |
Q
|
_StateCov
|
Process noise covariance or operator, shape |
required |
R
|
_ObsCov
|
Observation noise covariance or operator, shape |
required |
m0
|
Float[Array, ' N']
|
Initial state mean, shape |
required |
P0
|
Float[Array, 'N N']
|
Initial state covariance, shape |
required |
n_steps
|
int
|
Sequence length |
required |
obs_mask
|
Bool[Array, 'T M']
|
Observation mask, shape |
required |
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import jax.numpy as jnp, jax.random as jr
>>> from gaussx import MaskedLGSSM
>>> mask = jr.bernoulli(jr.key(0), 0.7, (10, 2))
>>> d = MaskedLGSSM(0.9 * jnp.eye(2), jnp.eye(2), 0.1 * jnp.eye(2),
... 0.2 * jnp.eye(2), jnp.zeros(2), jnp.eye(2),
... n_steps=10, obs_mask=mask)
>>> d.log_prob(d.sample(jr.key(1))).shape
()
Source code in src/gaussx/_distributions/_lgssm.py
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 | |
LGSSMFactory
¶
Bases: Module
Callable obs_mask -> MaskedLGSSM, for conditional use.
An equinox.Module rather than a closure so the state-space
parameters stay visible to equinox.filter_grad when passed as
gauss_flows.NumpyroBase(dist_factory=...).
Attributes:
| Name | Type | Description |
|---|---|---|
A |
_Transition
|
Transition matrix or operator, shape |
H |
_Emission
|
Emission matrix or operator, shape |
Q |
_StateCov
|
Process noise covariance or operator, shape |
R |
_ObsCov
|
Observation noise covariance or operator, shape |
m0 |
Float[Array, ' N']
|
Initial state mean, shape |
P0 |
Float[Array, 'N N']
|
Initial state covariance, shape |
n_steps |
int
|
Sequence length |
Examples:
>>> import jax.numpy as jnp, jax.random as jr
>>> from gaussx import LGSSMFactory
>>> factory = LGSSMFactory(0.9 * jnp.eye(2), jnp.eye(2),
... 0.1 * jnp.eye(2), 0.2 * jnp.eye(2),
... jnp.zeros(2), jnp.eye(2), n_steps=10)
>>> d = factory(jr.bernoulli(jr.key(0), 0.7, (10, 2)))
>>> d.event_shape
(10, 2)
Source code in src/gaussx/_distributions/_lgssm.py
MarkovGaussian is the density over the states of a Gauss-Markov chain
rather than its observations: \(x_0 \sim \mathcal{N}(\mu_0, P_0)\),
\(x_{k+1} = A_k x_k + b_k + \varepsilon_k\), with event_shape \((T, d)\).
sample and log_prob use the chain factorisation at \(O(T d^3)\) and never
form the joint covariance, so it serves as a prior over a latent trajectory
or as a structured variational guide. Its precision is block-tridiagonal;
to_precision_form / from_precision_form convert to and from the
\((\mu, \Lambda)\) layout that spingp_posterior returns, through
the UDL factorisation, so a precision-form
posterior becomes a sampleable chain in one \(O(T d^3)\) pass. Requires
numpyro.
Structured linear algebra and Gaussian primitives for JAX.
MarkovGaussian
¶
Bases: Distribution
Chain-Markov Gaussian over \(x_{0:T-1}\) as a density over (T, d).
The canonical parameterisation is the generative state-space form
whose joint covariance has a block-tridiagonal inverse. event_shape
is (T, d), so log_prob of a trajectory is a scalar and the
distribution can be used directly as a NumPyro site — a prior over a
latent path, or a structured variational guide. Every view is derived
from the SSM tuple on demand:
| view | cost | route |
|---|---|---|
marginals, mean, variance |
\(O(T d^3)\) | forward moment propagation |
sample, log_prob |
\(O(T d^3)\) | chain factorisation, no joint Cholesky |
precision, to_precision_form |
\(O(T d^3)\) | udl_from_ssm_params |
from_precision_form |
\(O(T d^3)\) | udl_decomposition |
covariance_matrix |
\(O(T^2 d^3)\) | banded solves vs. identity; small \(T\) |
Where LGSSM is a density over the observations of a chain with
the states marginalised out, this is the density over the states
themselves. Requires the numpyro optional extra
(pip install "gaussx[numpyro]").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Float[Array, 'Tm1 d d']
|
Transition matrices, shape |
required |
Q
|
Float[Array, 'Tm1 d d']
|
Process noise covariances, shape |
required |
mu0
|
Float[Array, ' d']
|
Initial mean, shape |
required |
P0
|
Float[Array, 'd d']
|
Initial covariance, shape |
required |
b
|
Float[Array, 'Tm1 d'] | None
|
Optional transition offsets, shape |
None
|
validate_args
|
bool | None
|
Whether to validate input arguments. |
None
|
Examples:
>>> import jax, jax.numpy as jnp, gaussx
>>> T, d = 10, 2
>>> A = jnp.broadcast_to(0.9 * jnp.eye(d), (T - 1, d, d))
>>> Q = jnp.broadcast_to(0.1 * jnp.eye(d), (T - 1, d, d))
>>> chain = gaussx.MarkovGaussian(A, Q, jnp.zeros(d), jnp.eye(d))
>>> xs = chain.sample(jax.random.key(0), (4,))
>>> xs.shape, chain.log_prob(xs).shape
((4, 10, 2), (4,))
>>> mean, precision = chain.to_precision_form()
>>> mean.shape, precision.as_matrix().shape
((20,), (20, 20))
Source code in src/gaussx/_distributions/_markov_gaussian.py
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 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 205 206 207 208 209 210 211 212 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 | |
horizon: int
property
¶
Number of states T in the chain.
state_dim: int
property
¶
State dimension d.
precision: BlockTriDiag
property
¶
Joint precision \(\Lambda\) as a BlockTriDiag.
Raw \(\Lambda\), not the \(-\tfrac12 \Lambda\) natural-parameter
convention of ssm_to_naturals.
marginals() -> tuple[Float[Array, 'T d'], Float[Array, 'T d d']]
¶
Marginal means and covariances of every state.
Forward propagation \(m_{k+1} = A_k m_k + b_k\), \(P_{k+1} = A_k P_k A_k^{\top} + Q_k\).
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, 'T d'], Float[Array, 'T d d']]
|
Tuple |
Source code in src/gaussx/_distributions/_markov_gaussian.py
mean() -> Float[Array, 'T d']
¶
variance() -> Float[Array, 'T d']
¶
cross_covariances() -> Float[Array, 'Tm1 d d']
¶
\(\mathrm{Cov}(x_{k+1}, x_k) = A_k P_k\) for each consecutive pair.
pairwise_marginals() -> tuple[Float[Array, 'Tm1 two_d'], Float[Array, 'Tm1 two_d two_d']]
¶
Joint \(p(x_k, x_{k+1})\) for each consecutive pair.
Returns:
| Type | Description |
|---|---|
Float[Array, 'Tm1 two_d']
|
Tuple |
Float[Array, 'Tm1 two_d two_d']
|
and |
tuple[Float[Array, 'Tm1 two_d'], Float[Array, 'Tm1 two_d two_d']]
|
|
Source code in src/gaussx/_distributions/_markov_gaussian.py
covariance_matrix() -> Float[Array, 'Td Td']
¶
Dense joint covariance \(\Lambda^{-1}\), shape (Td, Td).
Built column by column through UDLDecomposition.solve, so it
costs \(O(T^2 d^3)\) and is meant for small \(T\) (reference checks,
dense downstream consumers).
Source code in src/gaussx/_distributions/_markov_gaussian.py
udl() -> UDLDecomposition
¶
The chain's precision, already factorised as \(U \tilde{D} U^{\top}\).
to_precision_form() -> tuple[Float[Array, ' Td'], BlockTriDiag]
¶
The chain as (mean, precision).
This is the layout spingp_posterior consumes and returns: pass
mean as its prior_mean (it assumes a zero-mean prior
otherwise) and hand its output to from_precision_form. The
natural location parameter, if needed, is \(\eta = \Lambda \mu\),
i.e. precision.mv(mean).
Returns:
| Type | Description |
|---|---|
Float[Array, ' Td']
|
Tuple |
BlockTriDiag
|
shape |
Source code in src/gaussx/_distributions/_markov_gaussian.py
from_precision_form(mean: Float[Array, ' Td'] | Float[Array, 'T d'], precision: BlockTriDiag) -> MarkovGaussian
classmethod
¶
Recover the generative chain from (mean, precision).
One udl_decomposition pass gives \(A_k = -U_k^{\top}\) and
\(Q_k = \tilde{D}_k^{-1}\); the offsets follow from the mean chain
as \(b_k = m_{k+1} - A_k m_k\). Round-trips to_precision_form
exactly (to floating point).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
Float[Array, ' Td'] | Float[Array, 'T d']
|
Marginal means, flattened |
required |
precision
|
BlockTriDiag
|
Symmetric positive-definite joint precision. |
required |
Returns:
| Type | Description |
|---|---|
MarkovGaussian
|
The equivalent |
Source code in src/gaussx/_distributions/_markov_gaussian.py
Gaussian sugar ops¶
evaluated through structured solve + logdet, plus entropy, quadratic
forms, KL divergences, conditioning, and the numerically stable Joseph-form
covariance update.
Structured linear algebra and Gaussian primitives for JAX.
gaussian_log_prob(loc: Float[Array, ' N'], cov_operator: lx.AbstractLinearOperator, value: Float[Array, ' N'], *, solver: AbstractSolverStrategy | None = None, key: jax.Array | None = None) -> Float[Array, '']
¶
Multivariate normal log-probability.
Computes:
log N(value | loc, Sigma)
= -0.5 * (N log(2 pi) + log|Sigma| + (value - loc)^T Sigma^{-1} (value - loc))
All expensive operations (solve, logdet) dispatch on
operator structure automatically, or through an explicit solver.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loc
|
Float[Array, ' N']
|
Mean vector, shape |
required |
cov_operator
|
AbstractLinearOperator
|
Covariance operator Sigma, shape |
required |
value
|
Float[Array, ' N']
|
Observation vector, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy (needs both solve and logdet).
When |
None
|
key
|
Array | None
|
PRNG key for a stochastic logdet strategy's probes. |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
Scalar log-probability. |
Source code in src/gaussx/_distributions/_gaussian.py
gaussian_entropy(cov_operator: lx.AbstractLinearOperator, *, solver: AbstractLogdetStrategy | None = None, key: jax.Array | None = None) -> Float[Array, '']
¶
Entropy of a multivariate normal N(mu, Sigma).
Computes:
H = 0.5 * (N * (1 + log(2 pi)) + log|Sigma|)
Independent of the mean.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_operator
|
AbstractLinearOperator
|
Covariance operator, shape |
required |
solver
|
AbstractLogdetStrategy | None
|
Optional logdet strategy. When |
None
|
key
|
Array | None
|
PRNG key for a stochastic logdet strategy's probes. |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
Scalar entropy. |
Source code in src/gaussx/_distributions/_gaussian.py
quadratic_form(operator: lx.AbstractLinearOperator, x: Float[Array, ' N'], *, solver: AbstractSolveStrategy | None = None) -> Float[Array, '']
¶
Compute x^T A^{-1} x via a single solve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
operator
|
AbstractLinearOperator
|
A non-singular linear operator A. |
required |
x
|
Float[Array, ' N']
|
Vector, shape |
required |
solver
|
AbstractSolveStrategy | None
|
Optional solve strategy. When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
Scalar |
Source code in src/gaussx/_distributions/_gaussian.py
kl_standard_normal(m: Float[Array, ' N'], S: lx.AbstractLinearOperator, *, solver: AbstractLogdetStrategy | None = None, key: jax.Array | None = None) -> Float[Array, '']
¶
KL(first ‖ second): KL(N(m, S) || N(0, I)).
Special case of gaussian_kl
with q_loc = 0 and q_cov = I. The identity prior means no
matrix inversion is required, making this more efficient than calling
the general form directly.
Computes:
KL = 0.5 * (tr(S) + m^T m - N - log|S|)
Ubiquitous in variational inference as the prior KL term.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
m
|
Float[Array, ' N']
|
Mean vector, shape |
required |
S
|
AbstractLinearOperator
|
Covariance operator, shape |
required |
solver
|
AbstractLogdetStrategy | None
|
Optional logdet strategy. When |
None
|
key
|
Array | None
|
PRNG key for a stochastic logdet strategy's probes. |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
Scalar KL divergence. |
See Also
gaussian_kl: General KL
between two multivariate normals with arbitrary lineax covariance
operators.
Source code in src/gaussx/_distributions/_gaussian.py
gaussian_kl(p_loc: Float[Array, ' N'], p_cov: lx.AbstractLinearOperator, q_loc: Float[Array, ' N'], q_cov: lx.AbstractLinearOperator) -> Float[Array, '']
¶
KL(first ‖ second): KL(p || q) between two multivariate normals.
This is the canonical KL implementation for lineax-operator covariances. The specialised variants below all compute the same quantity but with different parameterisations suited to their use cases:
kl_standard_normal— special caseKL(N(m, S) || N(0, I)); avoids matrix inversion.gauss_kl— Cholesky-parameterised form for GP/SVGP models; supports multi-output and diagonalq_sqrt.kl_divergence— Bregman-divergence form operating on natural parameters for the exponential family.
Exploits structured operators for the trace and logdet terms.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p_loc
|
Float[Array, ' N']
|
Mean of distribution p, shape |
required |
p_cov
|
AbstractLinearOperator
|
Covariance operator of distribution p. |
required |
q_loc
|
Float[Array, ' N']
|
Mean of distribution q, shape |
required |
q_cov
|
AbstractLinearOperator
|
Covariance operator of distribution q. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
Scalar KL divergence. |
Source code in src/gaussx/_distributions/_kl.py
conditional(loc: Float[Array, ' N'], cov: lx.AbstractLinearOperator, obs_idx: Int[Array, ' M'], obs_values: Float[Array, ' M'], *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, ' R'], lx.AbstractLinearOperator]
¶
Compute p(x_A | x_B = b) from a joint Gaussian p(x_A, x_B).
Given a joint distribution \(\mathcal{N}(\mu, \Sigma)\) and observed indices B with values b, returns the conditional distribution over the remaining indices A:
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loc
|
Float[Array, ' N']
|
Mean vector of the joint distribution, shape |
required |
cov
|
AbstractLinearOperator
|
Covariance operator of the joint distribution, shape |
required |
obs_idx
|
Int[Array, ' M']
|
Indices of the observed variables, shape |
required |
obs_values
|
Float[Array, ' M']
|
Observed values, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' R']
|
Tuple |
AbstractLinearOperator
|
conditional distribution over unobserved variables. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
EquinoxRuntimeError
|
At run time, if a traced |
Note
Usable under jax.jit. Concrete indices (NumPy or JAX constants,
including ones closed over by the jitted function) are validated
on the host; traced indices are validated at run time with
equinox.error_if, since an out-of-bounds gather would otherwise
be silently clamped.
Source code in src/gaussx/_distributions/_conditional.py
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 122 123 124 125 126 127 128 | |
joseph_update(P_pred: Float[Array, 'N N'], K: Float[Array, 'N M'], H: Float[Array, 'M N'], R: Float[Array, 'M M']) -> Float[Array, 'N N']
¶
Numerically stable Joseph-form covariance update.
Computes the updated covariance after a Kalman measurement update:
P_update = (I - K H) P_pred (I - K H)^T + K R K^T
This form is more numerically stable than the simplified
P = P_pred - K S K^T or P = (I - K H) P_pred because it
guarantees symmetry and is more robust when the Kalman gain K
is approximate or the system is poorly conditioned.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
P_pred
|
Float[Array, 'N N']
|
Predicted covariance, shape |
required |
K
|
Float[Array, 'N M']
|
Kalman gain, shape |
required |
H
|
Float[Array, 'M N']
|
Observation model, shape |
required |
R
|
Float[Array, 'M M']
|
Observation noise covariance, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N N']
|
Updated covariance, shape |
Source code in src/gaussx/_distributions/_joseph.py
add_jitter(operator: lx.AbstractLinearOperator, jitter: float = 1e-06) -> lx.AbstractLinearOperator
¶
Add diagonal jitter for numerical stability: A + eps * I.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
operator
|
AbstractLinearOperator
|
A linear operator, shape |
required |
jitter
|
float
|
Scalar jitter value. Default |
1e-06
|
Returns:
| Type | Description |
|---|---|
AbstractLinearOperator
|
|
Source code in src/gaussx/_distributions/_gaussian.py
project(K_XZ: Float[Array, 'B M'], L_Z: lx.AbstractLinearOperator) -> Float[Array, 'B M']
¶
Compute A_X = K_XZ @ K_ZZ^{-1} via Cholesky solve.
Solves L_Z @ L_Z^T @ A_X^T = K_XZ^T using forward/backward
substitution. Used in sparse variational GPs to project test
points onto the inducing space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
K_XZ
|
Float[Array, 'B M']
|
Cross-covariance matrix, shape |
required |
L_Z
|
AbstractLinearOperator
|
Lower-triangular Cholesky factor of K_ZZ, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'B M']
|
Projection matrix A_X, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/gaussx/_distributions/_project.py
Structured sampling¶
sample_mvn draws \(x = \mu + L\varepsilon\) with \(LL^\top = K\), picking the
factor \(L\) from the structure of the covariance operator: element-wise for a
diagonal, per factor for a Kronecker product, per block for a block-diagonal,
FFT circulant embedding for a Toeplitz, and \(L_B z_1 + U\sqrt{D}z_2\) for a
low-rank update, so a \(10^4 \times 10^4\) Kronecker covariance is never
factorised densely. Every branch is exact. A SumOfKroneckers therefore takes
the dense symmetric square root: its matrix-free sampler,
sum_of_kroneckers_sample, is a truncated Lanczos approximation and stays an
explicit opt-in. The one-off helpers toeplitz_sample and
kronecker_sum_sample also remain available for callers that want them
directly.
sample_joint_conditional draws a partitioned Gaussian \((x_a, x_b)\) jointly
without forming its \((N + M)^2\) covariance: the observed block through
sample_mvn, so it keeps its structure, and the other through the gain
\(K_{to}K_{oo}^{-1}\) plus a square root of the Schur complement. Given an
observed value it also returns conditional draws that share the same noise,
which makes them exactly the matheron_update of the joint draws.
Structured linear algebra and Gaussian primitives for JAX.
sample_mvn(mean: Float[Array, '*batch N'], covariance: lx.AbstractLinearOperator, *, key: jax.Array, num_samples: int = 1) -> Float[Array, 'num_samples *batch N']
¶
Draw samples from \(\mathcal{N}(\mu, K)\), dispatching on \(K\)'s structure.
Every branch draws \(x = \mu + L\varepsilon\) with \(\varepsilon \sim \mathcal{N}(0, I)\) and \(LL^{\top} = K\) exactly -- no branch approximates the covariance -- and only the factor \(L\) changes:
| Covariance \(K\) | Factor | Cost |
|---|---|---|
lineax.DiagonalLinearOperator |
\(\sqrt{d} \odot z\) | \(O(N)\) |
gaussx.BlockDiag |
each block by its own rule | per block |
gaussx.Kronecker |
per-factor eigendecomposition | \(O(\sum_i N_i^3)\) |
gaussx.KroneckerSum |
per-factor eigendecomposition | \(O(n_A^3 + n_B^3)\) |
gaussx.BlockTriDiag |
block-banded Cholesky | \(O(T d^3)\) |
gaussx.Toeplitz |
FFT circulant embedding | \(O(N \log N)\) |
gaussx.LowRankUpdate |
\(L_B z_1 + U\sqrt{D} z_2\) | \(B\)'s cost \(+ O(Nr)\) |
| anything else | dense symmetric square root | \(O(N^3)\) |
lineax.TaggedLinearOperator wrappers are looked through, and a scalar
multiple \(cK\) (with \(c > 0\)) samples \(K\) and rescales by \(\sqrt{c}\), so
wrapping never costs the structure underneath.
The Kronecker branches work in the factors' eigenbases, where the
eigenvalues of the whole operator are products (or sums) of the factors'
ones. That keeps them exact when a factor is indefinite but the product
is not -- (-A) \otimes (-B) -- and when the covariance is only
semi-definite. The dense fallback is the symmetric square root for the
same reason: a Cholesky factor of a singular covariance is NaN.
A gaussx.SumOfKroneckers takes the dense fallback: its matrix-free
square root, gaussx.sum_of_kroneckers_sample, is a truncated Lanczos
approximation, so it stays an explicit opt-in rather than a default.
Some structured routes hold only for some values, and each takes the dense square root otherwise -- so every covariance the operator can represent is sampled exactly, and pathwise gradients stay finite:
- a scalar multiple \(cK\) needs \(c > 0\);
- a
gaussx.Toeplitzneeds a circulant embedding (2, 4, 8 or 16 timesN) whose spectrum is strictly positive -- a zero eigenvalue there would putsqrt(0)in the pathwise derivative; - a
gaussx.LowRankUpdate\(B + UDV^{\top}\) needs \(V = U\) established (the same array, or equal values), a base \(B\) that is a covariance in its own right (tagged positive semi-definite, or a strictly positive diagonal), and \(D > 0\) -- a zero weight would again putsqrt(0)in the derivative.
When those values are only known at run time (under jax.jit), the
choice is a jax.lax.cond, which runs only the branch it takes; under
jax.vmap it evaluates both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
Float[Array, '*batch N']
|
Mean \(\mu\) with shape |
required |
covariance
|
AbstractLinearOperator
|
Positive-definite covariance operator \(K\) of size
|
required |
key
|
Array
|
PRNG key. |
required |
num_samples
|
int
|
Number of independent draws. |
1
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'num_samples *batch N']
|
Samples of shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/gaussx/_distributions/_sample.py
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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
sample_joint_conditional(joint_mean: tuple[Float[Array, ' N'], Float[Array, ' M']], joint_covariance: dict[str, lx.AbstractLinearOperator], *, key: jax.Array, observed_index: int = 1, observed_value: Float[Array, ' K'] | None = None, num_samples: int = 1, solver: AbstractSolveStrategy | None = None) -> dict[str, Any]
¶
Joint draws of a partitioned Gaussian, and conditional draws from them.
For
write \(o\) for the observed block and \(t\) for the other (with
observed_index=1, \(o = b\) and \(t = a\)). The joint draw uses the block
factorisation
with \(L_o L_o^{\top} = K_{oo}\) drawn by gaussx.sample_mvn -- so the
observed block keeps its structure -- and \(L_S L_S^{\top} = S = K_{tt} -
K_{to} K_{oo}^{-1} K_{ot}\) the Schur complement. The joint covariance of
size \((N + M)^2\) is never formed.
When observed_value \(\beta\) is given, the conditional draws of
\(x_t \mid x_o = \beta\) reuse the same \(z_2\):
which is exactly gaussx.matheron_update applied to the joint draws.
The Schur complement is dense in general: \(K_{to} K_{oo}^{-1} K_{ot}\) is
dense whatever the structure of \(K_{tt}\), so \(S\) is factorised at
\(O(N_t^3)\) plus \(N_t\) solves against \(K_{oo}\). Its square root is the
symmetric one from dense_symmetric_sqrt, with round-off negative
eigenvalues clipped. That tolerates the rank deficiency of target points
that coincide with observed ones -- a Cholesky would return NaNs there --
and its custom JVP keeps gradients finite when \(S\) has repeated
eigenvalues.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
joint_mean
|
tuple[Float[Array, ' N'], Float[Array, ' M']]
|
|
required |
joint_covariance
|
dict[str, AbstractLinearOperator]
|
Operators |
required |
key
|
Array
|
PRNG key. |
required |
observed_index
|
int
|
Which block is conditioned on: |
1
|
observed_value
|
Float[Array, ' K'] | None
|
Optional value \(\beta\) of the observed block. When given, the result also holds conditional draws. |
None
|
num_samples
|
int
|
Number of independent draws. |
1
|
solver
|
AbstractSolveStrategy | None
|
Optional solver strategy for the solves against \(K_{oo}\)
(e.g. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
dict[str, Any]
|
|
dict[str, Any]
|
|
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
On a missing covariance block, mismatched shapes,
|
Source code in src/gaussx/_distributions/_sample.py
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 | |
Exponential family¶
The Gaussian in natural form: \(\eta_1 = \Lambda\mu\), \(\eta_2 = -\tfrac12 \Lambda\). Conversions between mean/covariance, natural, and expectation parameterizations — multivariate (operator-aware) and univariate (per-site diagonal) — plus the log-partition, Fisher information, and sufficient statistics that natural-gradient and EP updates are built from.
Structured linear algebra and Gaussian primitives for JAX.
GaussianExpFam
¶
Bases: Module
Gaussian in natural (exponential family) parameters.
where:
- Natural parameters:
eta1 = Lambda @ mu,eta2 = -0.5 * Lambda - Sufficient statistics:
T(x) = [x, x x^T] - Log-partition (as
log_partitionreturns it): ``A(eta) = -0.25 * eta1^T eta2^{-1} eta1 - 0.5 * log|-2 eta2| - 0.5 * N * log(2 pi)``
- Base measure:
h(x) = 1, solog q(x) = eta^T T(x) - A(eta). The(2 pi)^{-N/2}normaliser lives inA, not inh.
Attributes:
| Name | Type | Description |
|---|---|---|
eta1 |
Float[Array, ' N']
|
Natural location parameter, shape |
eta2 |
AbstractLinearOperator
|
Natural precision-like operator, shape |
Source code in src/gaussx/_expfam/_gaussian.py
from_mean_cov(mu: Float[Array, ' N'], Sigma: lx.AbstractLinearOperator) -> GaussianExpFam
staticmethod
¶
Construct from mean and covariance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
Float[Array, ' N']
|
Mean vector, shape |
required |
Sigma
|
AbstractLinearOperator
|
Covariance operator, shape |
required |
Returns:
| Type | Description |
|---|---|
GaussianExpFam
|
A |
Source code in src/gaussx/_expfam/_gaussian.py
from_mean_prec(mu: Float[Array, ' N'], Lambda: lx.AbstractLinearOperator) -> GaussianExpFam
staticmethod
¶
Construct from mean and precision.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
Float[Array, ' N']
|
Mean vector, shape |
required |
Lambda
|
AbstractLinearOperator
|
Precision operator, shape |
required |
Returns:
| Type | Description |
|---|---|
GaussianExpFam
|
A |
Source code in src/gaussx/_expfam/_gaussian.py
to_mean_cov(expfam: GaussianExpFam) -> tuple[Float[Array, ' N'], lx.AbstractLinearOperator]
¶
Convert natural parameters to the mean and covariance.
The inverse of GaussianExpFam.from_mean_cov. These are not the
expectation parameters (mu, mu mu^T + Sigma): for those, use
gaussx.natural_to_expectation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expfam
|
GaussianExpFam
|
Gaussian in natural form. |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, ' N'], AbstractLinearOperator]
|
Tuple |
Source code in src/gaussx/_expfam/_gaussian.py
mean_cov_to_natural(mu: Float[Array, ' N'], Sigma: lx.AbstractLinearOperator, *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, ' N'], lx.AbstractLinearOperator]
¶
Convert mean/covariance to natural parameters (operator form).
Given mean mu and covariance Sigma:
eta1 = solve(Sigma, mu)eta2 = -0.5 * inv(Sigma)
Operator structure (diagonal, Kronecker, …) is exploited via
structural dispatch. For dense-array inputs see
mean_chol_to_natural.
For block-tridiagonal (SSM) inputs see
gaussx._ssm._ssm_natural.ssm_to_naturals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
Float[Array, ' N']
|
Mean vector, shape |
required |
Sigma
|
AbstractLinearOperator
|
Covariance operator, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy. When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' N']
|
Tuple |
AbstractLinearOperator
|
eta2 is a linear operator. |
Source code in src/gaussx/_expfam/_natural.py
natural_to_mean_cov(eta1: Float[Array, ' N'], eta2: lx.AbstractLinearOperator, *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, ' N'], lx.AbstractLinearOperator]
¶
Convert natural parameters to mean/covariance (operator form).
Given natural parameters (eta1, eta2) where
eta1 = Lambda @ mu and eta2 = -0.5 * Lambda:
mu = solve(-2 * eta2, eta1)Sigma = inv(-2 * eta2)
Operator structure (diagonal, Kronecker, …) is exploited via
structural dispatch. For dense-array inputs see
natural_to_mean_chol.
For block-tridiagonal (SSM) inputs see
gaussx._ssm._ssm_natural.naturals_to_ssm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
eta1
|
Float[Array, ' N']
|
Natural location parameter, shape |
required |
eta2
|
AbstractLinearOperator
|
Natural precision-like operator, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy. When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' N']
|
Tuple |
AbstractLinearOperator
|
Sigma is a linear operator. |
Source code in src/gaussx/_expfam/_natural.py
mean_chol_to_natural(mu: Float[Array, '*batch N'], S_sqrt: Float[Array, '*batch N N']) -> tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
¶
Convert mean/variance (Cholesky) to natural parameters.
Given mu and lower-triangular S_sqrt such that
Sigma = S_sqrt @ S_sqrt^T:
eta1 = Sigma^{-1} mueta2 = -0.5 * Sigma^{-1}
Uses the Cholesky factor directly via triangular solves; no solver parameter is exposed because the underlying systems are triangular rather than symmetric/PSD, and iterative strategies (CG, BBMM, PreconditionedCG, MINRES) are not valid here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
Float[Array, '*batch N']
|
Mean vector, shape |
required |
S_sqrt
|
Float[Array, '*batch N N']
|
Lower-triangular Cholesky factor, shape |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
|
Tuple |
Source code in src/gaussx/_expfam/_natural.py
natural_to_mean_chol(eta1: Float[Array, '*batch N'], eta2: Float[Array, '*batch N N'], *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
¶
Convert natural parameters to mean/variance (Cholesky).
Given eta1 = Lambda @ mu and eta2 = -0.5 * Lambda:
Sigma = (-2 * eta2)^{-1}mu = Sigma @ eta1S_sqrt = cholesky(Sigma)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
eta1
|
Float[Array, '*batch N']
|
Natural location parameter, shape |
required |
eta2
|
Float[Array, '*batch N N']
|
Natural quadratic parameter, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, '*batch N']
|
Tuple |
Float[Array, '*batch N N']
|
Cholesky factor of the covariance. |
Source code in src/gaussx/_expfam/_natural.py
mean_chol_to_expectation(mu: Float[Array, '*batch N'], S_sqrt: Float[Array, '*batch N N']) -> tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
¶
Convert mean/variance (Cholesky) to expectation parameters.
Given mu and S_sqrt (lower-triangular Cholesky of Sigma):
m1 = mum2 = mu @ mu^T + Sigma = mu @ mu^T + S_sqrt @ S_sqrt^T
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
Float[Array, '*batch N']
|
Mean vector, shape |
required |
S_sqrt
|
Float[Array, '*batch N N']
|
Lower-triangular Cholesky factor, shape |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
|
Tuple |
Source code in src/gaussx/_expfam/_natural.py
expectation_to_mean_chol(m1: Float[Array, '*batch N'], m2: Float[Array, '*batch N N']) -> tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
¶
Convert expectation parameters to mean/variance (Cholesky).
Given m1 = mu and m2 = mu @ mu^T + Sigma:
mu = m1Sigma = m2 - m1 @ m1^TS_sqrt = cholesky(Sigma)
No solver parameter is exposed because the only linear-algebra operation is Cholesky factorization, which is structurally fixed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
m1
|
Float[Array, '*batch N']
|
First moment (mean), shape |
required |
m2
|
Float[Array, '*batch N N']
|
Second moment, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*batch N']
|
Tuple |
Float[Array, '*batch N N']
|
Cholesky factor of the covariance. |
Source code in src/gaussx/_expfam/_natural.py
expectation_to_natural(m1: Float[Array, '*batch N'], m2: Float[Array, '*batch N N'], *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
¶
Convert expectation parameters to natural parameters.
Given m1 = mu and m2 = mu @ mu^T + Sigma:
Sigma = m2 - m1 @ m1^Teta1 = Sigma^{-1} @ m1eta2 = -0.5 * Sigma^{-1}
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
m1
|
Float[Array, '*batch N']
|
First moment (mean), shape |
required |
m2
|
Float[Array, '*batch N N']
|
Second moment, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
|
Tuple |
Source code in src/gaussx/_expfam/_natural.py
natural_to_expectation(eta1: Float[Array, '*batch N'], eta2: Float[Array, '*batch N N'], *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
¶
Convert natural parameters to expectation parameters.
Given eta1 = Lambda @ mu and eta2 = -0.5 * Lambda:
Sigma = (-2 * eta2)^{-1}mu = Sigma @ eta1m1 = mum2 = mu @ mu^T + Sigma
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
eta1
|
Float[Array, '*batch N']
|
Natural location parameter, shape |
required |
eta2
|
Float[Array, '*batch N N']
|
Natural quadratic parameter, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
|
Tuple |
Source code in src/gaussx/_expfam/_natural.py
log_partition(expfam: GaussianExpFam) -> Float[Array, '']
¶
Log-partition function A(eta).
With the base measure \(h(x) = 1\) (see GaussianExpFam), so that
\(\log q(x) = \eta^T T(x) - A(\eta)\) is the normalised log-density.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expfam
|
GaussianExpFam
|
Gaussian in natural form. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
Scalar log-partition value. |
Source code in src/gaussx/_expfam/_gaussian.py
fisher_info(expfam: GaussianExpFam) -> lx.AbstractLinearOperator
¶
Fisher information with respect to the mean, \(F_\mu = \Sigma^{-1}\).
Returns the precision \(\Lambda = -2\eta_2\), which is the Fisher information of the mean parameter \(\mu\) (the \(\mu\) block that natural-gradient descent on a \((\mu, \Sigma)\)-parameterised objective uses). It is not the Fisher information in natural coordinates, \(\nabla^2 A(\eta)\): that is the \((N + N^2)\)-dimensional covariance of \(T(x) = [x, x x^T]\), whose \(\eta_1\)-\(\eta_1\) block is \(\Sigma\), not \(\Lambda\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expfam
|
GaussianExpFam
|
Gaussian in natural form. |
required |
Returns:
| Type | Description |
|---|---|
AbstractLinearOperator
|
Precision operator \(\Lambda = -2\eta_2\). |
Source code in src/gaussx/_expfam/_gaussian.py
sufficient_stats(x: Float[Array, '*batch N']) -> tuple[Float[Array, '*batch N'], Float[Array, '*batch N N']]
¶
Compute sufficient statistics T(x) = [x, x x^T].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Float[Array, '*batch N']
|
Data, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*batch N']
|
Tuple |
Float[Array, '*batch N N']
|
|
Source code in src/gaussx/_expfam/_gaussian.py
kl_divergence(q: GaussianExpFam, p: GaussianExpFam) -> Float[Array, '']
¶
KL(first ‖ second): KL(q || p) via the Bregman form on natural parameters.
The first argument is the distribution the expectation is taken under,
whatever its parameter name: kl_divergence(a, b) = KL(a || b).
Exponential-family expression of the KL divergence in terms of the
log-partition A and the natural parameters of q and p.
Mathematically equivalent to
gaussian_kl.
The current implementation evaluates the Bregman form by routing
through to_mean_cov for the natural-gradient term
(eta_p - eta_q)^T nabla A(eta_q). The second-moment contraction
splits into a quadratic form (operator matvecs) plus
gaussx.trace_product, so structured eta2 / Sigma_q
operators are never materialized. The benefit relative to
gaussian_kl is keeping the gradient flowing in
natural-parameter space (suitable inside a natural-gradient loop).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
q
|
GaussianExpFam
|
First Gaussian (the "true" distribution). |
required |
p
|
GaussianExpFam
|
Second Gaussian (the "approximate" distribution). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
Scalar KL divergence. |
See Also
gaussian_kl: General KL
in mean/covariance form with lineax operators.
Source code in src/gaussx/_expfam/_gaussian.py
Deprecated aliases¶
These names still work but warn on use and will be removed in gaussx 0.7.0 (see the deprecation policy).
Structured linear algebra and Gaussian primitives for JAX.
to_natural(mu: Float[Array, ' N'], Sigma: lx.AbstractLinearOperator) -> tuple[Float[Array, ' N'], lx.AbstractLinearOperator]
¶
Deprecated alias of gaussx.mean_cov_to_natural.
It takes the mean and covariance, not expectation parameters. It will be removed in gaussx 0.7.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
Float[Array, ' N']
|
Mean vector, shape |
required |
Sigma
|
AbstractLinearOperator
|
Covariance operator, shape |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, ' N'], AbstractLinearOperator]
|
Tuple |
Source code in src/gaussx/_expfam/_gaussian.py
to_expectation(expfam: GaussianExpFam) -> tuple[Float[Array, ' N'], lx.AbstractLinearOperator]
¶
Deprecated alias of to_mean_cov.
Despite its name it returns the mean and covariance (mu, Sigma),
not the expectation parameters (mu, mu mu^T + Sigma) (see
gaussx.natural_to_expectation). It will be removed in gaussx 0.7.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expfam
|
GaussianExpFam
|
Gaussian in natural form. |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, ' N'], AbstractLinearOperator]
|
Tuple |