State-Space Models & Kalman¶
Layer 3 recipes for linear-Gaussian state-space models. Stationary 1-D GP kernels with rational spectral densities admit exact SDE representations
turning \(O(N^3)\) GP inference into \(O(N d^3)\) Kalman filtering. This page covers the SDE kernel zoo, the filters and smoothers (sequential, parallel associative-scan, square-root, and steady-state), and the natural-parameter / site machinery for non-conjugate likelihoods.
SDE kernels¶
Structured linear algebra and Gaussian primitives for JAX.
SDEKernel
¶
Bases: Module
Abstract base class for state-space kernel representations.
Subclasses implement sde_params to provide the continuous-time
SDE matrices (F, L, H, Q_c, P_inf). The default discretise
uses the matrix exponential for discretization; subclasses may override
with closed-form solutions.
Source code in src/gaussx/_ssm/_sde_kernel.py
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 | |
state_dim: int
abstractmethod
property
¶
Dimension of the latent state vector.
stationary: bool
property
¶
Whether the process has a stationary distribution.
True for every kernel in the zoo, which is why that is the
default; a non-stationary kernel such as
gaussx.IntegratedWienerSDE overrides it. Consumers should
branch on this rather than on sde_params().P_inf is None:
the two are different questions, since a stationary kernel may
report P_inf=None when it has no closed form for it (a
learned drift, say).
sde_params() -> SDEParams
abstractmethod
¶
initial_covariance() -> Float[Array, 'd d']
¶
Return the covariance of the state at the first time point.
For a stationary kernel the process is assumed started in its stationary distribution, so this is \(P_\infty\) — the default implementation returns it and existing kernels need no change. A non-stationary kernel has no such limit to start from and must override this with an explicit choice.
Returns:
| Type | Description |
|---|---|
Float[Array, 'd d']
|
Initial state covariance, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/gaussx/_ssm/_sde_kernel.py
discretise(dt: Float[Array, '']) -> tuple[Float[Array, 'd d'], Float[Array, 'd d']]
¶
Discretise the SDE at time step dt.
Default implementation computes:
A = expm(F * dt)
Q = P_inf - A @ P_inf @ A^T
When sde_params() returns P_inf=None — a kernel with no
closed-form stationary covariance, such as one whose drift is a
learned parameter — this falls back to gaussx.discretise_mfd,
which recovers both A and Q from one matrix exponential and
is well defined for every F. The fallback is chosen at trace
time from a static None check, so kernels that do supply
P_inf keep the stationary route and its precision exactly.
Subclasses may override with closed-form expressions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
Float[Array, '']
|
Time step (scalar, non-negative). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'd d']
|
Tuple |
Float[Array, 'd d']
|
Q is the process noise covariance. |
Source code in src/gaussx/_ssm/_sde_kernel.py
discretise_sequence(dt: Float[Array, ' N']) -> tuple[Float[Array, 'N d d'], Float[Array, 'N d d']]
¶
Discretise the SDE at multiple time steps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
Float[Array, ' N']
|
Time steps, shape |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, 'N d d'], Float[Array, 'N d d']]
|
Tuple |
Source code in src/gaussx/_ssm/_sde_kernel.py
SDEParams
¶
Bases: NamedTuple
Continuous-time SDE parameters for a linear SDE.
Defines the linear time-invariant SDE:
dx = F x dt + L dW, W ~ N(0, Q_c dt)
with observation model y = H x.
Attributes:
| Name | Type | Description |
|---|---|---|
F |
Float[Array, 'd d']
|
Drift matrix, shape |
L |
Float[Array, 'd s']
|
Diffusion matrix, shape |
H |
Float[Array, '1 d']
|
Observation matrix, shape |
Q_c |
Float[Array, 's s']
|
Spectral density, shape |
P_inf |
Float[Array, 'd d'] | None
|
Stationary covariance, shape |
Source code in src/gaussx/_ssm/_sde_kernel.py
ConstantSDE
¶
Bases: SDEKernel
State-space representation of a constant kernel.
Models \(k(\tau) = \sigma^2\) — a degenerate kernel with zero dynamics and zero diffusion. State dimension is 1.
Attributes:
| Name | Type | Description |
|---|---|---|
variance |
Float[Array, '']
|
Signal variance \(\sigma^2\). |
Source code in src/gaussx/_ssm/_constant.py
sde_params() -> SDEParams
¶
Return SDE parameters for the constant kernel.
Source code in src/gaussx/_ssm/_constant.py
discretise(dt: Float[Array, '']) -> tuple[Float[Array, 'd d'], Float[Array, 'd d']]
¶
Closed-form: A = I, Q = 0 (no dynamics).
Source code in src/gaussx/_ssm/_constant.py
MaternSDE
¶
Bases: SDEKernel
State-space representation of the Matern kernel.
Supports orders 0 (Matern-1/2), 1 (Matern-3/2), and 2 (Matern-5/2).
The state dimension is order + 1.
Attributes:
| Name | Type | Description |
|---|---|---|
variance |
Float[Array, '']
|
Signal variance \(\sigma^2\). |
lengthscale |
Float[Array, '']
|
Lengthscale \(\ell\). |
order |
int
|
Matern order (0, 1, or 2). |
Source code in src/gaussx/_ssm/_matern.py
sde_params() -> SDEParams
¶
Compute SDE parameters for the Matern kernel.
Source code in src/gaussx/_ssm/_matern.py
PeriodicSDE
¶
Bases: SDEKernel
State-space representation of the periodic (MacKay) kernel.
Approximates the periodic kernel via Fourier series truncation
to n_harmonics terms. State dimension is 2 * n_harmonics.
Attributes:
| Name | Type | Description |
|---|---|---|
variance |
Float[Array, '']
|
Signal variance \(\sigma^2\). |
lengthscale |
Float[Array, '']
|
Lengthscale \(\ell\). |
period |
Float[Array, '']
|
Period \(T\). |
n_harmonics |
int
|
Number of Fourier harmonics (truncation order). |
Source code in src/gaussx/_ssm/_periodic.py
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 | |
sde_params() -> SDEParams
¶
Return SDE parameters for the periodic kernel.
Source code in src/gaussx/_ssm/_periodic.py
discretise(dt: Float[Array, '']) -> tuple[Float[Array, 'd d'], Float[Array, 'd d']]
¶
Closed-form: block-diagonal rotation matrices.
Source code in src/gaussx/_ssm/_periodic.py
QuasiPeriodicSDE
¶
Bases: ProductSDE
Quasi-periodic kernel: product of Matern and Periodic SDE.
Attributes:
| Name | Type | Description |
|---|---|---|
kernel1 |
SDEKernel
|
Modulating kernel (typically Matern). |
kernel2 |
SDEKernel
|
Periodic kernel. |
Source code in src/gaussx/_ssm/_composition.py
CosineSDE
¶
Bases: SDEKernel
State-space representation of the cosine kernel.
Models \(k(\tau) = \sigma^2 \cos(\omega_0 \tau)\) via a 2-D rotation SDE. State dimension is 2.
Attributes:
| Name | Type | Description |
|---|---|---|
variance |
Float[Array, '']
|
Signal variance \(\sigma^2\). |
frequency |
Float[Array, '']
|
Angular frequency \(\omega_0\). |
Source code in src/gaussx/_ssm/_periodic.py
sde_params() -> SDEParams
¶
Return SDE parameters for the cosine kernel.
Source code in src/gaussx/_ssm/_periodic.py
discretise(dt: Float[Array, '']) -> tuple[Float[Array, 'd d'], Float[Array, 'd d']]
¶
Closed-form rotation matrix discretization.
Source code in src/gaussx/_ssm/_periodic.py
IntegratedWienerSDE
¶
Bases: SDEKernel
State-space representation of an integrated Wiener process.
The \(p\)-times integrated Wiener process — the local linear trend
prior at order=1 — with state
\(x(t) = [f(t), f'(t), \dots, f^{(p)}(t)]\) and
so the top derivative is white noise and every lower component is its
integral. State dimension is order + 1. The SDE matrices are
with \(F\) nilpotent, which is what makes the process non-stationary:
its marginal variance grows without bound, so no \(P_\infty\) exists and
sde_params reports P_inf=None. discretise is overridden with
the exact closed form (below) and never needs one, but the filter has
to be started from an explicit initial_covariance instead.
That non-stationarity is the point: unlike a Matérn prior, the local linear trend commits to no lengthscale — it is smooth and linear unless the data push back — so a long record may drift without the model asserting a scale on which it must revert.
Attributes:
| Name | Type | Description |
|---|---|---|
diffusion |
Float[Array, '']
|
Diffusion intensity \(q\), the spectral density of the white noise driving the top derivative. Must be non-negative — \(Q\) is linear in it, so a negative value returns something that is not a covariance. Like every hyperparameter in the kernel zoo this is assumed rather than checked; constrain it with a positive transform when it is learned. |
order |
int
|
Number of integrations \(p\). |
P_0 |
Float[Array, 'd d'] | None
|
Initial state covariance, shape |
Source code in src/gaussx/_ssm/_wiener.py
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 | |
stationary: bool
property
¶
False — the marginal variance grows without bound.
sde_params() -> SDEParams
¶
Return SDE parameters, with P_inf=None.
The drift is nilpotent, so no stationary covariance exists; see
initial_covariance for what starts the filter instead.
Source code in src/gaussx/_ssm/_wiener.py
initial_covariance() -> Float[Array, 'd d']
¶
Return the initial state covariance \(P_0\).
Defaults to a diffuse kappa * I when the P_0 field is
None, with kappa set from the dtype's precision by
_default_diffuse_variance — a vaguer prior than that is not
merely wasteful but actively wrong, since the Kalman update
cancels it to a zero-variance first estimate.
Pass a P_0 to encode what is actually known about the level
and its derivatives at the first time point — e.g.
diag(kappa, s^2) for a vague level and a slope of scale
s. Do so in particular when the observation noise is large:
the default is diffuse relative to a noise variance of order
one, not relative to every scale.
Source code in src/gaussx/_ssm/_wiener.py
discretise(dt: Float[Array, '']) -> tuple[Float[Array, 'd d'], Float[Array, 'd d']]
¶
Closed-form discretisation — no expm, no \(P_\infty\).
The drift is nilpotent, so its exponential terminates:
which at order=1 is the familiar
Both are exact, so this route is cheaper and more accurate than
the gaussx.discretise_mfd fallback the P_inf=None base
implementation would otherwise take.
Note
The coefficients carry \(1/(p-i)!\), which outruns float64
from roughly order=90 up and underflows to zero there
(XLA flushes the subnormals in between). Nothing is
silently wrong — the entries are zero rather than garbage —
but a prior that integrates white noise ninety times is not
a numerically meaningful object in any dtype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
Float[Array, '']
|
Time step. Must be non-negative — a negative step would
return a |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, 'd d'], Float[Array, 'd d']]
|
Tuple |
Source code in src/gaussx/_ssm/_wiener.py
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 | |
ProductSDE
¶
Bases: SDEKernel
Product of two SDE kernels via Kronecker composition.
Attributes:
| Name | Type | Description |
|---|---|---|
kernel1 |
SDEKernel
|
First component kernel. |
kernel2 |
SDEKernel
|
Second component kernel. |
Source code in src/gaussx/_ssm/_composition.py
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 | |
stationary: bool
property
¶
Stationary only if both factors are.
sde_params() -> SDEParams
¶
Return Kronecker-structured SDE parameters.
The drift is the Kronecker sum \(F_1 \oplus F_2\) and the stationary covariance the Kronecker product \(P_1 \otimes P_2\). Substituting those into the Lyapunov equation \(F P + P F^\top + B = 0\) fixes the composite diffusion at
which is not \(B_1 \otimes B_2\) — the value a naive
\(L_1 \otimes L_2\), \(Q_{c,1} \otimes Q_{c,2}\) pair would imply.
Reporting the latter used to hand out a tuple that failed its own
Lyapunov equation (gh-219); for a Matérn ⊗ Cosine product it was
identically zero, since CosineSDE has \(Q_c = 0\).
The sum of two Kronecker products is still expressible in the
(L, Q_c) form, by widening the noise dimension and carrying
each factor's stationary covariance in the spectral density:
so that \(L Q_c L^\top\) telescopes to exactly the \(B\) above by the
mixed-product property. Writing it this way rather than through
square roots \(S_i S_i^\top = P_i\) keeps the result exact for
singular or zero \(P_\infty\) (where a Cholesky would need jitter,
and would then violate the very Lyapunov equation this enforces)
and keeps sde_params reverse-mode differentiable.
Note
SDEParams currently types its fields as dense
jaxtyping.Float[Array, ...]. The Kronecker products
below are dense materializations of size
(state_dim, state_dim), where state_dim is
kernel1.state_dim * kernel2.state_dim — for typical SSM
kernels (Matérn-3/2, periodic) this is ≤ 32, so the
materialization is bounded and cheap. A future refactor
could expose a parallel sde_operators() method that
returns gaussx.Kronecker operators for downstream
filters that can exploit the structure (issue #153).
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If either factor lacks a stationary covariance. The composite diffusion needs both, so the tuple cannot be built — and reporting the inconsistent Kronecker product instead is what gh-219 was about. |
Source code in src/gaussx/_ssm/_composition.py
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 | |
discretise(dt: Float[Array, '']) -> tuple[Float[Array, 'd d'], Float[Array, 'd d']]
¶
Discretise via the Kronecker matrix-exponential identity.
For a product kernel F = F_1 \oplus F_2 = F_1 \otimes I + I \otimes F_2,
the factors F_1 \otimes I and I \otimes F_2 commute, so
This computes two expm calls of size d_1 and d_2
each, plus one Kronecker product, instead of one expm of
size d_1 \cdot d_2. Numerically equivalent to the dense
expm on F but cheaper for moderate factor sizes.
Q = P_\infty - A P_\infty A^T exploits the same factorisation.
By the mixed-product property,
so the congruence is evaluated per factor via
gaussx.process_noise_covariance on gaussx.Kronecker
operands — O(d_1^3 + d_2^3) instead of the
O((d_1 d_2)^3) triple product on the full matrix. Only the
final Q is materialised, to keep the consumer-facing
(A, Q) interface unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
Float[Array, '']
|
Time step (scalar, positive). |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, 'd d'], Float[Array, 'd d']]
|
Tuple |
Source code in src/gaussx/_ssm/_composition.py
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 | |
SumSDE
¶
Bases: SDEKernel
Sum of SDE kernels via block-diagonal composition.
Attributes:
| Name | Type | Description |
|---|---|---|
kernels |
tuple[SDEKernel, ...]
|
Tuple of component SDE kernels. |
Source code in src/gaussx/_ssm/_composition.py
stationary: bool
property
¶
Stationary only if every component is.
initial_covariance() -> Float[Array, 'd d']
¶
Return the block-diagonal initial covariance.
The components are independent, so their initial covariances stack block-diagonally — which lets a sum mix stationary and non-stationary components (a local linear trend plus a Matern seasonal, say), each started from its own.
Source code in src/gaussx/_ssm/_composition.py
sde_params() -> SDEParams
¶
Return block-diagonal SDE parameters.
Source code in src/gaussx/_ssm/_composition.py
sde_autocovariance(kernel: SDEKernel, tau: Float[Array, ' *batch']) -> Float[Array, ' *batch']
¶
Compute the stationary autocovariance of an SDE kernel.
Evaluates:
K(\tau) = H \, \exp(F |\tau|) \, P_\infty \, H^T
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kernel
|
SDEKernel
|
An SDE kernel with |
required |
tau
|
Float[Array, ' *batch']
|
Lag values, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' *batch']
|
Autocovariance values |
Source code in src/gaussx/_ssm/_autocovariance.py
Stationary and non-stationary kernels¶
Most of the zoo is stationary: the process is assumed started in its
stationary distribution, so SDEKernel.initial_covariance returns
\(P_\infty\) and sde_autocovariance can report \(K(\tau)\).
IntegratedWienerSDE is not. Its drift is nilpotent, the marginal
variance grows without bound, and the covariance depends on both times
rather than on their difference — so there is no \(P_\infty\) and no
\(K(\tau)\). It reports stationary = False and sde_params().P_inf =
None, and the filter is started instead from an explicit
initial_covariance (diffuse by default). That is the local linear
trend prior at order=1: smooth, and linear unless the data push back,
with no lengthscale to choose.
Consumers should branch on SDEKernel.stationary rather than on whether
P_inf is None — the two are different questions, since a stationary
kernel may have no closed form for \(P_\infty\) (a learned drift, say).
A SumSDE may mix the two: it is stationary only if all its components
are, and stacks their initial covariances block-diagonally. A
ProductSDE needs both factors' \(P_\infty\) and rejects a
non-stationary one.
Discretisation¶
Turning the continuous-time SDE into \(x_k = A x_{k-1} + q_k\) takes two routes, and which one applies depends on whether a stationary covariance exists.
The default, SDEKernel.discretise, uses the stationary route
\(Q = P_\infty - A P_\infty A^\top\) (process_noise_covariance,
documented under Process noise below). It is
exact and cheap, and every stationary kernel in the zoo above supplies
the \(P_\infty\) it needs. IntegratedWienerSDE overrides discretise
with an exact closed form instead — its nilpotent drift makes the
exponential terminate — so it needs neither route.
discretise_mfd is the fallback for when that covariance is not
available — most importantly when \(F\) is a learned parameter rather
than derived from a kernel. Recovering \(P_\infty\) then means solving the
Lyapunov equation \(F P + P F^\top + Q_c = 0\), which has a unique solution
only if \(\lambda_i(F) + \lambda_j(F) \neq 0\) for every pair of
eigenvalues. That condition fails for any undamped oscillatory mode,
where \(\lambda = \pm i\omega\) and so \(\lambda + \bar\lambda = 0\)
identically — CosineSDE has exactly that drift, and sidesteps it with a
closed form that a learned \(F\) cannot use.
Matrix-fraction decomposition needs no \(P_\infty\) at all: it recovers both \(A\) and \(Q\) from a single \(2d \times 2d\) matrix exponential (Van Loan 1978) and is well defined for every \(F\). Reach for it when the drift is fitted; keep the stationary route otherwise. Note the obstruction is degeneracy of that Sylvester system, not instability — constraining \(F\) to be Hurwitz would not fix it, and would forbid the oscillatory modes MFD exists to support.
Structured linear algebra and Gaussian primitives for JAX.
discretise_mfd(F: Float[Array, 'd d'], Q_c: Float[Array, 'd d'], dt: Float[Array, '']) -> tuple[Float[Array, 'd d'], Float[Array, 'd d']]
¶
Discretise a linear SDE by matrix-fraction decomposition.
Returns \((A, Q)\) for the discrete-time model \(x_k = A x_{k-1} + q_k\) with \(q_k \sim N(0, Q)\), where
Both come from one \(2d \times 2d\) matrix exponential (Van Loan 1978). For the augmented generator
the integral is \(Q = C D^{-1}\), and \(D = e^{-F^\top \Delta t}\) is always invertible with \(D^{-1} = A^\top\) — so no inverse or solve is formed here.
Unlike the stationary route in gaussx.SDEKernel.discretise, this
needs no \(P_\infty\) and is well defined for every \(F\), including a
learned \(F\) whose eigenvalues sum to zero in pairs. The stationary
route recovers \(P_\infty\) from the Lyapunov equation
\(F P + P F^\top + Q_c = 0\), which has a unique solution only when
\(\lambda_i(F) + \lambda_j(F) \neq 0\) for all \(i, j\). An undamped
oscillatory mode has \(\lambda = \pm i\omega\), so \(\lambda + \bar\lambda
= 0\) always and the Lyapunov route degenerates.
Note
The obstruction is degeneracy of that Sylvester system, not instability. The identity \(Q = P_\infty - A P_\infty A^\top\) holds for unstable \(F\), and even when the Lyapunov solution is not PSD and so is not a valid covariance. Constraining \(F\) to be Hurwitz would not fix the degeneracy, and would forbid exactly the oscillatory modes this function exists to support.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
F
|
Float[Array, 'd d']
|
Continuous-time drift matrix, shape |
required |
Q_c
|
Float[Array, 'd d']
|
Continuous-time diffusion covariance \(L Q_c L^\top\), shape
|
required |
dt
|
Float[Array, '']
|
Time step. Must be non-negative; checked with
|
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'd d']
|
Tuple |
Float[Array, 'd d']
|
|
Source code in src/gaussx/_ssm/_discretise.py
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 | |
discretise_mfd_sequence(F: Float[Array, 'd d'], Q_c: Float[Array, 'd d'], dt: Float[Array, ' N']) -> tuple[Float[Array, 'N d d'], Float[Array, 'N d d']]
¶
Vectorised gaussx.discretise_mfd over a vector of time steps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
F
|
Float[Array, 'd d']
|
Continuous-time drift matrix, shape |
required |
Q_c
|
Float[Array, 'd d']
|
Continuous-time diffusion covariance, shape |
required |
dt
|
Float[Array, ' N']
|
Time steps, shape |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, 'N d d'], Float[Array, 'N d d']]
|
Tuple |
Source code in src/gaussx/_ssm/_discretise.py
Nonlinear filters¶
nonlinear_kalman_filter and nonlinear_rts_smoother take callables
rather than matrices, and moment-match them through an
integrator. The choice of integrator is the choice of
filter — one loop yields the whole textbook family:
| integrator | filter |
|---|---|
TaylorIntegrator |
extended Kalman filter (EKF) |
UnscentedIntegrator |
unscented Kalman filter (UKF) |
CubatureIntegrator |
cubature Kalman filter (CKF), degree 3 |
FifthOrderCubatureIntegrator |
degree-5 cubature filter |
GaussHermiteIntegrator |
Gauss-Hermite Kalman filter (GHKF) |
MonteCarloIntegrator |
Monte-Carlo Kalman filter |
The gain is \(K = C S^{-1}\), built from the integrator's cross-covariance \(C\), so no Jacobian is formed anywhere — that is what makes the EKF and the UKF the same code. An integrator that does not supply a cross-covariance is rejected rather than silently producing a zero gain.
Two behaviours differ from kalman_filter, both deliberately:
log_likelihoodis a moment-matched surrogate, not the exact marginal likelihood, because \(S\) is the matched innovation covariance rather than the true one. It coincides with the exact value when the maps are affine. Maximising it to tune hyperparameters is standard practice for nonlinear Gaussian filters, but it is a surrogate.- The covariance update defaults to Joseph form (
joseph=True). \(K = C S^{-1}\) is only approximately the optimal gain, and \(P^- - K S K^\top\) is guaranteed PSD only for the optimal gain, whereas Joseph form is a sum of two PSD terms for any \(K\). The effective observation matrix it needs is the statistical-linearisation gain \(H_{\text{eff}} = C^\top (P^-)^{-1}\) — whatstatistical_linear_regressionreturns asA— and its noise is \(R + \Omega\), not \(R\), with \(\Omega\) the linearisation residual.
With that residual included the two forms are analytically identical for
any consistent matched joint, not merely for affine maps. joseph is
therefore a numerical choice, not a modelling one: it selects how the
same covariance is computed, and should not change results beyond
floating point. Dropping \(\Omega\) — the naive reading of Joseph here —
would instead understate the posterior by \(K \Omega K^\top\).
With affine dynamics and obs_fn the filter reproduces kalman_filter
— means, covariances and log-likelihood — and the smoother reproduces
rts_smoother, for every deterministic rule, since each is exact for
affine maps. MonteCarloIntegrator is the exception: it propagates
finite-sample empirical moments, so it converges to the linear filter at
the usual \(O(1/\sqrt{n})\) rate rather than matching it. A discrepancy
there is sampling error, not a bug.
Driving your own loop¶
The wrappers above are a jax.lax.scan over three public per-step
functions, which are usable on their own when the loop is the part you
want to control — an irregular time grid, a custom gating rule, a filter
interleaved with something else, or a bank of filters:
| function | step |
|---|---|
nonlinear_kalman_predict |
\(m^-, P^- = \mathcal{T}[f](m, P)\), \(P^- \mathrel{+}= Q\) |
nonlinear_kalman_update |
match \(h\), then \(K = C S^{-1}\) and the Gaussian update |
nonlinear_rts_step |
one RTS backward correction |
Each takes the same integrator, so the choice of filter carries through
unchanged. nonlinear_kalman_update returns its log-likelihood increment
rather than accumulating, leaving the accumulation to the caller.
Structured linear algebra and Gaussian primitives for JAX.
nonlinear_kalman_filter(dynamics: Callable[[Float[Array, ' N']], Float[Array, ' N']], obs_fn: Callable[[Float[Array, ' N']], Float[Array, ' M']], process_noise: Float[Array, '*T N N'] | lx.AbstractLinearOperator, obs_noise: Float[Array, '*T M M'] | lx.AbstractLinearOperator, observations: Float[Array, 'T M'], init_mean: Float[Array, ' N'], init_cov: Float[Array, 'N N'], *, integrator: AbstractIntegrator | None = None, mask: Bool[Array, ' T'] | Bool[Array, 'T M'] | None = None, joseph: bool = True, solver: AbstractSolverStrategy | None = None) -> FilterState
¶
Moment-matched nonlinear Kalman filter.
Propagates a Gaussian belief through nonlinear dynamics and
obs_fn by moment matching, using integrator for both the
predict and the update step. The choice of integrator is the choice
of filter:
| integrator | filter |
|---|---|
gaussx.TaylorIntegrator |
extended Kalman filter (EKF) |
gaussx.UnscentedIntegrator |
unscented Kalman filter (UKF) |
gaussx.CubatureIntegrator |
cubature Kalman filter (CKF), \(2N\) pts |
gaussx.FifthOrderCubatureIntegrator |
degree-5 cubature, \(2N^2+1\) pts |
gaussx.GaussHermiteIntegrator |
Gauss-Hermite Kalman filter (GHKF) |
gaussx.MonteCarloIntegrator |
Monte-Carlo Kalman filter |
Each step is
where \(\mathcal{T}\) is the integrator's moment transform. The gain comes from the integrator's cross-covariance, so no Jacobian appears anywhere — that is what makes the EKF and the UKF the same code.
Note
log_likelihood is a moment-matched surrogate, not the exact
marginal likelihood: \(S\) is the matched innovation covariance
rather than the true one. It reduces to the exact value when
dynamics and obs_fn are affine. Users maximising it to tune
hyperparameters are maximising a surrogate, which is standard
practice for nonlinear Gaussian filters but worth knowing.
Note
Unlike gaussx.kalman_filter, the covariance update defaults to
Joseph form. \(K = C S^{-1}\) is only approximately the optimal gain,
and \(P^- - K S K^\top\) is guaranteed PSD only for the optimal
gain, whereas Joseph form is a sum of two PSD terms for any \(K\).
Joseph form needs an \(H\), which a moment-matched filter does not
have; the stand-in is the statistical-linearisation gain
\(H_{\text{eff}} = C^\top (P^-)^{-1}\) — what
gaussx.statistical_linear_regression returns as A, and
exactly \(H\) when obs_fn is linear. Its noise is \(R + \Omega\),
with \(\Omega\) the linearisation residual, not \(R\): dropping
\(\Omega\) would understate the posterior covariance by
\(K \Omega K^\top\) on nonlinear maps. With it included the two
forms agree analytically, so joseph selects how the same
covariance is computed, not which covariance you get.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dynamics
|
Callable[[Float[Array, ' N']], Float[Array, ' N']]
|
State transition |
required |
obs_fn
|
Callable[[Float[Array, ' N']], Float[Array, ' M']]
|
Observation operator |
required |
process_noise
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
\(Q\), additive in state space. Shape |
required |
obs_noise
|
Float[Array, '*T M M'] | AbstractLinearOperator
|
\(R\), additive in observation space. Shape |
required |
observations
|
Float[Array, 'T M']
|
Observed data, shape |
required |
init_mean
|
Float[Array, ' N']
|
Initial state mean, shape |
required |
init_cov
|
Float[Array, 'N N']
|
Initial state covariance, shape |
required |
integrator
|
AbstractIntegrator | None
|
Moment-matching rule. Defaults to
The |
None
|
mask
|
Bool[Array, ' T'] | Bool[Array, 'T M'] | None
|
Optional observation mask, with the same semantics as
|
None
|
joseph
|
bool
|
Use the Joseph-form covariance update. Defaults to
|
True
|
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for the innovation solve. When
|
None
|
Returns:
| Type | Description |
|---|---|
FilterState
|
A |
FilterState
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Source code in src/gaussx/_ssm/_nonlinear_kalman.py
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 685 686 687 688 689 690 691 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 | |
nonlinear_rts_smoother(filter_state: FilterState, dynamics: Callable[[Float[Array, ' N']], Float[Array, ' N']], process_noise: Float[Array, '*T N N'] | lx.AbstractLinearOperator | None = None, *, integrator: AbstractIntegrator | None = None, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, 'T N'], Float[Array, 'T N N']]
¶
Moment-matched nonlinear Rauch-Tung-Striebel smoother.
The backward pass of gaussx.nonlinear_kalman_filter, sharing the same
moment transform — the smoother gain uses the integrator's
cross-covariance between \(x_t\) and \(x_{t+1}\):
with the matching covariance recursion. As in the filter, no Jacobian
is formed; for linear dynamics the gain reduces to
\(P_t A^\top (P^-_{t+1})^{-1}\) and the whole pass to
gaussx.rts_smoother.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filter_state
|
FilterState
|
Output of |
required |
dynamics
|
Callable[[Float[Array, ' N']], Float[Array, ' N']]
|
State transition |
required |
process_noise
|
Float[Array, '*T N N'] | AbstractLinearOperator | None
|
Accepted for API symmetry with
|
None
|
integrator
|
AbstractIntegrator | None
|
Moment-matching rule. Defaults to
|
None
|
solver
|
AbstractSolverStrategy | None
|
Accepted for API symmetry with |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, 'T N'], Float[Array, 'T N N']]
|
Tuple |
Source code in src/gaussx/_ssm/_nonlinear_kalman.py
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 | |
nonlinear_kalman_predict(dynamics: Callable[[Float[Array, ' N']], Float[Array, ' N']], mean: Float[Array, ' N'], cov: Float[Array, 'N N'], process_noise: Float[Array, 'N N'], *, integrator: AbstractIntegrator | None = None) -> tuple[Float[Array, ' N'], Float[Array, 'N N']]
¶
One moment-matched predict step.
Exposed alongside gaussx.nonlinear_kalman_update so a caller can
drive their own loop -- an irregular time grid, a custom gating rule,
a filter interleaved with something else -- without reimplementing the
moment transform. gaussx.nonlinear_kalman_filter is exactly a
jax.lax.scan over these two.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dynamics
|
Callable[[Float[Array, ' N']], Float[Array, ' N']]
|
State transition |
required |
mean
|
Float[Array, ' N']
|
Current mean, shape |
required |
cov
|
Float[Array, 'N N']
|
Current covariance, shape |
required |
process_noise
|
Float[Array, 'N N']
|
\(Q\), shape |
required |
integrator
|
AbstractIntegrator | None
|
Moment-matching rule. Defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, ' N'], Float[Array, 'N N']]
|
Tuple |
Source code in src/gaussx/_ssm/_nonlinear_kalman.py
nonlinear_kalman_update(obs_fn: Callable[[Float[Array, ' N']], Float[Array, ' M']], mean: Float[Array, ' N'], cov: Float[Array, 'N N'], observation: Float[Array, ' M'], obs_noise: Float[Array, 'M M'], *, integrator: AbstractIntegrator | None = None, mask: Bool[Array, ' M'] | None = None, joseph: bool = True, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, ' N'], Float[Array, 'N N'], Float[Array, '']]
¶
One moment-matched update step.
Moment-matches obs_fn at the predicted belief and runs the ordinary
linear-Gaussian update against that matched joint:
The gain is built from the cross-covariance directly, so no Jacobian
appears. See gaussx.nonlinear_kalman_filter for the meaning of
joseph and the caveat on the returned log-likelihood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obs_fn
|
Callable[[Float[Array, ' N']], Float[Array, ' M']]
|
Observation operator |
required |
mean
|
Float[Array, ' N']
|
Predicted mean \(m^-\), shape |
required |
cov
|
Float[Array, 'N N']
|
Predicted covariance \(P^-\), shape |
required |
observation
|
Float[Array, ' M']
|
Observed vector \(y\), shape |
required |
obs_noise
|
Float[Array, 'M M']
|
\(R\), shape |
required |
integrator
|
AbstractIntegrator | None
|
Moment-matching rule. Defaults to
|
None
|
mask
|
Bool[Array, ' M'] | None
|
Optional per-channel mask, shape |
None
|
joseph
|
bool
|
Use the Joseph-form covariance update. Defaults to |
True
|
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy. |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' N']
|
Tuple |
Float[Array, 'N N']
|
increment is the exact marginal over the observed channels. |
Source code in src/gaussx/_ssm/_nonlinear_kalman.py
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 | |
nonlinear_rts_step(dynamics: Callable[[Float[Array, ' N']], Float[Array, ' N']], mean_filtered: Float[Array, ' N'], cov_filtered: Float[Array, 'N N'], mean_predicted: Float[Array, ' N'], cov_predicted: Float[Array, 'N N'], mean_smoothed: Float[Array, ' N'], cov_smoothed: Float[Array, 'N N'], *, integrator: AbstractIntegrator | None = None, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, ' N'], Float[Array, 'N N']]
¶
One moment-matched RTS backward step.
with the matching covariance recursion. Exposed for the same reason as
the filter steps: gaussx.nonlinear_rts_smoother is a
jax.lax.scan over this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dynamics
|
Callable[[Float[Array, ' N']], Float[Array, ' N']]
|
State transition |
required |
mean_filtered
|
Float[Array, ' N']
|
Filtered mean at \(t\). |
required |
cov_filtered
|
Float[Array, 'N N']
|
Filtered covariance at \(t\). |
required |
mean_predicted
|
Float[Array, ' N']
|
Predicted mean at \(t + 1\). |
required |
cov_predicted
|
Float[Array, 'N N']
|
Predicted covariance at \(t + 1\). |
required |
mean_smoothed
|
Float[Array, ' N']
|
Smoothed mean at \(t + 1\). |
required |
cov_smoothed
|
Float[Array, 'N N']
|
Smoothed covariance at \(t + 1\). |
required |
integrator
|
AbstractIntegrator | None
|
Moment-matching rule; use the one the filter used. |
None
|
solver
|
AbstractSolverStrategy | None
|
Accepted for API symmetry with |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, ' N'], Float[Array, 'N N']]
|
Tuple |
Source code in src/gaussx/_ssm/_nonlinear_kalman.py
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 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 | |
masked_moment_inputs(obs_cov: Float[Array, 'M M'], cross_cov: Float[Array, 'N M'], obs_noise: Float[Array, 'M M'], y: Float[Array, ' M'], y_hat: Float[Array, ' M'], mask: Bool[Array, ' M']) -> tuple[Float[Array, 'M M'], Float[Array, 'N M'], Float[Array, 'M M'], Float[Array, ' M'], Float[Array, '']]
¶
Make masked observation channels inert in a moment-matched update.
Marginalising channel \(i\) out of a Kalman update is equivalent to keeping it but making it carry no information. For a linear filter that means zeroing row \(i\) of \(H\) and substituting a unit block into \(R\). A moment-matched filter has no \(H\) to zero, so the same substitution is applied to the matched moments instead:
- zero the masked rows and columns of \(\mathrm{Cov}[h(x)]\) (jointly, what zeroing a row of \(H\) would do to \(H P H^\top\)),
- zero the masked columns of \(\mathrm{Cov}[x, h(x)]\) (likewise for \(P H^\top\)),
- substitute a unit block into \(R\),
- zero the residual entry.
The innovation then splits as
so the gain \(K = C S^{-1}\) has zero columns on the masked channels and
they cannot move the state. The posterior is exactly the
channel-deleted filter, with no branching — which is what lets the
per-channel path run inside a jax.lax.scan without a cond.
Exposed because the substitution is reusable: any moment-matched update — a custom filter loop, a smoother variant, an ensemble method — needs the same rewrite to handle partially observed vectors, and getting the joint row/column masking subtly wrong is easy.
Note
Residuals are formed from separately-masked \(y\) and \(\hat y\)
rather than by masking their difference, so a masked \(y\) entry may
be NaN — the usual "not measured" encoding — without poisoning
the reverse-mode gradient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obs_cov
|
Float[Array, 'M M']
|
Matched \(\mathrm{Cov}[h(x)]\), shape |
required |
cross_cov
|
Float[Array, 'N M']
|
Matched \(\mathrm{Cov}[x, h(x)]\), shape |
required |
obs_noise
|
Float[Array, 'M M']
|
Observation noise \(R\), shape |
required |
y
|
Float[Array, ' M']
|
Observation vector, shape |
required |
y_hat
|
Float[Array, ' M']
|
Matched \(\mathbb{E}[h(x)]\), shape |
required |
mask
|
Bool[Array, ' M']
|
Per-channel mask, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'M M']
|
Tuple ``(obs_cov_eff, cross_cov_eff, obs_noise_eff, residual, |
Float[Array, 'N M']
|
n_missing) |
Float[Array, 'M M']
|
each contributed \(-\tfrac{1}{2}\log 2\pi\) of dummy density to the |
Float[Array, ' M']
|
full-vector log-likelihood, so adding |
Float[Array, '']
|
|
tuple[Float[Array, 'M M'], Float[Array, 'N M'], Float[Array, 'M M'], Float[Array, ' M'], Float[Array, '']]
|
over the observed entries. |
Source code in src/gaussx/_ssm/_nonlinear_kalman.py
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 | |
Kalman filtering & smoothing¶
The forward filter and RTS smoother, their \(O(\log N)\) parallel (associative-scan) counterparts, and the steady-state (infinite-horizon) variants built on the discrete algebraic Riccati equation.
Observation masks¶
kalman_filter and parallel_kalman_filter take an optional mask,
dispatched on its rank:
| shape | meaning |
|---|---|
(T,) |
Per-step gate. False runs the predict step only and contributes nothing to the log-likelihood — the usual way to predict on a merged train/test grid. |
(T, M) |
Per-channel gate, for partially observed multivariate series where different channels are measured at different times. |
The per-channel path marginalises unobserved channels exactly: row \(i\) of
\(H_t\) is zeroed, a unit block is substituted into \(R_t\), and the residual
entry is set to zero. The innovation covariance is then block-diagonal in the
observed/masked split, so column \(i\) of the gain vanishes and the masked
channel cannot move the state — the posterior reproduces the row-deleted
filter to machine precision, with no branching. A dummy block also
contributes \(-\tfrac12 \log 2\pi\) per masked channel to the full-vector
density, which is stripped per step, so log_likelihood is the exact marginal
\(\log p(y_{\mathrm{obs}})\) and is invariant to both the mask pattern and the
dummy variance.
An all-False row of a (T, M) mask is equivalent to a False entry in the
(T,) form, and M == 1 is unambiguous either way. Masked entries of
observations are never read, so they may be NaN. Operator-typed
obs_model / obs_noise are materialised under a (T, M) mask, since
zeroing rows is inherently dense; form="sqrt" supports the (T,) mask only
and raises NotImplementedError otherwise. rts_smoother needs no mask of
its own — it consumes filtered/predicted moments, which are already
mask-aware.
Mean-field (block-diagonal) filtering¶
When the state decomposes into \(L\) independent blocks of size \(d\) (e.g. a
multi-output temporal GP with one SDE per output), meanfield_kalman_filter
and meanfield_rts_smoother run \(L\) parallel \(d\)-state filters under
jax.vmap — \(O(T\,L\,d^3)\) total instead of the full filter's
\(O(T\,L^3 d^3)\). The trade-off is the mean-field approximation: inputs and
posterior covariance are projected onto their diagonal blocks, so posterior
cross-block covariance is dropped (the returned \((T, D, D)\) covariances are
exactly zero off-block, and the log-likelihood is the sum of per-block
log-likelihoods). The approximation is exact when the true cross-block
dynamics are zero, which makes the decoupled case a useful consistency check
against kalman_filter. A BlockDiag operator whose sub-operators match the
blocking is split structurally, without materialising the full \((D, D)\)
matrix; parallel=True routes each block through the associative-scan
filter/smoother.
Structured linear algebra and Gaussian primitives for JAX.
EmissionModel
¶
Bases: Module
Observation (emission) model wrapping a linear observation matrix.
Provides named methods for common Kalman filter projection operations with observation matrix H ∈ ℝᴹˣᴺ.
Attributes:
| Name | Type | Description |
|---|---|---|
H |
Float[Array, 'M N']
|
Observation matrix, shape |
Source code in src/gaussx/_ssm/_emission.py
project_mean(mean: Float[Array, ' N']) -> Float[Array, ' M']
¶
Project state mean to observation space: ŷ = H x.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
Float[Array, ' N']
|
State mean, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' M']
|
Projected mean, shape |
Source code in src/gaussx/_ssm/_emission.py
project_covariance(cov: Float[Array, 'N N'], noise: Float[Array, 'M M'] | None = None) -> Float[Array, 'M M']
¶
Project state covariance: S = H P Hᵀ [+ R].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov
|
Float[Array, 'N N']
|
State covariance P, shape |
required |
noise
|
Float[Array, 'M M'] | None
|
Optional observation noise R, shape |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'M M']
|
Innovation covariance S, shape |
Source code in src/gaussx/_ssm/_emission.py
innovation(y: Float[Array, ' M'], x_pred: Float[Array, ' N']) -> Float[Array, ' M']
¶
Compute innovation (measurement residual): v = y − H x.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
Float[Array, ' M']
|
Observation, shape |
required |
x_pred
|
Float[Array, ' N']
|
Predicted state mean, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' M']
|
Innovation vector v, shape |
Source code in src/gaussx/_ssm/_emission.py
back_project_precision(noise_prec: Float[Array, 'M M']) -> Float[Array, 'N N']
¶
Back-project observation precision: Hᵀ R⁻¹ H.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
noise_prec
|
Float[Array, 'M M']
|
Observation noise precision R⁻¹, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N N']
|
Information matrix contribution, shape |
Source code in src/gaussx/_ssm/_emission.py
back_project_info(y: Float[Array, ' M'], noise_prec: Float[Array, 'M M']) -> Float[Array, ' N']
¶
Back-project observation to information vector: Hᵀ R⁻¹ y.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
Float[Array, ' M']
|
Observation, shape |
required |
noise_prec
|
Float[Array, 'M M']
|
Observation noise precision R⁻¹, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' N']
|
Information vector contribution, shape |
Source code in src/gaussx/_ssm/_emission.py
FilterState
¶
Bases: Module
Output of kalman_filter.
Attributes:
| Name | Type | Description |
|---|---|---|
filtered_means |
Float[Array, 'T N']
|
Shape |
filtered_covs |
Float[Array, 'T N N']
|
Shape |
predicted_means |
Float[Array, 'T N']
|
Shape |
predicted_covs |
Float[Array, 'T N N']
|
Shape |
log_likelihood |
Float[Array, '']
|
Scalar — total log-likelihood. |
Source code in src/gaussx/_ssm/_kalman.py
InfiniteHorizonState
¶
Bases: Module
Output of infinite_horizon_filter.
Attributes:
| Name | Type | Description |
|---|---|---|
filtered_means |
Float[Array, 'T N']
|
Filtered state estimates, shape |
filtered_covs |
Float[Array, 'T N N']
|
Filtered covariances (constant), shape |
predicted_means |
Float[Array, 'T N']
|
Predicted state estimates, shape |
predicted_covs |
Float[Array, 'T N N']
|
Predicted covariances (constant), shape |
log_likelihood |
Float[Array, '']
|
Total log-likelihood (scalar). |
Source code in src/gaussx/_ssm/_infinite_horizon_kalman.py
DAREResult
¶
Bases: Module
Result of DARE solver.
Attributes:
| Name | Type | Description |
|---|---|---|
P_inf |
Float[Array, 'D D']
|
Steady-state covariance, shape |
K_inf |
Float[Array, 'D M']
|
Steady-state Kalman gain, shape |
converged |
Bool[Array, '']
|
Scalar boolean indicating convergence. |
Source code in src/gaussx/_ssm/_dare.py
kalman_filter(transition: Float[Array, '*T N N'] | lx.AbstractLinearOperator, obs_model: Float[Array, '*T M N'] | lx.AbstractLinearOperator, process_noise: Float[Array, '*T N N'] | lx.AbstractLinearOperator, obs_noise: Float[Array, '*T M M'] | lx.AbstractLinearOperator, observations: Float[Array, 'T M'], init_mean: Float[Array, ' N'], init_cov: Float[Array, 'N N'], *, mask: Bool[Array, ' T'] | Bool[Array, 'T M'] | None = None, solver: AbstractSolverStrategy | None = None, woodbury_innovation: bool = False) -> FilterState
¶
Kalman filter forward pass via jax.lax.scan.
Implements the predict-update cycle for a (possibly time-varying) linear-Gaussian state-space model:
x_t = A_t @ x_{t-1} + q_t, q_t ~ N(0, Q_t)
y_t = H_t @ x_t + r_t, r_t ~ N(0, R_t)
Time-invariant inputs (single (N, N) / (M, N) etc.) are
automatically broadcast along the time axis. Time-varying inputs
are passed as (T, …) stacks (e.g. from
discretise_sequence).
Operator inputs (lineax BlockDiag / Kronecker /
DiagonalLinearOperator / MaskedOperator / etc.) are accepted
in the time-invariant signature only. The structural matvec
(A @ x, H @ x) runs through the operator's mv;
operator-typed Q / R are materialised to dense arrays once
outside the scan (the per-step sandwiches A P A^T / H P H^T
themselves run inside the scan because they depend on the evolving
P_filt).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transition
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
State transition matrix |
required |
obs_model
|
Float[Array, '*T M N'] | AbstractLinearOperator
|
Observation matrix |
required |
process_noise
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
Process noise covariance |
required |
obs_noise
|
Float[Array, '*T M M'] | AbstractLinearOperator
|
Observation noise covariance |
required |
observations
|
Float[Array, 'T M']
|
Observed data, shape |
required |
init_mean
|
Float[Array, ' N']
|
Initial state mean, shape |
required |
init_cov
|
Float[Array, 'N N']
|
Initial state covariance, shape |
required |
mask
|
Bool[Array, ' T'] | Bool[Array, 'T M'] | None
|
Optional observation mask. Disambiguated by rank, so no
extra keyword is needed (
Defaults to all-True. Operator-typed |
None
|
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy. When |
None
|
woodbury_innovation
|
bool
|
When |
False
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If operator-typed inputs are mixed with 3D |
Returns:
| Type | Description |
|---|---|
FilterState
|
A |
FilterState
|
and total log-likelihood. |
Source code in src/gaussx/_ssm/_kalman.py
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 | |
rts_smoother(filter_state: FilterState, transition: Float[Array, '*T N N'] | lx.AbstractLinearOperator, process_noise: Float[Array, '*T N N'] | lx.AbstractLinearOperator, *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, 'T N'], Float[Array, 'T N N']]
¶
Rauch-Tung-Striebel backward smoother.
Accepts the same time-invariant / time-varying / operator forms for
transition and process_noise as kalman_filter. When
a step was masked off in the filter (mask[t] == 0), the
smoother formula degenerates harmlessly because filtered ==
predicted at that step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filter_state
|
FilterState
|
Output of |
required |
transition
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
State transition matrix or operator. |
required |
process_noise
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
Process noise covariance or operator. (Not
currently used by the standard RTS recurrence — kept for
API symmetry with |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, 'T N'], Float[Array, 'T N N']]
|
Tuple |
Source code in src/gaussx/_ssm/_kalman.py
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 | |
kalman_gain(P: lx.AbstractLinearOperator, H: lx.AbstractLinearOperator, R: lx.AbstractLinearOperator, *, solver: AbstractSolverStrategy | None = None, woodbury_innovation: bool = False) -> Float[Array, 'N M']
¶
Compute Kalman gain K = P @ H^T @ (H @ P @ H^T + R)^{-1}.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
P
|
AbstractLinearOperator
|
Prior covariance operator, shape |
required |
H
|
AbstractLinearOperator
|
Observation model operator, shape |
required |
R
|
AbstractLinearOperator
|
Observation noise operator, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy. When |
None
|
woodbury_innovation
|
bool
|
When |
False
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'N M']
|
Kalman gain matrix of shape |
Source code in src/gaussx/_ssm/_kalman.py
parallel_kalman_filter(transition: Float[Array, '*T N N'] | lx.AbstractLinearOperator, obs_model: Float[Array, '*T M N'] | lx.AbstractLinearOperator, process_noise: Float[Array, '*T N N'] | lx.AbstractLinearOperator, obs_noise: Float[Array, '*T M M'] | lx.AbstractLinearOperator, observations: Float[Array, 'T M'], init_mean: Float[Array, ' N'], init_cov: Float[Array, 'N N'], *, mask: Bool[Array, ' T'] | Bool[Array, 'T M'] | None = None, solver: AbstractSolverStrategy | None = None, woodbury_innovation: bool = False, form: str = 'covariance') -> FilterState
¶
Parallel Kalman filter via jax.lax.associative_scan.
Numerically equivalent to gaussx.kalman_filter but with
O(log T) parallel depth on accelerators. Same generalised
contract (TI / TV / operator-typed inputs, optional mask, scalar
log-likelihood). Empty observation windows (T == 0) return a
zero-length FilterState with log_likelihood == 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transition
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
State transition matrix or operator. |
required |
obs_model
|
Float[Array, '*T M N'] | AbstractLinearOperator
|
Observation matrix or operator. |
required |
process_noise
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
Process noise covariance or operator. |
required |
obs_noise
|
Float[Array, '*T M M'] | AbstractLinearOperator
|
Observation noise covariance or operator. |
required |
observations
|
Float[Array, 'T M']
|
Observed data, shape |
required |
init_mean
|
Float[Array, ' N']
|
Initial state mean, shape |
required |
init_cov
|
Float[Array, 'N N']
|
Initial state covariance, shape |
required |
mask
|
Bool[Array, ' T'] | Bool[Array, 'T M'] | None
|
Optional observation mask, dispatched on rank exactly as
in |
None
|
solver
|
AbstractSolverStrategy | None
|
Accepted for API symmetry with |
None
|
woodbury_innovation
|
bool
|
When |
False
|
form
|
str
|
Either |
'covariance'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Returns:
| Type | Description |
|---|---|
FilterState
|
|
FilterState
|
and the total log-likelihood. |
Source code in src/gaussx/_ssm/_parallel_kalman.py
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 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 | |
parallel_rts_smoother(filter_state: FilterState, transition: Float[Array, '*T N N'] | lx.AbstractLinearOperator, process_noise: Float[Array, '*T N N'] | lx.AbstractLinearOperator, *, solver: AbstractSolverStrategy | None = None, form: str = 'covariance') -> tuple[Float[Array, 'T N'], Float[Array, 'T N N']]
¶
Parallel RTS smoother via reverse jax.lax.associative_scan.
Pairs with parallel_kalman_filter. Numerically equivalent to
gaussx.rts_smoother with O(log T) parallel depth.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filter_state
|
FilterState
|
Output of |
required |
transition
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
State transition matrix or operator. |
required |
process_noise
|
Float[Array, '*T N N'] | AbstractLinearOperator
|
Unused — kept for API symmetry with the sequential smoother. |
required |
solver
|
AbstractSolverStrategy | None
|
Accepted for API symmetry; not currently threaded through. |
None
|
form
|
str
|
Either |
'covariance'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, 'T N'], Float[Array, 'T N N']]
|
Tuple |
Source code in src/gaussx/_ssm/_parallel_kalman.py
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 | |
meanfield_kalman_filter(transition: Float[Array, '*T D D'] | lx.AbstractLinearOperator, obs_model: Float[Array, '*T M D'] | lx.AbstractLinearOperator, process_noise: Float[Array, '*T D D'] | lx.AbstractLinearOperator, obs_noise: Float[Array, '*T M M'] | lx.AbstractLinearOperator, observations: Float[Array, 'T M'], init_mean: Float[Array, ' D'], init_cov: Float[Array, 'D D'], *, block_size: int, mask: Bool[Array, ' T'] | Bool[Array, 'T M'] | None = None, solver: AbstractSolverStrategy | None = None, parallel: bool = False) -> FilterState
¶
Mean-field Kalman filter over L = D // block_size state blocks.
Runs L independent block_size-state Kalman filters under
jax.vmap — O(T L d^3) total work instead of the full filter's
O(T L^3 d^3) — at the cost of dropping posterior cross-block
covariances. All inputs are projected onto their diagonal blocks;
off-block entries are discarded, which is the mean-field projection
(exact when the true cross-block couplings are zero). The returned
log-likelihood is the sum of the per-block log-likelihoods,
Accepts the same time-invariant / time-varying / operator input
forms as gaussx.kalman_filter. A gaussx.BlockDiag whose
sub-operators match the blocking is split structurally without
materialising the full (D, D) matrix. The observation dimension
must factorise the same way: channel block \ell (of size
M // L) is observed through state block \ell.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transition
|
Float[Array, '*T D D'] | AbstractLinearOperator
|
State transition |
required |
obs_model
|
Float[Array, '*T M D'] | AbstractLinearOperator
|
Observation matrix |
required |
process_noise
|
Float[Array, '*T D D'] | AbstractLinearOperator
|
Process noise covariance |
required |
obs_noise
|
Float[Array, '*T M M'] | AbstractLinearOperator
|
Observation noise covariance |
required |
observations
|
Float[Array, 'T M']
|
Observed data, shape |
required |
init_mean
|
Float[Array, ' D']
|
Initial state mean, shape |
required |
init_cov
|
Float[Array, 'D D']
|
Initial state covariance, shape |
required |
block_size
|
int
|
State block size |
required |
mask
|
Bool[Array, ' T'] | Bool[Array, 'T M'] | None
|
Optional observation mask, as in |
None
|
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for the per-block innovation solves. Sequential mode only (the parallel filter does not thread it through). |
None
|
parallel
|
bool
|
When |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Returns:
| Type | Description |
|---|---|
FilterState
|
A |
FilterState
|
Covariances are block-diagonal |
FilterState
|
per-block covariances (exact zeros off-block). |
Source code in src/gaussx/_ssm/_meanfield_kalman.py
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 | |
meanfield_rts_smoother(filter_state: FilterState, transition: Float[Array, '*T D D'] | lx.AbstractLinearOperator, process_noise: Float[Array, '*T D D'] | lx.AbstractLinearOperator, *, block_size: int, solver: AbstractSolverStrategy | None = None, parallel: bool = False) -> tuple[Float[Array, 'T D'], Float[Array, 'T D D']]
¶
Mean-field RTS smoother over L = D // block_size state blocks.
Backward pass paired with meanfield_kalman_filter: runs L
independent gaussx.rts_smoother passes under jax.vmap, one per
diagonal block. transition / process_noise accept the same
forms as the filter and are projected onto their diagonal blocks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filter_state
|
FilterState
|
Output of |
required |
transition
|
Float[Array, '*T D D'] | AbstractLinearOperator
|
State transition matrix or operator. |
required |
process_noise
|
Float[Array, '*T D D'] | AbstractLinearOperator
|
Process noise covariance or operator. (Unused by the standard RTS recurrence — kept for API symmetry.) |
required |
block_size
|
int
|
State block size |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for the per-block smoother gains. Sequential mode only. |
None
|
parallel
|
bool
|
When |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Returns:
| Type | Description |
|---|---|
Float[Array, 'T D']
|
Tuple |
Float[Array, 'T D D']
|
|
tuple[Float[Array, 'T D'], Float[Array, 'T D D']]
|
embeddings of the per-block smoothed covariances. |
Source code in src/gaussx/_ssm/_meanfield_kalman.py
infinite_horizon_filter(transition: Float[Array, 'N N'] | lx.AbstractLinearOperator, obs_model: Float[Array, 'M N'] | lx.AbstractLinearOperator, process_noise: Float[Array, 'N N'] | lx.AbstractLinearOperator, obs_noise: Float[Array, 'M M'] | lx.AbstractLinearOperator, observations: Float[Array, 'T M'], init_mean: Float[Array, ' N'] | None = None, *, dare_result: DAREResult | None = None, max_iter: int = 100, tol: float = 1e-08, solver: AbstractSolverStrategy | None = None, woodbury_innovation: bool = False) -> InfiniteHorizonState
¶
Infinite-horizon Kalman filter with fixed steady-state gain.
Uses the DARE solution for a constant Kalman gain K∞, avoiding per-step Riccati updates. For dense matrices, the per-step cost is O(N² + MN + M²) instead of O(N³) for the standard Kalman filter:
Predict: x⁻ₜ = A xₜ₋₁
Update: vₜ = yₜ − H x⁻ₜ
xₜ = x⁻ₜ + K∞ vₜ
All four operator/array arguments accept either a raw JAX array or
a lineax.AbstractLinearOperator. Operator inputs preserve
their structural matvec inside the per-step scan; the sandwiches
materialise once outside the scan.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transition
|
Float[Array, 'N N'] | AbstractLinearOperator
|
State transition matrix or operator, shape |
required |
obs_model
|
Float[Array, 'M N'] | AbstractLinearOperator
|
Observation matrix or operator, shape |
required |
process_noise
|
Float[Array, 'N N'] | AbstractLinearOperator
|
Process noise covariance or operator, shape |
required |
obs_noise
|
Float[Array, 'M M'] | AbstractLinearOperator
|
Observation noise covariance or operator, shape |
required |
observations
|
Float[Array, 'T M']
|
Observed data y, shape |
required |
init_mean
|
Float[Array, ' N'] | None
|
Initial state mean, shape |
None
|
dare_result
|
DAREResult | None
|
Precomputed DARE result. If |
None
|
max_iter
|
int
|
Maximum DARE iterations (used only if |
100
|
tol
|
float
|
DARE convergence tolerance (used only if |
1e-08
|
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
woodbury_innovation
|
bool
|
When |
False
|
Returns:
| Type | Description |
|---|---|
InfiniteHorizonState
|
An |
InfiniteHorizonState
|
covariances, and total log-likelihood. |
Source code in src/gaussx/_ssm/_infinite_horizon_kalman.py
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 | |
infinite_horizon_smoother(filter_state: InfiniteHorizonState, transition: Float[Array, 'N N'] | lx.AbstractLinearOperator, dare_result: DAREResult, process_noise: Float[Array, 'N N'] | lx.AbstractLinearOperator, *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, 'T N'], Float[Array, 'T N N']]
¶
Infinite-horizon RTS smoother with fixed steady-state gain.
Precomputes the steady-state smoother gain G∞ = P∞ Aᵀ P⁻pred⁻¹, then runs a backward scan with fixed G∞. The steady-state smoothed covariance is the solution of the discrete Lyapunov equation:
P_smooth = P∞ + G∞ (P_smooth − P⁻pred) G∞ᵀ
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filter_state
|
InfiniteHorizonState
|
Output of |
required |
transition
|
Float[Array, 'N N'] | AbstractLinearOperator
|
State transition matrix or operator, shape |
required |
dare_result
|
DAREResult
|
DARE result used in the filter. |
required |
process_noise
|
Float[Array, 'N N'] | AbstractLinearOperator
|
Process noise covariance or operator, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'T N']
|
Tuple |
Float[Array, 'T N N']
|
|
Source code in src/gaussx/_ssm/_infinite_horizon_kalman.py
dare(A: Float[Array, 'D D'] | lx.AbstractLinearOperator, H: Float[Array, 'M D'] | lx.AbstractLinearOperator, Q: Float[Array, 'D D'] | lx.AbstractLinearOperator, R: Float[Array, 'M M'] | lx.AbstractLinearOperator, *, P_init: Float[Array, 'D D'] | None = None, max_iter: int = 100, tol: float = 1e-08, solver: AbstractSolverStrategy | None = None, woodbury_innovation: bool = False) -> DAREResult
¶
Discrete Algebraic Riccati Equation solver.
Iterates the Kalman predict-update equations until convergence:
Predict: P⁻ = A P Aᵀ + Q
Update: S = H P⁻ Hᵀ + R
K = P⁻ Hᵀ S⁻¹
P = (I - KH) P⁻
Convergence is declared when max|P_new - P_old| < tol.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Float[Array, 'D D'] | AbstractLinearOperator
|
Transition matrix or operator, shape |
required |
H
|
Float[Array, 'M D'] | AbstractLinearOperator
|
Observation matrix or operator, shape |
required |
Q
|
Float[Array, 'D D'] | AbstractLinearOperator
|
Process noise covariance or operator, shape |
required |
R
|
Float[Array, 'M M'] | AbstractLinearOperator
|
Observation noise covariance or operator, shape |
required |
P_init
|
Float[Array, 'D D'] | None
|
Initial covariance guess, shape |
None
|
max_iter
|
int
|
Maximum number of iterations. |
100
|
tol
|
float
|
Convergence tolerance on the element-wise max absolute change. |
1e-08
|
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
woodbury_innovation
|
bool
|
When |
False
|
Returns:
| Type | Description |
|---|---|
DAREResult
|
A |
DAREResult
|
Kalman gain, and convergence flag. |
Source code in src/gaussx/_ssm/_dare.py
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 | |
pairwise_marginals(means: Float[Array, 'T d'], covariances: Float[Array, 'T d d'], cross_covariances: Float[Array, 'Tm1 d d']) -> tuple[Float[Array, 'Tm1 two_d'], Float[Array, 'Tm1 two_d two_d']]
¶
Joint p(x_k, x_{k+1}) for each consecutive pair.
For each pair (k, k+1), the joint distribution is:
p(x_k, x_{k+1}) = N([mu_k; mu_{k+1}],
[[P_k, C_k^T],
[C_k, P_{k+1}]])
where C_k = Cov[x_{k+1}, x_k] is the pairwise cross-covariance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
means
|
Float[Array, 'T d']
|
Smoothed means, shape |
required |
covariances
|
Float[Array, 'T d d']
|
Smoothed covariances, shape |
required |
cross_covariances
|
Float[Array, 'Tm1 d d']
|
Pairwise cross-covariances
|
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'Tm1 two_d']
|
Tuple |
Float[Array, 'Tm1 two_d two_d']
|
|
tuple[Float[Array, 'Tm1 two_d'], Float[Array, 'Tm1 two_d two_d']]
|
|
Source code in src/gaussx/_ssm/_pairwise_marginals.py
SpInGP¶
State-space (sparse-in-time) GP inference: marginal likelihood and posterior through the SSM representation.
Structured linear algebra and Gaussian primitives for JAX.
spingp_log_likelihood(prior_precision: BlockTriDiag, emission_model: Array, obs_noise: lx.AbstractLinearOperator, observations: Float[Array, 'N d_obs'], *, solver: AbstractSolverStrategy | None = None) -> Float[Array, '']
¶
Log marginal likelihood via sparse inverse GP formulation.
Computes the log marginal likelihood using the precision-form Kalman filter (SpInGP):
1. Likelihood precision sites: $\Lambda_{lik} = H^T R^{-1} H$
2. Posterior precision: $\Lambda_{post} = \Lambda_{prior} + \Lambda_{lik}$
3. log p(y) via banded Cholesky logdet and quadratic form
The full expression is:
log p(y) = -0.5 * (N_{obs} * log(2\pi) + log|R|_{total}
+ y^T R^{-1} y - \eta^T \Lambda_{post}^{-1} \eta
+ log|\Lambda_{post}| - log|\Lambda_{prior}|)
where \(\eta = H^T R^{-1} y\).
All operations exploit banded structure for O(Nd³) cost.
The solver parameter controls the algorithm used for the
large-scale posterior precision operations (solve, logdet).
Observation noise operations always use structural dispatch
since obs_noise is typically a small dense matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prior_precision
|
BlockTriDiag
|
Prior precision as |
required |
emission_model
|
Array
|
Emission matrix H. Shape |
required |
obs_noise
|
AbstractLinearOperator
|
Observation noise covariance R operator. |
required |
observations
|
Float[Array, 'N d_obs']
|
Observations y, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for posterior precision
operations. When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
Scalar log marginal likelihood. |
Source code in src/gaussx/_ssm/_spingp.py
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 | |
spingp_posterior(prior_precision: BlockTriDiag, emission_model: Array, obs_noise: lx.AbstractLinearOperator, observations: Float[Array, 'N d_obs'], *, prior_mean: Float[Array, ' Nd'] | None = None, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, ' Nd'], BlockTriDiag]
¶
Posterior mean and precision via SpInGP.
Computes the posterior by adding likelihood precision sites to the prior precision and solving for the posterior mean:
\Lambda_{post} = \Lambda_{prior} + H^T R^{-1} H
\mu_{post} = \Lambda_{post}^{-1} (H^T R^{-1} y + \Lambda_{prior} \mu_{prior})
With prior_mean=None the prior is taken to be zero-mean and the
second term vanishes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prior_precision
|
BlockTriDiag
|
Prior precision as |
required |
emission_model
|
Array
|
Emission matrix H. Shape |
required |
obs_noise
|
AbstractLinearOperator
|
Observation noise covariance R operator. |
required |
observations
|
Float[Array, 'N d_obs']
|
Observations y, shape |
required |
prior_mean
|
Float[Array, ' Nd'] | None
|
Optional prior mean |
None
|
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for posterior precision
operations. When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' Nd']
|
Tuple |
BlockTriDiag
|
|
tuple[Float[Array, ' Nd'], BlockTriDiag]
|
|
Source code in src/gaussx/_ssm/_spingp.py
Precision-form chains¶
The joint prior or posterior of a Gauss-Markov chain has a block-tridiagonal precision \(\Lambda\). The UDL factorisation \(\Lambda = U \tilde{D} U^{\top}\) — unit upper block-bidiagonal \(U\), block-diagonal \(\tilde{D}\) — is computed last block first by Schur complementation,
in \(O(T d^3)\). Unlike the banded Cholesky factor behind
cholesky, its blocks have a physical reading: they are
the state-space model with that precision, \(A^{\mathrm{ssm}}_k = -U_k^{\top}\)
and \(Q_k^{-1} = \tilde{D}_k\). One pass therefore turns a precision-form
posterior — the output of spingp_posterior, or a CVI / natural-gradient
site update — into a chain that the Kalman machinery can sample, predict
and interpolate from, without materialising the \((Td) \times (Td)\)
covariance.
MarkovGaussian is the
distribution that carries both views: its canonical parameterisation is the
generative tuple \((A_k, b_k, Q_k, \mu_0, P_0)\), and to_precision_form /
from_precision_form round-trip through the UDL factors exactly.
Structured linear algebra and Gaussian primitives for JAX.
UDLDecomposition
¶
Bases: Module
Banded factorisation \(\Lambda = U \tilde{D} U^{\top}\) of a BlockTriDiag.
\(U\) is unit upper block-bidiagonal with super-diagonal blocks \(U_k\) and \(\tilde{D} = \mathrm{blockdiag}(\tilde{D}_1, \dots, \tilde{D}_T)\). The factors are produced by the backward Schur-complement recurrence
where \(D_k\) / \(A_k\) are the diagonal / sub-diagonal blocks of
\(\Lambda\). This is not the Cholesky factor of \(\Lambda\): for a
Gauss-Markov chain \(x_{k+1} = A^{\mathrm{ssm}}_k x_k + \varepsilon_k\)
the blocks are exactly \(U_k^{\top} = -A^{\mathrm{ssm}}_k\) and
\(\tilde{D}_k = Q_k^{-1}\), so one pass converts a precision-form
posterior into a sampleable state-space model
(see udl_to_ssm_params).
Every method costs \(O(T d^3)\) (or \(O(T d^2)\) for solve) and never
forms the dense \((Td) \times (Td)\) matrix.
Attributes:
| Name | Type | Description |
|---|---|---|
U_sub |
Float[Array, 'Tm1 d d']
|
Sub-diagonal blocks \(U_k^{\top} = \tilde{D}_{k+1}^{-1} A_k\)
of \(U^{\top}\), shape |
D_diag |
Float[Array, 'T d d']
|
Block-diagonal factors \(\tilde{D}_k\), shape |
chol_D |
Float[Array, 'T d d']
|
Lower Cholesky factor of each \(\tilde{D}_k\), shape
|
Source code in src/gaussx/_ssm/_udl.py
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 | |
num_blocks: int
property
¶
Number of blocks T.
block_size: int
property
¶
Block dimension d.
solve(rhs: Float[Array, ' Td']) -> Float[Array, ' Td']
¶
Solve \(\Lambda x = b\) through the factors in \(O(T d^2)\).
Three banded sweeps: \(U z = b\) (backward), \(\tilde{D} w = z\) (block-wise, via the cached Cholesky factors) and \(U^{\top} x = w\) (forward).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rhs
|
Float[Array, ' Td']
|
Right-hand side, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' Td']
|
Solution, shape |
Source code in src/gaussx/_ssm/_udl.py
logdet() -> Float[Array, '']
¶
\(\log|\Lambda| = \sum_k \log|\tilde{D}_k|\), from the cached factors.
as_block_tridiag() -> BlockTriDiag
¶
Reassemble \(\Lambda = U \tilde{D} U^{\top}\) as a BlockTriDiag.
Inverts the recurrence: \(A_k = \tilde{D}_{k+1} U_k^{\top}\) and \(D_k = \tilde{D}_k + A_k^{\top} \tilde{D}_{k+1}^{-1} A_k = \tilde{D}_k + U_k \tilde{D}_{k+1} U_k^{\top}\).
Source code in src/gaussx/_ssm/_udl.py
udl_decomposition(precision: BlockTriDiag) -> UDLDecomposition
¶
Factorise a symmetric block-tridiagonal precision as \(U \tilde{D} U^{\top}\).
Runs the backward Schur-complement recurrence as a jax.lax.scan
from the last block to the first, at \(O(T d^3)\) time and \(O(T d^2)\)
memory. Each \(\tilde{D}_k\) is Cholesky-factored on the fly, so the
input must be positive definite.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
precision
|
BlockTriDiag
|
Symmetric positive-definite |
required |
Returns:
| Type | Description |
|---|---|
UDLDecomposition
|
The |
Examples:
>>> import jax.numpy as jnp, gaussx
>>> diag = jnp.broadcast_to(2.0 * jnp.eye(2), (4, 2, 2))
>>> sub = jnp.broadcast_to(-0.5 * jnp.eye(2), (3, 2, 2))
>>> udl = gaussx.udl_decomposition(gaussx.BlockTriDiag(diag, sub))
>>> udl.U_sub.shape, udl.D_diag.shape
((3, 2, 2), (4, 2, 2))
Source code in src/gaussx/_ssm/_udl.py
udl_to_ssm_params(udl: UDLDecomposition) -> tuple[Float[Array, 'Tm1 d d'], Float[Array, 'T d d'], Float[Array, 'T d d']]
¶
Read the equivalent Gauss-Markov chain off a UDLDecomposition.
A chain \(x_0 \sim \mathcal{N}(\mu_0, P_0)\), \(x_{k+1} = A_k x_k + \varepsilon_k\), \(\varepsilon_k \sim \mathcal{N}(0, Q_{k+1})\) has precision \(\Lambda = U \tilde{D} U^{\top}\) with
so the factors are the SSM. The returned Q follows the
ssm_to_naturals / naturals_to_ssm layout: Q[0] is \(P_0\) and
Q[k] for \(k \ge 1\) is the process noise entering state \(k\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
udl
|
UDLDecomposition
|
Factorisation of the chain's precision. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'Tm1 d d']
|
Tuple |
Float[Array, 'T d d']
|
covariances of shape |
Float[Array, 'T d d']
|
factors of shape |
Source code in src/gaussx/_ssm/_udl.py
udl_from_ssm_params(A: Float[Array, 'Tm1 d d'], Q: Float[Array, 'T d d']) -> UDLDecomposition
¶
Build the UDLDecomposition of a Gauss-Markov chain's precision.
Inverse of udl_to_ssm_params: sets \(U_k^{\top} = -A_k\) and
\(\tilde{D}_k = Q_k^{-1}\) directly, so the chain's block-tridiagonal
precision is available through UDLDecomposition.as_block_tridiag
without ever factorising it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Float[Array, 'Tm1 d d']
|
Transition matrices, shape |
required |
Q
|
Float[Array, 'T d d']
|
Covariances, shape |
required |
Returns:
| Type | Description |
|---|---|
UDLDecomposition
|
The factorisation of the chain's precision. |
Source code in src/gaussx/_ssm/_udl.py
Sites & natural parameters¶
Conjugate-computation VI (CVI) site updates and the conversions between SSM moment, expectation, and natural parameterizations used by non-conjugate temporal inference.
Structured linear algebra and Gaussian primitives for JAX.
GaussianSites
¶
Bases: Module
Time-varying Gaussian likelihood sites in natural parameterization.
Stores per-timestep natural parameters for N Gaussian sites,
following the \eta_2 = -\tfrac{1}{2}\Lambda convention
(consistent with gaussx.mean_cov_to_natural).
Attributes:
| Name | Type | Description |
|---|---|---|
nat1 |
Float[Array, 'N d']
|
Natural location parameters, shape |
nat2 |
Float[Array, 'N d d']
|
Natural precision parameters, shape |
Source code in src/gaussx/_ssm/_cvi.py
cvi_update_sites(sites: GaussianSites, grad_nat1: Float[Array, 'N d'], grad_nat2: Float[Array, 'N d d'], rho: float) -> GaussianSites
¶
Natural gradient update for CVI sites.
Performs a damped update in natural parameter space:
\theta \leftarrow (1 - \rho) \theta + \rho \nabla
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sites
|
GaussianSites
|
Current Gaussian sites. |
required |
grad_nat1
|
Float[Array, 'N d']
|
Natural gradient for location, shape |
required |
grad_nat2
|
Float[Array, 'N d d']
|
Natural gradient for precision, shape |
required |
rho
|
float
|
Step size / damping factor in |
required |
Returns:
| Type | Description |
|---|---|
GaussianSites
|
Updated |
Source code in src/gaussx/_ssm/_cvi.py
sites_to_precision(sites: GaussianSites) -> BlockTriDiag
¶
Convert Gaussian sites to a block-tridiagonal precision.
Returns a block-diagonal BlockTriDiag (zero
sub-diagonals) representing the precision contribution of the
sites. This can be added to a prior precision via .add()
or + to form the posterior precision:
\Lambda_{post} = \Lambda_{prior} + \Lambda_{sites}
Since nat2 stores -\tfrac{1}{2}\Lambda, the precision
blocks are -2 \cdot nat2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sites
|
GaussianSites
|
Gaussian sites with |
required |
Returns:
| Type | Description |
|---|---|
BlockTriDiag
|
Block-diagonal |
Source code in src/gaussx/_ssm/_cvi.py
cavity_from_marginal(marg_mean: Float[Array, ' *batch'], marg_var: Float[Array, ' *batch'], site_nat1: Float[Array, ' *batch'], site_nat2: Float[Array, ' *batch']) -> tuple[Float[Array, ' *batch'], Float[Array, ' *batch']]
¶
Compute cavity distribution by removing a site from the marginal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
marg_mean
|
Float[Array, ' *batch']
|
Marginal distribution means. |
required |
marg_var
|
Float[Array, ' *batch']
|
Marginal distribution variances (positive). |
required |
site_nat1
|
Float[Array, ' *batch']
|
Site precision-weighted means to remove. |
required |
site_nat2
|
Float[Array, ' *batch']
|
Site precisions to remove. |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, ' *batch'], Float[Array, ' *batch']]
|
Tuple |
Source code in src/gaussx/_ssm/_site_natural.py
site_natural_from_tilted(tilted_mean: Float[Array, ' *batch'], tilted_var: Float[Array, ' *batch'], cav_mean: Float[Array, ' *batch'], cav_var: Float[Array, ' *batch']) -> tuple[Float[Array, ' *batch'], Float[Array, ' *batch']]
¶
Compute site natural parameters from tilted and cavity moments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tilted_mean
|
Float[Array, ' *batch']
|
Tilted distribution means. |
required |
tilted_var
|
Float[Array, ' *batch']
|
Tilted distribution variances (positive). |
required |
cav_mean
|
Float[Array, ' *batch']
|
Cavity distribution means. |
required |
cav_var
|
Float[Array, ' *batch']
|
Cavity distribution variances (positive). |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, ' *batch'], Float[Array, ' *batch']]
|
Tuple |
Source code in src/gaussx/_ssm/_site_natural.py
site_mean_var_from_natural(site_nat1: Float[Array, ' *batch'], site_nat2: Float[Array, ' *batch']) -> tuple[Float[Array, ' *batch'], Float[Array, ' *batch']]
¶
Convert per-site natural parameters to mean/variance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
site_nat1
|
Float[Array, ' *batch']
|
Site precision-weighted means. |
required |
site_nat2
|
Float[Array, ' *batch']
|
Site precisions (positive for valid Gaussians). |
required |
Returns:
| Type | Description |
|---|---|
tuple[Float[Array, ' *batch'], Float[Array, ' *batch']]
|
Tuple |
Source code in src/gaussx/_ssm/_site_natural.py
expectations_to_ssm(eta1: Float[Array, ' Nd'], eta2: BlockTriDiag) -> tuple[Float[Array, 'N d'], Float[Array, 'N d d'], Float[Array, 'Nm1 d d']]
¶
Convert expectation parameters back to SSM marginals.
Recovers (means, covs, cross_covs) from the expectation
parameters of the joint Gaussian.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
eta1
|
Float[Array, ' Nd']
|
Concatenated means, shape |
required |
eta2
|
BlockTriDiag
|
Second-moment |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N d']
|
Tuple |
Float[Array, 'N d d']
|
|
Float[Array, 'Nm1 d d']
|
|
tuple[Float[Array, 'N d'], Float[Array, 'N d d'], Float[Array, 'Nm1 d d']]
|
|
Source code in src/gaussx/_ssm/_ssm_natural.py
naturals_to_ssm(theta_linear: Float[Array, ' Nd'], theta_precision: BlockTriDiag, *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, 'Nm1 d d'], Float[Array, 'N d d'], Float[Array, ' d'], Float[Array, 'd d']]
¶
Convert natural parameters back to SSM parameters.
Recovers (A, Q, \mu_0, P_0) from the block-tridiagonal natural
parameters via a backward recurrence on the precision blocks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta_linear
|
Float[Array, ' Nd']
|
Natural location parameter, shape |
required |
theta_precision
|
BlockTriDiag
|
Natural precision parameter as
|
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'Nm1 d d']
|
Tuple |
Float[Array, 'N d d']
|
|
Float[Array, ' d']
|
|
Float[Array, 'd d']
|
|
tuple[Float[Array, 'Nm1 d d'], Float[Array, 'N d d'], Float[Array, ' d'], Float[Array, 'd d']]
|
|
Source code in src/gaussx/_ssm/_ssm_natural.py
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 | |
ssm_to_expectations(means: Float[Array, 'N d'], covs: Float[Array, 'N d d'], cross_covs: Float[Array, 'Nm1 d d']) -> tuple[Float[Array, ' Nd'], BlockTriDiag]
¶
Convert SSM marginals to expectation parameters.
Given filtered or smoothed marginals, computes the expectation
parameters (eta1, eta2) of the joint Gaussian where:
eta1 = E[x](concatenated means)eta2is aBlockTriDiagstoring the block-tridiagonal subset ofE[xx^T](second moments matching the Gauss-Markov sparsity pattern, not the full dense matrix)
The diagonal blocks of eta2 are E[x_k x_k^T] = P_k + m_k m_k^T
and the sub-diagonal blocks are
E[x_{k+1} x_k^T] = C_k + m_{k+1} m_k^T where C_k is the
cross-covariance Cov(x_{k+1}, x_k).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
means
|
Float[Array, 'N d']
|
Marginal means, shape |
required |
covs
|
Float[Array, 'N d d']
|
Marginal covariances, shape |
required |
cross_covs
|
Float[Array, 'Nm1 d d']
|
Cross-covariances |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' Nd']
|
Tuple |
BlockTriDiag
|
and |
Source code in src/gaussx/_ssm/_ssm_natural.py
ssm_to_naturals(A: Float[Array, 'Nm1 d d'], Q: Float[Array, 'N d d'], mu_0: Float[Array, ' d'], P_0: Float[Array, 'd d'], *, solver: AbstractSolverStrategy | None = None) -> tuple[Float[Array, ' Nd'], BlockTriDiag]
¶
Convert SSM parameters to natural parameters.
For a linear-Gaussian state-space model:
x_0 \sim N(\mu_0, P_0)
x_{k+1} = A_k x_k + \epsilon_k,\quad \epsilon_k \sim N(0, Q_{k+1})
the joint prior p(x_0, \ldots, x_{N-1}) has a block-tridiagonal
precision matrix. This function returns its natural parameters
(\theta_1, \theta_2) where \theta_2 = -\tfrac{1}{2}\Lambda
(matching the convention in gaussx.mean_cov_to_natural).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Float[Array, 'Nm1 d d']
|
Transition matrices, shape |
required |
Q
|
Float[Array, 'N d d']
|
Process noise covariances, shape |
required |
mu_0
|
Float[Array, ' d']
|
Initial mean, shape |
required |
P_0
|
Float[Array, 'd d']
|
Initial covariance, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional solver strategy for structured linear algebra.
When |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' Nd']
|
Tuple |
BlockTriDiag
|
|
tuple[Float[Array, ' Nd'], BlockTriDiag]
|
|
tuple[Float[Array, ' Nd'], BlockTriDiag]
|
in the |
Source code in src/gaussx/_ssm/_ssm_natural.py
Process noise¶
The exact discretisation \(Q = P_\infty - A P_\infty A^\top\), which is the
forward direction of the discrete Lyapunov equation that
discrete_lyapunov_solve inverts.
The congruence is delegated to
cov_transform, so passing operators
returns a lazy operator with structure intact — matched Kronecker factors stay
factorised, a diagonal \(P_\infty\) skips its \((N, N)\) materialization. Passing
arrays returns an array.
Structured linear algebra and Gaussian primitives for JAX.
process_noise_covariance(A: Float[Array, 'N N'] | lx.AbstractLinearOperator, Pinf: Float[Array, 'N N'] | lx.AbstractLinearOperator) -> Float[Array, 'N N'] | lx.AbstractLinearOperator
¶
Compute process noise from stationary covariance.
Computes:
Q = Pinf - A @ Pinf @ A^T
For a discrete-time state-space model with stationary covariance
Pinf and transition matrix A. This is the standard
"steady-state trick": it follows from stationarity of the discretised
recursion Pinf = A Pinf A^T + Q, and avoids matrix-fraction
integration (Särkkä & Solin 2019, §6.4).
It is the forward direction of the discrete Lyapunov equation that
gaussx.discrete_lyapunov_solve inverts: this maps
Pinf -> Q, that one maps Q -> Pinf for the same A.
The A Pinf A^T term is delegated to
gaussx.cov_transform, so structure is exploited rather than
re-derived: a diagonal Pinf skips its (N, N) materialization, and
matched gaussx.Kronecker / gaussx.BlockDiag
operands stay factorised through gaussx.sandwich.
Passing operators returns a lazy operator — nothing is materialized,
and the structural class of the result follows gaussx.sandwich.
Passing arrays returns an array, as before.
The result is not symmetrised. Callers that need a covariance clean of
floating-point asymmetry should wrap it in gaussx.symmetrize,
as gaussx.SDEKernel.discretise does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Float[Array, 'N N'] | AbstractLinearOperator
|
State transition matrix, shape |
required |
Pinf
|
Float[Array, 'N N'] | AbstractLinearOperator
|
Stationary covariance, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N N'] | AbstractLinearOperator
|
Process noise covariance Q, shape |
Float[Array, 'N N'] | AbstractLinearOperator
|
either argument is an operator, otherwise an array. |