sparsetrace commited on
Commit
3703e2e
·
verified ·
1 Parent(s): 722ca8c

Create LISA.py

Browse files
Files changed (1) hide show
  1. LISA.py +535 -0
LISA.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LISA.py
2
+ # ============================================================
3
+ # LISA = NLSA encoder + GPLM baseline forecaster
4
+ # + in-context full Gaussian Process Regression (GPR)
5
+ # on residuals in diffusion (psi) space.
6
+ #
7
+ # Auto behavior:
8
+ # - if prefix length ell == L: baseline-only AR rollout
9
+ # - if ell > L: baseline + GP residual IC correction (LISA)
10
+ #
11
+ # Repo expectations:
12
+ # - NLSAEncoder available as:
13
+ # from nlsa_encoder import NLSAEncoder
14
+ # (or from NLSA import NLSAEncoder depending on your naming)
15
+ # - GPLM available as:
16
+ # from gplm import GPLM
17
+ #
18
+ # Dependencies:
19
+ # numpy, scipy
20
+ # ============================================================
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass
25
+ from typing import Any, Dict, Optional, Tuple, Union
26
+
27
+ import numpy as np
28
+ import scipy.linalg as la
29
+ from numpy.lib.stride_tricks import sliding_window_view
30
+
31
+
32
+ # ------------------------------------------------------------
33
+ # Imports from your repo (robust fallbacks)
34
+ # ------------------------------------------------------------
35
+
36
+ # NLSA encoder
37
+ try:
38
+ from .nlsa_encoder import NLSAEncoder # type: ignore
39
+ except Exception:
40
+ try:
41
+ from nlsa_encoder import NLSAEncoder # type: ignore
42
+ except Exception:
43
+ # if you named the file NLSA.py
44
+ from NLSA import NLSAEncoder # type: ignore
45
+
46
+ # GPLM decoder (GP/KRR mean map)
47
+ try:
48
+ from .gplm import GPLM # type: ignore
49
+ except Exception:
50
+ try:
51
+ from gplm import GPLM # type: ignore
52
+ except Exception:
53
+ # if you named the file GPLM.py
54
+ from GPLM import GPLM # type: ignore
55
+
56
+
57
+ Array = np.ndarray
58
+
59
+
60
+ # ============================================================
61
+ # Utilities
62
+ # ============================================================
63
+
64
+ def _as_2d(X: Array) -> Array:
65
+ X = np.asarray(X, dtype=float)
66
+ if X.ndim == 1:
67
+ X = X[:, None]
68
+ return X
69
+
70
+
71
+ def _sliding_windows(F_tD: Array, L: int) -> Array:
72
+ """
73
+ Return windows W (K,L,D) from F (N,D) with K=N-L+1.
74
+ Handles numpy stride ordering differences.
75
+ """
76
+ F = _as_2d(F_tD)
77
+ W = sliding_window_view(F, window_shape=int(L), axis=0)
78
+
79
+ # W can be (K,L,D) or (K,D,L)
80
+ a, b = W.shape[1], W.shape[2]
81
+ if (a, b) == (L, F.shape[1]):
82
+ return np.ascontiguousarray(W)
83
+ if (a, b) == (F.shape[1], L):
84
+ return np.ascontiguousarray(np.transpose(W, (0, 2, 1)))
85
+ raise ValueError(f"Unexpected window shape {W.shape} for L={L}, D={F.shape[1]}.")
86
+
87
+
88
+ def _pairwise_d2(X: Array) -> Array:
89
+ """
90
+ Dense pairwise squared distances (n,n) for moderate n.
91
+ """
92
+ X = np.asarray(X, dtype=np.float64)
93
+ x2 = np.sum(X * X, axis=1, keepdims=True)
94
+ d2 = x2 + x2.T - 2.0 * (X @ X.T)
95
+ np.maximum(d2, 0.0, out=d2)
96
+ return d2
97
+
98
+
99
+ def _median_ell_from_d2(d2_mat: Array, q: float = 0.5, eps: float = 1e-12, min_ell: float = 1e-6) -> float:
100
+ """
101
+ If k(x,y) = exp(-||x-y||^2/(2 ell^2)), heuristic:
102
+ ell^2 ~= quantile(d^2)/2
103
+ """
104
+ iu = np.triu_indices_from(d2_mat, k=1)
105
+ vals = d2_mat[iu]
106
+ if vals.size == 0:
107
+ return 1.0
108
+ v = float(np.quantile(vals, q))
109
+ ell = np.sqrt(max(v / 2.0, eps))
110
+ return float(max(ell, min_ell))
111
+
112
+
113
+ # ============================================================
114
+ # Kernel helpers (for the in-context GP on residuals)
115
+ # ============================================================
116
+
117
+ def _kernel_matrix(Psi: Array, *, kind: str, ell: Optional[float]) -> Tuple[Array, Optional[float]]:
118
+ """
119
+ Build dense kernel matrix K(Psi,Psi).
120
+ kind: "linear" or "rbf"
121
+ ell: lengthscale for RBF. If None -> caller estimates separately.
122
+ """
123
+ kind = kind.lower().strip()
124
+ Psi = np.asarray(Psi, dtype=np.float64)
125
+
126
+ if kind == "linear":
127
+ return Psi @ Psi.T, None
128
+
129
+ if kind != "rbf":
130
+ raise ValueError("kernel kind must be 'linear' or 'rbf'.")
131
+
132
+ if ell is None:
133
+ raise ValueError("RBF kernel requires ell != None (estimate ell before calling).")
134
+
135
+ d2 = _pairwise_d2(Psi)
136
+ K = np.exp(-0.5 * d2 / (float(ell) ** 2 + 1e-12))
137
+ return K, float(ell)
138
+
139
+
140
+ def _kernel_eval(Psi_ctx: Array, psi: Array, *, kind: str, ell: Optional[float]) -> Array:
141
+ """
142
+ Vector k(Psi_ctx, psi) shape (K_ctx,).
143
+ """
144
+ kind = kind.lower().strip()
145
+ Psi_ctx = np.asarray(Psi_ctx, dtype=np.float64)
146
+ psi = np.asarray(psi, dtype=np.float64).reshape(-1)
147
+
148
+ if kind == "linear":
149
+ return Psi_ctx @ psi
150
+
151
+ if kind != "rbf":
152
+ raise ValueError("kernel kind must be 'linear' or 'rbf'.")
153
+
154
+ if ell is None:
155
+ raise ValueError("RBF kernel requires ell != None.")
156
+
157
+ diff = Psi_ctx - psi[None, :]
158
+ d2 = np.einsum("kr,kr->k", diff, diff, optimize=True)
159
+ return np.exp(-0.5 * d2 / (float(ell) ** 2 + 1e-12))
160
+
161
+
162
+ def _k_qq(psi: Array, *, kind: str) -> float:
163
+ """k(psi,psi)."""
164
+ kind = kind.lower().strip()
165
+ if kind == "linear":
166
+ return float(np.dot(psi, psi))
167
+ return 1.0 # RBF
168
+
169
+
170
+ def _gate_from_var(var: float, *, tau2: float, mode: str) -> float:
171
+ """
172
+ Convert predictive variance into a [0,1] trust weight.
173
+ - "rational": tau2/(tau2+var)
174
+ - "exp": exp(-var/tau2)
175
+ """
176
+ v = max(float(var), 0.0)
177
+ tau2 = max(float(tau2), 1e-18)
178
+ mode = mode.lower().strip()
179
+
180
+ if mode == "exp":
181
+ return float(np.exp(-v / tau2))
182
+ if mode == "rational":
183
+ return float(tau2 / (tau2 + v))
184
+ raise ValueError("gate_mode must be 'rational' or 'exp'.")
185
+
186
+
187
+ # ============================================================
188
+ # LISA
189
+ # ============================================================
190
+
191
+ @dataclass
192
+ class LISAConfig:
193
+ # NLSA encoder
194
+ L: int = 128
195
+ rank: int = 32
196
+ beta: Optional[float] = None
197
+ alpha: float = 1.0
198
+ center: bool = True
199
+ drop_first: bool = True
200
+ max_K_dense: int = 6000
201
+ seed: int = 0
202
+
203
+ # GPLM baseline (psi -> next sample)
204
+ gplm_kwargs: Optional[Dict[str, Any]] = None
205
+
206
+ # In-context GP residual model
207
+ ctx_min_windows: Optional[int] = None # default r+1
208
+ ctx_k0: float = 10.0 # base blending gate: K_ctx/(K_ctx+k0)
209
+
210
+ gp_kernel: str = "rbf" # "rbf" or "linear"
211
+ gp_noise2: float = 1e-3 # σ_n^2
212
+ gp_rbf_ell: Optional[float] = None # if None -> estimate from context
213
+ gp_rbf_q: float = 0.5 # quantile for ell estimation
214
+ gp_jitter: float = 1e-10 # add to diag for numeric stability
215
+
216
+ # Variance gating
217
+ use_var_gate: bool = True
218
+ gate_tau2: float = 1.0
219
+ gate_mode: str = "rational" # "rational" or "exp"
220
+
221
+
222
+ class LISA:
223
+ """
224
+ LISA = NLSAEncoder + GPLM baseline + in-context full GP residual correction.
225
+
226
+ Training (`__init__`):
227
+ 1) Train NLSAEncoder on F_train
228
+ 2) Fit GPLM baseline: psi(window) -> next sample (centered space)
229
+
230
+ Inference (`__call__`):
231
+ Given prefix F_prefix (ell,D):
232
+ - If ell == L: baseline-only AR rollout (NLSA+GPLM)
233
+ - If ell > L: use prefix context to fit residual GP, then rollout:
234
+ y = y_glob + w_eff * e_gp
235
+ where w_eff = w_ctx_base * trust(var_gp)
236
+
237
+ Notes:
238
+ - GP residual is multi-output via shared kernel and independent outputs:
239
+ E ~ GP(0, k(ψ,ψ')) per output dimension.
240
+ - Predictive variance is scalar (same for all dims) because kernel is shared.
241
+ """
242
+
243
+ def __init__(self, F_train: Array, *, config: Optional[LISAConfig] = None, **kwargs: Any):
244
+ if config is None:
245
+ config = LISAConfig(**kwargs)
246
+ self.cfg = config
247
+
248
+ F_train = _as_2d(F_train)
249
+ self.D = int(F_train.shape[1])
250
+ self.L = int(self.cfg.L)
251
+
252
+ # ---- 1) NLSA encoder ----
253
+ self.enc = NLSAEncoder(
254
+ F_train,
255
+ L=int(self.cfg.L),
256
+ rank=int(self.cfg.rank),
257
+ beta=self.cfg.beta,
258
+ alpha=float(self.cfg.alpha),
259
+ center=bool(self.cfg.center),
260
+ drop_first=bool(self.cfg.drop_first),
261
+ max_K_dense=int(self.cfg.max_K_dense),
262
+ seed=int(self.cfg.seed),
263
+ )
264
+
265
+ self.r = 0 if self.enc.psi_ is None else int(self.enc.psi_.shape[1])
266
+ if self.r <= 0:
267
+ raise ValueError("NLSAEncoder produced r=0 diffusion dims. Increase rank or set drop_first=False.")
268
+
269
+ # mean (same mean used inside encoder)
270
+ self.center = bool(self.cfg.center)
271
+ self.mu_X = self.enc.mu_.reshape(-1).astype(np.float64) if self.center else np.zeros((self.D,), dtype=np.float64)
272
+
273
+ # ---- 2) GPLM baseline: psi -> next sample ----
274
+ # Training pairs: for each training window T=0..K-2, target is F[T+L]
275
+ # We can use the training diffusion coords directly: Psi_tr = enc.psi_[:K-1]
276
+ K = int(self.enc.K_)
277
+ n_pairs = K - 1
278
+ Psi_tr = np.asarray(self.enc.psi_[:n_pairs, :], dtype=np.float64) # (K-1, r)
279
+
280
+ # Targets in centered space consistent with encoder centering
281
+ F_centered = _as_2d(F_train) - self.mu_X[None, :] if self.center else _as_2d(F_train)
282
+ Y_tr = np.asarray(F_centered[self.L : self.L + n_pairs, :], dtype=np.float64) # (K-1, D)
283
+
284
+ gkw = {} if self.cfg.gplm_kwargs is None else dict(self.cfg.gplm_kwargs)
285
+ # Critical: Y_tr is already centered (if center=True), so do NOT let GPLM recenter outputs.
286
+ gkw.setdefault("center_X", False)
287
+ gkw.setdefault("seed", int(self.cfg.seed))
288
+
289
+ # A sane default regularization if user didn't provide one:
290
+ gkw.setdefault("sigma2", 1e-4)
291
+ gkw.setdefault("jitter", 1e-8)
292
+
293
+ # inducing size default
294
+ gkw.setdefault("m", min(1024, n_pairs))
295
+ gkw.setdefault("inducing", "kmeans_medoids")
296
+
297
+ self.baseline = GPLM(Psi_tr, Y_tr, **gkw)
298
+
299
+ # IC defaults
300
+ if self.cfg.ctx_min_windows is None:
301
+ self.ctx_min_windows = max(1, self.r + 1)
302
+ else:
303
+ self.ctx_min_windows = int(self.cfg.ctx_min_windows)
304
+
305
+ self._rng = np.random.default_rng(int(self.cfg.seed))
306
+
307
+ # for diagnostics
308
+ self.last_gp_ell_ = None
309
+
310
+ # -------------------------
311
+ # baseline step
312
+ # -------------------------
313
+
314
+ def predict_one_step_baseline(self, W_LD: Array) -> Array:
315
+ """
316
+ One-step baseline prediction from a window (L,D) -> (D,).
317
+ Uses: psi = enc.encode_window(window), then GPLM(psi).
318
+ """
319
+ W = np.asarray(W_LD, dtype=float)
320
+ if W.ndim == 1:
321
+ W = W[:, None]
322
+ if W.shape != (self.L, self.D):
323
+ raise ValueError(f"Expected window shape {(self.L, self.D)}, got {W.shape}")
324
+
325
+ psi = self.enc.encode_window(W) # (r,)
326
+ y_c = np.asarray(self.baseline(psi), dtype=np.float64).reshape(-1) # centered
327
+ y = y_c + self.mu_X if self.center else y_c
328
+ return y
329
+
330
+ # -------------------------
331
+ # main rollout
332
+ # -------------------------
333
+
334
+ def __call__(
335
+ self,
336
+ prefix: Array,
337
+ steps: int,
338
+ *,
339
+ return_var: bool = False,
340
+ sample: bool = False,
341
+ rng: Optional[np.random.Generator] = None,
342
+ include_obs_noise: bool = True,
343
+ ) -> Union[Array, Tuple[Array, Array]]:
344
+ """
345
+ Forecast from prefix context.
346
+
347
+ prefix: (ell,D), ell >= L
348
+ steps: horizon H
349
+
350
+ If ell == L: baseline-only AR rollout.
351
+ If ell > L : full LISA with dense GP residual IC correction.
352
+
353
+ return_var:
354
+ returns scalar residual GP variance per step (H,)
355
+
356
+ sample:
357
+ samples residual from GP posterior at each step (generative mode).
358
+ """
359
+ prefix = _as_2d(prefix)
360
+ ell, D = prefix.shape
361
+ if D != self.D:
362
+ raise ValueError(f"LISA trained with D={self.D}, got prefix D={D}.")
363
+ if ell < self.L:
364
+ raise ValueError(f"Need prefix length ell >= L={self.L}.")
365
+
366
+ H = int(steps)
367
+ if H <= 0:
368
+ preds = np.zeros((0, self.D), dtype=np.float64)
369
+ return (preds, np.zeros((0,), dtype=np.float64)) if return_var else preds
370
+
371
+ if rng is None:
372
+ rng = self._rng
373
+
374
+ # seed window always last L
375
+ cur = prefix[-self.L :, :].copy() # (L,D)
376
+
377
+ # ----------------------------------------------------
378
+ # If no extra context, run baseline-only AR
379
+ # ----------------------------------------------------
380
+ if ell == self.L or self.r <= 0:
381
+ preds = self._rollout_baseline(cur, H)
382
+ if return_var:
383
+ return preds, np.zeros((H,), dtype=np.float64)
384
+ return preds
385
+
386
+ # ----------------------------------------------------
387
+ # Build context windows and targets from prefix
388
+ # ----------------------------------------------------
389
+ W_all = _sliding_windows(prefix, self.L) # (K_n, L, D)
390
+ K_n = int(W_all.shape[0])
391
+ K_ctx = K_n - 1 # number of (window -> next) pairs inside prefix
392
+
393
+ if K_ctx < self.ctx_min_windows:
394
+ preds = self._rollout_baseline(cur, H)
395
+ if return_var:
396
+ return preds, np.zeros((H,), dtype=np.float64)
397
+ return preds
398
+
399
+ W_ctx = np.ascontiguousarray(W_all[:K_ctx, :, :]) # (K_ctx,L,D)
400
+ Y_true = np.asarray(prefix[self.L : self.L + K_ctx, :], float) # (K_ctx,D)
401
+
402
+ # centered targets
403
+ Y_true_c = (Y_true - self.mu_X[None, :]) if self.center else Y_true
404
+
405
+ # encode context
406
+ Psi_ctx = np.zeros((K_ctx, self.r), dtype=np.float64)
407
+ for i in range(K_ctx):
408
+ Psi_ctx[i] = self.enc.encode_window(W_ctx[i])
409
+
410
+ # baseline on context
411
+ Y_glob_ctx_c = np.asarray(self.baseline(Psi_ctx), dtype=np.float64) # (K_ctx,D) centered
412
+
413
+ # residual table
414
+ E_ctx = Y_true_c - Y_glob_ctx_c # (K_ctx,D)
415
+
416
+ # ----------------------------------------------------
417
+ # Fit dense GP on residuals in psi-space
418
+ # ----------------------------------------------------
419
+ kernel = self.cfg.gp_kernel.lower().strip()
420
+
421
+ if kernel == "rbf":
422
+ ell_used = self.cfg.gp_rbf_ell
423
+ if ell_used is None:
424
+ d2 = _pairwise_d2(Psi_ctx)
425
+ ell_used = _median_ell_from_d2(d2, q=float(self.cfg.gp_rbf_q))
426
+ ell_used = float(ell_used)
427
+ self.last_gp_ell_ = ell_used
428
+ else:
429
+ ell_used = None
430
+ self.last_gp_ell_ = None
431
+
432
+ K_mat, _ = _kernel_matrix(Psi_ctx, kind=kernel, ell=ell_used)
433
+
434
+ # (K + σ_n^2 I + jitter I)
435
+ noise2 = float(self.cfg.gp_noise2)
436
+ jitter = float(self.cfg.gp_jitter)
437
+ K_reg = K_mat + (noise2 + jitter) * np.eye(K_ctx, dtype=np.float64)
438
+
439
+ # cholesky
440
+ try:
441
+ cF = la.cho_factor(K_reg, lower=True, check_finite=False)
442
+ alpha = la.cho_solve(cF, E_ctx, check_finite=False) # (K_ctx, D)
443
+ Lfac, lower = cF
444
+ chol_ok = True
445
+ except la.LinAlgError:
446
+ # fall back to solve (no variance)
447
+ alpha = np.linalg.solve(K_reg, E_ctx)
448
+ Lfac, lower = None, True
449
+ chol_ok = False
450
+
451
+ # base mixing (more context => stronger)
452
+ k0 = float(self.cfg.ctx_k0)
453
+ w_ctx_base = float(K_ctx) / float(K_ctx + k0) if k0 > 0 else 1.0
454
+
455
+ # ----------------------------------------------------
456
+ # Rollout with IC correction
457
+ # ----------------------------------------------------
458
+ preds = np.zeros((H, self.D), dtype=np.float64)
459
+ vars_out = np.zeros((H,), dtype=np.float64) if return_var else None
460
+
461
+ for h in range(H):
462
+ psi = self.enc.encode_window(cur) # (r,)
463
+
464
+ # baseline
465
+ y_glob_c = np.asarray(self.baseline(psi), dtype=np.float64).reshape(-1) # (D,)
466
+
467
+ # GP residual mean
468
+ k_eval = _kernel_eval(Psi_ctx, psi, kind=kernel, ell=ell_used) # (K_ctx,)
469
+ e_mean = k_eval @ alpha # (D,)
470
+
471
+ # GP residual variance (scalar)
472
+ var_f = 0.0
473
+ if chol_ok and Lfac is not None:
474
+ # u = L^{-1} k ; quad = ||u||^2
475
+ u = la.solve_triangular(Lfac, k_eval, lower=True, check_finite=False)
476
+ quad = float(np.dot(u, u))
477
+ var_f = max(0.0, _k_qq(psi, kind=kernel) - quad)
478
+
479
+ if return_var:
480
+ vars_out[h] = float(var_f)
481
+
482
+ # variance trust gate
483
+ if self.cfg.use_var_gate:
484
+ w_gate = _gate_from_var(var_f, tau2=self.cfg.gate_tau2, mode=self.cfg.gate_mode)
485
+ else:
486
+ w_gate = 1.0
487
+
488
+ w_eff = w_ctx_base * float(w_gate)
489
+
490
+ # optional sampling (generative)
491
+ e_use = e_mean
492
+ if sample:
493
+ var_y = var_f + (noise2 if include_obs_noise else 0.0)
494
+ var_y = max(0.0, float(var_y))
495
+ if var_y > 0:
496
+ e_use = e_mean + np.sqrt(var_y) * rng.standard_normal(size=(self.D,))
497
+
498
+ # combine
499
+ y_c = y_glob_c + w_eff * e_use
500
+ y = y_c + self.mu_X if self.center else y_c
501
+
502
+ preds[h] = y
503
+
504
+ # advance seed window
505
+ if self.L > 1:
506
+ cur[:-1] = cur[1:]
507
+ cur[-1] = y
508
+
509
+ if return_var:
510
+ return preds, vars_out
511
+ return preds
512
+
513
+ def _rollout_baseline(self, seed_LD: Array, H: int) -> Array:
514
+ """
515
+ Baseline-only autoregressive rollout using (NLSA encode) + GPLM decode.
516
+ """
517
+ cur = np.asarray(seed_LD, dtype=float).copy()
518
+ out = np.zeros((int(H), self.D), dtype=np.float64)
519
+ for h in range(int(H)):
520
+ y = self.predict_one_step_baseline(cur) # (D,)
521
+ out[h] = y
522
+ if self.L > 1:
523
+ cur[:-1] = cur[1:]
524
+ cur[-1] = y
525
+ return out
526
+
527
+ def __repr__(self) -> str:
528
+ return (
529
+ f"LISA(L={self.L}, D={self.D}, r={self.r}, "
530
+ f"center={self.center}, gp_kernel={self.cfg.gp_kernel}, "
531
+ f"gp_noise2={self.cfg.gp_noise2})"
532
+ )
533
+
534
+
535
+ __all__ = ["LISA", "LISAConfig"]