kandellab 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. kandellab-1.0.0/KandelLab/__init__.py +7 -0
  2. kandellab-1.0.0/KandelLab/__main__.py +8 -0
  3. kandellab-1.0.0/KandelLab/cells/__init__.py +8 -0
  4. kandellab-1.0.0/KandelLab/cells/goldman.py +109 -0
  5. kandellab-1.0.0/KandelLab/cells/hodgkin_huxley.py +212 -0
  6. kandellab-1.0.0/KandelLab/cells/lif.py +131 -0
  7. kandellab-1.0.0/KandelLab/cells/nernst.py +82 -0
  8. kandellab-1.0.0/KandelLab/cells/synapse.py +139 -0
  9. kandellab-1.0.0/KandelLab/circuits/__init__.py +7 -0
  10. kandellab-1.0.0/KandelLab/circuits/hebbian.py +126 -0
  11. kandellab-1.0.0/KandelLab/circuits/kuramoto.py +101 -0
  12. kandellab-1.0.0/KandelLab/circuits/lateral_inhibition.py +86 -0
  13. kandellab-1.0.0/KandelLab/circuits/wilson_cowan.py +160 -0
  14. kandellab-1.0.0/KandelLab/cognitive/__init__.py +6 -0
  15. kandellab-1.0.0/KandelLab/cognitive/ddm.py +90 -0
  16. kandellab-1.0.0/KandelLab/cognitive/encoding.py +105 -0
  17. kandellab-1.0.0/KandelLab/cognitive/sdt.py +134 -0
  18. kandellab-1.0.0/KandelLab/config.py +172 -0
  19. kandellab-1.0.0/KandelLab/experiments.py +598 -0
  20. kandellab-1.0.0/KandelLab/main.py +91 -0
  21. kandellab-1.0.0/KandelLab/systems/__init__.py +8 -0
  22. kandellab-1.0.0/KandelLab/systems/audition.py +128 -0
  23. kandellab-1.0.0/KandelLab/systems/memory.py +188 -0
  24. kandellab-1.0.0/KandelLab/systems/motor.py +77 -0
  25. kandellab-1.0.0/KandelLab/systems/reward.py +158 -0
  26. kandellab-1.0.0/KandelLab/systems/vision.py +138 -0
  27. kandellab-1.0.0/KandelLab/utils/__init__.py +1 -0
  28. kandellab-1.0.0/KandelLab/utils/neuro.py +171 -0
  29. kandellab-1.0.0/KandelLab/utils/output.py +94 -0
  30. kandellab-1.0.0/KandelLab/utils/visualization.py +284 -0
  31. kandellab-1.0.0/LICENSE +21 -0
  32. kandellab-1.0.0/PKG-INFO +20 -0
  33. kandellab-1.0.0/kandellab.egg-info/PKG-INFO +20 -0
  34. kandellab-1.0.0/kandellab.egg-info/SOURCES.txt +39 -0
  35. kandellab-1.0.0/kandellab.egg-info/dependency_links.txt +1 -0
  36. kandellab-1.0.0/kandellab.egg-info/entry_points.txt +2 -0
  37. kandellab-1.0.0/kandellab.egg-info/requires.txt +5 -0
  38. kandellab-1.0.0/kandellab.egg-info/top_level.txt +1 -0
  39. kandellab-1.0.0/pyproject.toml +40 -0
  40. kandellab-1.0.0/setup.cfg +4 -0
  41. kandellab-1.0.0/tests/test_models.py +242 -0
@@ -0,0 +1,7 @@
1
+ """KandelLab — 神经科学原理仿真系统。
2
+
3
+ 细胞 → 回路 → 系统 → 认知,四层递进,12 个核心概念的 Python 从零实现。
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+ __all__ = ["__version__"]
@@ -0,0 +1,8 @@
1
+ """支持 `python -m KandelLab` 运行 CLI。"""
2
+
3
+ import sys
4
+
5
+ from .main import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,8 @@
1
+ """cells — 细胞层:单神经元的电生理模型。
2
+
3
+ nernst 离子平衡电位
4
+ goldman GHK 方程静息电位
5
+ hodgkin_huxley 动作电位
6
+ lif 漏电积分发放模型
7
+ synapse 突触 PSP 与时空总和
8
+ """
@@ -0,0 +1,109 @@
1
+ """Goldman–Hodgkin–Katz(GHK)方程:通透性加权的静息电位。
2
+
3
+ 核心概念 #2:静息电位由多种离子的相对通透性共同决定。
4
+
5
+ 模型
6
+ ----
7
+ V = (RT/F) · ln((P_K[K]_o + P_Na[Na]_o + P_Cl[Cl]_i)
8
+ / (P_K[K]_i + P_Na[Na]_i + P_Cl[Cl]_o))
9
+
10
+ P_X 为相对通透性;Cl⁻ 因价态为 −1 而取内外翻转。
11
+
12
+ 验证锚点:
13
+ 单离子极限(其余 P=0)→ 还原为 Nernst 方程;
14
+ 生理通透性下静息电位 ≈ −70 mV。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import numpy as np
20
+
21
+ from .. import config
22
+
23
+
24
+ def goldman_voltage(permeabilities, c_out, c_in, T=None, z=None):
25
+ """GHK 方程计算静息电位(mV)。
26
+
27
+ Parameters
28
+ ----------
29
+ permeabilities : dict[str, float]
30
+ {"K": P_K, "Na": P_Na, "Cl": P_Cl, ...},值为相对通透性。
31
+ c_out, c_in : dict[str, float]
32
+ {"K": …, "Na": …, "Cl": …} 膜外/膜内浓度(mM)。
33
+ T : float | None
34
+ 绝对温度(K)。
35
+ z : dict[str, int] | None
36
+ 各离子价态;None 时使用 config.ION_CONCENTRATIONS 的价态。
37
+
38
+ Returns
39
+ -------
40
+ float : 静息电位(mV)。
41
+ """
42
+ T = config.T_KELVIN if T is None else T
43
+ rt_f = config.R * T / config.F * 1000.0 # mV
44
+
45
+ if z is None:
46
+ z = {ion: config.ION_CONCENTRATIONS[ion]["z"] for ion in permeabilities}
47
+
48
+ numer = 0.0
49
+ denom = 0.0
50
+ for ion, P in permeabilities.items():
51
+ zi = z[ion]
52
+ co = c_out[ion]
53
+ ci = c_in[ion]
54
+ if zi > 0:
55
+ numer += P * co
56
+ denom += P * ci
57
+ else:
58
+ numer += P * ci
59
+ denom += P * co
60
+
61
+ if numer <= 0 or denom <= 0:
62
+ raise ValueError("GHK 分子/分母必须为正(所有 P 均为 0 时无定义)")
63
+ return rt_f * np.log(numer / denom)
64
+
65
+
66
+ def resting_potential(T=None):
67
+ """使用 config 生理浓度与默认通透性计算静息电位(mV)。"""
68
+ perm = dict(config.PERMEABILITIES)
69
+ conc = config.ION_CONCENTRATIONS
70
+ c_out = {ion: conc[ion]["o"] for ion in perm}
71
+ c_in = {ion: conc[ion]["i"] for ion in perm}
72
+ return goldman_voltage(perm, c_out, c_in, T)
73
+
74
+
75
+ def single_ion_limit(ion, T=None):
76
+ """计算"仅某离子通透"时的极限电位,应等于该离子 Nernst 电位。"""
77
+ conc = config.ION_CONCENTRATIONS
78
+ perm = {ion: 1.0}
79
+ c_out = {ion: conc[ion]["o"]}
80
+ c_in = {ion: conc[ion]["i"]}
81
+ return goldman_voltage(perm, c_out, c_in, T)
82
+
83
+
84
+ def permeability_scan(na_frac, T=None, cl_perm=None):
85
+ """扫描 Na⁺ 相对通透性(PK=1)对静息电位的影响。
86
+
87
+ Parameters
88
+ ----------
89
+ na_frac : array_like
90
+ P_Na 取值序列。
91
+ T : float | None
92
+ cl_perm : float | None
93
+ P_Cl 固定值;None 使用 config 默认 0.45。
94
+
95
+ Returns
96
+ -------
97
+ (na_frac, V) : 通透性序列与对应电位。
98
+ """
99
+ conc = config.ION_CONCENTRATIONS
100
+ c_out = {ion: conc[ion]["o"] for ion in ("K", "Na", "Cl")}
101
+ c_in = {ion: conc[ion]["i"] for ion in ("K", "Na", "Cl")}
102
+ if cl_perm is None:
103
+ cl_perm = config.PERMEABILITIES["Cl"]
104
+ na_frac = np.asarray(na_frac, dtype=float)
105
+ V = np.array([
106
+ goldman_voltage({"K": 1.0, "Na": p, "Cl": cl_perm}, c_out, c_in, T)
107
+ for p in na_frac
108
+ ])
109
+ return na_frac, V
@@ -0,0 +1,212 @@
1
+ """Hodgkin–Huxley(HH)模型:动作电位的电压门控离子通道动力学。
2
+
3
+ 核心概念 #3:动作电位是电压门控 Na⁺/K⁺ 通道的协同动力学产物。
4
+
5
+ 模型(经典 4 变量 ODE,HH 1952,单位 mV / ms / µA/cm²)
6
+ ------------------------------------------------------
7
+ C_m · dV/dt = −(g_Na·m³·h·(V−E_Na) + g_K·n⁴·(V−E_K) + g_L·(V−E_L)) + I_ext
8
+ dm/dt = α_m(V)(1−m) − β_m(V)·m (Na 激活)
9
+ dh/dt = α_h(V)(1−h) − β_h(V)·h (Na 失活)
10
+ dn/dt = α_n(V)(1−n) − β_n(V)·n (K 激活)
11
+
12
+ 门控速率函数(V 以 mV 计,经典取值):
13
+ α_m = 0.1(V+40)/(1−exp(−(V+40)/10)) β_m = 4·exp(−(V+65)/18)
14
+ α_h = 0.07·exp(−(V+65)/20) β_h = 1/(1+exp(−(V+35)/10))
15
+ α_n = 0.01(V+55)/(1−exp(−(V+55)/10)) β_n = 0.125·exp(−(V+65)/80)
16
+
17
+ 验证锚点:静息 ≈ −65 mV、峰 ≈ +35 mV、阈值 ≈ −55 mV、绝对不应期 ≈ 2 ms。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import numpy as np
23
+
24
+ from .. import config
25
+ from ..utils.neuro import integrate_ode, detect_spikes
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # 门控速率函数
30
+ # ---------------------------------------------------------------------------
31
+ def alpha_m(v):
32
+ v = np.asarray(v, dtype=float)
33
+ z = (v + 40.0) / 10.0
34
+ with np.errstate(over="ignore", invalid="ignore"):
35
+ denom = 1.0 - np.exp(-z)
36
+ zz = np.where(np.abs(z) < 1e-8, 1e-8, z) # z→0 极限 = 1
37
+ out = zz / denom
38
+ return np.where(np.isfinite(out), out, 0.0)
39
+
40
+
41
+ def beta_m(v):
42
+ return 4.0 * np.exp(-(np.asarray(v, dtype=float) + 65.0) / 18.0)
43
+
44
+
45
+ def alpha_h(v):
46
+ return 0.07 * np.exp(-(np.asarray(v, dtype=float) + 65.0) / 20.0)
47
+
48
+
49
+ def beta_h(v):
50
+ v = np.asarray(v, dtype=float)
51
+ return 1.0 / (1.0 + np.exp(-(v + 35.0) / 10.0))
52
+
53
+
54
+ def alpha_n(v):
55
+ v = np.asarray(v, dtype=float)
56
+ z = (v + 55.0) / 10.0
57
+ with np.errstate(over="ignore", invalid="ignore"):
58
+ denom = 1.0 - np.exp(-z)
59
+ zz = np.where(np.abs(z) < 1e-8, 1e-8, z) # z→0 极限 = 0.1
60
+ out = 0.1 * zz / denom
61
+ return np.where(np.isfinite(out), out, 0.0)
62
+
63
+
64
+ def beta_n(v):
65
+ return 0.125 * np.exp(-(np.asarray(v, dtype=float) + 65.0) / 80.0)
66
+
67
+
68
+ def gate_steady_state(v):
69
+ """各门控的稳态值(m∞, h∞, n∞)与时间常数。"""
70
+ v = np.asarray(v, dtype=float)
71
+ a_m, b_m = alpha_m(v), beta_m(v)
72
+ a_h, b_h = alpha_h(v), beta_h(v)
73
+ a_n, b_n = alpha_n(v), beta_n(v)
74
+ return {
75
+ "m_inf": a_m / (a_m + b_m),
76
+ "h_inf": a_h / (a_h + b_h),
77
+ "n_inf": a_n / (a_n + b_n),
78
+ "tau_m": 1.0 / (a_m + b_m),
79
+ "tau_h": 1.0 / (a_h + b_h),
80
+ "tau_n": 1.0 / (a_n + b_n),
81
+ }
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # HH 动力学
86
+ # ---------------------------------------------------------------------------
87
+ class HodgkinHuxley:
88
+ """HH 模型封装。默认参数来自 config.HH_DEFAULTS。"""
89
+
90
+ def __init__(self, **kwargs):
91
+ p = dict(config.HH_DEFAULTS)
92
+ p.update(kwargs)
93
+ self.C_m = p["C_m"]
94
+ self.g_Na = p["g_Na"]
95
+ self.g_K = p["g_K"]
96
+ self.g_L = p["g_L"]
97
+ self.E_Na = p["E_Na"]
98
+ self.E_K = p["E_K"]
99
+ self.E_L = p["E_L"]
100
+ self.V_rest = p["V_rest"]
101
+
102
+ # -- 门控动力学(供 integrate_ode 使用的向量场) -------------------
103
+ def dvdt(self, v, m, h, n, i_ext):
104
+ i_na = self.g_Na * m ** 3 * h * (v - self.E_Na)
105
+ i_k = self.g_K * n ** 4 * (v - self.E_K)
106
+ i_l = self.g_L * (v - self.E_L)
107
+ return (-(i_na + i_k + i_l) + i_ext) / self.C_m
108
+
109
+ def vector_field(self, y, t, i_ext_fn=None):
110
+ v, m, h, n = y
111
+ i_ext = 0.0 if i_ext_fn is None else i_ext_fn(t)
112
+ dm = alpha_m(v) * (1.0 - m) - beta_m(v) * m
113
+ dh = alpha_h(v) * (1.0 - h) - beta_h(v) * h
114
+ dn = alpha_n(v) * (1.0 - n) - beta_n(v) * n
115
+ return np.array([self.dvdt(v, m, h, n, i_ext), dm, dh, dn])
116
+
117
+ def initial_state(self):
118
+ g = gate_steady_state(self.V_rest)
119
+ return np.array([self.V_rest, g["m_inf"], g["h_inf"], g["n_inf"]])
120
+
121
+ def simulate(self, t_max, dt=None, i_ext_fn=None, v0=None, method="rk4"):
122
+ """模拟 HH 模型。
123
+
124
+ Parameters
125
+ ----------
126
+ t_max : float
127
+ 仿真时长(ms)。
128
+ dt : float | None
129
+ 时间步长。
130
+ i_ext_fn : callable | None
131
+ i_ext(t) 返回注入电流密度(µA/cm²)。
132
+ v0 : array_like | None
133
+ 初值 [V, m, h, n];None 用静息稳态。
134
+ method : str
135
+ "rk4" / "euler"。
136
+
137
+ Returns
138
+ -------
139
+ t : np.ndarray
140
+ y : np.ndarray (N, 4),列为 V, m, h, n
141
+ """
142
+ y0 = self.initial_state() if v0 is None else np.asarray(v0, dtype=float)
143
+ t, y = integrate_ode(self.vector_field, y0, t_max, dt, method,
144
+ i_ext_fn=i_ext_fn)
145
+ return t, y
146
+
147
+ def spikes(self, t, y, v_thresh=None, refractory_ms=1.0):
148
+ v = y[:, 0]
149
+ dt = t[1] - t[0]
150
+ return detect_spikes(v, v_thresh, dt, refractory_ms)
151
+
152
+ def fI_curve(self, currents, t_max=120.0, dt=0.01, t_stim=(10.0, 110.0),
153
+ method="rk4"):
154
+ """不同注入电流下的发放率(Hz)。电流为恒定阶跃。
155
+
156
+ Returns
157
+ -------
158
+ (I, f) : 电流序列与发放率序列。
159
+ """
160
+ I = np.asarray(currents, dtype=float)
161
+ f = np.empty_like(I)
162
+ for i, cur in enumerate(I):
163
+ def i_ext_fn(tt):
164
+ return cur if t_stim[0] <= tt <= t_stim[1] else 0.0
165
+ t, y = self.simulate(t_max, dt, i_ext_fn=i_ext_fn, method=method)
166
+ spikes = self.spikes(t, y)
167
+ duration = t_stim[1] - t_stim[0]
168
+ spikes = spikes[(spikes >= t_stim[0]) & (spikes <= t_stim[1])]
169
+ f[i] = spikes.size / (duration / 1000.0)
170
+ return I, f
171
+
172
+
173
+ def threshold_finder(hh, t_max=60.0, dt=0.01, amp_scan=(0.0, 12.0, 40),
174
+ pulse_ms=1.0, t_on=5.0, amp_tol=0.01):
175
+ """用脉冲电流扫描确定发放阈值(mV)。
176
+
177
+ 做法:在静息电位基础上给一个短促电流脉冲,二分搜索临界电流幅度
178
+ (恰好触发动作电位的注入电流),返回临界电流下亚阈值响应的
179
+ 峰值膜电位,作为阈值电压估计。对经典 HH 参数应 ≈ −55 mV。
180
+
181
+ amp_tol : float
182
+ 二分搜索的电流精度(mV);防止过度逼近临界点导致
183
+ 亚阈值峰值被 RK4 数值误差放大。
184
+ """
185
+ lo, hi = amp_scan[0], amp_scan[1]
186
+ v_peak_sub = None
187
+ for _ in range(amp_scan[2]):
188
+ mid = 0.5 * (lo + hi)
189
+
190
+ def i_ext_fn(tt):
191
+ return mid if t_on <= tt <= t_on + pulse_ms else 0.0
192
+
193
+ t, y = hh.simulate(t_max, dt, i_ext_fn=i_ext_fn, method="rk4")
194
+ sp = hh.spikes(t, y, refractory_ms=0.5)
195
+ has_spike = sp.size > 0
196
+ if has_spike:
197
+ hi = mid
198
+ else:
199
+ v_peak_sub = float(y[:, 0].max())
200
+ lo = mid
201
+ if hi - lo < amp_tol:
202
+ break
203
+ return v_peak_sub if v_peak_sub is not None else 0.5 * (lo + hi)
204
+
205
+
206
+ def step_current(amp, t_on=5.0, t_off=np.inf):
207
+ """构造阶跃电流函数 i_ext(t),在 [t_on, t_off) 注入恒定 amp。"""
208
+
209
+ def fn(t):
210
+ return amp if t_on <= t < t_off else 0.0
211
+
212
+ return fn
@@ -0,0 +1,131 @@
1
+ """漏电积分发放(Leaky Integrate-and-Fire, LIF)模型。
2
+
3
+ 核心概念 #4:神经元以脉冲序列(频率编码)传递信息。
4
+
5
+ 模型
6
+ ----
7
+ τ_m · dV/dt = −(V − E_L) + R_m · I(t)
8
+
9
+ V ≥ V_th 时发放一次并复位到 V_reset(含绝对不应期 τ_ref)。
10
+
11
+ 解析发放率(恒定电流 I,V∞ = E_L + R·I):
12
+ f = 1 / (τ_ref + τ_m · ln((V∞ − V_reset) / (V∞ − V_th)))
13
+
14
+ 验证锚点:数值模拟的 f-I 曲线与解析公式逐点吻合。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import numpy as np
20
+
21
+ from .. import config
22
+ from ..utils.neuro import detect_spikes
23
+
24
+
25
+ class LIF:
26
+ """LIF 神经元。默认参数来自 config.LIF_DEFAULTS。"""
27
+
28
+ def __init__(self, **kwargs):
29
+ p = dict(config.LIF_DEFAULTS)
30
+ p.update(kwargs)
31
+ self.tau_m = p["tau_m"]
32
+ self.R_m = p["R_m"]
33
+ self.E_L = p["E_L"]
34
+ self.V_th = p["V_th"]
35
+ self.V_reset = p["V_reset"]
36
+ self.V_peak = p["V_peak"]
37
+ self.tau_ref = p["tau_ref"]
38
+
39
+ # -- 数值模拟 -------------------------------------------------------
40
+ def simulate(self, I, t_max, dt=0.01, v0=None):
41
+ """恒定/时变电流下的仿真。
42
+
43
+ Parameters
44
+ ----------
45
+ I : float | np.ndarray
46
+ 恒定电流(nA)或与 t 对齐的电流序列。
47
+ t_max : float
48
+ 时长(ms)。
49
+ dt : float
50
+ 步长(ms)。
51
+ v0 : float | None
52
+ 初始电位;None 用 E_L。
53
+
54
+ Returns
55
+ -------
56
+ t : np.ndarray
57
+ v : np.ndarray
58
+ spike_times : np.ndarray
59
+ """
60
+ n = int(np.ceil(t_max / dt)) + 1
61
+ t = np.arange(n) * dt
62
+ v = np.empty(n)
63
+ if np.isscalar(I):
64
+ I = np.full(n, float(I))
65
+ I = np.asarray(I, dtype=float)
66
+ if I.ndim == 0:
67
+ I = np.full(n, float(I))
68
+
69
+ v[0] = self.E_L if v0 is None else v0
70
+ spike_times = []
71
+ ref_end = -1.0
72
+ for i in range(n - 1):
73
+ tt = t[i]
74
+ if i < ref_end:
75
+ v[i] = self.V_reset
76
+ v[i + 1] = self.V_reset
77
+ continue
78
+ v_inf = self.E_L + self.R_m * I[i]
79
+ dv = (-(v[i] - self.E_L) + self.R_m * I[i]) / self.tau_m
80
+ v[i + 1] = v[i] + dt * dv
81
+ if v[i + 1] >= self.V_th:
82
+ v[i + 1] = self.V_peak
83
+ spike_times.append(tt + dt)
84
+ ref_end = i + 1 + int(np.ceil(self.tau_ref / dt))
85
+ return t, v, np.array(spike_times)
86
+
87
+ # -- 解析 -----------------------------------------------------------
88
+ def analytical_rate(self, I):
89
+ """恒定电流下的解析发放率(Hz)。I 低于阈时返回 0。"""
90
+ v_inf = self.E_L + self.R_m * float(I)
91
+ if v_inf <= self.V_th:
92
+ return 0.0
93
+ t_s = self.tau_m * np.log((v_inf - self.V_reset) / (v_inf - self.V_th))
94
+ if t_s < 0:
95
+ return 0.0
96
+ return 1.0 / ((self.tau_ref + t_s) / 1000.0)
97
+
98
+ def rheobase(self):
99
+ """基强度电流(nA):恰好使 V∞ = V_th。"""
100
+ return (self.V_th - self.E_L) / self.R_m
101
+
102
+ def fI_curve(self, currents, t_max=1000.0, dt=0.01, burn_ms=100.0):
103
+ """数值 f-I 曲线(Hz),burn_ms 用于丢弃起始瞬态。"""
104
+ I = np.asarray(currents, dtype=float)
105
+ f_num = np.empty_like(I)
106
+ for k, cur in enumerate(I):
107
+ t, v, sp = self.simulate(float(cur), t_max, dt)
108
+ sp = sp[sp >= burn_ms]
109
+ f_num[k] = sp.size / ((t_max - burn_ms) / 1000.0)
110
+ f_ana = np.array([self.analytical_rate(float(c)) for c in I])
111
+ return I, f_num, f_ana
112
+
113
+
114
+ def raster_simulation(currents, n_trials=20, t_max=300.0, dt=0.01, seed=None,
115
+ **lif_kwargs):
116
+ """多次试验的栅栏图模拟:返回 {电流: spike_times 列表}。
117
+
118
+ 通过在每个 trial 添加微小高斯噪声模拟响应变异性。
119
+ """
120
+ from ..utils.neuro import rng
121
+ r = rng(seed)
122
+ lif = LIF(**lif_kwargs)
123
+ out = {}
124
+ for cur in currents:
125
+ trial_spikes = []
126
+ for _ in range(n_trials):
127
+ noisy = cur + r.normal(0.0, abs(cur) * 0.05 + 0.1)
128
+ t, v, sp = lif.simulate(noisy, t_max, dt)
129
+ trial_spikes.append(sp)
130
+ out[float(cur)] = trial_spikes
131
+ return out
@@ -0,0 +1,82 @@
1
+ """Nernst 方程:离子浓度差决定平衡电位。
2
+
3
+ 核心概念 #1:细胞膜两侧离子浓度差 + 选择性通透 → 膜电位。
4
+
5
+ 模型
6
+ ----
7
+ E = (RT / zF) · ln([X]_o / [X]_i)
8
+
9
+ 其中 R 为气体常数、T 为绝对温度、z 为离子价态、F 为法拉第常数、
10
+ [X]_o / [X]_i 为膜外/膜内浓度。
11
+
12
+ 教科书锚点(37 °C,Kandel 标准浓度):
13
+ K⁺ ≈ −90 mV,Na⁺ ≈ +60 mV,Cl⁻ ≈ −90 mV,Ca²⁺ ≈ +132 mV
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import numpy as np
19
+
20
+ from .. import config
21
+
22
+
23
+ def nernst_potential(z, c_out, c_in, T=None):
24
+ """计算单个离子的 Nernst 平衡电位(mV)。
25
+
26
+ Parameters
27
+ ----------
28
+ z : int | float
29
+ 离子价态(K⁺/Na⁺ 取 +1,Ca²⁺ 取 +2,Cl⁻ 取 −1)。
30
+ c_out, c_in : float
31
+ 膜外 / 膜内浓度(mM,同单位即可)。
32
+ T : float | None
33
+ 绝对温度(K);None 使用 config 生理温度 37 °C。
34
+
35
+ Returns
36
+ -------
37
+ float : 平衡电位(mV)。
38
+ """
39
+ if z == 0:
40
+ raise ValueError("z 不能为 0")
41
+ if c_in <= 0 or c_out <= 0:
42
+ raise ValueError("浓度必须为正数")
43
+ T = config.T_KELVIN if T is None else T
44
+ rt_f = config.R * T / config.F # 单位 V
45
+ return rt_f * 1000.0 / z * np.log(c_out / c_in)
46
+
47
+
48
+ def ion_equilibrium(ion, T=None):
49
+ """使用 config 默认浓度计算某离子的平衡电位(mV)。
50
+
51
+ Parameters
52
+ ----------
53
+ ion : str
54
+ "K" / "Na" / "Ca" / "Cl"。
55
+ """
56
+ if ion not in config.ION_CONCENTRATIONS:
57
+ raise KeyError(f"未知离子: {ion}")
58
+ spec = config.ION_CONCENTRATIONS[ion]
59
+ return nernst_potential(spec["z"], spec["o"], spec["i"], T)
60
+
61
+
62
+ def all_equilibria(T=None):
63
+ """返回全部离子的平衡电位 dict。"""
64
+ return {ion: ion_equilibrium(ion, T) for ion in config.ION_CONCENTRATIONS}
65
+
66
+
67
+ def concentration_scan(ion, c_out_range, T=None):
68
+ """扫描膜外浓度并返回 (浓度数组, 电位数组)。
69
+
70
+ 用于实验 #1:改变 [K⁺]_o → 观察静息电位(Nernst 预测)。
71
+ """
72
+ spec = config.ION_CONCENTRATIONS[ion]
73
+ c_out = np.asarray(c_out_range, dtype=float)
74
+ E = np.array([nernst_potential(spec["z"], co, spec["i"], T) for co in c_out])
75
+ return c_out, E
76
+
77
+
78
+ def temperature_scan(c_out, c_in, z=1, T_range=(280.0, 320.0, 41)):
79
+ """温度扫描:返回 (温度数组, 电位数组)。"""
80
+ T = np.linspace(T_range[0], T_range[1], T_range[2])
81
+ E = np.array([nernst_potential(z, c_out, c_in, Ti) for Ti in T])
82
+ return T, E
@@ -0,0 +1,139 @@
1
+ """突触模型:EPSP / IPSP 与时间、空间总和。
2
+
3
+ 核心概念 #5:突触输入在时间与空间上整合,决定神经元是否发放。
4
+
5
+ 模型
6
+ ----
7
+ PSP(t) = w · (t/τ) · exp(−t/τ) (α 函数,t ≥ 0)
8
+
9
+ 多突触事件的响应线性叠加(在亚阈值范围内近似成立):
10
+ V(t) = Σ_i w_i · PSP(t − t_i)
11
+
12
+ 验证锚点:
13
+ 时间总和:ISI 短 → 双脉冲峰值 > 单脉冲峰值;
14
+ 空间总和:多输入同刻 → 幅度随输入数近似线性增长。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import numpy as np
20
+
21
+ from .. import config
22
+
23
+
24
+ def psp_alpha(t, w=1.0, tau=None):
25
+ """α 函数型突触后电位。
26
+
27
+ Parameters
28
+ ----------
29
+ t : array_like
30
+ 时间(ms)。
31
+ w : float
32
+ 突触权重(幅度;IPSP 传负值)。
33
+ tau : float | None
34
+ 时间常数(ms);None 用 config.SYNAPSE_DEFAULTS["tau_decay"]。
35
+
36
+ Returns
37
+ -------
38
+ np.ndarray
39
+ """
40
+ tau = config.SYNAPSE_DEFAULTS["tau_decay"] if tau is None else tau
41
+ t = np.asarray(t, dtype=float)
42
+ out = np.zeros_like(t)
43
+ pos = t >= 0
44
+ out[pos] = w * (t[pos] / tau) * np.exp(-t[pos] / tau)
45
+ return out
46
+
47
+
48
+ def psp_alpha_peak(w, tau):
49
+ """α 函数峰值幅度(归一化到 w)。"""
50
+ return w * np.exp(-1.0)
51
+
52
+
53
+ def psp_peak_time(tau):
54
+ """α 函数到达峰值的时间(ms)。"""
55
+ return tau
56
+
57
+
58
+ def simulate_psp(spike_times, t, w=1.0, tau=None):
59
+ """对单一输入的多次发放做时间总和(线性叠加)。
60
+
61
+ Parameters
62
+ ----------
63
+ spike_times : array_like
64
+ 发放时刻(ms)。
65
+ t : array_like
66
+ 观测时间网格(ms)。
67
+ w, tau : float
68
+ 权重与时间常数。
69
+
70
+ Returns
71
+ -------
72
+ np.ndarray : V(t)
73
+ """
74
+ t = np.asarray(t, dtype=float)
75
+ v = np.zeros_like(t)
76
+ for st in np.atleast_1d(spike_times):
77
+ v = v + psp_alpha(t - st, w, tau)
78
+ return v
79
+
80
+
81
+ def temporal_sum_peak(isi, w=1.0, tau=None, dt=0.01):
82
+ """双脉冲(间隔 ISI)时间总和后的峰值电位。
83
+
84
+ Returns
85
+ -------
86
+ float : 叠加后峰值(mV)。
87
+ """
88
+ tau = config.SYNAPSE_DEFAULTS["tau_decay"] if tau is None else tau
89
+ t = np.arange(0.0, max(8 * tau, isi + 6 * tau), dt)
90
+ v = simulate_psp([0.0, isi], t, w, tau)
91
+ return float(v.max())
92
+
93
+
94
+ def temporal_sum_scan(isis, w=1.0, tau=None, dt=0.01):
95
+ """扫描双脉冲间隔:返回 (ISI 序列, 峰值序列, 单脉冲峰值)。"""
96
+ isis = np.asarray(isis, dtype=float)
97
+ single = psp_alpha_peak(w, config.SYNAPSE_DEFAULTS["tau_decay"] if tau is None else tau)
98
+ peaks = np.array([temporal_sum_peak(isi, w, tau, dt) for isi in isis])
99
+ return isis, peaks, single
100
+
101
+
102
+ def spatial_sum(spike_times_list, t, weights=None, taus=None):
103
+ """多输入(每条输入一个发放时刻序列)的空间总和。
104
+
105
+ Parameters
106
+ ----------
107
+ spike_times_list : list[array_like]
108
+ 每条输入对应的发放时刻。
109
+ t : array_like
110
+ 时间网格。
111
+ weights : list[float] | None
112
+ 各输入权重;None 全为 1。
113
+ taus : list[float] | None
114
+ 各输入时间常数;None 全用默认。
115
+
116
+ Returns
117
+ -------
118
+ np.ndarray : V(t)
119
+ """
120
+ n_in = len(spike_times_list)
121
+ if weights is None:
122
+ weights = [1.0] * n_in
123
+ if taus is None:
124
+ taus = [None] * n_in
125
+ t = np.asarray(t, dtype=float)
126
+ v = np.zeros_like(t)
127
+ for spikes, w, tau in zip(spike_times_list, weights, taus):
128
+ v = v + simulate_psp(spikes, t, w, tau)
129
+ return v
130
+
131
+
132
+ def spatial_sum_peak(spike_times_list, t, weights=None, taus=None):
133
+ """多输入空间总和后的峰值。"""
134
+ return float(spatial_sum(spike_times_list, t, weights, taus).max())
135
+
136
+
137
+ def isi_to_firing(isi):
138
+ """由发放间隔(ms)换算发放率(Hz)。"""
139
+ return 1000.0 / float(isi) if isi > 0 else np.inf