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.
- kandellab-1.0.0/KandelLab/__init__.py +7 -0
- kandellab-1.0.0/KandelLab/__main__.py +8 -0
- kandellab-1.0.0/KandelLab/cells/__init__.py +8 -0
- kandellab-1.0.0/KandelLab/cells/goldman.py +109 -0
- kandellab-1.0.0/KandelLab/cells/hodgkin_huxley.py +212 -0
- kandellab-1.0.0/KandelLab/cells/lif.py +131 -0
- kandellab-1.0.0/KandelLab/cells/nernst.py +82 -0
- kandellab-1.0.0/KandelLab/cells/synapse.py +139 -0
- kandellab-1.0.0/KandelLab/circuits/__init__.py +7 -0
- kandellab-1.0.0/KandelLab/circuits/hebbian.py +126 -0
- kandellab-1.0.0/KandelLab/circuits/kuramoto.py +101 -0
- kandellab-1.0.0/KandelLab/circuits/lateral_inhibition.py +86 -0
- kandellab-1.0.0/KandelLab/circuits/wilson_cowan.py +160 -0
- kandellab-1.0.0/KandelLab/cognitive/__init__.py +6 -0
- kandellab-1.0.0/KandelLab/cognitive/ddm.py +90 -0
- kandellab-1.0.0/KandelLab/cognitive/encoding.py +105 -0
- kandellab-1.0.0/KandelLab/cognitive/sdt.py +134 -0
- kandellab-1.0.0/KandelLab/config.py +172 -0
- kandellab-1.0.0/KandelLab/experiments.py +598 -0
- kandellab-1.0.0/KandelLab/main.py +91 -0
- kandellab-1.0.0/KandelLab/systems/__init__.py +8 -0
- kandellab-1.0.0/KandelLab/systems/audition.py +128 -0
- kandellab-1.0.0/KandelLab/systems/memory.py +188 -0
- kandellab-1.0.0/KandelLab/systems/motor.py +77 -0
- kandellab-1.0.0/KandelLab/systems/reward.py +158 -0
- kandellab-1.0.0/KandelLab/systems/vision.py +138 -0
- kandellab-1.0.0/KandelLab/utils/__init__.py +1 -0
- kandellab-1.0.0/KandelLab/utils/neuro.py +171 -0
- kandellab-1.0.0/KandelLab/utils/output.py +94 -0
- kandellab-1.0.0/KandelLab/utils/visualization.py +284 -0
- kandellab-1.0.0/LICENSE +21 -0
- kandellab-1.0.0/PKG-INFO +20 -0
- kandellab-1.0.0/kandellab.egg-info/PKG-INFO +20 -0
- kandellab-1.0.0/kandellab.egg-info/SOURCES.txt +39 -0
- kandellab-1.0.0/kandellab.egg-info/dependency_links.txt +1 -0
- kandellab-1.0.0/kandellab.egg-info/entry_points.txt +2 -0
- kandellab-1.0.0/kandellab.egg-info/requires.txt +5 -0
- kandellab-1.0.0/kandellab.egg-info/top_level.txt +1 -0
- kandellab-1.0.0/pyproject.toml +40 -0
- kandellab-1.0.0/setup.cfg +4 -0
- kandellab-1.0.0/tests/test_models.py +242 -0
|
@@ -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
|