repro-aggregate-models-not-explanations-improving-feature-importance-estimation / source_code /scripts /multiscale_hippo.py
| """ | |
| Multiscale HiPPO vs regular HiPPO | |
| Explicit reconstruction of the past from 16 Legendre coefficients. | |
| What we do: | |
| - Sample OU-mixture signals. | |
| - Roll out three multiscale systems + H regular HiPPO systems. | |
| - For each lag L in `lags`: | |
| * Multiscale systems: interpret their (n*m) state at final time T-1 as an OP over | |
| the *normalized* time coordinate s \in [0,1] where s=0 is time T-1 (present) and | |
| s=1 is time T-1-L (past endpoint). | |
| We reconstruct x_hat(s) = \Sigma_{k=0}^{n-1} c_k L_k(s) using 128 s-samples, compare to the | |
| true signal along that window (linear interpolation at fractional indices), and | |
| accumulate mean MSE. | |
| * Regular HiPPO (each base timescale \tau_h): its coefficients represent the fixed window | |
| [T-1-\tau_h, T-1]. For each lag L we still evaluate the *last* L steps portion by | |
| sampling s \in [0,1] over the whole \tau_h window and comparing on the last L-step segment. | |
| (So if L > \tau_h, the earliest part is outside the window; we clamp to available region.) | |
| - Plot average MSE vs lag (log x-axis), one curve per model; regular HiPPO gives H curves. | |
| Important conventions: | |
| - For multiscale: s=0 <-> time T (here T means last index T-1) and s=1 <-> time T-lag. | |
| So time(s) = (T-1) - s*lag. | |
| - For regular HiPPO with window \tau_h: time(s) = (T-1) - s*\tau_h. | |
| We compare over the interval [T-1-lag, T-1] by restricting s to [0, lag/\tau_h] (if lag<=\tau_h), | |
| otherwise we compare over the full window and treat the extra as outside (0 target). | |
| """ | |
| from dataclasses import dataclass | |
| from typing import Tuple | |
| import jax | |
| import jax.numpy as jnp | |
| import jax.random as jr | |
| from jax import jit | |
| from jax.lax import scan | |
| from jax.scipy.linalg import expm | |
| from scipy.special import iv | |
| jax.config.update("jax_enable_x64", True) | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from matplotlib.lines import Line2D | |
| from tqdm import tqdm | |
| from mpm import get_system_params | |
| # ---------------------------- | |
| # 1) OU-mixture signal | |
| # ---------------------------- | |
| def sample_ou_mixture_1d( | |
| key: jax.Array, | |
| T: int, | |
| ou_components: int, | |
| tau_min: float, | |
| tau_max: float, | |
| dt: float = 1.0, | |
| taus: jax.Array | None = None, | |
| ) -> Tuple[jax.Array, jax.Array]: | |
| if taus is None: | |
| key_tau, key_init, key_noise = jr.split(key, 3) | |
| log_tau = jr.uniform( | |
| key_tau, (ou_components,), minval=jnp.log(tau_min), maxval=jnp.log(tau_max) | |
| ) | |
| taus = jnp.exp(log_tau) | |
| else: | |
| key_init, key_noise = jr.split(key, 2) | |
| a = jnp.exp(-dt / taus) | |
| b = jnp.sqrt(jnp.maximum(0.0, 1.0 - a * a)) | |
| z0 = jr.normal(key_init, (ou_components,)) | |
| eps = jr.normal(key_noise, (T - 1, ou_components)) | |
| def step(z, e): | |
| z_next = a * z + b * e | |
| return z_next, z_next | |
| _, zs = scan(step, z0, eps) | |
| zs = jnp.concatenate([z0[None, :], zs], axis=0) | |
| x = jnp.sum(zs, axis=1) | |
| x = x / jnp.sqrt(float(ou_components)) | |
| return x.astype(jnp.float32), taus | |
| # ---------------------------- | |
| # 2) Multiscale matrices: C, B and dense ZOH discretization | |
| # ---------------------------- | |
| def vecT(S): | |
| return jnp.reshape(S.T, (-1,)) | |
| def unvecT(v, n, m): | |
| return jnp.reshape(v, (m, n)).T | |
| def legendre_shifted_mult_by_c_matrix(m: int) -> jax.Array: | |
| n = jnp.arange(m, dtype=jnp.float32) | |
| a_up = (n[:-1] + 1.0) / jnp.sqrt((2.0 * n[:-1] + 1.0) * (2.0 * n[:-1] + 3.0)) | |
| a_dn = (n[1:]) / jnp.sqrt((2.0 * n[1:] + 1.0) * (2.0 * n[1:] - 1.0)) | |
| Mx = jnp.zeros((m, m), dtype=jnp.float32) | |
| Mx = Mx.at[jnp.arange(m - 1), jnp.arange(1, m)].set(a_up) | |
| Mx = Mx.at[jnp.arange(1, m), jnp.arange(m - 1)].set(a_dn) | |
| C = 0.5 * jnp.eye(m, dtype=jnp.float32) + 0.5 * Mx | |
| return C | |
| def legendre_orthonormal_Jx(m: int, dtype=jnp.float64) -> jax.Array: | |
| n = jnp.arange(m - 1, dtype=dtype) | |
| off = (n + 1.0) / jnp.sqrt((2.0 * n + 1.0) * (2.0 * n + 3.0)) | |
| J = jnp.zeros((m, m), dtype=dtype) | |
| J = J.at[jnp.arange(m - 1), jnp.arange(1, m)].set(off) | |
| J = J.at[jnp.arange(1, m), jnp.arange(m - 1)].set(off) | |
| return J | |
| def build_G_log_g(m: int, eps: float, dtype=jnp.float64) -> jax.Array: | |
| L = jnp.log(1.0 / jnp.asarray(eps, dtype=dtype)) | |
| kappa = 0.5 * L | |
| Jx = legendre_orthonormal_Jx(m, dtype=dtype) | |
| G = jnp.sqrt(jnp.asarray(eps, dtype=dtype)) * expm(kappa * Jx) | |
| G = 0.5 * (G + G.T) | |
| return G | |
| def modified_spherical_in(n: jax.Array, z: jax.Array) -> jax.Array: | |
| n = jnp.asarray(n) | |
| z = jnp.asarray(z) | |
| v = n + 0.5 | |
| return jnp.sqrt(jnp.pi / (2.0 * z)) * jnp.array(iv(np.array(v), np.array(z))) | |
| def beta_for_exp_u_in_u_legendre(m: int, eps: float, dtype=jnp.float64) -> jax.Array: | |
| L = jnp.log(1.0 / jnp.asarray(eps, dtype=dtype)) | |
| kappa = 0.5 * L | |
| n = jnp.arange(m, dtype=dtype) | |
| i_n = modified_spherical_in(n, kappa) | |
| beta = jnp.sqrt(jnp.asarray(eps, dtype=dtype)) * jnp.sqrt((2.0 * n + 1.0) * L) * i_n | |
| return beta | |
| def build_B_from_b_log_g(b: jax.Array, m: int, eps: float, dtype=jnp.float64) -> jax.Array: | |
| beta = beta_for_exp_u_in_u_legendre(m, eps, dtype=dtype) | |
| return jnp.asarray(b, dtype=dtype)[:, None] * beta[None, :] | |
| def build_B_from_b(b: jax.Array, m: int) -> jax.Array: | |
| n = b.shape[0] | |
| B = jnp.zeros((n, m), dtype=b.dtype) | |
| B = B.at[:, 0].set(0.5 * b) | |
| if m >= 2: | |
| B = B.at[:, 1].set((0.5 / jnp.sqrt(3.0)) * b) | |
| return B | |
| def build_B_from_b_jeffreys(b: jax.Array, m: int, eps: float) -> jax.Array: | |
| b = jnp.asarray(b) | |
| mu0 = jnp.log(1.0 / jnp.asarray(eps, dtype=jnp.float64)) | |
| mu1 = (1.0 - eps) | |
| mu2 = 0.5 * (1.0 - eps * eps) | |
| beta0 = (mu1 / jnp.sqrt(mu0)).astype(b.dtype) | |
| beta1 = jnp.sqrt(jnp.maximum(mu2 - (mu1 * mu1) / mu0, 0.0)).astype(b.dtype) | |
| B = jnp.zeros((b.shape[0], m), dtype=b.dtype) | |
| B = B.at[:, 0].set(beta0 * b) | |
| if m >= 2: | |
| B = B.at[:, 1].set(beta1 * b) | |
| return B | |
| def jeffreys_eval_basis(g: jax.Array, a: jax.Array, b: jax.Array, eps: float, m: int) -> jax.Array: | |
| """ | |
| Return Q(g) = [Q_0(g), ..., Q_{m-1}(g)] for Jeffreys-orthonormal polynomials. | |
| Recurrence: | |
| g Q_n = a_{n+1} Q_{n+1} + b_n Q_n + a_n Q_{n-1}, a_0=0. | |
| """ | |
| g = jnp.asarray(g) | |
| mu0 = jnp.log(1.0 / jnp.asarray(eps, dtype=g.dtype)) | |
| Q0 = 1.0 / jnp.sqrt(mu0) | |
| # Q_0 | |
| Q = [jnp.broadcast_to(Q0, g.shape)] | |
| if m == 1: | |
| return jnp.stack(Q, axis=-1) | |
| # Q_1 = ((g - b0)/a1) Q0 | |
| Q1 = ((g - b[0]) / a[1]) * Q[0] | |
| Q.append(Q1) | |
| # Q_{n+1} = ((g - b_n) Q_n - a_n Q_{n-1}) / a_{n+1} | |
| for n in range(1, m - 1): | |
| Qnp1 = ((g - b[n]) * Q[n] - a[n] * Q[n - 1]) / a[n + 1] | |
| Q.append(Qnp1) | |
| return jnp.stack(Q, axis=-1) # (..., m) | |
| def jeffreys_clenshaw(g: jax.Array, coeffs: jax.Array, a: jax.Array, b: jax.Array, eps: float) -> jax.Array: | |
| """ | |
| Evaluate sum_{k=0}^{m-1} coeffs[k] Q_k(g) using Clenshaw. | |
| Here a has length >= m+2 (we return a_0..a_{m+2}), b has length >= m. | |
| """ | |
| g = jnp.asarray(g) | |
| coeffs = jnp.asarray(coeffs) | |
| m = coeffs.shape[-1] | |
| mu0 = jnp.log(1.0 / jnp.asarray(eps, dtype=g.dtype)) | |
| Q0 = 1.0 / jnp.sqrt(mu0) | |
| def one(x, c): | |
| d1 = jnp.asarray(0.0, dtype=x.dtype) | |
| d2 = jnp.asarray(0.0, dtype=x.dtype) | |
| for k in range(m - 1, -1, -1): | |
| d0 = c[k] + ((x - b[k]) / a[k + 1]) * d1 - (a[k + 1] / a[k + 2]) * d2 | |
| d2, d1 = d1, d0 | |
| return Q0 * d1 | |
| xflat = g.reshape(-1) | |
| if coeffs.ndim == 1: | |
| cflat = jnp.broadcast_to(coeffs, (xflat.shape[0], m)) | |
| else: | |
| cflat = coeffs.reshape((-1, m)) | |
| out = jax.vmap(one)(xflat, cflat) | |
| return out.reshape(g.shape) | |
| def zoh_discretize_dense(K: jax.Array, g: jax.Array, dt: float) -> Tuple[jax.Array, jax.Array]: | |
| D = K.shape[0] | |
| Z = jnp.zeros((D + 1, D + 1), dtype=K.dtype) | |
| Z = Z.at[:D, :D].set(K) | |
| Z = Z.at[:D, D].set(g) | |
| E = expm(dt * Z) | |
| A_d = E[:D, :D] | |
| B_d = E[:D, D] | |
| return A_d.astype(jnp.float32), B_d.astype(jnp.float32) | |
| def jeffreys_nodes_weights(eps: float, Q: int, dtype=jnp.float64): | |
| eps = jnp.asarray(eps, dtype=dtype) | |
| u0 = jnp.log(eps) | |
| u1 = jnp.array(0.0, dtype=dtype) | |
| i = jnp.arange(Q, dtype=dtype) | |
| du = (u1 - u0) / Q | |
| u = u0 + (i + 0.5) * du | |
| g = jnp.exp(u) | |
| w = jnp.full((Q,), du, dtype=dtype) # integral is \int f(exp(u)) du | |
| return g, w | |
| def jeffreys_recurrence_discrete(m: int, eps: float, Q: int = 8192, dtype=jnp.float64): | |
| g, w = jeffreys_nodes_weights(eps, Q, dtype=dtype) | |
| def inner(x, y): | |
| return jnp.sum(w * x * y) | |
| mu0 = jnp.log(1.0 / jnp.asarray(eps, dtype=dtype)) | |
| p_prev = jnp.zeros((Q,), dtype=dtype) | |
| p = jnp.full((Q,), 1.0 / jnp.sqrt(mu0), dtype=dtype) | |
| a = [jnp.array(0.0, dtype=dtype)] | |
| b = [] | |
| a_n = jnp.array(0.0, dtype=dtype) | |
| for _n in range(m + 1): | |
| gp = g * p | |
| b_n = inner(gp, p) | |
| r = gp - b_n * p - a_n * p_prev | |
| a_np1 = jnp.sqrt(jnp.maximum(inner(r, r), 0.0)) | |
| b.append(b_n) | |
| a.append(a_np1) | |
| p_next = jnp.where(a_np1 > 0, r / a_np1, jnp.zeros_like(r)) | |
| p_prev, p = p, p_next | |
| a_n = a_np1 | |
| # one more for Clenshaw safety | |
| gp = g * p | |
| b_mp1 = inner(gp, p) | |
| r = gp - b_mp1 * p - a_n * p_prev | |
| a_mp2 = jnp.sqrt(jnp.maximum(inner(r, r), 0.0)) | |
| a.append(a_mp2) | |
| return jnp.stack(a), jnp.stack(b) | |
| def jeffreys_mult_by_g_c_matrix_discrete( | |
| m: int, eps: float, Q: int = 8192, dtype=jnp.float64 | |
| ): | |
| a, b = jeffreys_recurrence_discrete(m=m, eps=eps, Q=Q, dtype=dtype) | |
| diag = b[:m] | |
| off = a[1:m] | |
| C = jnp.zeros((m, m), dtype=dtype) | |
| C = C.at[jnp.arange(m), jnp.arange(m)].set(diag) | |
| if m >= 2: | |
| C = C.at[jnp.arange(m - 1), jnp.arange(1, m)].set(off) | |
| C = C.at[jnp.arange(1, m), jnp.arange(m - 1)].set(off) | |
| return 0.5 * (C + C.T) | |
| # ---------------------------- | |
| # 3) Orthonormal shifted Legendre evaluation on [0,1] | |
| # ---------------------------- | |
| def shifted_legendre_orthonormal_vals(s: jax.Array, n: int) -> jax.Array: | |
| """ | |
| L_k(s) orthonormal on [0,1]: | |
| L_k(s) = sqrt(2k+1) P_k(2s-1) | |
| Returns shape (..., n) | |
| """ | |
| s = jnp.asarray(s) | |
| x = 2.0 * s - 1.0 | |
| # P_0, P_1 | |
| P0 = jnp.ones_like(x) | |
| if n == 1: | |
| return P0[..., None] * jnp.sqrt(1.0) | |
| P1 = x | |
| Ps = [P0, P1] | |
| for k in range(1, n - 1): | |
| Pkp1 = ((2 * k + 1) * x * Ps[k] - k * Ps[k - 1]) / (k + 1) | |
| Ps.append(Pkp1) | |
| P = jnp.stack(Ps[:n], axis=-1) # (..., n) | |
| scale = jnp.sqrt(2.0 * jnp.arange(n, dtype=s.dtype) + 1.0) | |
| return P * scale | |
| # ---------------------------- | |
| # 4) Config | |
| # ---------------------------- | |
| class Config: | |
| T: int = 30_000 | |
| dt: float = 1.0 | |
| n: int = 16 | |
| m: int = 128 | |
| measure: str = "legt" | |
| base_timescale: float = 5.0 | |
| hippo_base_timescales: Tuple[float, ...] = (10.0, 100.0, 1000.0, 10000.0) | |
| epsilon: float = 1e-4 | |
| num_iters: int = 64 | |
| num_lags: int = 20 | |
| num_samps: int = 128 # samples in [0,1] for reconstruction | |
| lag_min: int = 10 | |
| lag_max: int = 30000 | |
| # Signal | |
| ou_components: int = 20 | |
| tau_min: float = 2.0 | |
| tau_max: float = 2000.0 | |
| # ---------------------------- | |
| # 5) Run: roll out systems and evaluate MSE-vs-lag | |
| # ---------------------------- | |
| def run(seed: int = 0): | |
| cfg = Config() | |
| key = jr.PRNGKey(seed) | |
| # HiPPO continuous params | |
| (A, b), _, _ = get_system_params(cfg.measure, cfg.n) | |
| print("A", A.shape, "b", b.shape) | |
| A = (A / cfg.base_timescale).astype(jnp.float32) | |
| b = (b / cfg.base_timescale).astype(jnp.float32) | |
| (A_nm, b_nm), _, _ = get_system_params(cfg.measure, cfg.n) | |
| A_nm = A_nm.astype(jnp.float32) | |
| b_nm = b_nm.astype(jnp.float32) | |
| # Multiscale (c) | |
| C = legendre_shifted_mult_by_c_matrix(cfg.m).astype(jnp.float32) | |
| print("C", C.shape) | |
| B = build_B_from_b(b, cfg.m).astype(jnp.float32) | |
| K_ms = jnp.kron(C.T, A) | |
| g_ms = vecT(B) | |
| Ams_d, Bms_d = zoh_discretize_dense(K_ms, g_ms, dt=cfg.dt) | |
| # Multiscale (Jeffreys) | |
| C_jeff = jeffreys_mult_by_g_c_matrix_discrete(cfg.m, cfg.epsilon, Q=8192, dtype=jnp.float64).astype(jnp.float32) | |
| B_jeff = build_B_from_b_jeffreys(b, cfg.m, cfg.epsilon).astype(jnp.float32) | |
| K_ms_jeff = jnp.kron(C_jeff.T, A) | |
| g_ms_jeff = vecT(B_jeff) | |
| Amsj_d, Bmsj_d = zoh_discretize_dense(K_ms_jeff, g_ms_jeff, dt=cfg.dt) | |
| # Multiscale (log-g) | |
| G = build_G_log_g(cfg.m, cfg.epsilon, dtype=jnp.float64).astype(jnp.float32) | |
| B_log = build_B_from_b_log_g(b, cfg.m, cfg.epsilon, dtype=jnp.float64).astype(jnp.float32) | |
| K_ms_log = jnp.kron(G.T, A) | |
| g_ms_log = vecT(B_log) | |
| Amsg_d, Bmsg_d = zoh_discretize_dense(K_ms_log, g_ms_log, dt=cfg.dt) | |
| # Regular HiPPO at multiple base timescales | |
| hippo_base_timescales = jnp.array(cfg.hippo_base_timescales, dtype=jnp.float32) | |
| def hippo_one(scale): | |
| Kk = (A_nm / scale).astype(jnp.float32) | |
| gk = (b_nm / scale).astype(jnp.float32) | |
| return zoh_discretize_dense(Kk, gk, dt=cfg.dt) | |
| Ahp_d, Bhp_d = jax.vmap(hippo_one)(hippo_base_timescales) | |
| # Rollout step (stores states after ingesting u_t) | |
| def step(carry, u_t): | |
| s_ms, s_msj, s_msg, s_hp = carry | |
| s_ms_next = Ams_d @ s_ms + Bms_d * u_t | |
| s_msj_next = Amsj_d @ s_msj + Bmsj_d * u_t | |
| s_msg_next = Amsg_d @ s_msg + Bmsg_d * u_t | |
| s_hp_next = jnp.einsum("hnm,hm->hn", Ahp_d, s_hp) + Bhp_d * u_t | |
| return (s_ms_next, s_msj_next, s_msg_next, s_hp_next), (s_ms_next, s_msj_next, s_msg_next, s_hp_next) | |
| # Lags (log-spaced, unique, >= lag_min and <= lag_max) | |
| lag_min = max(1, int(cfg.lag_min)) | |
| lag_max = min(int(cfg.lag_max), cfg.T - 1) | |
| lags = jnp.exp(jnp.linspace(jnp.log(float(lag_min)), jnp.log(float(lag_max)), cfg.num_lags)) | |
| lags = jnp.unique(jnp.clip(jnp.round(lags).astype(jnp.int32), lag_min, lag_max)) | |
| ms_sum = jnp.zeros((lags.shape[0],), dtype=jnp.float64) | |
| msj_sum = jnp.zeros((lags.shape[0],), dtype=jnp.float64) | |
| msg_sum = jnp.zeros((lags.shape[0],), dtype=jnp.float64) | |
| hp_sum = jnp.zeros((len(cfg.hippo_base_timescales), lags.shape[0]), dtype=jnp.float64) | |
| ms_sum_sq = jnp.zeros((lags.shape[0],), dtype=jnp.float64) | |
| msj_sum_sq = jnp.zeros((lags.shape[0],), dtype=jnp.float64) | |
| msg_sum_sq = jnp.zeros((lags.shape[0],), dtype=jnp.float64) | |
| hp_sum_sq = jnp.zeros((len(cfg.hippo_base_timescales), lags.shape[0]), dtype=jnp.float64) | |
| oracle_sum = jnp.zeros((lags.shape[0],), dtype=jnp.float64) | |
| oracle_sum_sq = jnp.zeros((lags.shape[0],), dtype=jnp.float64) | |
| # Precompute s-grid and Legendre values on [0,1] | |
| s_grid = jnp.linspace(0.0, 1.0, cfg.num_samps, dtype=jnp.float32) # (S,) | |
| L_grid_flip = shifted_legendre_orthonormal_vals(1.0 - s_grid, cfg.n).astype(jnp.float32) # (S,n) | |
| # Oracle projection matrix P = Phi @ (Phi.T @ Phi)^{-1} @ Phi.T shape (S, S) | |
| Phi_oracle = L_grid_flip.astype(jnp.float64) # (S, n) | |
| P_oracle = Phi_oracle @ jnp.linalg.solve(Phi_oracle.T @ Phi_oracle, Phi_oracle.T) # (S, S) | |
| # Helper: linear interpolation of signal at fractional indices | |
| def interp_signal(sig: jax.Array, t_float: jax.Array) -> jax.Array: | |
| """ | |
| sig: (T,) | |
| t_float: (...,) float time index | |
| Returns sig(t_float) via linear interpolation, with out-of-range -> 0. | |
| """ | |
| T = sig.shape[0] | |
| t0 = jnp.floor(t_float).astype(jnp.int32) | |
| t1 = t0 + 1 | |
| w = t_float - t0.astype(t_float.dtype) | |
| in0 = (t0 >= 0) & (t0 < T) | |
| in1 = (t1 >= 0) & (t1 < T) | |
| t0c = jnp.clip(t0, 0, T - 1) | |
| t1c = jnp.clip(t1, 0, T - 1) | |
| v0 = sig[t0c] * in0.astype(sig.dtype) | |
| v1 = sig[t1c] * in1.astype(sig.dtype) | |
| return (1.0 - w) * v0 + w * v1 | |
| # Helper: compute per-lag MSE for one coefficient vector c (n,) representing [T..T-lag] | |
| def mse_for_coeffs_over_lag(sig: jax.Array, coeff: jax.Array, lag: int) -> jax.Array: | |
| """ | |
| coeff: (n,) in shifted orthonormal Legendre basis over s\in[0,1] | |
| s=0 at time T (index T-1), s=1 at time T-lag. | |
| Compare on 128 s-grid points. | |
| """ | |
| # predicted curve on s_grid | |
| yhat = L_grid_flip @ coeff # (S,) | |
| # true curve: time(s) = (T-1) - s*lag | |
| Tlast = sig.shape[0] - 1 | |
| t_float = Tlast - s_grid.astype(jnp.float32) * float(lag) | |
| ytrue = interp_signal(sig, t_float) # (S,) | |
| return jnp.mean((yhat.astype(jnp.float64) - ytrue.astype(jnp.float64)) ** 2) | |
| def mse_for_coeffs_over_lag_regular(sig, coeff, tau, lag): | |
| tau = jnp.asarray(tau, jnp.float32) | |
| lagf = jnp.asarray(lag, jnp.float32) | |
| Tlast = sig.shape[0] - 1 | |
| # Evaluate over the *lag* window | |
| t_float = Tlast - s_grid * lagf | |
| ytrue = interp_signal(sig, t_float) | |
| # Map those times into the HiPPO window coordinate | |
| s_tau = (Tlast - t_float) / tau # = s_grid * lag/tau | |
| valid = (s_tau >= 0.0) & (s_tau <= 1.0) | |
| s_eval = 1.0 - s_tau | |
| L_hp = shifted_legendre_orthonormal_vals(jnp.clip(s_eval, 0.0, 1.0), cfg.n).astype(jnp.float32) | |
| yhat_in = L_hp @ coeff # defined everywhere but only meaningful on valid | |
| yhat = jnp.where(valid, yhat_in, 0.0) # zero prediction outside window | |
| return jnp.mean((yhat.astype(jnp.float64) - ytrue.astype(jnp.float64))**2) | |
| def oracle_mse_for_lag(sig: jax.Array, lag: int) -> jax.Array: | |
| """MSE of the best possible projection of the true signal onto the n-Legendre basis.""" | |
| Tlast = sig.shape[0] - 1 | |
| t_float = Tlast - s_grid.astype(jnp.float32) * float(lag) | |
| ytrue = interp_signal(sig, t_float).astype(jnp.float64) # (S,) | |
| yhat = P_oracle @ ytrue # (S,) | |
| return jnp.mean((ytrue - yhat) ** 2) | |
| # Main Monte Carlo loop | |
| example = None | |
| for _itr in tqdm(range(cfg.num_iters)): | |
| key, subkey = jr.split(key) | |
| signal, _taus = sample_ou_mixture_1d( | |
| subkey, | |
| cfg.T, | |
| cfg.ou_components, | |
| cfg.tau_min, | |
| cfg.tau_max, | |
| dt=cfg.dt, | |
| ) | |
| # Roll out states across the whole signal | |
| s_ms0 = jnp.zeros((cfg.n * cfg.m,), dtype=jnp.float32) | |
| s_msj0 = jnp.zeros((cfg.n * cfg.m,), dtype=jnp.float32) | |
| s_msg0 = jnp.zeros((cfg.n * cfg.m,), dtype=jnp.float32) | |
| s_hp0 = jnp.zeros((len(cfg.hippo_base_timescales), cfg.n), dtype=jnp.float32) | |
| (s_ms_T, s_msj_T, s_msg_T, s_hp_T), states = scan( | |
| step, (s_ms0, s_msj0, s_msg0, s_hp0), signal | |
| ) | |
| # Use final states at T-1 | |
| S_ms_T = unvecT(s_ms_T, cfg.n, cfg.m) # (n,m) | |
| S_msj_T = unvecT(s_msj_T, cfg.n, cfg.m) | |
| S_msg_T = unvecT(s_msg_T, cfg.n, cfg.m) | |
| # s_hp_T: (H,n) | |
| if _itr == 0: | |
| example = { | |
| "signal": np.array(signal), | |
| "s_ms_T": np.array(s_ms_T), | |
| "s_msj_T": np.array(s_msj_T), | |
| "s_msg_T": np.array(s_msg_T), | |
| "s_hp_T": np.array(s_hp_T), | |
| } | |
| # For each lag, extract coefficients and compute MSE | |
| for li in range(lags.shape[0]): | |
| lag = int(lags[li]) | |
| # Multiscale (c): interpret the *lag window* using timescale-axis coordinate c = base_timescale / lag, | |
| # clipped into [0,1] since the c-basis is on [0,1]. | |
| c = float(cfg.base_timescale) / float(lag) | |
| c = float(np.clip(c, 0.0, 1.0)) | |
| q_c = shifted_legendre_orthonormal_vals(jnp.array([c], dtype=jnp.float32), cfg.m)[0] # (m,) | |
| coeff_ms = (unvecT(s_ms_T, cfg.n, cfg.m) @ q_c).astype(jnp.float32) # (n,) | |
| # Multiscale (Jeffreys g): g = base_timescale / lag, clipped into [eps,1] | |
| g = float(cfg.base_timescale) / float(lag) | |
| g = float(np.clip(g, float(cfg.epsilon), 1.0)) | |
| # Evaluate Jeffreys basis via Clenshaw by feeding one-hot coeffs (cheap at m=16) | |
| eye_m = jnp.eye(cfg.m, dtype=jnp.float32) # (m,m) | |
| a_rec, b_rec = jeffreys_recurrence_discrete(cfg.m, cfg.epsilon, Q=8192, dtype=jnp.float64) | |
| a_rec = a_rec.astype(jnp.float32) | |
| b_rec = b_rec.astype(jnp.float32) | |
| q_g = jeffreys_eval_basis(g=jnp.array(g, dtype=jnp.float32), a=a_rec, b=b_rec, eps=cfg.epsilon, m=cfg.m) # (m,) | |
| coeff_msj = unvecT(s_msj_T, cfg.n, cfg.m) @ q_g # (n,) | |
| # Multiscale (log-g): u = log(g) in [log eps, 0], use mapped Legendre on u | |
| # We'll implement basis eval in u by mapping to x in [-1,1] and using orthonormal Legendre. | |
| u0 = float(np.log(cfg.epsilon)) | |
| L = -u0 | |
| u_val = float(np.log(g)) | |
| x = (2.0 * u_val - u0) / L # in [-1,1] | |
| # compute orthonormal Legendre phi_n(x)=sqrt((2n+1)/2)P_n(x), then scale by sqrt(2/L) for uniform-u | |
| # reuse shifted_legendre_orthonormal_vals by mapping: shifted on [0,1] isn't convenient; do direct P_n. | |
| # We'll do direct recurrence for P_n at scalar x. | |
| xj = jnp.array(x, dtype=jnp.float32) | |
| P = [jnp.array(1.0, dtype=jnp.float32)] | |
| if cfg.m >= 2: | |
| P.append(xj) | |
| for k in range(1, cfg.m - 1): | |
| Pkp1 = ((2 * k + 1) * xj * P[k] - k * P[k - 1]) / (k + 1) | |
| P.append(Pkp1) | |
| P = jnp.stack(P[:cfg.m], axis=0) # (m,) | |
| phi_scale = jnp.sqrt((2.0 * jnp.arange(cfg.m, dtype=jnp.float32) + 1.0) / 2.0) | |
| phi = P * phi_scale | |
| q_u = jnp.sqrt(2.0 / L) * phi # (m,) | |
| coeff_msg = (unvecT(s_msg_T, cfg.n, cfg.m) @ q_u).astype(jnp.float32) | |
| # MSEs | |
| mse_ms = mse_for_coeffs_over_lag(signal, coeff_ms, lag) | |
| mse_msj = mse_for_coeffs_over_lag(signal, coeff_msj, lag) | |
| mse_msg = mse_for_coeffs_over_lag(signal, coeff_msg, lag) | |
| ms_sum = ms_sum.at[li].add(mse_ms) | |
| msj_sum = msj_sum.at[li].add(mse_msj) | |
| msg_sum = msg_sum.at[li].add(mse_msg) | |
| ms_sum_sq = ms_sum_sq.at[li].add(mse_ms**2) | |
| msj_sum_sq = msj_sum_sq.at[li].add(mse_msj**2) | |
| msg_sum_sq = msg_sum_sq.at[li].add(mse_msg**2) | |
| mse_oracle = oracle_mse_for_lag(signal, lag) | |
| oracle_sum = oracle_sum.at[li].add(mse_oracle) | |
| oracle_sum_sq = oracle_sum_sq.at[li].add(mse_oracle ** 2) | |
| # Regular HiPPO systems | |
| for hi in range(len(cfg.hippo_base_timescales)): | |
| tau = float(cfg.hippo_base_timescales[hi]) | |
| coeff_hp = s_hp_T[hi].astype(jnp.float32) # (n,) | |
| mse_hp = mse_for_coeffs_over_lag_regular(signal, coeff_hp, tau=tau, lag=lag) | |
| hp_sum = hp_sum.at[hi, li].add(mse_hp) | |
| hp_sum_sq = hp_sum_sq.at[hi, li].add(mse_hp**2) | |
| num_iters = float(cfg.num_iters) | |
| ms_avg_mses = ms_sum / num_iters | |
| msj_avg_mses = msj_sum / num_iters | |
| msg_avg_mses = msg_sum / num_iters | |
| hp_avg_mses = hp_sum / num_iters | |
| ms_sem = jnp.sqrt(jnp.maximum(ms_sum_sq / num_iters - ms_avg_mses**2, 0.0) / num_iters) | |
| msj_sem = jnp.sqrt(jnp.maximum(msj_sum_sq / num_iters - msj_avg_mses**2, 0.0) / num_iters) | |
| msg_sem = jnp.sqrt(jnp.maximum(msg_sum_sq / num_iters - msg_avg_mses**2, 0.0) / num_iters) | |
| hp_sem = jnp.sqrt(jnp.maximum(hp_sum_sq / num_iters - hp_avg_mses**2, 0.0) / num_iters) | |
| oracle_avg_mses = oracle_sum / num_iters | |
| oracle_sem = jnp.sqrt(jnp.maximum(oracle_sum_sq / num_iters - oracle_avg_mses**2, 0.0) / num_iters) | |
| return np.array(lags), ( | |
| np.array(ms_avg_mses), | |
| np.array(msj_avg_mses), | |
| np.array(msg_avg_mses), | |
| np.array(hp_avg_mses), | |
| np.array(ms_sem), | |
| np.array(msj_sem), | |
| np.array(msg_sem), | |
| np.array(hp_sem), | |
| np.array(oracle_avg_mses), | |
| np.array(oracle_sem), | |
| ), example | |
| # ---------------------------- | |
| # 6) Plot | |
| # ---------------------------- | |
| def save_mse_npz(out, filepath: str = "mse_results.npz") -> None: | |
| lags, (ms_mse, msj_mse, msg_mse, hp_mse, ms_sem, msj_sem, msg_sem, hp_sem, oracle_mse, oracle_sem), example = out | |
| save_kwargs = dict( | |
| lags=np.array(lags), | |
| ms_mse=np.array(ms_mse), | |
| msj_mse=np.array(msj_mse), | |
| msg_mse=np.array(msg_mse), | |
| hp_mse=np.array(hp_mse), | |
| ms_sem=np.array(ms_sem), | |
| msj_sem=np.array(msj_sem), | |
| msg_sem=np.array(msg_sem), | |
| hp_sem=np.array(hp_sem), | |
| oracle_mse=np.array(oracle_mse), | |
| oracle_sem=np.array(oracle_sem), | |
| ) | |
| if example is not None: | |
| for k, v in example.items(): | |
| save_kwargs[f"example_{k}"] = np.array(v) | |
| np.savez(filepath, **save_kwargs) | |
| print(f"Saved MSE data to {filepath}") | |
| def load_mse_npz(filepath: str = "mse_results.npz"): | |
| data = np.load(filepath) | |
| lags = data["lags"] | |
| ms_mse = data["ms_mse"] | |
| msj_mse = data["msj_mse"] | |
| msg_mse = data["msg_mse"] | |
| hp_mse = data["hp_mse"] | |
| ms_sem = data["ms_sem"] | |
| msj_sem = data["msj_sem"] | |
| msg_sem = data["msg_sem"] | |
| hp_sem = data["hp_sem"] | |
| oracle_mse = data["oracle_mse"] | |
| oracle_sem = data["oracle_sem"] | |
| example_keys = ["signal", "s_ms_T", "s_msj_T", "s_msg_T", "s_hp_T"] | |
| if all(f"example_{k}" in data for k in example_keys): | |
| example = {k: data[f"example_{k}"] for k in example_keys} | |
| else: | |
| example = None | |
| out = ( | |
| lags, | |
| (ms_mse, msj_mse, msg_mse, hp_mse, | |
| ms_sem, msj_sem, msg_sem, hp_sem, | |
| oracle_mse, oracle_sem), | |
| example, | |
| ) | |
| return out | |
| def plot_combined( | |
| out, | |
| cfg: Config, | |
| plot_reconstructions: bool = True, | |
| plot_other_multiscale: bool = False, | |
| filename: str = "multiscale_figure.png", | |
| ): | |
| lags, (ms_mse, msj_mse, msg_mse, hp_mse, ms_sem, msj_sem, msg_sem, hp_sem, oracle_mse, oracle_sem), example = out | |
| if plot_reconstructions and example is None: | |
| raise ValueError("Expected example data for reconstruction plot.") | |
| multiscale_colors = { | |
| "c": "tab:orange", | |
| "jeffreys": "tab:green", | |
| "log_g": "tab:red", | |
| } | |
| hippo_colors = plt.cm.Blues(np.linspace(0.35, 0.85, len(cfg.hippo_base_timescales))) | |
| if plot_reconstructions: | |
| fig, (ax_example, ax_mse) = plt.subplots(2, 1, figsize=(5.5, 5)) | |
| else: | |
| fig, ax_mse = plt.subplots(figsize=(8,3)) | |
| if plot_reconstructions: | |
| signal = jnp.asarray(example["signal"]) | |
| s_msg_T = jnp.asarray(example["s_msg_T"]) | |
| s_hp_T = jnp.asarray(example["s_hp_T"]) | |
| Tlast = int(signal.shape[0] - 1) | |
| horizons = [30, 100, 200, 300, 1000] | |
| max_horizon = 300 | |
| t_plot = jnp.arange(Tlast - max_horizon, Tlast + 1, dtype=jnp.float32) | |
| def s_from_time(t, horizon): | |
| return (Tlast - t) / horizon | |
| def log_g_coeffs_for_horizon(horizon: int) -> jax.Array: | |
| g = float(cfg.base_timescale) / float(horizon) | |
| g = float(np.clip(g, float(cfg.epsilon), 1.0)) | |
| u0 = float(np.log(cfg.epsilon)) | |
| L = -u0 | |
| u_val = float(np.log(g)) | |
| x = (2.0 * u_val - u0) / L | |
| xj = jnp.array(x, dtype=jnp.float32) | |
| P0 = jnp.array(1.0, dtype=jnp.float32) | |
| Ps = [P0] | |
| if cfg.m >= 2: | |
| Ps.append(xj) | |
| for k in range(1, cfg.m - 1): | |
| Ps.append(((2 * k + 1) * xj * Ps[k] - k * Ps[k - 1]) / (k + 1)) | |
| P = jnp.stack(Ps[:cfg.m], axis=0) | |
| phi_scale = jnp.sqrt((2.0 * jnp.arange(cfg.m, dtype=jnp.float32) + 1.0) / 2.0) | |
| phi = P * phi_scale | |
| q_u = jnp.sqrt(2.0 / L) * phi | |
| return (unvecT(s_msg_T, cfg.n, cfg.m) @ q_u).astype(jnp.float32) | |
| t_np = np.array(t_plot).astype(np.int32) | |
| t_fig = t_np - np.max(t_np) | |
| true_signal = np.array(signal)[t_np] | |
| ax_example.plot(t_fig, true_signal, color="k", alpha=0.7, linewidth=1.5, label="True signal") | |
| red_shades = plt.cm.Reds(np.linspace(0.35, 0.9, len(horizons))) | |
| for horizon, color in zip(horizons, red_shades): | |
| s_win = s_from_time(t_plot, float(horizon)) | |
| valid = (s_win >= 0.0) & (s_win <= 1.0) | |
| # Make a safe s for basis evaluation (anything inside [0,1] works for invalid points) | |
| s_safe = jnp.where(valid, s_win, 0.0) | |
| L_plot = shifted_legendre_orthonormal_vals(1.0 - s_safe, cfg.n).astype(jnp.float32) | |
| coeff_msg = log_g_coeffs_for_horizon(horizon) | |
| y_msg = L_plot @ coeff_msg | |
| # Now mask for plotting (NaNs break the line outside valid) | |
| y_msg = jnp.where(valid, y_msg, jnp.nan) | |
| label = "Multiscale HiPPO" if horizon == 200 else None | |
| ax_example.plot( | |
| t_fig, | |
| y_msg, | |
| color=color, | |
| alpha=0.9, | |
| lw=2.0, | |
| label=label, | |
| ) | |
| for hi, tau in enumerate(cfg.hippo_base_timescales): | |
| if tau < 100 or tau > 1000: | |
| continue | |
| s_raw = (Tlast - t_plot) / tau | |
| valid = (s_raw >= 0.0) & (s_raw <= 1.0) | |
| s_eval = (1.0 - s_raw) | |
| L_hp = shifted_legendre_orthonormal_vals(jnp.clip(s_eval, 0.0, 1.0), cfg.n) | |
| y_hp = L_hp @ s_hp_T[hi] | |
| y_hp = np.where(np.array(valid), np.array(y_hp), np.nan) | |
| label = "Vanilla HiPPOs" if tau == 100 else None | |
| ax_example.plot( | |
| t_fig, | |
| y_hp, | |
| linestyle="--", | |
| alpha=0.9, | |
| lw=2.0, | |
| color=hippo_colors[hi], | |
| label=label, | |
| ) | |
| ax_example.set_xlabel("Relative Time") | |
| ax_example.set_ylabel("Signal") | |
| ax_example.set_ylim(-1.3, 3) | |
| ax_example.set_title("Signal Reconstruction") | |
| ax_example.grid(True, alpha=0.25) | |
| ax_example.legend(frameon=False, ncol=1) | |
| if plot_other_multiscale: | |
| ax_mse.plot( | |
| lags, | |
| ms_mse, | |
| color=multiscale_colors["c"], | |
| label="Multiscale (Basic)", | |
| ) | |
| ax_mse.fill_between( | |
| lags, | |
| ms_mse - ms_sem, | |
| ms_mse + ms_sem, | |
| color=multiscale_colors["c"], | |
| alpha=0.2, | |
| linewidth=0, | |
| ) | |
| ax_mse.plot( | |
| lags, | |
| msj_mse, | |
| color=multiscale_colors["jeffreys"], | |
| label="Multiscale (Jeffreys-style)", | |
| ) | |
| ax_mse.fill_between( | |
| lags, | |
| msj_mse - msj_sem, | |
| msj_mse + msj_sem, | |
| color=multiscale_colors["jeffreys"], | |
| alpha=0.2, | |
| linewidth=0, | |
| ) | |
| ax_mse.plot( | |
| lags, | |
| msg_mse, | |
| color=multiscale_colors["log_g"], | |
| # label="Multiscale (Log-Timescale)", | |
| ) | |
| ax_mse.fill_between( | |
| lags, | |
| msg_mse - msg_sem, | |
| msg_mse + msg_sem, | |
| color=multiscale_colors["log_g"], | |
| alpha=0.2, | |
| linewidth=0, | |
| ) | |
| if plot_other_multiscale: | |
| ax_mse.legend(frameon=False, ncol=1) | |
| for hi, tau in enumerate(cfg.hippo_base_timescales): | |
| ax_mse.plot( | |
| lags, | |
| hp_mse[hi], | |
| linestyle="--", | |
| color=hippo_colors[hi], | |
| # label=f"HiPPO (base={tau:g})", | |
| ) | |
| ax_mse.fill_between( | |
| lags, | |
| hp_mse[hi] - hp_sem[hi], | |
| hp_mse[hi] + hp_sem[hi], | |
| color=hippo_colors[hi], | |
| alpha=0.2, | |
| linewidth=0, | |
| ) | |
| ax_mse.plot( | |
| lags, | |
| oracle_mse, | |
| color="gray", | |
| linestyle="-", | |
| linewidth=1.5, | |
| ) | |
| ax_mse.fill_between( | |
| lags, | |
| oracle_mse - oracle_sem, | |
| oracle_mse + oracle_sem, | |
| color="gray", | |
| alpha=0.2, | |
| linewidth=0, | |
| ) | |
| custom_lines = [ | |
| Line2D([0], [0], color="tab:blue", lw=2.0, linestyle="--"), | |
| Line2D([0], [0], color="tab:red", lw=2.0), | |
| Line2D([0], [0], color="gray", lw=1.5), | |
| ] | |
| ax_mse.legend( | |
| custom_lines, | |
| ["Vanilla HiPPOs", "Multiscale HiPPO", "Oracle Error"], | |
| loc="lower right", | |
| frameon=False, | |
| ) | |
| ax_mse.set_xscale("log") | |
| ax_mse.set_ylim(0, 1.06) | |
| ax_mse.set_xlabel("Horizon") | |
| ax_mse.set_ylabel("Average MSE") | |
| ax_mse.set_title("Reconstruction error from 16 Legendre coeffs") | |
| ax_mse.grid(True, which="both", alpha=0.25) | |
| fig.tight_layout() | |
| fig.savefig(filename, dpi=200) | |
| fig.savefig("multiscale_figure.pdf") | |
| print(f"Saved combined plot to {filename}") | |
| if __name__ == "__main__": | |
| # cfg = Config() | |
| # out = run(seed=0) | |
| # save_mse_npz(out) | |
| # plot_combined(out, cfg, plot_reconstructions=True, plot_other_multiscale=False) | |
| cfg = Config() | |
| out = load_mse_npz() | |
| plot_combined(out, cfg, plot_reconstructions=True, plot_other_multiscale=False) | |