ProCreations's picture
Publish validated HiPPO Zoo reproduction nB0TrIRAs1
1cd8a52 verified
Raw
History Blame Contribute Delete
12 kB
"""Orthogonal polynomial utilities for building multiscale systems."""
from __future__ import annotations
from typing import Callable, Tuple
import jax
from jax import jit, vmap, lax
import jax.numpy as jnp
import numpy as np
from numpy.polynomial.legendre import legval as np_legval
from scipy.special import eval_legendre
SystemParams = Tuple[np.ndarray, ...]
EvalFn = Callable[..., np.ndarray]
def get_system_params(
measure: str,
n: int,
m: int | None = None,
*,
multiscale: bool = False,
implicit: bool = False,
**measure_args,
) -> Tuple[SystemParams, EvalFn, float]:
if multiscale:
if m is None:
raise ValueError("Multiscale systems require `m`")
return _get_multiscale_system_params(
measure, n, m, implicit=implicit, **measure_args
)
return _transition(measure, n, implicit=implicit, **measure_args)
def get_output_vector(measure: str, n: int, fourier_overlap: float = 0.99) -> np.ndarray:
if measure == "legt":
return np.sqrt(2 * np.arange(n) + 1.0) * np.power(-1, np.arange(n))
if measure == "fout":
B = np.zeros(n)
m = 2 * np.pi * np.arange(n // 2)
B[0::2] = np.sqrt(2.0) * np.cos(m * fourier_overlap)
B[1::2] = np.sqrt(2.0) * np.sin(m * fourier_overlap)
B[0] = 1.0
return B
raise NotImplementedError(measure)
def _transition(
measure: str,
N: int,
*,
implicit: bool = False,
**measure_args,
) -> Tuple[SystemParams, EvalFn, float]:
if measure == "lagt":
if implicit:
raise ValueError("Laguerre systems are not implemented in implicit form")
b = measure_args.get("beta", 1.0)
A = np.eye(N) / 2 - np.tril(np.ones((N, N)))
B = b * np.ones((N, 1))
eval_func = legval
tmx_max = 1.0
elif measure == "fout":
freqs = 2 * np.arange(N // 2)
d = np.stack([np.zeros(N // 2), freqs], axis=-1).reshape(-1)[1:]
A = np.pi * (-np.diag(d, 1) + np.diag(d, -1))
B = np.zeros(N)
B[0::2] = np.sqrt(2.0)
B[0] = 1.0
A = A - 2 * B[:, None] * B[None, :]
B = 2 * B[:, None]
eval_func = fourier_val
tmx_max = 1.0
elif measure == "legt":
Q = np.arange(N, dtype=np.float64)
R = (2 * Q + 1) ** 0.5
if implicit:
a_func = get_implicit_legt_A(N)
b = R
params = (a_func, b)
else:
j, i = np.meshgrid(Q, Q)
A = R[:, None] * np.where(i < j, (-1.0) ** (i - j), 1) * R[None, :]
B = R[:, None]
A = -A
params = (A, B.flatten())
eval_func = legval
tmx_max = 1.0
elif measure == "legs":
if implicit:
raise ValueError("Scaled Legendre systems do not support implicit mode")
q = np.arange(N, dtype=np.float64)
col, row = np.meshgrid(q, q)
r = 2 * q + 1
M = -(np.where(row >= col, r, 0) - np.diag(q))
T = np.sqrt(np.diag(2 * q + 1))
A = T @ M @ np.linalg.inv(T)
B = np.diag(T)[:, None]
eval_func = legsval
tmx_max = 5.0
params = (A, B.flatten())
else:
raise NotImplementedError(measure)
if not implicit and measure != "legt":
params = (A, B.flatten())
return params, eval_func, tmx_max
def _get_multiscale_system_params(
measure: str,
n: int,
m: int,
*,
implicit: bool = False,
**measure_args,
) -> Tuple[SystemParams, EvalFn, float]:
params, eval_func, tmx_max = _transition(
measure, n, implicit=implicit, **measure_args
)
if implicit:
a_func, B = params
m_func = get_implicit_M(m)
implicit_A = vmap(a_func, in_axes=1, out_axes=1)
implicit_M = vmap(m_func, in_axes=1, out_axes=1)
B = B.reshape(-1, 1)
B = np.block([B / 2, B / (2 * np.sqrt(3)), np.zeros((n, m - 2))])
params = (implicit_A, B, implicit_M)
else:
A, B = params
M = make_m_mat(m)
B = B.reshape(-1, 1)
B = np.block([B / 2, B / (2 * np.sqrt(3)), np.zeros((n, m - 2))])
params = (A, B, M)
def ms_eval_func(tmx, taus, c):
coeffs = legval(taus, c * np.power(-1, np.arange(c.shape[1]))[None]).T
return eval_func(tmx, coeffs)
return params, ms_eval_func, tmx_max * 2
def get_L_vector(m: int, tau: float) -> np.ndarray:
res = np.zeros(m)
vec = np.sqrt(2 * np.arange(m) + 1.0)
for i in range(m):
oh = np.zeros(m)
oh[i] = 1.0
res[i] = np_legval(2 * tau - 1, vec * oh)
return res
def make_m_mat(m: int) -> np.ndarray:
out = np.eye(m)
idx = np.arange(m - 1)
vec = np.arange(1, m) / np.sqrt(
np.arange(1, 2 * m - 1, 2) * np.arange(3, 2 * m + 1, 2)
)
out[idx, idx + 1] = vec
out[idx + 1, idx] = vec
return out / 2
def get_implicit_M(m: int):
vec = jnp.arange(1, m) / jnp.sqrt(
jnp.arange(1, 2 * m - 1, 2) * jnp.arange(3, 2 * m + 1, 2)
)
@jit
def matvec(v):
out = v[:]
out = out.at[1:].add(vec * v[:-1])
out = out.at[:-1].add(vec * v[1:])
return out / 2
return matvec
def get_implicit_legt_A(n: int):
if n % 2 != 0:
raise ValueError("Implicit Legendre systems require an even order")
if n <= 1:
raise ValueError("n must be greater than 1")
sqrt_filter = jnp.sqrt(1.0 + 2.0 * jnp.arange(n))
alt_filter = (-1) ** jnp.arange(n)
@jit
def matvec(v):
temp = v * sqrt_filter
alt_cum_sum = jnp.cumsum(temp * alt_filter)
result = jnp.cumsum(temp) + (alt_cum_sum[-1] - alt_cum_sum) * alt_filter
return -result * sqrt_filter
return matvec
def legval(x: np.ndarray, c: np.ndarray) -> np.ndarray:
"""
Orthonormal Legendre polynomial evaluation
x: input values between 0 and 1.
Zero represents the present.
One represents the timescale **in the past**, as far back as the system can remember.
c: coefficient array
"""
poly_len = c.shape[-1]
batch_shape = c.shape[:-1]
idx = np.arange(poly_len)
eval_matrix = eval_legendre(idx[:, None], 2 * x - 1).T
eval_matrix *= np.sqrt(2 * idx + 1) * (-1) ** idx
eval_matrix = eval_matrix[None, ...]
result = eval_matrix * c[..., None, :]
return np.sum(result, axis=-1).reshape(batch_shape + (len(x),))
def fourier_val(x_vals: np.ndarray, coeffs: np.ndarray) -> np.ndarray:
N = coeffs.shape[-1] // 2
x_vals = x_vals[:, np.newaxis]
m = np.arange(N)[None]
two_pi_mx = 2 * np.pi * m * x_vals
terms = np.sqrt(2.0) * np.exp(1j * two_pi_mx)
terms[:, :1] /= np.sqrt(2.0)
cos_terms = terms.real
sin_terms = terms.imag
cosine_coeffs = coeffs[..., 2 * m]
sine_coeffs = coeffs[..., 2 * m + 1]
fourier_sum = np.sum(cosine_coeffs * cos_terms + sine_coeffs * sin_terms, axis=-1)
return fourier_sum
def legsval(x: np.ndarray, c: np.ndarray) -> np.ndarray:
poly_len = c.shape[-1]
batch_shape = c.shape[:-1]
idx = np.arange(poly_len)
eval_matrix = eval_legendre(idx[:, None], 1 - 2 * np.exp(-x)).T
eval_matrix *= np.sqrt(2 * idx + 1) * (-1) ** idx
eval_matrix = eval_matrix[None, ...]
result = eval_matrix * c[..., None, :]
return np.sum(result, axis=-1).reshape(batch_shape + (len(x),))
def _legendre_series_eval_single_z(z: jnp.ndarray, c: jnp.ndarray) -> jnp.ndarray:
"""
Evaluate sum_k c[k] * sqrt(2k+1) * (-1)^k * P_k(z) for a single scalar z.
z: scalar in [-1, 1] (ideally)
c: shape (N,)
returns: scalar
"""
N = c.shape[0]
k = jnp.arange(N, dtype=c.dtype)
weights = jnp.sqrt(2.0 * k + 1.0) * jnp.power(-1.0, k)
a = c * weights
# N == 1
def case_N1():
return a[0]
# N == 2
def case_N2():
return a[0] + a[1] * z
# N >= 3
def case_Nge3():
P0 = jnp.ones_like(z)
P1 = z
out0 = a[0] * P0 + a[1] * P1
# iterate k_int = 1..N-2 to build P_{k+1}
ks = jnp.arange(1, N - 1, dtype=jnp.int32)
def step(carry, k_int):
Pkm1, Pk, out = carry
kf = k_int.astype(c.dtype) # float for recurrence math
Pkp1 = ((2.0 * kf + 1.0) * z * Pk - kf * Pkm1) / (kf + 1.0)
out = out + a[k_int + 1] * Pkp1
return (Pk, Pkp1, out), None
(_, _, out), _ = lax.scan(step, (P0, P1, out0), ks)
return out
return lax.cond(
N == 1,
case_N1,
lambda: lax.cond(N == 2, case_N2, case_Nge3),
)
# vmap over z, and over batch dims of c
_eval_over_z = jax.vmap(_legendre_series_eval_single_z, in_axes=(0, None), out_axes=0)
def legval_jax(x: jnp.ndarray, c: jnp.ndarray) -> jnp.ndarray:
"""
JAX version of :func:`legval`.
x: shape (T,), ideally in [0,1]
c: shape (..., N)
returns: shape (..., T)
"""
# Map x in [0,1] to z in [-1,1]
z = 2.0 * x - 1.0
# Flatten batch dims so we can vmap cleanly
batch_shape = c.shape[:-1]
N = c.shape[-1]
c2 = c.reshape((-1, N)) # (B, N)
def eval_one_coeff_row(ci):
return _eval_over_z(z, ci) # (T,)
y2 = jax.vmap(eval_one_coeff_row, in_axes=0, out_axes=0)(c2) # (B, T)
return y2.reshape(batch_shape + (x.shape[0],))
def legsval_jax(x: jnp.ndarray, c: jnp.ndarray) -> jnp.ndarray:
"""
JAX version of :func:`legsval`.
x: shape (T,), typically x>=0
c: shape (..., N)
returns: shape (..., T)
"""
# Stable computation of z = 1 - 2*exp(-x)
# exp(-x) = 1 + expm1(-x) => z = 1 - 2*(1 + expm1(-x)) = -1 - 2*expm1(-x)
z = -1.0 - 2.0 * jnp.expm1(-x)
batch_shape = c.shape[:-1]
N = c.shape[-1]
c2 = c.reshape((-1, N)) # (B, N)
def eval_one_coeff_row(ci):
return _eval_over_z(z, ci) # (T,)
y2 = jax.vmap(eval_one_coeff_row, in_axes=0, out_axes=0)(c2) # (B, T)
return y2.reshape(batch_shape + (x.shape[0],))
__all__ = [
"EvalFn",
"SystemParams",
"fourier_val",
"get_L_vector",
"get_output_vector",
"get_system_params",
"get_implicit_M",
"get_implicit_legt_A",
"legval",
"legsval",
"legval_jax",
"legsval_jax",
"make_m_mat",
]
if __name__ == "__main__":
# -----------------------
# Config
# -----------------------
key = jax.random.PRNGKey(0)
T = 50 # number of evaluation points
N = 32 # polynomial order
batch_shape = (4, 3) # arbitrary batch dims
# -----------------------
# Generate test inputs
# -----------------------
# x in [0,1] for legval
key, sub = jax.random.split(key)
x_leg = jax.random.uniform(sub, (T,), minval=0.0, maxval=1.0)
# x >= 0 for legsval
key, sub = jax.random.split(key)
x_legs = jax.random.exponential(sub, (T,))
# coefficients
key, sub = jax.random.split(key)
c = jax.random.normal(sub, batch_shape + (N,))
# Convert to NumPy for reference implementations
x_leg_np = np.array(x_leg)
x_legs_np = np.array(x_legs)
c_np = np.array(c)
# -----------------------
# Evaluate
# -----------------------
y_leg_np = legval(x_leg_np, c_np)
y_leg_jax = legval_jax(x_leg, c)
y_legs_np = legsval(x_legs_np, c_np)
y_legs_jax = legsval_jax(x_legs, c)
# -----------------------
# Errors
# -----------------------
leg_abs_err = np.max(np.abs(y_leg_np - np.array(y_leg_jax)))
leg_rel_err = leg_abs_err / (np.max(np.abs(y_leg_np)) + 1e-12)
legs_abs_err = np.max(np.abs(y_legs_np - np.array(y_legs_jax)))
legs_rel_err = legs_abs_err / (np.max(np.abs(y_legs_np)) + 1e-12)
# -----------------------
# Report
# -----------------------
print("legval vs legval_jax")
print(f" max abs error: {leg_abs_err:.3e}")
print(f" max rel error: {leg_rel_err:.3e}")
print("\nlegsval vs legsval_jax")
print(f" max abs error: {legs_abs_err:.3e}")
print(f" max rel error: {legs_rel_err:.3e}")