| import numpy as np |
| import matplotlib.pyplot as plt |
|
|
| from metrics import compare_all_fast_with_curves |
|
|
| COLORS = { |
| "ALSA": "C0", |
| "LISA": "C3", |
| } |
|
|
| METRICS = { |
| "MSE": ["mse", "MSE", "mse_mean"], |
| "Spectral (JS/KL)": ["spectral_kl", "spectral_js", "spectral_div", "spec_kl", "spectral"], |
| "ACF-MSE": ["acf_mse", "acf_l2", "acf_err", "acf"], |
| "MMD$^2$ (RFF)": ["mmd2_rff", "mmd_rff", "mmd2", "mmd"], |
| } |
|
|
| styles = { |
| "Truth": dict(color="k", lw=2.3, ls="-"), |
| "NLSA": dict(color="0.2", lw=2.0, ls="--"), |
| "ALSA": dict(color=COLORS["ALSA"], lw=2.0, ls="-.", alpha=0.95), |
| "LISA": dict(color=COLORS["LISA"], lw=2.1, ls=":", alpha=0.95), |
| } |
|
|
| def standardize_global(F_tX: np.ndarray, eps: float = 1e-12): |
| mu = F_tX.mean(axis=0, keepdims=True) |
| sd = F_tX.std(axis=0, keepdims=True) + eps |
| return (F_tX - mu) / sd, mu, sd |
|
|
| def plot_3d_phase_multi( |
| bg: np.ndarray, |
| truth: np.ndarray, |
| preds: dict[str, np.ndarray], |
| *, |
| title: str, |
| elev: float = 20, |
| azim: float = 35, |
| bg_stride: int = 5, |
| traj_stride: int = 1, |
| max_bg_points: int = 20000, |
| styles: dict[str, dict] | None = None, |
| bg_style: dict | None = None, |
| axis_names: tuple[str, str, str] = ("x", "y", "z"), |
| ): |
| """ |
| bg: (Tbg, 3) background attractor (e.g. train) |
| truth: (T, 3) |
| preds: name -> (T, 3) |
| |
| styles: dict mapping curve name -> matplotlib kwargs |
| e.g. styles["Truth"] = {...}, styles["NLSA"] = {...} |
| keys should match preds keys. |
| """ |
|
|
| bg = np.asarray(bg) |
| truth = np.asarray(truth) |
| if bg.ndim != 2 or truth.ndim != 2 or bg.shape[1] != 3 or truth.shape[1] != 3: |
| raise ValueError(f"bg and truth must be (T,3). Got bg={bg.shape}, truth={truth.shape}") |
|
|
| for k, v in preds.items(): |
| v = np.asarray(v) |
| if v.ndim != 2 or v.shape[1] != 3: |
| raise ValueError(f"pred '{k}' must be (T,3). Got {v.shape}") |
| preds[k] = v |
|
|
| if styles is None: |
| styles = {} |
|
|
| if bg_style is None: |
| bg_style = dict(color="0.75", lw=0.8, alpha=0.35) |
|
|
| |
| if bg.shape[0] > max_bg_points: |
| idx = np.linspace(0, bg.shape[0] - 1, max_bg_points).astype(int) |
| bgp = bg[idx] |
| else: |
| bgp = bg |
| bgp = bgp[::max(1, int(bg_stride))] |
|
|
| t = truth[::max(1, int(traj_stride))] |
|
|
| fig = plt.figure(figsize=(10, 8)) |
| ax = fig.add_subplot(111, projection="3d") |
|
|
| ax.plot(bgp[:, 0], bgp[:, 1], bgp[:, 2], label="Background (train)", **bg_style) |
|
|
| |
| truth_style = dict(color="k", lw=2.2) |
| truth_style.update(styles.get("Truth", {})) |
| ax.plot(t[:, 0], t[:, 1], t[:, 2], label="Truth", **truth_style) |
|
|
| |
| cycle = plt.rcParams["axes.prop_cycle"].by_key().get("color", ["C0", "C1", "C2", "C3", "C4"]) |
| for i, (name, P) in enumerate(preds.items()): |
| p = P[::max(1, int(traj_stride))] |
| style = dict(color=cycle[i % len(cycle)], lw=1.8, alpha=0.95) |
| style.update(styles.get(name, {})) |
| ax.plot(p[:, 0], p[:, 1], p[:, 2], label=name, **style) |
|
|
| |
| ax.scatter(t[0, 0], t[0, 1], t[0, 2], color=truth_style.get("color", "k"), s=60) |
|
|
| ax.set_title(title) |
| ax.set_xlabel(axis_names[0]) |
| ax.set_ylabel(axis_names[1]) |
| ax.set_zlabel(axis_names[2]) |
| ax.view_init(elev=elev, azim=azim) |
| ax.legend(frameon=False, loc="upper left") |
| plt.tight_layout() |
| plt.show() |
| return None |
|
|
| def make_task(F_test: np.ndarray, a_start: int, ell_ctx: int, steps: int): |
| """ |
| Returns: |
| prefix: (ell_ctx, D) |
| truth : (steps, D) |
| """ |
| assert a_start >= ell_ctx |
| assert a_start + steps <= F_test.shape[0] |
| prefix = F_test[a_start - ell_ctx: a_start, :] |
| truth = F_test[a_start: a_start + steps, :] |
| return prefix, truth |
|
|
|
|
| def pick_metric_key(available_keys: list[str], candidates: list[str]) -> str: |
| """Pick the first existing key from a list of candidate names.""" |
| for c in candidates: |
| if c in available_keys: |
| return c |
| |
| for c in candidates: |
| for k in available_keys: |
| if c.lower() in k.lower(): |
| return k |
| raise KeyError(f"None of candidates {candidates} found in keys: {available_keys}") |
|
|
|
|
| def eval_multistart( |
| model_name: str, |
| predictor_fn, |
| F_test: np.ndarray, |
| *, |
| starts: np.ndarray, |
| ell_ctx: int, |
| steps: int, |
| burn_in_metrics: int = 0, |
| mmd_sample: int = 1024, |
| seed: int = 0, |
| dt=0.01, |
| ): |
| """ |
| Returns: |
| scalars_mean: dict[str,float] |
| scalars_std : dict[str,float] |
| curves_mean : dict[str,np.ndarray] |
| """ |
| scalar_keys = None |
| scalars_acc = {} |
| curves_acc = {"mse_by_horizon": [], "mse_per_feature": []} |
|
|
| for i, a_start in enumerate(starts): |
| prefix, truth = make_task(F_test, int(a_start), ell_ctx, steps) |
| pred = predictor_fn(prefix) |
|
|
| s, c = compare_all_fast_with_curves( |
| truth, pred, |
| burn_in=burn_in_metrics, |
| acf_max_lag=min(200, steps - 2), |
| mmd_sample=min(mmd_sample, steps), |
| seed=seed + i, |
| include_mmd_rff=True, |
| dt=dt, |
| ) |
|
|
| if scalar_keys is None: |
| scalar_keys = list(s.keys()) |
| for k in scalar_keys: |
| scalars_acc[k] = [] |
|
|
| for k in scalar_keys: |
| scalars_acc[k].append(s[k]) |
|
|
| curves_acc["mse_by_horizon"].append(c["mse_by_horizon"]) |
| curves_acc["mse_per_feature"].append(c["mse_per_feature"]) |
|
|
| scalars_mean = {k: float(np.nanmean(v)) for k, v in scalars_acc.items()} |
| scalars_std = {k: float(np.nanstd(v)) for k, v in scalars_acc.items()} |
|
|
| curves_mean = { |
| "mse_by_horizon": np.nanmean(np.stack(curves_acc["mse_by_horizon"], axis=0), axis=0), |
| "mse_per_feature": np.nanmean(np.stack(curves_acc["mse_per_feature"], axis=0), axis=0), |
| } |
|
|
| print(f"\n[{model_name}] ℓ={ell_ctx} mean±std over {len(starts)} starts:") |
| for k in scalars_mean.keys(): |
| print(f" {k:16s}: {scalars_mean[k]:.6g} ± {scalars_std[k]:.3g}") |
|
|
| return scalars_mean, scalars_std, curves_mean |
|
|