Buckets:

redmingwei's picture
download
raw
8.74 kB
#!/usr/bin/env python3
"""exp53 — τ 在长片段上的收敛性(三个检验)。
旧版把每段切成 800 ms 的窗,而 τ ≈ 200 ms,每窗只装 4 个 τ。
v3 存了原始计数,片段长 20 s–1000 s,可以回答三个问题:
Q1 收敛:τ 随分析窗口长度 W 如何变化?
若 τ 是真实时间常数,W 超过几个 τ 后就该稳定。
若随 W 单调变化,τ 是 W 的函数,不是系统的性质。
Q2 分裂信度:同一片段的前半与后半,τ 是否一致?
这是比「分辨率检验」更强的信度检验,因为它不改变采样率。
Q3 物种:猕猴与人类在 M1 上的不变量 τ 是否不同?
同时报告可观测性(top-1 占比、有效维度)作为对照。
全部用不变量估计量 tr C(τ)/tr C(0)。
"""
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
V3 = Path("/domains/counts-v3")
OUT = Path("/domains/runs/tau-long")
OUT.mkdir(parents=True, exist_ok=True)
DT = 20.0
MAX_LAG = 500 # 10 s
WS = [250, 500, 1000, 2000, 4000, 8000, 16000, 10**9] # 5s..全段
SEED = 0
def weighted_acf(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)
a = np.fft.irfft(f * np.conj(f), n=n, axis=0)[:max_lag + 1]
norms = np.arange(T, T - max_lag - 1, -1)[:, None]
a = a / np.maximum(norms, 1)
return a.sum(axis=1) / max(a[0].sum(), 1e-12)
def fit_tau(a, dt=DT, lo=2.0, hi=100000.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
def tau_int(a, dt=DT, c=5.0):
taus = 1.0 + 2.0 * np.cumsum(a[1:])
Ms = np.arange(1, len(a))
m = Ms >= c * np.maximum(taus, 1e-9)
if np.any(m):
return float(taus[int(np.argmax(m))] * dt)
return float(taus[-1] * dt)
def top1(x):
xc = x - x.mean(axis=0, keepdims=True)
s = np.linalg.svd(xc, compute_uv=False) ** 2
return float(s[0] / max(s.sum(), 1e-12)), float((s.sum() ** 2) / max((s ** 2).sum(), 1e-12))
def main():
t0 = time.time()
W = np.load(V3 / "counts.npy", mmap_mode="r")
idx = pd.read_parquet(V3 / "counts_index.parquet")
print(f"counts {W.shape} | segments {len(idx)}", flush=True)
# ---------------- 逐片段:全段 + 窗口长度扫描
rows = []
for n, (_, r) in enumerate(idx.iterrows()):
x = np.asarray(W[r.start:r.end, :r.n_units], dtype=np.float32)
if x.shape[0] < 600:
continue
t1, eff = top1(x)
# 收敛曲线
curve = {}
for w in WS:
L = min(w, x.shape[0])
if L < 600:
continue
a = weighted_acf(x[:L], MAX_LAG)
curve[str(w)] = {"tau_fit": fit_tau(a), "tau_int": tau_int(a),
"acf1": float(a[1])}
# 分裂信度(把全段一分为二)
h = x.shape[0] // 2
ah = weighted_acf(x[:h], MAX_LAG)
bh = weighted_acf(x[h:], MAX_LAG)
a_all = weighted_acf(x, MAX_LAG)
rows.append({
"idx": int(n), "source": r.source, "region": r.region,
"file": r.file, "n_units": int(r.n_units),
"n_bins": int(x.shape[0]), "hours": round(x.shape[0] * DT / 3.6e6, 2),
"rate": float(x.mean()), "fano": float(x.var() / max(x.mean(), 1e-9)),
"top1": round(t1, 4), "eff_dim": round(eff, 2),
"tau_all_fit": curve.get(str(10**9), {}).get("tau_fit"),
"tau_all_int": curve.get(str(10**9), {}).get("tau_int"),
"acf1_all": curve.get(str(10**9), {}).get("acf1"),
"tau_half1": fit_tau(ah), "tau_half2": fit_tau(bh),
"tau_int_half1": tau_int(ah), "tau_int_half2": tau_int(bh),
"curve": json.dumps(curve),
"n_tau_in_window": round(x.shape[0] * DT / max(curve.get(str(10**9), {}).get("tau_fit") or 1e9, 1e-9), 1),
})
if n % 10 == 0:
print(f" [{n+1}/{len(idx)}] {r.file} {x.shape[0]}b {r.n_units}u "
f"tau={rows[-1]['tau_all_fit']} [{time.time()-t0:.0f}s]", flush=True)
del x
df = pd.DataFrame(rows)
df.to_csv(OUT / "per_segment.csv", index=False)
rep = {"task": "tau_long_convergence", "dt_ms": DT,
"n_segments": int(len(df)), "max_lag_bins": MAX_LAG,
"windows_bins": [w for w in WS]}
# ---------------- Q1 收敛
conv = {}
for w in WS:
vals = []
for c in df.curve:
v = json.loads(c).get(str(w), {}).get("tau_fit")
if v is not None:
vals.append(v)
if vals:
conv[str(w)] = {"n": len(vals), "median": round(float(np.median(vals)), 1),
"p10": round(float(np.percentile(vals, 10)), 1),
"p90": round(float(np.percentile(vals, 90)), 1)}
rep["Q1_convergence_tau_fit"] = conv
# 逐片段:全段 tau 与短窗 tau 的比值
rat = {}
full = [json.loads(c).get(str(10**9), {}).get("tau_fit") for c in df.curve]
for w in WS[:-1]:
vals = []
for c, f in zip(df.curve, full):
v = json.loads(c).get(str(w), {}).get("tau_fit")
if v and f:
vals.append(v / f)
if vals:
rat[str(w)] = {"n": len(vals), "median_ratio_vs_full": round(float(np.median(vals)), 3)}
rep["Q1_ratio_short_over_full"] = rat
# ---------------- Q2 分裂信度
m = df.dropna(subset=["tau_half1", "tau_half2"])
if len(m) > 3:
from scipy.stats import spearmanr, pearsonr
r_p = pearsonr(m.tau_half1, m.tau_half2)
r_s = spearmanr(m.tau_half1, m.tau_half2)
ratio = (m.tau_half1 / m.tau_half2)
# Spearman-Brown 校正
sb = 2 * r_p[0] / (1 + r_p[0]) if r_p[0] > -1 else None
rep["Q2_split_half"] = {
"n": int(len(m)),
"pearson_r": round(float(r_p[0]), 3), "pearson_p": float(r_p[1]),
"spearman_rho": round(float(r_s[0]), 3), "spearman_p": float(r_s[1]),
"spearman_brown": round(float(sb), 3) if sb is not None else None,
"ratio_median": round(float(ratio.median()), 3),
"ratio_iqr": round(float(ratio.quantile(.75) - ratio.quantile(.25)), 3),
}
mi = df.dropna(subset=["tau_int_half1", "tau_int_half2"])
if len(mi) > 3:
from scipy.stats import spearmanr
r = spearmanr(mi.tau_int_half1, mi.tau_int_half2)
rep["Q2_split_half_tau_int"] = {"n": int(len(mi)),
"rho": round(float(r[0]), 3), "p": float(r[1])}
# ---------------- Q3 物种
by = {}
for s, g in df.groupby("source"):
by[s] = {
"n": int(len(g)),
"tau_all_fit_median": round(float(g.tau_all_fit.median()), 1),
"tau_all_fit_p10": round(float(g.tau_all_fit.quantile(.1)), 1),
"tau_all_fit_p90": round(float(g.tau_all_fit.quantile(.9)), 1),
"tau_int_median": round(float(g.tau_all_int.median()), 1),
"top1_median": round(float(g.top1.median()), 4),
"eff_dim_median": round(float(g.eff_dim.median()), 1),
"rate_median": round(float(g.rate.median()), 3),
"fano_median": round(float(g.fano.median()), 3),
"hours_median": round(float(g.hours.median()), 2),
}
rep["Q3_by_source"] = by
m1 = df[df.shared_node == "M1"] if "shared_node" in df else df
g1 = df[(df.region == "M1") & (df.source == "macaque_nlb")].tau_all_fit.dropna()
g2 = df[(df.region == "M1") & (df.source == "human_intracortical")].tau_all_fit.dropna()
if len(g1) > 2 and len(g2) > 2:
from scipy.stats import mannwhitneyu
u = mannwhitneyu(g1, g2)
rep["Q3_m1_between_species"] = {
"macaque_n": int(len(g1)), "macaque_median": round(float(g1.median()), 1),
"human_n": int(len(g2)), "human_median": round(float(g2.median()), 1),
"ratio": round(float(g1.median() / g2.median()), 2),
"mannwhitney_p": float(u.pvalue)}
(OUT / "report.json").write_text(json.dumps(rep, indent=2, ensure_ascii=False),
encoding="utf-8")
print("\n" + json.dumps(rep, indent=2, ensure_ascii=False), flush=True)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
8.74 kB
·
Xet hash:
529b1e3512bd9ec79e65b3d37c59407f4dcb67fd90ed0b3c8fcedf47560779db

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