Buckets:

redmingwei's picture
download
raw
8.82 kB
#!/usr/bin/env python3
"""exp60 — 用「快速指数 + 平台」重拟合。
exp58 的双指数里,tau_slow 在 78.6% 的片段上撞到 10 s 上界,
即 exp(−τ/tau_slow) ≈ 常数。所以真实形式是
ρ(τ) = A·exp(−τ/t) + c
三个参数,t 可识别,c 是 10 s 内不衰减的部分。
本脚本用这个模型重拟合,并与双指数比较 AIC。若 AIC 相当(4 参 vs 3 参,
ΔAIC < 2 即认为无改进),则平台模型是正确的描述,双指数的 tau_slow 是伪参数。
同时报告:
- A、t、c 的中位数与按物种/任务分组
- t 与放电率、c 与放电率的相关
- 匹配放电率后的物种差异(t 与 c)
"""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.optimize import curve_fit
from scipy.stats import mannwhitneyu, spearmanr
V3 = Path("/domains/counts-v3")
OUT = Path("/domains/runs/plateau")
OUT.mkdir(parents=True, exist_ok=True)
DT = 20.0
MAX_LAG = 500
N_NULL = 3
SEED = 0
def n2(n):
k = 1
while k < n:
k <<= 1
return k
def rho_curve(x, max_lag):
T, d = x.shape
xc = x - x.mean(axis=0, keepdims=True)
N = n2(2 * T)
F = np.fft.rfft(xc, n=N, axis=0)
Fs = F.sum(axis=1)
tot = np.fft.irfft(Fs * np.conj(Fs), n=N)[:max_lag + 1]
dia = np.fft.irfft((np.abs(F) ** 2).sum(axis=1), n=N)[:max_lag + 1]
nrm = np.arange(T, T - max_lag - 1, -1)
tot = tot / np.maximum(nrm, 1)
dia = dia / np.maximum(nrm, 1)
t0 = max(tot[0], 1e-12)
return (tot - dia) / t0, float((tot[0] - dia[0]) / t0)
def null_noise(x, ml, rng, n=N_NULL):
prof = []
for _ in range(n):
xs = np.empty_like(x)
for j in range(x.shape[1]):
xs[:, j] = np.roll(x[:, j], int(rng.integers(1, x.shape[0])))
r, _ = rho_curve(xs, ml)
prof.append(r)
return float(np.sqrt((np.array(prof)[:, 2:] ** 2).mean()))
def m_plateau(t, A, tau, c):
return A * np.exp(-t / np.maximum(tau, 1e-6)) + c
def m_two(t, A1, t1, A2, t2):
return A1 * np.exp(-t / np.maximum(t1, 1e-6)) + A2 * np.exp(-t / np.maximum(t2, 1e-6))
def fit_both(t, y):
out = {}
n = len(t)
for name, fn, p0, bd in [
("plateau", m_plateau, [float(y[0]), 200.0, max(float(y[-1]), 0.0)],
([0.0, 2.0, -1.0], [10.0, 1e5, 1.0])),
("two_exp", m_two, [float(y[0]) * 0.6, 100.0, float(y[0]) * 0.4, 5000.0],
([0.0, 2.0, 0.0, 2.0], [10.0, 1e5, 10.0, 1e7])),
]:
try:
p, _ = curve_fit(fn, t, y, p0=p0, bounds=bd, maxfev=60000)
r = y - fn(t, *p)
out[name] = {"params": [float(v) for v in p],
"aic": float(n * np.log(np.mean(r ** 2) + 1e-20)),
"r2": float(1 - np.var(r) / max(np.var(y), 1e-20))}
except Exception:
pass
return out
def task_of(source, fname):
if source == "human_intracortical":
return "H1_reach_grasp" if fname.startswith("h1") else "H2_handwriting"
if "churchland" in fname:
return "reach"
if "dmfc" in fname:
return "timing"
if "area2" in fname:
return "bump"
return "?"
def main():
import time
t0 = time.time()
W = np.load(V3 / "counts.npy", mmap_mode="r")
idx = pd.read_parquet(V3 / "counts_index.parquet")
rng = np.random.default_rng(SEED)
rows = []
for n, (_, r) in enumerate(idx.iterrows()):
x = np.asarray(W[r.bin_start:r.bin_end, :r.n_units], dtype=np.float32)
if x.shape[0] < 1200 or x.shape[1] < 8:
continue
ml = min(MAX_LAG, x.shape[0] - 2)
noise = null_noise(x, ml, rng)
rho, shared = rho_curve(x, ml)
snr = float(rho[1] / max(noise, 1e-12))
usable = np.flatnonzero((rho > 2 * noise) & np.isfinite(rho))
usable = usable[(usable >= 2) & (usable <= ml)]
if snr < 3.0 or len(usable) < 10:
print(f" [{n+1}/{len(idx)}] {r.file} DROP snr={snr:.1f}", flush=True)
del x
continue
t = usable.astype(float) * DT
y = rho[usable]
f = fit_both(t, y)
best = min(f, key=lambda k: f[k]["aic"]) if f else None
pl = f.get("plateau")
tw = f.get("two_exp")
rows.append({
"source": r.source, "task": task_of(r.source, r.file),
"file": r.file, "region": r.region,
"n_units": int(x.shape[1]), "n_bins": int(x.shape[0]),
"hours": round(x.shape[0] * DT / 3.6e6, 3),
"rate": float(x.mean()), "fano": float(x.var() / max(x.mean(), 1e-9)),
"shared_frac": round(float(shared), 5), "snr": round(snr, 1),
"n_points": int(len(usable)),
"A": pl["params"][0] if pl else None,
"tau_ms": pl["params"][1] if pl else None,
"c": pl["params"][2] if pl else None,
"r2_plateau": round(pl["r2"], 4) if pl else None,
"aic_plateau": round(pl["aic"], 2) if pl else None,
"aic_two": round(tw["aic"], 2) if tw else None,
"best": best,
})
if n % 15 == 0:
print(f" [{n+1}/{len(idx)}] {r.file} snr={snr:.0f} best={best} "
f"tau={rows[-1]['tau_ms'] and round(rows[-1]['tau_ms'],1)} "
f"c={rows[-1]['c'] and round(rows[-1]['c'],3)} "
f"[{time.time()-t0:.0f}s]", flush=True)
del x
d = pd.DataFrame(rows)
d.to_csv(OUT / "per_segment.csv", index=False)
rep = {"task": "plateau_model", "dt_ms": DT, "n": int(len(d))}
def s(c, g=d):
v = g[c].dropna()
if not len(v):
return None
return {"n": int(len(v)), "median": round(float(v.median()), 2),
"p10": round(float(v.quantile(.1)), 2),
"p90": round(float(v.quantile(.9)), 2)}
rep["model_wins"] = d.best.value_counts().to_dict()
rep["aic_delta_two_minus_plateau"] = {
"median": round(float((d.aic_two - d.aic_plateau).median()), 1),
"note": "> 0 表示平台模型(3 参)更好或相当"}
rep["params"] = {c: s(c) for c in ["A", "tau_ms", "c", "r2_plateau", "shared_frac"]}
rep["by_task"] = {k: {"n": int(len(g)),
**{c: s(c, g) for c in ["A", "tau_ms", "c", "rate", "snr"]}}
for k, g in d.groupby("task")}
mac = d[d.source == "macaque_nlb"]
hum = d[d.source == "human_intracortical"]
rep["species"] = {}
for c in ["tau_ms", "c", "A", "shared_frac"]:
a, b = mac[c].dropna(), hum[c].dropna()
if len(a) > 5 and len(b) > 5:
rep["species"][c] = {"macaque": round(float(a.median()), 3),
"human": round(float(b.median()), 3),
"ratio": round(float(a.median() / max(b.median(), 1e-9)), 2),
"p": float(mannwhitneyu(a, b).pvalue)}
# 匹配放电率(低率层:人类全部落在 0.13–0.40)
lo, hi = float(hum.rate.quantile(.05)), float(hum.rate.quantile(.95))
mm = mac[(mac.rate >= lo) & (mac.rate <= hi)]
hh = hum[(hum.rate >= lo) & (hum.rate <= hi)]
rep["rate_matched"] = {"rate_range": [round(lo, 3), round(hi, 3)],
"n_macaque": int(len(mm)), "n_human": int(len(hh))}
for c in ["tau_ms", "c"]:
a, b = mm[c].dropna(), hh[c].dropna()
if len(a) > 3 and len(b) > 3:
rep["rate_matched"][c] = {
"macaque": round(float(a.median()), 2),
"human": round(float(b.median()), 2),
"ratio": round(float(a.median() / max(b.median(), 1e-9)), 2),
"p": float(mannwhitneyu(a, b).pvalue)}
# 相关
rep["correlates"] = {}
for c in ["rate", "snr", "shared_frac", "n_units", "hours", "fano"]:
for y in ["tau_ms", "c"]:
m = d[[y, c]].dropna()
if len(m) > 10:
r_ = spearmanr(m[y], m[c])
rep["correlates"][f"{y}_vs_{c}"] = {"rho": round(float(r_[0]), 3),
"p": float(r_[1]), "n": int(len(m))}
# 猕猴内部
mt = mac[mac.task.isin(["reach", "timing", "bump"])]
rep["macaque_by_task"] = {k: {"n": int(len(g)),
"rate": round(float(g.rate.median()), 3),
"tau_ms": round(float(g.tau_ms.median()), 1),
"c": round(float(g.c.median()), 3)}
for k, g in mt.groupby("task")}
(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.82 kB
·
Xet hash:
f54ea0a8de91f4c33d927b597c71c241ff07f84686757169b85eba7289140b3b

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