Buckets:

redmingwei's picture
download
raw
12.1 kB
#!/usr/bin/env python3
"""exp51 — 用正交不变的 tau 重跑 exp42 与 exp43。
旧估计量(exp39/40/42/43):
a = median(acf_fft(P, max_lag), axis=1)
tau = fit_exp(a)
exp49/50 证明它在 spike 上是噪声(6.9 vs 271.8 ms,旋转下变 65%),
在 EEG 上偏 6%(190.3 vs 202.4 ms)。
本脚本两处都换成
tau = fit_exp( tr C(k) / tr C(0) )
即按方差加权的 trace-ACF,对任何正交混合不变。
A. exp42 重算:10 被试,40 图像,不变量 tau;解码准确率直接沿用
已存的 runs/tau-vs-decode/report.json(acc 与 tau 估计量无关),
重算 Spearman / 偏相关 / LOO。
B. exp43 重算:502 试次,逐试次不变量 tau;重算与计时误差的相关和置换检验。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.optimize import curve_fit
from scipy.stats import spearmanr
EEG = Path("/domains/omnibrain4d/eeg")
NWB = Path("/domains/primate/dmfc_rsg/sub-Haydn_desc-train_ecephys.nwb")
OLD = Path("/domains/runs/tau-vs-decode/report.json")
OUT = Path("/domains/runs/tau-fixed")
OUT.mkdir(parents=True, exist_ok=True)
FS = 250.0
DT = 1000.0 / FS
N_PCS = 32
N_IMG = 40
SUBJECTS = [f"sub-{i:02d}" for i in range(1, 11)]
MAX_LAG_MS = 1000.0
MAX_LAG_TRIAL_MS = 400.0
WIN_AFTER_SET_MS = 600.0
N_PCS_TRIAL = 16
# ---------------------------------------------------------------- ACF
def acf_cols(x, max_lag):
T = x.shape[0]
xc = x - x.mean(axis=0, keepdims=True)
n = 1
while n < 2 * T:
n <<= 1
f = np.fft.rfft(xc, n=n, axis=0)
acf = np.fft.irfft(f * np.conj(f), n=n, axis=0)[:max_lag + 1]
norms = np.arange(T, T - max_lag - 1, -1)[:, None]
acf = acf / np.maximum(norms, 1)
return acf / np.maximum(acf[0:1], 1e-12)
def weighted_acf(x, max_lag):
"""tr C(k)/tr C(0)。正交不变。"""
T = x.shape[0]
xc = x - x.mean(axis=0, keepdims=True)
n = 1
while n < 2 * T:
n <<= 1
f = np.fft.rfft(xc, n=n, axis=0)
acf = np.fft.irfft(f * np.conj(f), n=n, axis=0)[:max_lag + 1]
norms = np.arange(T, T - max_lag - 1, -1)[:, None]
acf = acf / np.maximum(norms, 1)
return acf.sum(axis=1) / max(acf[0].sum(), 1e-12)
def fit_tau(a, dt, lo=2.0, hi=50000.0):
ok = np.isfinite(a) & (a > 0.02) & (a < 0.995)
if ok.sum() < 5:
return None
t = np.arange(len(a))[ok] * dt
y = a[ok]
def f(tt, T):
return np.exp(-tt / np.maximum(T, 1e-3))
try:
popt, _ = curve_fit(f, t, y, p0=[200.0], bounds=([lo], [hi]), maxfev=8000)
return float(popt[0])
except Exception:
return None
# ---------------------------------------------------------------- A. exp42
def part_a():
old = json.loads(OLD.read_text())
acc = {r["subject"]: r["acc_k200"] for r in old["per_subject"]}
old_tau = {r["subject"]: r["tau_ms"] for r in old["per_subject"]}
rows = []
t0 = time.time()
for sm in SUBJECTS:
f = EEG / f"{sm}.npz"
if not f.exists():
continue
eeg = np.load(f)["eeg"]
n_img, n_rep, n_ch, T = eeg.shape
ml = int(min(MAX_LAG_MS / DT, T - 2))
taus_new, taus_old, ev1s = [], [], []
for ii in range(min(N_IMG, n_img)):
seg = eeg[ii].astype(np.float32)
X = seg.transpose(1, 0, 2).reshape(n_ch, n_rep * T)
mu = X.mean(axis=1, keepdims=True)
sd = X.std(axis=1, keepdims=True) + 1e-6
Z = ((X - mu) / sd).T
Zc = Z - Z.mean(axis=0, keepdims=True)
tn = fit_tau(weighted_acf(Zc, ml), DT)
if tn:
taus_new.append(tn)
U, S, _ = np.linalg.svd(Zc, full_matrices=False)
d = min(N_PCS, n_ch)
P = (U[:, :d] * S[:d]).astype(np.float64)
to = fit_tau(np.median(acf_cols(P, ml), axis=1), DT)
if to:
taus_old.append(to)
lam = S ** 2
ev1s.append(float(lam[0] / lam[:d].sum()))
rows.append({
"subject": sm,
"tau_invariant": round(float(np.median(taus_new)), 1) if taus_new else None,
"tau_oldmedian": round(float(np.median(taus_old)), 1) if taus_old else None,
"tau_stored": old_tau.get(sm),
"acc_k200": acc.get(sm),
"ev_top1": round(float(np.median(ev1s)), 4) if ev1s else None,
"n_img": len(taus_new),
})
print(f" {sm}: inv={rows[-1]['tau_invariant']} old={rows[-1]['tau_oldmedian']} "
f"stored={old_tau.get(sm)} acc={acc.get(sm)} [{time.time()-t0:.0f}s]",
flush=True)
del eeg
df = pd.DataFrame(rows)
df.to_csv(OUT / "exp42_fixed.csv", index=False)
d = df.dropna(subset=["tau_invariant", "acc_k200"])
n = len(d)
def rep(x, y):
r, p = spearmanr(x, y)
return float(r), float(p)
r_inv, p_inv = rep(d.tau_invariant, d.acc_k200)
r_old, p_old = rep(d.tau_oldmedian, d.acc_k200)
r_sto, p_sto = rep(df.dropna(subset=["tau_stored", "acc_k200"]).tau_stored,
df.dropna(subset=["tau_stored", "acc_k200"]).acc_k200)
# 偏相关(控制 eff_dim 与 ev_top1)
def partial(x, y, z):
rx = pd.Series(x).rank().to_numpy()
ry = pd.Series(y).rank().to_numpy()
rz = pd.Series(z).rank().to_numpy()
A = np.c_[np.ones(len(rz)), rz]
ex = rx - A @ np.linalg.lstsq(A, rx, rcond=None)[0]
ey = ry - A @ np.linalg.lstsq(A, ry, rcond=None)[0]
r, p = spearmanr(ex, ey)
return float(r), float(p)
pr_ev, pp_ev = partial(d.tau_invariant, d.acc_k200, d.ev_top1)
loos = []
for i in range(n):
s = d.drop(d.index[i])
loos.append(float(spearmanr(s.tau_invariant, s.acc_k200)[0]))
res = {
"task": "exp42_fixed_invariant_tau",
"n_subjects": int(n),
"acc_source": "runs/tau-vs-decode/report.json(acc 与 tau 估计量无关,直接沿用)",
"tau_oldmedian_vs_acc": {"rho": round(r_old, 3), "p": round(p_old, 4)},
"tau_stored_vs_acc": {"rho": round(r_sto, 3), "p": round(p_sto, 4)},
"tau_invariant_vs_acc": {"rho": round(r_inv, 3), "p": round(p_inv, 4)},
"tau_invariant_vs_acc_ctrl_evtop1": {"rho": round(pr_ev, 3), "p": round(pp_ev, 4)},
"loo": {"min": round(min(loos), 3), "max": round(max(loos), 3),
"median": round(float(np.median(loos)), 3),
"sign_consistent": bool(all(np.sign(v) == np.sign(r_inv) for v in loos))},
"tau_shift": {
"inv_minus_old_pct_median": round(float(
((d.tau_invariant - d.tau_oldmedian) / d.tau_oldmedian * 100).median()), 2)},
}
if p_inv < 0.05 and abs(r_inv) > 0.5:
res["verdict"] = f"显著 (rho={r_inv:+.2f}, p={p_inv:.3f})"
elif abs(r_inv) < 0.3:
res["verdict"] = (f"无相关 (rho={r_inv:+.2f}, p={p_inv:.3f}) — "
"换成不变量后结论不变:tau 不预测解码")
else:
res["verdict"] = (f"功效不足 (rho={r_inv:+.2f}, p={p_inv:.3f}),n=10 需 |rho|>0.65")
return df, res
# ---------------------------------------------------------------- B. exp43
def part_b():
import h5py
if not NWB.exists():
return None, {"error": f"missing {NWB}"}
with h5py.File(NWB, "r") as f:
t = f["intervals"]["trials"]
tr = {k: t[k][:] for k in
["ready_time", "set_time", "go_time", "is_outlier", "is_short"]}
u = f["units"]
st = u["spike_times"][:]
si = u["spike_times_index"][:]
bounds = np.concatenate([[0], si])
units = [st[bounds[i]:bounds[i + 1]] for i in range(len(si))]
print(f" trials {len(tr['go_time'])} | units {len(units)}", flush=True)
rt, s_, gt = tr["ready_time"], tr["set_time"], tr["go_time"]
t1 = s_ - rt
t2 = gt - s_
ok = (np.isfinite(t1) & np.isfinite(t2) & (t1 > 0.1) & (t2 > 0.05)
& (~tr["is_outlier"]) & (~tr["is_short"]))
err = (t2 - t1) * 1000.0
bin_s = 4.0 / 1000.0
win_s = WIN_AFTER_SET_MS / 1000.0
ml = int(MAX_LAG_TRIAL_MS / 4.0)
rows = []
t0 = time.time()
for i in np.flatnonzero(ok):
a0, a1 = s_[i], s_[i] + win_s
edges = np.arange(a0, a1 + bin_s, bin_s)
if len(edges) < 6:
continue
M = np.zeros((len(edges) - 1, len(units)), dtype=np.float32)
for j, sp in enumerate(units):
if len(sp) == 0:
continue
sel = sp[(sp >= a0) & (sp < a1)]
if len(sel) == 0:
continue
bi = np.clip(((sel - a0) / bin_s).astype(int), 0, M.shape[0] - 1)
np.add.at(M[:, j], bi, 1.0)
if M.shape[0] < 8 or M.sum() < 10:
continue
mu = M.mean(axis=0, keepdims=True)
sd = M.std(axis=0, keepdims=True) + 1e-6
Z = (M - mu) / sd
d = min(N_PCS_TRIAL, Z.shape[1] - 1)
try:
U, S, _ = np.linalg.svd(Z, full_matrices=False)
except np.linalg.LinAlgError:
continue
P = (U[:, :d] * S[:d]).astype(np.float64)
lam = S ** 2
tn = fit_tau(weighted_acf(Z, ml), 4.0, lo=2.0, hi=5000.0)
to = fit_tau(np.median(acf_cols(P, ml), axis=1), 4.0, lo=2.0, hi=5000.0)
rows.append({
"trial": int(i), "abs_err_ms": float(abs(err[i])),
"rel_err": float(abs(err[i]) / (t1[i] * 1000)),
"tau_inv": tn, "tau_old": to,
"ev_top1": float(lam[0] / lam[:d].sum()),
"n_spikes": int(M.sum()),
})
if len(rows) % 100 == 0:
print(f" {len(rows)} trials [{time.time()-t0:.0f}s]", flush=True)
df = pd.DataFrame(rows)
df.to_csv(OUT / "exp43_fixed.csv", index=False)
d = df.dropna(subset=["tau_inv", "abs_err_ms"])
n = len(d)
r_inv, p_inv = spearmanr(d.tau_inv, d.abs_err_ms)
sub_old = df.dropna(subset=["tau_old", "abs_err_ms"])
r_old, p_old = spearmanr(sub_old.tau_old, sub_old.abs_err_ms)
rng = np.random.default_rng(0)
perm = np.array([spearmanr(d.tau_inv, rng.permutation(d.abs_err_ms.to_numpy()))[0]
for _ in range(2000)])
p_perm = float((np.abs(perm) >= abs(r_inv)).mean())
res = {
"task": "exp43_fixed_invariant_tau",
"n_trials": int(n),
"n_trials_old": int(len(sub_old)),
"tau_old_vs_abs_err": {"rho": round(float(r_old), 4), "p": float(p_old)},
"tau_invariant_vs_abs_err": {"rho": round(float(r_inv), 4), "p": float(p_inv),
"p_perm": p_perm, "n_perm": 2000},
"permutation_ci95": [round(float(np.percentile(perm, 2.5)), 4),
round(float(np.percentile(perm, 97.5)), 4)],
"tau_medians": {"old": round(float(df.tau_old.median()), 1),
"invariant": round(float(df.tau_inv.median()), 1)},
"ev_top1_median": round(float(df.ev_top1.median()), 4),
}
if p_perm < 0.05:
res["verdict"] = f"显著 (rho={r_inv:+.3f}, perm p={p_perm:.4f})"
else:
res["verdict"] = (f"无相关 (rho={r_inv:+.3f}, perm p={p_perm:.4f}) — "
"换成不变量后结论不变")
return df, res
def main():
print("=" * 78, flush=True)
print("A. exp42 重算(EEG 250 Hz,正交不变 tau)", flush=True)
print("=" * 78, flush=True)
df_a, res_a = part_a()
print("\n" + json.dumps(res_a, indent=2, ensure_ascii=False), flush=True)
(OUT / "report_exp42_fixed.json").write_text(
json.dumps(res_a, indent=2, ensure_ascii=False), encoding="utf-8")
print("\n" + "=" * 78, flush=True)
print("B. exp43 重算(DMFC 逐试次,正交不变 tau)", flush=True)
print("=" * 78, flush=True)
df_b, res_b = part_b()
print("\n" + json.dumps(res_b, indent=2, ensure_ascii=False), flush=True)
(OUT / "report_exp43_fixed.json").write_text(
json.dumps(res_b, indent=2, ensure_ascii=False), encoding="utf-8")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
12.1 kB
·
Xet hash:
3db9c62bebd5129f9393407ffda1ff87d2d4f3c83d444f037703cd9f6b719153

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.