summaryrefslogtreecommitdiff
path: root/analysis_2x2/analyze_maze_connectivity.py
diff options
context:
space:
mode:
Diffstat (limited to 'analysis_2x2/analyze_maze_connectivity.py')
-rw-r--r--analysis_2x2/analyze_maze_connectivity.py87
1 files changed, 87 insertions, 0 deletions
diff --git a/analysis_2x2/analyze_maze_connectivity.py b/analysis_2x2/analyze_maze_connectivity.py
new file mode 100644
index 0000000..f2fb01b
--- /dev/null
+++ b/analysis_2x2/analyze_maze_connectivity.py
@@ -0,0 +1,87 @@
+"""Maze with CONNECTIVITY as the success criterion (not exact-match).
+Genuine failure = predicted path does NOT connect start->goal (broken/incomplete answer).
+Valid alternative paths (connected but != labeled) count as 'complete answer'.
+
+Joins per-cell preds (my dump, seed 20260616) with the friend's FTLE/drift npz (same seed/idx),
+and asks: do BROKEN (disconnected) predictions WANDER while CONNECTED ones SETTLE?
+If yes, the dynamical signal tracks answer-COMPLETENESS, and exact-match was the wrong lens for Maze.
+"""
+from __future__ import annotations
+from pathlib import Path
+from collections import deque
+import glob
+import numpy as np
+
+HERE = Path(__file__).resolve().parent
+FU = HERE / "maze_followup"
+FRIEND = "/tmp/friend_maze/maze_all_ckpts_lyap"
+
+
+def is_connected(inp, pred):
+ g = inp.reshape(30, 30); pr = pred.reshape(30, 30)
+ se = np.argwhere((g == 3) | (g == 4))
+ if len(se) < 2:
+ return True # can't judge -> treat as connected (won't happen)
+ s, e = tuple(se[0]), tuple(se[1])
+ pathset = set(map(tuple, np.argwhere(pr == 5))) | {s, e}
+ if any(g[r, c] == 1 for r, c in pathset):
+ return False # crosses wall = invalid
+ seen = {s}; q = deque([s])
+ while q:
+ r, c = q.popleft()
+ if (r, c) == e:
+ return True
+ for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
+ nr, nc = r + dr, c + dc
+ if 0 <= nr < 30 and 0 <= nc < 30 and (nr, nc) in pathset and (nr, nc) not in seen:
+ seen.add((nr, nc)); q.append((nr, nc))
+ return False
+
+
+def auc(score, y):
+ p, n = score[y == 1], score[y == 0]
+ if len(p) == 0 or len(n) == 0:
+ return float("nan")
+ a = np.concatenate([p, n]); o = np.argsort(a); r = np.empty(len(a)); r[o] = np.arange(1, len(a) + 1)
+ return float((r[:len(p)].sum() - len(p) * (len(p) + 1) / 2) / (len(p) * len(n)))
+
+
+def cohend(a, b):
+ if len(a) < 2 or len(b) < 2:
+ return float("nan")
+ s = np.sqrt(((len(a) - 1) * a.var(ddof=1) + (len(b) - 1) * b.var(ddof=1)) / (len(a) + len(b) - 2))
+ return (a.mean() - b.mean()) / s if s > 0 else float("nan")
+
+
+for step in [13020, 52080, 130200]:
+ pred_f = FU / f"mazepreds_step_{step}_seed20260616.npz"
+ fr = glob.glob(f"{FRIEND}/maze_step_{step}_*.npz")
+ if not pred_f.exists() or not fr:
+ print(f"[pending] step {step}")
+ continue
+ P = np.load(pred_f); F = np.load(fr[0])
+ common, pi, fi = np.intersect1d(P["idx"], F["idx"], return_indices=True)
+ preds = P["preds"][pi]; inputs = P["inputs"][pi]
+ exact = P["exact_correct"][pi].astype(int)
+ l1 = F["lyap_spec"][fi, 0].astype(float)
+ late_drift = np.log10(np.clip(F["drift_zH"][fi, -4:].mean(1), 1e-12, None))
+ conn = np.array([is_connected(inputs[k], preds[k]) for k in range(len(common))]).astype(int)
+ nb = int((conn == 0).sum())
+ print(f"\n=== step {step} (joined n={len(common)}) ===")
+ print(f" exact-match acc={exact.mean():.3f} | CONNECTIVITY acc (valid complete path)={conn.mean():.3f} | broken={nb}")
+ if nb < 3 or nb > len(common) - 3:
+ print(f" too few broken/connected to condition dynamics (broken={nb})")
+ continue
+ # dynamics conditioned on CONNECTIVITY (broken=0 vs connected=1)
+ print(f" late-drift (settling): connected median={np.median(late_drift[conn==1]):.2f} broken median={np.median(late_drift[conn==0]):.2f}")
+ print(f" AUC(-late-drift -> connected) = {auc(-late_drift, conn):.3f} Cohen d(broken-conn)={cohend(late_drift[conn==0], late_drift[conn==1]):+.2f}")
+ print(f" lambda1: connected median={np.median(l1[conn==1]):+.4f} broken median={np.median(l1[conn==0]):+.4f}")
+ print(f" AUC(-lambda1 -> connected) = {auc(-l1, conn):.3f}")
+ # compare: does connectivity separate dynamics BETTER than exact-match?
+ print(f" [vs exact-match] AUC(-late-drift -> exact_correct) = {auc(-late_drift, exact):.3f}, "
+ f"AUC(-lambda1 -> exact) = {auc(-l1, exact):.3f}")
+ # within CONNECTED, does exact-match still separate? (should NOT, if dynamics track completeness)
+ m = conn == 1
+ if 0 < exact[m].mean() < 1:
+ print(f" within CONNECTED (n={m.sum()}): AUC(-late-drift -> exact) = {auc(-late_drift[m], exact[m]):.3f} "
+ f"(near 0.5 => dynamics track completeness, not correctness)")