Skip to content

Commit 30edf68

Browse files
committed
add stepwise functionality, tests, notebook
1 parent 7496336 commit 30edf68

File tree

5 files changed

+461
-19
lines changed

5 files changed

+461
-19
lines changed

notebooks/stepwise.ipynb

Lines changed: 379 additions & 0 deletions
Large diffs are not rendered by default.

poetry.lock

Lines changed: 35 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ nbdev = "^2.3.12"
3737
brian2 = "^2.5.4"
3838
nbmake = "^1.5.0"
3939
jupyter = "^1.0.0"
40+
pytest-xdist = "^3.5.0"
4041

4142
[build-system]
4243
requires = ["poetry-core"]
4344
build-backend = "poetry.core.masonry.api"
4445

4546
[tool.pytest.ini_options]
4647
addopts = "-q --nbmake"
47-
testpaths = ["tests", "README.ipynb"]
48+
testpaths = ["tests", "README.ipynb", "notebooks/real_time.ipynb"]

tests/test_wslfp.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
([1, 5, 9, 12, 17], [1, 3, 5, 9, 12], [], False),
2727
([2, 4, 6, 23, 25], [7, 10, 15, 21, 25], [12, 20, 24], True),
2828
([2, 3, 4, 5, 6], [50, 60, 70, 80, 90], [70, 100], False),
29+
([0], [6], [6], True),
30+
([6], [6], [6], False),
31+
([6], [0], [6], False),
2932
],
3033
)
3134
def test_calculate(t_ampa, t_gaba, t_eval, success, n_elec, seed):
@@ -38,8 +41,6 @@ def test_calculate(t_ampa, t_gaba, t_eval, success, n_elec, seed):
3841
I_gaba = -np.arange(len(t_gaba)) - 2
3942
I_gaba = I_gaba[:, np.newaxis] - np.arange(n_src)
4043

41-
# xs = ys = zs = np.array([1, 2, 3])
42-
# source_coords = np.column_stack((xs, ys, zs))
4344
source_coords = rng.uniform(-10, 10, (n_src, 3))
4445
elec_coords = rng.uniform(-500, 500, (n_elec, 3))
4546

@@ -56,7 +57,7 @@ def test_calculate(t_ampa, t_gaba, t_eval, success, n_elec, seed):
5657
)
5758
), "LFP increasing order does not match expectation"
5859
else:
59-
with pytest.raises(ValueError):
60+
with pytest.raises(Exception):
6061
calc.calculate(t_eval, t_ampa, I_ampa, t_gaba, I_gaba)
6162

6263

wslfp/__init__.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ def n_sources(self) -> int:
8080
def _interp_currents(self, t_ms, I, delay_ms, t_eval_ms):
8181
if not np.all(np.diff(t_ms) > 0):
8282
raise ValueError("t_ms must be monotonically increasing")
83+
if len(t_ms) == 0:
84+
return np.zeros((len(t_eval_ms), self.n_sources))
8385

8486
t_eval_delayed = np.subtract(t_eval_ms, delay_ms)
8587
t_needed = (np.min(t_eval_delayed), np.max(t_eval_delayed))
@@ -99,7 +101,12 @@ def _interp_currents(self, t_ms, I, delay_ms, t_eval_ms):
99101
f"Needed [{t_needed[0]}, {t_needed[1]}] ms, "
100102
f"provided [{t_provided[0]}, {t_provided[1]}] ms."
101103
)
102-
I_interp = PchipInterpolator(t_ms, I, extrapolate=False)(t_eval_delayed)
104+
if len(t_ms) > 1:
105+
interpolator = PchipInterpolator(t_ms, I, extrapolate=False)
106+
elif len(t_ms) == 1:
107+
interpolator = lambda t_eval: (t_eval == t_ms[0]) * I[0:1]
108+
109+
I_interp = interpolator(t_eval_delayed)
103110
assert I_interp.shape == (len(t_eval_ms), self.n_sources)
104111
I_interp[np.isnan(I_interp)] = 0
105112
return I_interp
@@ -112,6 +119,7 @@ def calculate(
112119
t_gaba_ms: np.ndarray,
113120
I_gaba: np.ndarray,
114121
normalize: bool = True,
122+
wsum_mean_std_for_norm: tuple[np.ndarray, np.ndarray] = None,
115123
) -> np.ndarray:
116124
"""Calculate WSLFP at requested times for initialized coordinates given currents
117125
@@ -124,8 +132,12 @@ def calculate(
124132
in milliseconds
125133
I_gaba (np.ndarray): GABAergic currents, shape (len(t_ampa_ms), n_sources)
126134
normalize (bool, optional): Whether to normalize to mean of 0 and variance of 1.
127-
The main reason not to normalize is if you are computing one time step at a time.
128-
Defaults to True.
135+
The main reason not to normalize is if you are computing one time step at a time
136+
(see `notebooks/stepwise.ipynb`). Defaults to True.
137+
wsum_mean_std_for_norm (tuple[np.ndarray, np.ndarray], optional): If provided,
138+
the mean and standard deviation of the weighted sum term are used to normalize
139+
before a realistic amplitude is applied. (see `notebooks/stepwise.ipynb`).
140+
Defaults to None.
129141
130142
Returns:
131143
np.ndarray: (len(t_eval_ms), n_elec) array of WSLFP at requested times for
@@ -153,18 +165,33 @@ def calculate(
153165
assert wsum.shape == (len(t_eval_ms), self.n_elec, self.n_sources)
154166
wsum = np.sum(wsum, axis=2)
155167
assert wsum.shape == (len(t_eval_ms), self.n_elec)
156-
if normalize:
157-
wsum = wsum - np.mean(wsum, axis=0)
158-
if len(t_eval_ms) > 1:
159-
wsum /= np.std(wsum, axis=0)
160-
assert wsum.shape == (len(t_eval_ms), self.n_elec)
161-
162-
assert np.allclose(wsum.mean(axis=0), 0)
163-
if len(t_eval_ms) > 1:
164-
assert np.allclose(wsum.std(axis=0), 1)
165168

166-
wsum *= np.abs(self.amp_uV.mean(axis=1))
167-
return wsum
169+
if normalize:
170+
if wsum_mean_std_for_norm:
171+
wsum_mean, wsum_std = wsum_mean_std_for_norm
172+
else:
173+
wsum_mean = np.mean(wsum, axis=0)
174+
if len(t_eval_ms) > 1:
175+
wsum_std = np.std(wsum, axis=0)
176+
else:
177+
wsum_std = 1
178+
179+
lfp = (wsum - wsum_mean) / wsum_std
180+
assert lfp.shape == (len(t_eval_ms), self.n_elec)
181+
182+
if not wsum_mean_std_for_norm:
183+
assert np.allclose(lfp.mean(axis=0), 0)
184+
if len(t_eval_ms) > 1:
185+
assert np.allclose(lfp.std(axis=0), 1)
186+
187+
lfp *= np.abs(self.amp_uV.mean(axis=1))
188+
else:
189+
lfp = wsum
190+
191+
if normalize and wsum_mean_std_for_norm:
192+
return lfp, wsum
193+
else:
194+
return lfp
168195

169196

170197
def from_rec_radius_depth(

0 commit comments

Comments
 (0)