Update LISAx.py
Browse files
LISAx.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
| 1 |
# LISA.py
|
| 2 |
# ============================================================
|
| 3 |
-
# LISA: NLSA encoder + GPLM
|
| 4 |
-
# + dense in-context Gaussian Process Regression (GPR)
|
| 5 |
# on residuals in diffusion coordinate space.
|
| 6 |
#
|
| 7 |
-
# This version
|
| 8 |
-
# -
|
| 9 |
-
#
|
| 10 |
-
# -
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
# ============================================================
|
| 12 |
|
| 13 |
from __future__ import annotations
|
|
@@ -29,12 +34,9 @@ except Exception:
|
|
| 29 |
from NLSA import NLSA # type: ignore
|
| 30 |
|
| 31 |
try:
|
| 32 |
-
from .
|
| 33 |
except Exception:
|
| 34 |
-
|
| 35 |
-
from GPLM import GPLM # type: ignore
|
| 36 |
-
except Exception:
|
| 37 |
-
from gplm import GPLM # type: ignore
|
| 38 |
|
| 39 |
|
| 40 |
# ============================================================
|
|
@@ -95,65 +97,25 @@ def _estimate_rbf_ell_from_d2(
|
|
| 95 |
return float(max(ell, min_ell))
|
| 96 |
|
| 97 |
|
| 98 |
-
def _auto_gplm_m(
|
| 99 |
-
n_train: int,
|
| 100 |
-
*,
|
| 101 |
-
budget: int = 1000**3,
|
| 102 |
-
m_min: int = 32,
|
| 103 |
-
m_max: Optional[int] = None,
|
| 104 |
-
) -> int:
|
| 105 |
-
"""
|
| 106 |
-
Choose inducing count m for GPLM given training size n_train,
|
| 107 |
-
constrained by an approximate compute budget.
|
| 108 |
-
|
| 109 |
-
GPLM training costs (dominant terms):
|
| 110 |
-
- Accumulating G = C^T C: O(n_train * m^2)
|
| 111 |
-
- Cholesky on A (m x m): O(m^3)
|
| 112 |
-
|
| 113 |
-
We choose:
|
| 114 |
-
m <= budget^(1/3) (controls m^3)
|
| 115 |
-
m <= sqrt(budget/n) (controls n*m^2)
|
| 116 |
-
|
| 117 |
-
and of course:
|
| 118 |
-
m <= n_train
|
| 119 |
-
m >= m_min
|
| 120 |
-
|
| 121 |
-
This yields:
|
| 122 |
-
- n=1000 -> m=1000 (exact within budget)
|
| 123 |
-
- n=5000 -> m≈447
|
| 124 |
-
- n=50000 -> m≈141
|
| 125 |
-
"""
|
| 126 |
-
n = int(max(1, n_train))
|
| 127 |
-
b = int(max(1, budget))
|
| 128 |
-
|
| 129 |
-
m_cubic = int(np.floor(b ** (1.0 / 3.0))) # budget for m^3
|
| 130 |
-
m_nm2 = int(np.floor(np.sqrt(b / float(n)))) # budget for n*m^2
|
| 131 |
-
|
| 132 |
-
m = min(n, m_cubic, m_nm2)
|
| 133 |
-
m = max(int(m_min), int(m))
|
| 134 |
-
|
| 135 |
-
if m_max is not None:
|
| 136 |
-
m = min(m, int(m_max))
|
| 137 |
-
|
| 138 |
-
# final sanity
|
| 139 |
-
m = int(max(1, min(m, n)))
|
| 140 |
-
return m
|
| 141 |
-
|
| 142 |
-
|
| 143 |
# ============================================================
|
| 144 |
# LISA main class
|
| 145 |
# ============================================================
|
| 146 |
|
| 147 |
class LISA:
|
| 148 |
"""
|
| 149 |
-
LISA: NLSA encoder + GPLM baseline + dense GPR residual correction.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
-
|
| 152 |
-
|
|
|
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
"""
|
| 158 |
|
| 159 |
def __init__(
|
|
@@ -171,11 +133,6 @@ class LISA:
|
|
| 171 |
nlsa_kwargs: Optional[dict] = None,
|
| 172 |
# ------------------ GPLM baseline decoder hyperparams ----------
|
| 173 |
gplm_kwargs: Optional[dict] = None,
|
| 174 |
-
# --- NEW: auto anchor/inducing scaling for large N -------------
|
| 175 |
-
gplm_auto_m: bool = True,
|
| 176 |
-
gplm_budget: int = 1000**3, # your requested budget baseline
|
| 177 |
-
gplm_m_min: int = 32,
|
| 178 |
-
gplm_m_max: Optional[int] = None,
|
| 179 |
# ------------------ IC / GPR controls -------------------------
|
| 180 |
ctx_min_windows: Optional[int] = None,
|
| 181 |
ctx_k0: float = 10.0, # base mixing K_ctx/(K_ctx+ctx_k0)
|
|
@@ -183,8 +140,8 @@ class LISA:
|
|
| 183 |
gp_kernel: str = "rbf", # "rbf" or "linear"
|
| 184 |
gp_rbf_ell: float | None = None, # fixed, or auto from context
|
| 185 |
gp_rbf_q: float = 0.5,
|
| 186 |
-
# --- NEW:
|
| 187 |
-
gp_max_ctx: Optional[int] = 4096, #
|
| 188 |
gp_ctx_mode: str = "recent", # "recent" or "uniform"
|
| 189 |
# ------------------ stability / trust gating -----------------
|
| 190 |
use_var_gate: bool = True,
|
|
@@ -219,7 +176,7 @@ class LISA:
|
|
| 219 |
self.D = int(base.D_)
|
| 220 |
self.r = int(base.psi_.shape[1])
|
| 221 |
|
| 222 |
-
# mean handling
|
| 223 |
self.center_outputs = bool(base.center)
|
| 224 |
self.mu_X = base.mu_.reshape(-1).astype(np.float64) if self.center_outputs else np.zeros((self.D,), dtype=np.float64)
|
| 225 |
|
|
@@ -235,32 +192,46 @@ class LISA:
|
|
| 235 |
dtype=np.float64
|
| 236 |
) # (K-1,D) centered if base.center
|
| 237 |
|
|
|
|
| 238 |
gkw = dict(gplm_kwargs)
|
| 239 |
gkw.setdefault("seed", int(seed))
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
gkw.setdefault("jitter", 1e-8)
|
| 243 |
-
gkw.setdefault("inducing", "kmeans_medoids")
|
| 244 |
-
gkw.setdefault("dtype", np.float32)
|
| 245 |
-
gkw.setdefault("fit_block", 8192)
|
| 246 |
-
|
| 247 |
-
# ------------------------------------------------------------
|
| 248 |
-
# NEW: automatic inducing m scaling (for N up to 50k+)
|
| 249 |
-
# ------------------------------------------------------------
|
| 250 |
-
if "m" not in gkw:
|
| 251 |
-
if bool(gplm_auto_m):
|
| 252 |
-
m_auto = _auto_gplm_m(
|
| 253 |
-
X_train.shape[0],
|
| 254 |
-
budget=int(gplm_budget),
|
| 255 |
-
m_min=int(gplm_m_min),
|
| 256 |
-
m_max=gplm_m_max,
|
| 257 |
-
)
|
| 258 |
-
gkw["m"] = int(m_auto)
|
| 259 |
-
else:
|
| 260 |
-
gkw["m"] = min(1024, X_train.shape[0])
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
self.gplm = GPLM(X_train, Y_train_c, **gkw)
|
| 266 |
|
|
@@ -278,7 +249,7 @@ class LISA:
|
|
| 278 |
self.gp_rbf_q = float(gp_rbf_q)
|
| 279 |
self.last_gp_rbf_ell_: Optional[float] = None
|
| 280 |
|
| 281 |
-
#
|
| 282 |
self.gp_max_ctx = None if gp_max_ctx is None else int(gp_max_ctx)
|
| 283 |
self.gp_ctx_mode = str(gp_ctx_mode).lower().strip()
|
| 284 |
if self.gp_ctx_mode not in ("recent", "uniform"):
|
|
@@ -315,7 +286,7 @@ class LISA:
|
|
| 315 |
Psi = np.asarray(Psi_Br, dtype=np.float64)
|
| 316 |
if Psi.ndim == 1:
|
| 317 |
Psi = Psi[None, :]
|
| 318 |
-
Yc = self.gplm(Psi) # (B,D)
|
| 319 |
return np.asarray(Yc, dtype=np.float64)
|
| 320 |
|
| 321 |
def _baseline_centered_one(self, psi_r: np.ndarray) -> np.ndarray:
|
|
@@ -335,7 +306,6 @@ class LISA:
|
|
| 335 |
if self.gp_kernel == "linear":
|
| 336 |
return Psi_ctx @ Psi_ctx.T, None
|
| 337 |
|
| 338 |
-
# RBF
|
| 339 |
d2 = _pairwise_sq_dists(Psi_ctx)
|
| 340 |
if self.gp_rbf_ell is None:
|
| 341 |
ell_used = _estimate_rbf_ell_from_d2(d2, q=self.gp_rbf_q)
|
|
@@ -382,7 +352,6 @@ class LISA:
|
|
| 382 |
|
| 383 |
if self.gate_mode == "exp":
|
| 384 |
return float(np.exp(-v / tau2))
|
| 385 |
-
# rational
|
| 386 |
return float(tau2 / (tau2 + v))
|
| 387 |
|
| 388 |
# ============================================================
|
|
@@ -417,22 +386,8 @@ class LISA:
|
|
| 417 |
"""
|
| 418 |
Autoregressive forecast from prefix (ℓ,D), ℓ >= L.
|
| 419 |
|
| 420 |
-
If ℓ == L
|
| 421 |
-
If ℓ > L: uses in-context
|
| 422 |
-
|
| 423 |
-
Parameters
|
| 424 |
-
----------
|
| 425 |
-
prefix : (ℓ,D)
|
| 426 |
-
steps : horizon H
|
| 427 |
-
return_var : return scalar GP function variance per step (H,)
|
| 428 |
-
sample : sample GP residual instead of mean (generative mode)
|
| 429 |
-
rng : RNG for sampling
|
| 430 |
-
include_obs_noise : if sample=True, draw using var_y = var_f + gp_noise2
|
| 431 |
-
|
| 432 |
-
Returns
|
| 433 |
-
-------
|
| 434 |
-
preds : (H,D) (or (D,) if H==1)
|
| 435 |
-
vars : (H,) if return_var=True
|
| 436 |
"""
|
| 437 |
prefix = _as_2d(prefix)
|
| 438 |
ell, D = prefix.shape
|
|
@@ -469,7 +424,7 @@ class LISA:
|
|
| 469 |
return preds[0] if H == 1 else preds
|
| 470 |
|
| 471 |
# ---------------------------
|
| 472 |
-
# Dense GP context cap (
|
| 473 |
# ---------------------------
|
| 474 |
if self.gp_max_ctx is None:
|
| 475 |
K_ctx = int(K_ctx_full)
|
|
@@ -481,17 +436,16 @@ class LISA:
|
|
| 481 |
start = int(K_ctx_full - K_ctx)
|
| 482 |
ctx_idx = None
|
| 483 |
else:
|
| 484 |
-
# uniform subsample of context indices
|
| 485 |
ctx_idx = np.linspace(0, K_ctx_full - 1, num=K_ctx, dtype=np.int64)
|
| 486 |
-
start = 0
|
| 487 |
|
| 488 |
# ---------------------------
|
| 489 |
# Build context windows + targets
|
| 490 |
# ---------------------------
|
| 491 |
-
W_all = _sliding_windows(prefix, self.L)
|
| 492 |
|
| 493 |
if ctx_idx is None:
|
| 494 |
-
W_ctx = np.ascontiguousarray(W_all[start : start + K_ctx, :, :])
|
| 495 |
Y_ctx = np.asarray(prefix[self.L + start : self.L + start + K_ctx, :], dtype=np.float64)
|
| 496 |
else:
|
| 497 |
W_ctx = np.ascontiguousarray(W_all[ctx_idx, :, :])
|
|
@@ -501,26 +455,24 @@ class LISA:
|
|
| 501 |
Y_ctx_c = (Y_ctx - self.mu_X[None, :]) if self.center_outputs else Y_ctx
|
| 502 |
|
| 503 |
# encode ψ for context
|
| 504 |
-
Psi_ctx = self._encode_batch(W_ctx)
|
| 505 |
|
| 506 |
# baseline predictions on context
|
| 507 |
-
Y_glob_ctx_c = self._baseline_centered_batch(Psi_ctx)
|
| 508 |
|
| 509 |
# residual table
|
| 510 |
-
E_ctx = Y_ctx_c - Y_glob_ctx_c
|
| 511 |
|
| 512 |
# ---------------------------
|
| 513 |
# Fit dense GP on residuals
|
| 514 |
# ---------------------------
|
| 515 |
-
K_mat, ell_used = self._gp_kernel_matrix(Psi_ctx)
|
| 516 |
K_reg = K_mat + self.gp_noise2 * np.eye(K_ctx, dtype=np.float64)
|
| 517 |
|
| 518 |
-
# Cholesky factorization
|
| 519 |
cf = cho_factor(K_reg, lower=True, check_finite=False)
|
| 520 |
-
alpha = cho_solve(cf, E_ctx, check_finite=False)
|
| 521 |
Lfac, lower = cf
|
| 522 |
|
| 523 |
-
# base context mixing weight
|
| 524 |
w_ctx_base = float(K_ctx) / float(K_ctx + self.ctx_k0) if self.ctx_k0 > 0 else 1.0
|
| 525 |
|
| 526 |
preds = np.zeros((H, self.D), dtype=np.float64)
|
|
@@ -530,14 +482,14 @@ class LISA:
|
|
| 530 |
# AR rollout with GP residual
|
| 531 |
# ---------------------------
|
| 532 |
for h in range(H):
|
| 533 |
-
psi_q = self.base.encode_window(cur)
|
| 534 |
|
| 535 |
# baseline
|
| 536 |
-
y_glob_c = self._baseline_centered_one(psi_q)
|
| 537 |
|
| 538 |
# GP residual mean
|
| 539 |
-
k_eval = self._gp_kernel_eval(Psi_ctx, psi_q, ell_used)
|
| 540 |
-
e_mean = k_eval @ alpha
|
| 541 |
|
| 542 |
# GP residual variance (function variance)
|
| 543 |
u = solve_triangular(Lfac, k_eval, lower=lower, check_finite=False)
|
|
@@ -547,11 +499,9 @@ class LISA:
|
|
| 547 |
if return_var:
|
| 548 |
vars_out[h] = float(var_f)
|
| 549 |
|
| 550 |
-
# trust gating from variance
|
| 551 |
w_gate = self._gate_from_var(var_f)
|
| 552 |
w_eff = w_ctx_base * w_gate
|
| 553 |
|
| 554 |
-
# optionally sample residual
|
| 555 |
e_use = e_mean
|
| 556 |
if sample:
|
| 557 |
var_y = var_f + (self.gp_noise2 if include_obs_noise else 0.0)
|
|
@@ -578,7 +528,7 @@ class LISA:
|
|
| 578 |
|
| 579 |
def _rollout_baseline(self, seed_LD: np.ndarray, H: int) -> np.ndarray:
|
| 580 |
"""
|
| 581 |
-
Baseline AR rollout only: NLSA encode + GPLM decode.
|
| 582 |
"""
|
| 583 |
cur = np.asarray(seed_LD, dtype=np.float64).copy()
|
| 584 |
out = np.zeros((H, self.D), dtype=np.float64)
|
|
|
|
| 1 |
# LISA.py
|
| 2 |
# ============================================================
|
| 3 |
+
# LISA: NLSA encoder + GPLM baseline decoder
|
| 4 |
+
# + (optional) dense in-context Gaussian Process Regression (GPR)
|
| 5 |
# on residuals in diffusion coordinate space.
|
| 6 |
#
|
| 7 |
+
# This version is compatible with the "sparse-kernel KRR" GPLM.py:
|
| 8 |
+
# - GPLM trains by building a sparse kNN kernel on training latents ψ
|
| 9 |
+
# - solves (K + sigma2 I) S = Y (mean GP / KRR)
|
| 10 |
+
# - predicts using only pred_k neighbors per query
|
| 11 |
+
#
|
| 12 |
+
# Key advantages vs Nyström GPLM:
|
| 13 |
+
# - No anchors/inducing points required
|
| 14 |
+
# - Strong nonlinearity => very local sparse kernel => scalable
|
| 15 |
+
#
|
| 16 |
# ============================================================
|
| 17 |
|
| 18 |
from __future__ import annotations
|
|
|
|
| 34 |
from NLSA import NLSA # type: ignore
|
| 35 |
|
| 36 |
try:
|
| 37 |
+
from .GPLMx import GPLM # type: ignore
|
| 38 |
except Exception:
|
| 39 |
+
from GPLMx import GPLM # type: ignore
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
# ============================================================
|
|
|
|
| 97 |
return float(max(ell, min_ell))
|
| 98 |
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
# ============================================================
|
| 101 |
# LISA main class
|
| 102 |
# ============================================================
|
| 103 |
|
| 104 |
class LISA:
|
| 105 |
"""
|
| 106 |
+
LISA: NLSA encoder + GPLM baseline + optional dense GPR residual correction.
|
| 107 |
+
|
| 108 |
+
- Baseline:
|
| 109 |
+
window W (L,D) -> ψ(W) in R^r (via NLSA Nyström OOS)
|
| 110 |
+
y_glob = GPLM(ψ) in R^D
|
| 111 |
|
| 112 |
+
- In-context (optional):
|
| 113 |
+
Given prefix length ℓ > L, fit dense GP on residuals e(ψ)
|
| 114 |
+
using context windows inside prefix, then apply during rollout.
|
| 115 |
|
| 116 |
+
Note:
|
| 117 |
+
The IC GP here is dense and scales O(K_ctx^2) memory / O(K_ctx^3) time.
|
| 118 |
+
A default cap gp_max_ctx prevents accidental blowups with huge prefixes.
|
| 119 |
"""
|
| 120 |
|
| 121 |
def __init__(
|
|
|
|
| 133 |
nlsa_kwargs: Optional[dict] = None,
|
| 134 |
# ------------------ GPLM baseline decoder hyperparams ----------
|
| 135 |
gplm_kwargs: Optional[dict] = None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
# ------------------ IC / GPR controls -------------------------
|
| 137 |
ctx_min_windows: Optional[int] = None,
|
| 138 |
ctx_k0: float = 10.0, # base mixing K_ctx/(K_ctx+ctx_k0)
|
|
|
|
| 140 |
gp_kernel: str = "rbf", # "rbf" or "linear"
|
| 141 |
gp_rbf_ell: float | None = None, # fixed, or auto from context
|
| 142 |
gp_rbf_q: float = 0.5,
|
| 143 |
+
# ---- NEW safety: cap dense GP context used in __call__ -------
|
| 144 |
+
gp_max_ctx: Optional[int] = 4096, # None disables cap
|
| 145 |
gp_ctx_mode: str = "recent", # "recent" or "uniform"
|
| 146 |
# ------------------ stability / trust gating -----------------
|
| 147 |
use_var_gate: bool = True,
|
|
|
|
| 176 |
self.D = int(base.D_)
|
| 177 |
self.r = int(base.psi_.shape[1])
|
| 178 |
|
| 179 |
+
# mean handling (consistent w/ NLSA centering)
|
| 180 |
self.center_outputs = bool(base.center)
|
| 181 |
self.mu_X = base.mu_.reshape(-1).astype(np.float64) if self.center_outputs else np.zeros((self.D,), dtype=np.float64)
|
| 182 |
|
|
|
|
| 192 |
dtype=np.float64
|
| 193 |
) # (K-1,D) centered if base.center
|
| 194 |
|
| 195 |
+
# ---- Defaults tuned for sparse-kernel GPLM ----
|
| 196 |
gkw = dict(gplm_kwargs)
|
| 197 |
gkw.setdefault("seed", int(seed))
|
| 198 |
+
|
| 199 |
+
# IMPORTANT: LISA already provides centered Y, so disable GPLM centering
|
| 200 |
+
gkw.setdefault("center_X", False)
|
| 201 |
+
|
| 202 |
+
# Regularization for sparse KRR (lambda in K + lambda I)
|
| 203 |
+
# (You may tune this up if solves are noisy/unstable.)
|
| 204 |
+
gkw.setdefault("sigma2", 1e-4)
|
| 205 |
+
|
| 206 |
+
# Numerical diagonal stabilizer
|
| 207 |
gkw.setdefault("jitter", 1e-8)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
+
# Sparse kernel build on ψ_train
|
| 210 |
+
gkw.setdefault("k_graph", 64) # training graph sparsity
|
| 211 |
+
gkw.setdefault("mutual_knn", True)
|
| 212 |
+
gkw.setdefault("include_self", True)
|
| 213 |
+
gkw.setdefault("normalize_rows", False)
|
| 214 |
+
|
| 215 |
+
# eps estimation for RBF weights (median kNN distance)
|
| 216 |
+
gkw.setdefault("k_eps", 256)
|
| 217 |
+
gkw.setdefault("eps_use_kth", True)
|
| 218 |
+
gkw.setdefault("eps_mul", 1.0)
|
| 219 |
+
|
| 220 |
+
# Solver controls for (K + lambda I) S = Y
|
| 221 |
+
gkw.setdefault("solve_method", "auto")
|
| 222 |
+
gkw.setdefault("solve_tol", 1e-6)
|
| 223 |
+
gkw.setdefault("solve_maxiter", 800)
|
| 224 |
+
gkw.setdefault("solve_verbose", False)
|
| 225 |
+
|
| 226 |
+
# Inference neighbor truncation
|
| 227 |
+
gkw.setdefault("pred_k", 128)
|
| 228 |
+
|
| 229 |
+
# Latent preconditioning (optional)
|
| 230 |
+
# On ψ it sometimes helps, sometimes hurts. Leave default False unless needed.
|
| 231 |
+
gkw.setdefault("whiten_latent", False)
|
| 232 |
+
|
| 233 |
+
# dtype for ANN storage
|
| 234 |
+
gkw.setdefault("dtype", np.float32)
|
| 235 |
|
| 236 |
self.gplm = GPLM(X_train, Y_train_c, **gkw)
|
| 237 |
|
|
|
|
| 249 |
self.gp_rbf_q = float(gp_rbf_q)
|
| 250 |
self.last_gp_rbf_ell_: Optional[float] = None
|
| 251 |
|
| 252 |
+
# Dense GP context cap
|
| 253 |
self.gp_max_ctx = None if gp_max_ctx is None else int(gp_max_ctx)
|
| 254 |
self.gp_ctx_mode = str(gp_ctx_mode).lower().strip()
|
| 255 |
if self.gp_ctx_mode not in ("recent", "uniform"):
|
|
|
|
| 286 |
Psi = np.asarray(Psi_Br, dtype=np.float64)
|
| 287 |
if Psi.ndim == 1:
|
| 288 |
Psi = Psi[None, :]
|
| 289 |
+
Yc = self.gplm(Psi) # (B,D) (already centered because center_X=False + mean_X=0)
|
| 290 |
return np.asarray(Yc, dtype=np.float64)
|
| 291 |
|
| 292 |
def _baseline_centered_one(self, psi_r: np.ndarray) -> np.ndarray:
|
|
|
|
| 306 |
if self.gp_kernel == "linear":
|
| 307 |
return Psi_ctx @ Psi_ctx.T, None
|
| 308 |
|
|
|
|
| 309 |
d2 = _pairwise_sq_dists(Psi_ctx)
|
| 310 |
if self.gp_rbf_ell is None:
|
| 311 |
ell_used = _estimate_rbf_ell_from_d2(d2, q=self.gp_rbf_q)
|
|
|
|
| 352 |
|
| 353 |
if self.gate_mode == "exp":
|
| 354 |
return float(np.exp(-v / tau2))
|
|
|
|
| 355 |
return float(tau2 / (tau2 + v))
|
| 356 |
|
| 357 |
# ============================================================
|
|
|
|
| 386 |
"""
|
| 387 |
Autoregressive forecast from prefix (ℓ,D), ℓ >= L.
|
| 388 |
|
| 389 |
+
If ℓ == L: baseline AR only (NLSA + sparse GPLM).
|
| 390 |
+
If ℓ > L: uses dense in-context GPR on residuals (optional).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
"""
|
| 392 |
prefix = _as_2d(prefix)
|
| 393 |
ell, D = prefix.shape
|
|
|
|
| 424 |
return preds[0] if H == 1 else preds
|
| 425 |
|
| 426 |
# ---------------------------
|
| 427 |
+
# Dense GP context cap (safety)
|
| 428 |
# ---------------------------
|
| 429 |
if self.gp_max_ctx is None:
|
| 430 |
K_ctx = int(K_ctx_full)
|
|
|
|
| 436 |
start = int(K_ctx_full - K_ctx)
|
| 437 |
ctx_idx = None
|
| 438 |
else:
|
|
|
|
| 439 |
ctx_idx = np.linspace(0, K_ctx_full - 1, num=K_ctx, dtype=np.int64)
|
| 440 |
+
start = 0
|
| 441 |
|
| 442 |
# ---------------------------
|
| 443 |
# Build context windows + targets
|
| 444 |
# ---------------------------
|
| 445 |
+
W_all = _sliding_windows(prefix, self.L) # (ell-L+1,L,D)
|
| 446 |
|
| 447 |
if ctx_idx is None:
|
| 448 |
+
W_ctx = np.ascontiguousarray(W_all[start : start + K_ctx, :, :])
|
| 449 |
Y_ctx = np.asarray(prefix[self.L + start : self.L + start + K_ctx, :], dtype=np.float64)
|
| 450 |
else:
|
| 451 |
W_ctx = np.ascontiguousarray(W_all[ctx_idx, :, :])
|
|
|
|
| 455 |
Y_ctx_c = (Y_ctx - self.mu_X[None, :]) if self.center_outputs else Y_ctx
|
| 456 |
|
| 457 |
# encode ψ for context
|
| 458 |
+
Psi_ctx = self._encode_batch(W_ctx) # (K_ctx,r)
|
| 459 |
|
| 460 |
# baseline predictions on context
|
| 461 |
+
Y_glob_ctx_c = self._baseline_centered_batch(Psi_ctx) # (K_ctx,D)
|
| 462 |
|
| 463 |
# residual table
|
| 464 |
+
E_ctx = Y_ctx_c - Y_glob_ctx_c # (K_ctx,D)
|
| 465 |
|
| 466 |
# ---------------------------
|
| 467 |
# Fit dense GP on residuals
|
| 468 |
# ---------------------------
|
| 469 |
+
K_mat, ell_used = self._gp_kernel_matrix(Psi_ctx) # (K_ctx,K_ctx)
|
| 470 |
K_reg = K_mat + self.gp_noise2 * np.eye(K_ctx, dtype=np.float64)
|
| 471 |
|
|
|
|
| 472 |
cf = cho_factor(K_reg, lower=True, check_finite=False)
|
| 473 |
+
alpha = cho_solve(cf, E_ctx, check_finite=False) # (K_ctx,D)
|
| 474 |
Lfac, lower = cf
|
| 475 |
|
|
|
|
| 476 |
w_ctx_base = float(K_ctx) / float(K_ctx + self.ctx_k0) if self.ctx_k0 > 0 else 1.0
|
| 477 |
|
| 478 |
preds = np.zeros((H, self.D), dtype=np.float64)
|
|
|
|
| 482 |
# AR rollout with GP residual
|
| 483 |
# ---------------------------
|
| 484 |
for h in range(H):
|
| 485 |
+
psi_q = self.base.encode_window(cur)
|
| 486 |
|
| 487 |
# baseline
|
| 488 |
+
y_glob_c = self._baseline_centered_one(psi_q)
|
| 489 |
|
| 490 |
# GP residual mean
|
| 491 |
+
k_eval = self._gp_kernel_eval(Psi_ctx, psi_q, ell_used)
|
| 492 |
+
e_mean = k_eval @ alpha
|
| 493 |
|
| 494 |
# GP residual variance (function variance)
|
| 495 |
u = solve_triangular(Lfac, k_eval, lower=lower, check_finite=False)
|
|
|
|
| 499 |
if return_var:
|
| 500 |
vars_out[h] = float(var_f)
|
| 501 |
|
|
|
|
| 502 |
w_gate = self._gate_from_var(var_f)
|
| 503 |
w_eff = w_ctx_base * w_gate
|
| 504 |
|
|
|
|
| 505 |
e_use = e_mean
|
| 506 |
if sample:
|
| 507 |
var_y = var_f + (self.gp_noise2 if include_obs_noise else 0.0)
|
|
|
|
| 528 |
|
| 529 |
def _rollout_baseline(self, seed_LD: np.ndarray, H: int) -> np.ndarray:
|
| 530 |
"""
|
| 531 |
+
Baseline AR rollout only: NLSA encode + sparse GPLM decode.
|
| 532 |
"""
|
| 533 |
cur = np.asarray(seed_LD, dtype=np.float64).copy()
|
| 534 |
out = np.zeros((H, self.D), dtype=np.float64)
|