Forward-Intersection Spectral Latent Dynamics / mini_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
 1"""Small noisy snapshot comparison: unfiltered versus forward-compatible EDMD."""
 2import json
 3import numpy as np
 4from forward_intersection import make_K, principal_filter
 5
 6
 7def one_trial(seed, T=80, noise=0.04, tau=0.20, horizon=10):
 8    rng = np.random.default_rng(seed)
 9    Q0 = np.eye(5)[:, :4]
10    K = make_K(.60)
11    # Snapshot inputs lie in the dictionary; outputs are noisy ambient observations.
12    X = rng.normal(size=(4, T))
13    Y = K @ (Q0 @ X) + noise * rng.normal(size=(5, T))
14    Khat = Y @ np.linalg.pinv(Q0 @ X)
15    Q1, _ = principal_filter(Q0, Khat @ Q0, tau)
16    A0 = Q0.T @ Khat @ Q0
17    Af = Q1.T @ Khat @ Q1
18
19    # Held-out initial conditions contain only represented stable modes.
20    Z = rng.normal(size=(3, 30))
21    truth_diag = np.array([.9, .7, .8])
22    err0, err1 = [], []
23    for h in range(1, horizon + 1):
24        target = truth_diag ** h @ Z
25        pred0 = (A0[:3, :3] ** 0) @ Z  # overwritten below; explicit matrix power follows
26        pred0 = np.linalg.matrix_power(A0[:3, :3], h) @ Z
27        # The selected Q1 is normally the first three coordinate directions; use
28        # its ambient prediction and compare only to the true supported trajectory.
29        pred1 = Q1 @ np.linalg.matrix_power(Af, h) @ (Q1.T @ (Q0[:,:3] @ Z))
30        err0.append(np.mean((pred0 - target) ** 2))
31        err1.append(np.mean((pred1[:3] - target) ** 2))
32    return {
33        "dim": int(Q1.shape[1]),
34        "spectral_radius_baseline": float(max(abs(np.linalg.eigvals(A0)))),
35        "spectral_radius_idea": float(max(abs(np.linalg.eigvals(Af)))),
36        "rollout_mse_10_baseline": float(err0[-1]),
37        "rollout_mse_10_idea": float(err1[-1]),
38    }
39
40
41def run():
42    results = [one_trial(s) for s in range(10)]
43    keys = ["spectral_radius_baseline", "spectral_radius_idea",
44            "rollout_mse_10_baseline", "rollout_mse_10_idea"]
45    means = {k: float(np.mean([r[k] for r in results])) for k in keys}
46    return {"settings": {"trials": 10, "T": 80, "noise_std": .04,
47                          "tau": .20, "horizon": 10},
48            "mean": means,
49            "dimensions": [r["dim"] for r in results], "trials": results}
50
51
52if __name__ == "__main__":
53    print(json.dumps(run(), indent=2))