Source code for smolgp.solvers.integrated.kalman

from __future__ import annotations

import jax
import jax.numpy as jnp

from smolgp.helpers import kalman_gain, transition_sequence
from smolgp.solvers.base import log_prob_from_v_S


[docs] def IntegratedKalmanFilter( kernel, X, y, t_states, obsid, instid, stateid, R, return_v_S=False ): """ Wrapper for integrated_kalman_filter function Parameters: kernel : IntegratedStateSpaceModel kernel X : Array of size N, data coordinates (e.g. (time, texp, instid)) y : Array of size (N, D), measurements at the data coordinates t_states: Array of size K, sorted time coordinate of all states (exposure starts and ends) obsid : Array of size N, which observation (0,...,N-1) is being made at each state k instid : Array of size N, which instrument (0,...,Ninst-1) recorded observation n stateid : Array of size K, 0 for exposure-start, 1 for exposure-end R : Observation noise covariance, shape (N, D, D) return_v_S : Whether to return innovation and its covariance (for likelihood computation) Returns: m_filtered : filtered means P_filtered : filtered covariances m_predicted: predicted means P_predicted: predicted covariances """ # Model components H_aug = kernel.observation_model A_aug = kernel.transition_matrix Q_aug = kernel.process_noise RESET = kernel.reset_matrix # Initial state and covariance # mean = jnp.zeros(kernel.d) # TODO: mean function of base kernel # m0 = jnp.block([mean] + kernel.num_insts*[jnp.zeros(kernel.d)]) m0 = jnp.zeros(kernel.dimension) P0 = kernel.stationary_covariance() output = integrated_kalman_filter( A_aug, Q_aug, H_aug, R, RESET, X, y, t_states, obsid, instid, stateid, m0, P0 ) if return_v_S: return output else: m_filtered, P_filtered, m_predicted, P_predicted, v, S = output return m_filtered, P_filtered, m_predicted, P_predicted
[docs] @jax.jit def integrated_kalman_filter( A_aug, Q_aug, H_aug, R, RESET, X, y, t_states, obsid, instid, stateid, m0, P0 ): """ Jax implementation of the integrated Kalman filter algorithm See Section 3.2.1 in Rubenzahl & Hattori et al. (in prep) for detailed description of the algorithm and notation. """ H = jax.vmap(H_aug)(X) A_all, Q_all = transition_sequence(A_aug, Q_aug, t_states) @jax.jit def step(carry, data): # Unpack previous state and covariance m_prev, P_prev = carry # k is still streamed: the body indexes obsid[k] and stateid[k], and # gathers per-observation H/y/R through n = obsid[k]. A_prev, Q_prev, k = data n = obsid[k] # Predict step is same m_pred = A_prev @ m_prev P_pred = A_prev @ P_prev @ A_prev.T + Q_prev # Update the end of the exposure def update_end(): Hk = H[n] y_pred = Hk @ m_pred # predicted observation v_k = y[n] - y_pred # "innovation" or "surprise" term S_k = Hk @ P_pred @ Hk.T + R[n] # uncertainy in predicted observation K_k = kalman_gain(S_k, P_pred @ Hk.T) # Kalman gain m_k = m_pred + K_k @ v_k # conditioned state estimate P_k = P_pred - K_k @ S_k @ K_k.T # conditioned covariance estimate return m_k, P_k, m_pred, P_pred, v_k, S_k # Update the start of the exposure, aka reset its z to zero def update_start(): Reset = RESET(instid[n]) m_k = Reset @ m_pred P_k = Reset @ P_pred @ Reset.T Hk = H[n] # TODO: change this and next two lines to use shapes? v_k = jnp.zeros_like(Hk @ m_pred) # maybe e.g. jax broadcast_shapes? S_k = jnp.zeros_like(Hk @ P_pred @ Hk.T) return m_k, P_k, m_pred, P_pred, v_k, S_k m_k, P_k, m_pred, P_pred, v_k, S_k = jax.lax.cond( stateid[k] == 0, lambda _: update_start(), lambda _: update_end(), operand=None, ) return (m_k, P_k), (m_k, P_k, m_pred, P_pred, v_k, S_k) # Initialize carry with prior state and covariance init_carry = (m0, P0) # Run the filter over all time steps, unpack, and return results _, outputs = jax.lax.scan( step, init_carry, (A_all, Q_all, jnp.arange(len(t_states))) ) m_filtered, P_filtered, m_predicted, P_predicted, v, S = outputs # only return v,S at exposure ends (where there is data) ends_idx = jnp.nonzero(stateid == 1, size=y.shape[0])[0] v_sel = jnp.take(v, ends_idx, axis=0) S_sel = jnp.take(S, ends_idx, axis=0) return m_filtered, P_filtered, m_predicted, P_predicted, v_sel, S_sel
[docs] @jax.jit def integrated_kalman_gains(A_aug, H_aug, RESET, R, X, t_states, obsid, instid, stateid, P_predicted): """ The `y`-independent per-state quantities needed to replay integrated_kalman_filter's mean-path recursion for many different observation/residual arrays, given P_predicted from ONE prior call to integrated_kalman_filter (any y -- P_predicted never depends on it). Computes BOTH the Kalman-gain ingredients (meaningful at exposure-end states) and the Reset matrix (meaningful at exposure-start states) at every state k unconditionally (cheap; no scan needed) -- the batched-mean recursion selects the right one per state via the same stateid==0 jax.lax.cond dispatch integrated_kalman_filter uses. Returns: A_all: shape (K, dim, dim) -- A_aug(0, Delta_k) H_all: shape (N, D, dim) -- H_aug(X), i.e. jax.vmap(H_aug)(X) K_all: shape (K, dim, D) -- Kalman gain (meaningful at end states) Reset_all: shape (K, dim, dim) -- RESET(instid[obsid[k]]) (meaningful at start states) """ K = len(t_states) H_all = jax.vmap(H_aug)(X) def gains_at_k(k): Delta = jax.lax.cond( k > 0, lambda i: t_states[i] - t_states[i - 1], lambda _: 0.0, k ) A_k = A_aug(0, Delta) n = obsid[k] Hk = H_all[n] P_pred_k = P_predicted[k] S_k = Hk @ P_pred_k @ Hk.T + R[n] K_k = kalman_gain(S_k, P_pred_k @ Hk.T) Reset_k = RESET(instid[n]) return A_k, K_k, Reset_k A_all, K_all, Reset_all = jax.vmap(gains_at_k)(jnp.arange(K)) return A_all, H_all, K_all, Reset_all
[docs] @jax.jit def integrated_kalman_filter_batched_mean( A_all, H_all, K_all, Reset_all, obsid, stateid, y_batch, m0 ): """ Batched-mean-path replay of integrated_kalman_filter, using PRECOMPUTED (integrated_kalman_gains) A_all/H_all/K_all/Reset_all. The exposure-start "reset" update is a pure per-sample-batched einsum with no y/data dependence at all (the innovation is always zero at a start state) -- `jnp.einsum("ij,mj->mi", Reset_k, m_pred_batch)`. The stateid==0 dispatch itself (which state gets which update) is data-independent (known from t_states/obsid alone), so the same jax.lax.cond structure as integrated_kalman_filter applies unchanged, just wrapping batched einsums instead of unbatched matrix-vector `@`. Parameters: A_all: (K, dim, dim), H_all: (N, D, dim), K_all: (K, dim, D), Reset_all: (K, dim, dim) -- all from integrated_kalman_gains obsid, stateid: (K,) bookkeeping arrays (same as integrated_kalman_filter) y_batch: (M, N, D) -- batch of M residual/observation arrays m0: (dim,) Returns: m_filtered_batch, m_predicted_batch: (M, K, dim) """ K = A_all.shape[0] M = y_batch.shape[0] dim = m0.shape[0] def step(carry, k): m_prev_batch = carry A_k = A_all[k] m_pred_batch = jnp.einsum("ij,mj->mi", A_k, m_prev_batch) n = obsid[k] def update_end_batch(): Hk, K_k = H_all[n], K_all[k] y_pred_batch = jnp.einsum("di,mi->md", Hk, m_pred_batch) v_batch = y_batch[:, n, :] - y_pred_batch return m_pred_batch + jnp.einsum("id,md->mi", K_k, v_batch) def update_start_batch(): Reset_k = Reset_all[k] return jnp.einsum("ij,mj->mi", Reset_k, m_pred_batch) m_k_batch = jax.lax.cond( stateid[k] == 0, lambda _: update_start_batch(), lambda _: update_end_batch(), operand=None, ) return m_k_batch, (m_k_batch, m_pred_batch) init_carry = jnp.broadcast_to(m0, (M, dim)) _, (m_filtered_T, m_predicted_T) = jax.lax.scan(step, init_carry, jnp.arange(K)) return jnp.moveaxis(m_filtered_T, 0, 1), jnp.moveaxis(m_predicted_T, 0, 1)