ibl-neuropixel 1.9.3__py3-none-any.whl → 1.10.0__py3-none-any.whl
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.
- {ibl_neuropixel-1.9.3.dist-info → ibl_neuropixel-1.10.0.dist-info}/METADATA +2 -2
- {ibl_neuropixel-1.9.3.dist-info → ibl_neuropixel-1.10.0.dist-info}/RECORD +17 -15
- {ibl_neuropixel-1.9.3.dist-info → ibl_neuropixel-1.10.0.dist-info}/WHEEL +1 -1
- ibldsp/fourier.py +2 -0
- ibldsp/sync.py +147 -0
- ibldsp/utils.py +23 -58
- ibldsp/voltage.py +197 -74
- neuropixel.py +136 -88
- spikeglx.py +39 -12
- tests/unit/test_ephys_np2.py +76 -106
- tests/unit/test_neuropixel.py +37 -0
- tests/unit/test_spikeglx.py +20 -7
- tests/unit/test_sync.py +104 -0
- tests/unit/test_utils.py +0 -49
- tests/unit/test_voltage.py +159 -19
- {ibl_neuropixel-1.9.3.dist-info → ibl_neuropixel-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {ibl_neuropixel-1.9.3.dist-info → ibl_neuropixel-1.10.0.dist-info}/top_level.txt +0 -0
tests/unit/test_voltage.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import tempfile
|
|
3
1
|
from pathlib import Path
|
|
2
|
+
import tempfile
|
|
4
3
|
import unittest
|
|
4
|
+
import unittest.mock
|
|
5
5
|
|
|
6
|
+
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
8
|
+
import scipy.signal
|
|
7
9
|
|
|
10
|
+
import neuropixel
|
|
8
11
|
import spikeglx
|
|
9
12
|
import ibldsp.voltage
|
|
10
13
|
import ibldsp.fourier
|
|
@@ -14,24 +17,27 @@ import ibldsp.cadzow
|
|
|
14
17
|
|
|
15
18
|
class TestDestripe(unittest.TestCase):
|
|
16
19
|
def test_destripe_parameters(self):
|
|
17
|
-
|
|
20
|
+
# ibldsp.voltage.apply_spatial_filter(x, k_filter, **kwargs)
|
|
21
|
+
x = np.random.randn(100, 1000)
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
# K-filter = True calls the spatial filter function
|
|
24
|
+
_, k_kwargs = ibldsp.voltage._get_destripe_parameters(
|
|
20
25
|
30_000, None, None, k_filter=True
|
|
21
26
|
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
with unittest.mock.patch("ibldsp.voltage.kfilt") as mock_fcn_spatial_filter:
|
|
28
|
+
ibldsp.voltage.apply_spatial_filter(x, k_filter=True, **k_kwargs)
|
|
29
|
+
# Assert the function was called
|
|
30
|
+
mock_fcn_spatial_filter.assert_called_once()
|
|
31
|
+
|
|
32
|
+
# K-filter = False calls the CAR function
|
|
33
|
+
with unittest.mock.patch("ibldsp.voltage.car") as mock_fcn_spatial_filter:
|
|
34
|
+
ibldsp.voltage.apply_spatial_filter(x, k_filter=False, **k_kwargs)
|
|
35
|
+
# Assert the function was called
|
|
36
|
+
mock_fcn_spatial_filter.assert_called_once()
|
|
37
|
+
|
|
38
|
+
# K-filter = None does not apply any filtering
|
|
39
|
+
xx = ibldsp.voltage.apply_spatial_filter(x, k_filter=None, **k_kwargs)
|
|
40
|
+
np.testing.assert_array_equal(xx, x)
|
|
35
41
|
|
|
36
42
|
def test_fk(self):
|
|
37
43
|
"""
|
|
@@ -93,9 +99,9 @@ class TestSaturation(unittest.TestCase):
|
|
|
93
99
|
sat = ibldsp.utils.fcn_cosine([0, 100])(
|
|
94
100
|
np.arange(nsat)
|
|
95
101
|
) - ibldsp.utils.fcn_cosine([150, 250])(np.arange(nsat))
|
|
102
|
+
n_expected_samples = np.sum(sat > 0.96)
|
|
96
103
|
range_volt = 0.0012
|
|
97
104
|
sat = (sat / s2v * 0.0012).astype(np.int16)
|
|
98
|
-
|
|
99
105
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
100
106
|
file_bin = Path(temp_dir) / "binary.bin"
|
|
101
107
|
data = np.memmap(file_bin, dtype=np.int16, mode="w+", shape=(ns, nc))
|
|
@@ -108,7 +114,10 @@ class TestSaturation(unittest.TestCase):
|
|
|
108
114
|
_sr, max_voltage=range_volt, n_jobs=1
|
|
109
115
|
)
|
|
110
116
|
df_sat = pd.read_parquet(file_saturation)
|
|
111
|
-
assert
|
|
117
|
+
assert (
|
|
118
|
+
np.sum(df_sat["stop_sample"] - df_sat["start_sample"])
|
|
119
|
+
== n_expected_samples
|
|
120
|
+
)
|
|
112
121
|
|
|
113
122
|
def test_saturation(self):
|
|
114
123
|
np.random.seed(7654)
|
|
@@ -148,3 +157,134 @@ class TestSaturation(unittest.TestCase):
|
|
|
148
157
|
saturation[45852:45865] = True
|
|
149
158
|
df_sat = ibldsp.voltage.saturation_samples_to_intervals(saturation)
|
|
150
159
|
self.assertEqual(81, np.sum(df_sat["stop_sample"] - df_sat["start_sample"]))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestLFP(unittest.TestCase):
|
|
163
|
+
def test_rsamp_cbin(self):
|
|
164
|
+
"""
|
|
165
|
+
Resamples a binary file by a factor of 5
|
|
166
|
+
:return:
|
|
167
|
+
"""
|
|
168
|
+
ns = int(125.83948 * 2500) + 3
|
|
169
|
+
nc = 12
|
|
170
|
+
fs = 2500
|
|
171
|
+
resamp_factor_q = 5
|
|
172
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
173
|
+
testfile = Path(temp_dir).joinpath("test.dat")
|
|
174
|
+
out_file = Path(temp_dir).joinpath("test_rs.dat")
|
|
175
|
+
# testfile = Path.home().joinpath('lfp', 'test.dat')
|
|
176
|
+
# out_file = Path.home().joinpath('lfp', 'test_rs.dat')
|
|
177
|
+
testfile.parent.mkdir(exist_ok=True, parents=True)
|
|
178
|
+
d = np.zeros((ns, nc), dtype=np.float32)
|
|
179
|
+
for ic in range(nc):
|
|
180
|
+
freq = 10 + ic * 4
|
|
181
|
+
print(freq)
|
|
182
|
+
d[:, ic] = np.sin(2 * np.pi * freq * np.arange(ns) / fs) * 1000
|
|
183
|
+
|
|
184
|
+
with open(testfile, "wb+") as f:
|
|
185
|
+
d.tofile(f)
|
|
186
|
+
|
|
187
|
+
sr = spikeglx.Reader(testfile, ns=ns, nc=nc, fs=2500, dtype=np.float32)
|
|
188
|
+
ibldsp.voltage.resample_denoise_lfp_cbin(
|
|
189
|
+
sr, output=out_file, dtype=np.float32
|
|
190
|
+
)
|
|
191
|
+
za = spikeglx.Reader(out_file, ns=ns // 5, nc=nc, fs=500, dtype=np.float32)
|
|
192
|
+
|
|
193
|
+
diff = d[0:-1:resamp_factor_q, :] - za[:]
|
|
194
|
+
np.testing.assert_array_less(np.abs(diff[1024:-1024] / 1000), 1e-3)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class TestDetectBadChannels(unittest.TestCase):
|
|
198
|
+
@staticmethod
|
|
199
|
+
def a_little_spike(nsw=121, nc=1):
|
|
200
|
+
# creates a kind of waveform that resembles a spike
|
|
201
|
+
wav = np.zeros(nsw)
|
|
202
|
+
wav[0] = 1
|
|
203
|
+
wav[5] = -0.1
|
|
204
|
+
wav[10] = -0.3
|
|
205
|
+
wav[15] = -0.1
|
|
206
|
+
sos = scipy.signal.butter(N=3, Wn=0.15, output="sos")
|
|
207
|
+
spike = scipy.signal.sosfilt(sos, wav)
|
|
208
|
+
spike = -spike / np.max(spike)
|
|
209
|
+
if nc > 1:
|
|
210
|
+
spike = spike[:, np.newaxis] * scipy.signal.hamming(nc)[np.newaxis, :]
|
|
211
|
+
return spike
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def make_synthetic_data(
|
|
215
|
+
ns=10000, nc=384, nss=121, ncs=21, nspikes=1200, tr=None, sample=None
|
|
216
|
+
):
|
|
217
|
+
if tr is None:
|
|
218
|
+
tr = np.random.randint(np.ceil(ncs / 2), nc - np.ceil(ncs / 2), nspikes)
|
|
219
|
+
if sample is None:
|
|
220
|
+
sample = np.random.randint(np.ceil(nss / 2), ns - np.ceil(nss / 2), nspikes)
|
|
221
|
+
h = neuropixel.trace_header(1)
|
|
222
|
+
icsmid = int(np.floor(ncs / 2))
|
|
223
|
+
issmid = int(np.floor(nss / 2))
|
|
224
|
+
template = TestDetectBadChannels.a_little_spike(121)
|
|
225
|
+
data = np.zeros((ns, nc))
|
|
226
|
+
for m in np.arange(tr.size):
|
|
227
|
+
itr = np.arange(tr[m] - icsmid, tr[m] + icsmid + 1)
|
|
228
|
+
iss = np.arange(sample[m] - issmid, sample[m] + issmid + 1)
|
|
229
|
+
offset = np.abs(
|
|
230
|
+
h["x"][itr[icsmid]]
|
|
231
|
+
+ 1j * h["y"][itr[icsmid]]
|
|
232
|
+
- h["x"][itr]
|
|
233
|
+
- 1j * h["y"][itr]
|
|
234
|
+
)
|
|
235
|
+
ampfac = 1 / (offset + 10) ** 1.3
|
|
236
|
+
ampfac = ampfac / np.max(ampfac)
|
|
237
|
+
tmp = template[:, np.newaxis] * ampfac[np.newaxis, :]
|
|
238
|
+
data[slice(iss[0], iss[-1] + 1), slice(itr[0], itr[-1] + 1)] += tmp
|
|
239
|
+
return data
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def synthetic_with_bad_channels():
|
|
243
|
+
np.random.seed(12345)
|
|
244
|
+
ns, nc, fs = (30000, 384, 30000)
|
|
245
|
+
data = TestDetectBadChannels.make_synthetic_data(ns=ns, nc=nc) * 1e-6 * 50
|
|
246
|
+
|
|
247
|
+
st = np.round(
|
|
248
|
+
np.cumsum(-np.log(np.random.rand(int(ns / fs * 50 * 1.5))) / 50) * fs
|
|
249
|
+
)
|
|
250
|
+
st = st[st < ns].astype(np.int32)
|
|
251
|
+
stripes = np.zeros(ns)
|
|
252
|
+
stripes[st] = 1
|
|
253
|
+
stripes = (
|
|
254
|
+
scipy.signal.convolve(stripes, ibldsp.utils.ricker(1200, 40), "same")
|
|
255
|
+
* 1e-6
|
|
256
|
+
* 2500
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
data = data + stripes[:, np.newaxis]
|
|
260
|
+
noise = np.random.randn(*data.shape) * 1e-6 * 10
|
|
261
|
+
|
|
262
|
+
channels = {
|
|
263
|
+
"idead": [29, 36, 39, 40, 191],
|
|
264
|
+
"inoisy": [133, 235],
|
|
265
|
+
"ioutside": np.arange(275, 384),
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
data[:, channels["idead"]] = data[:, channels["idead"]] / 20
|
|
269
|
+
noise[:, channels["inoisy"]] = noise[:, channels["inoisy"]] * 200
|
|
270
|
+
data[:, channels["idead"]] = data[:, channels["idead"]] / 20
|
|
271
|
+
data[:, channels["ioutside"]] = 0
|
|
272
|
+
data += noise
|
|
273
|
+
return data, channels
|
|
274
|
+
|
|
275
|
+
def test_channel_detections(self):
|
|
276
|
+
"""
|
|
277
|
+
This test creates a synthetic dataset with voltage stripes and 3 types of bad channels
|
|
278
|
+
1) dead channels or low amplitude
|
|
279
|
+
2) noisy
|
|
280
|
+
3) out of the brain
|
|
281
|
+
"""
|
|
282
|
+
data, channels = self.synthetic_with_bad_channels()
|
|
283
|
+
labels, xfeats = ibldsp.voltage.detect_bad_channels(data.T, fs=30000)
|
|
284
|
+
assert np.all(np.where(labels == 1)[0] == np.array(channels["idead"]))
|
|
285
|
+
assert np.all(np.where(labels == 2)[0] == np.array(channels["inoisy"]))
|
|
286
|
+
assert np.all(np.where(labels == 3)[0] == np.array(channels["ioutside"]))
|
|
287
|
+
# from easyqc.gui import viewseis
|
|
288
|
+
# eqc = viewseis(data, si=1 / 30000 * 1e3, h=h, title='synth', taxis=0)
|
|
289
|
+
# from ibllib.plots.figures import ephys_bad_channels
|
|
290
|
+
# ephys_bad_channels(data.T, 30000, labels, xfeats)
|
|
File without changes
|
|
File without changes
|