@@ -37,7 +37,11 @@ def __init__(self, root_dir):
37
37
38
38
self .root_dir = pathlib .Path (root_dir )
39
39
40
- meta_filepath = next (pathlib .Path (root_dir ).glob ('*.ap.meta' ))
40
+ try :
41
+ meta_filepath = next (pathlib .Path (root_dir ).glob ('*.ap.meta' ))
42
+ except StopIteration :
43
+ raise FileNotFoundError (f'No SpikeGLX file (.ap.meta) found at: { root_dir } ' )
44
+
41
45
self .root_name = meta_filepath .name .replace ('.ap.meta' , '' )
42
46
43
47
@property
@@ -50,11 +54,11 @@ def apmeta(self):
50
54
def ap_timeseries (self ):
51
55
"""
52
56
AP data: (sample x channel)
53
- Channels' gains (bit_volts) applied - unit: uV
57
+ Data are stored as np.memmap with dtype: int16
58
+ - to convert to microvolts, multiply with self.get_channel_bit_volts('ap')
54
59
"""
55
60
if self ._ap_timeseries is None :
56
61
self ._ap_timeseries = self ._read_bin (self .root_dir / (self .root_name + '.ap.bin' ))
57
- self ._ap_timeseries *= self .get_channel_bit_volts ('ap' )
58
62
return self ._ap_timeseries
59
63
60
64
@property
@@ -67,16 +71,16 @@ def lfmeta(self):
67
71
def lf_timeseries (self ):
68
72
"""
69
73
LFP data: (sample x channel)
70
- Channels' gains (bit_volts) applied - unit: uV
74
+ Data are stored as np.memmap with dtype: int16
75
+ - to convert to microvolts, multiply with self.get_channel_bit_volts('lf')
71
76
"""
72
77
if self ._lf_timeseries is None :
73
78
self ._lf_timeseries = self ._read_bin (self .root_dir / (self .root_name + '.lf.bin' ))
74
- self ._lf_timeseries *= self .get_channel_bit_volts ('lf' )
75
79
return self ._lf_timeseries
76
80
77
81
def get_channel_bit_volts (self , band = 'ap' ):
78
82
"""
79
- Extract the AP and LF channels' int16 to microvolts
83
+ Extract the recorded AP and LF channels' int16 to microvolts - no Sync (SY) channels
80
84
Following the steps specified in: https://billkarsh.github.io/SpikeGLX/Support/SpikeGLX_Datafile_Tools.zip
81
85
dataVolts = dataInt * Vmax / Imax / gain
82
86
"""
@@ -86,11 +90,13 @@ def get_channel_bit_volts(self, band='ap'):
86
90
imax = IMAX [self .apmeta .probe_model ]
87
91
imroTbl_data = self .apmeta .imroTbl ['data' ]
88
92
imroTbl_idx = 3
93
+ chn_ind = self .apmeta .get_recording_channels_indices (exclude_sync = True )
89
94
90
95
elif band == 'lf' :
91
96
imax = IMAX [self .lfmeta .probe_model ]
92
97
imroTbl_data = self .lfmeta .imroTbl ['data' ]
93
98
imroTbl_idx = 4
99
+ chn_ind = self .lfmeta .get_recording_channels_indices (exclude_sync = True )
94
100
else :
95
101
raise ValueError (f'Unsupported band: { band } - Must be "ap" or "lf"' )
96
102
@@ -102,25 +108,26 @@ def get_channel_bit_volts(self, band='ap'):
102
108
# 3A, 3B1, 3B2 (NP 1.0)
103
109
chn_gains = [c [imroTbl_idx ] for c in imroTbl_data ]
104
110
105
- return vmax / imax / np .array (chn_gains ) * 1e6 # convert to uV as well
111
+ chn_gains = np .array (chn_gains )[chn_ind ]
112
+
113
+ return vmax / imax / chn_gains * 1e6 # convert to uV as well
106
114
107
115
def _read_bin (self , fname ):
108
116
nchan = self .apmeta .meta ['nSavedChans' ]
109
117
dtype = np .dtype ((np .int16 , nchan ))
110
118
return np .memmap (fname , dtype , 'r' )
111
119
112
- def extract_spike_waveforms (self , spikes , channel , n_wf = 500 , wf_win = (- 32 , 32 ), bit_volts = 1 ):
120
+ def extract_spike_waveforms (self , spikes , channel_ind , n_wf = 500 , wf_win = (- 32 , 32 )):
113
121
"""
114
122
:param spikes: spike times (in second) to extract waveforms
115
- :param channel : channel (name, not indices ) to extract waveforms
123
+ :param channel_ind : channel indices (of shankmap ) to extract the waveforms from
116
124
:param n_wf: number of spikes per unit to extract the waveforms
117
125
:param wf_win: number of sample pre and post a spike
118
- :param bit_volts: scalar required to convert int16 values into microvolts (default of 1)
119
- :return: waveforms (sample x channel x spike)
126
+ :return: waveforms (in uV) - shape: (sample x channel x spike)
120
127
"""
128
+ channel_bit_volts = self .get_channel_bit_volts ('ap' )[channel_ind ]
121
129
122
130
data = self .ap_timeseries
123
- channel_idx = [np .where (self .apmeta .recording_channels == chn )[0 ][0 ] for chn in channel ]
124
131
125
132
spikes = np .round (spikes * self .apmeta .meta ['imSampRate' ]).astype (int ) # convert to sample
126
133
# ignore spikes at the beginning or end of raw data
@@ -130,10 +137,12 @@ def extract_spike_waveforms(self, spikes, channel, n_wf=500, wf_win=(-32, 32), b
130
137
spikes = spikes [:n_wf ]
131
138
if len (spikes ) > 0 :
132
139
# waveform at each spike: (sample x channel x spike)
133
- spike_wfs = np .dstack ([data [int (spk + wf_win [0 ]):int (spk + wf_win [- 1 ]), channel_idx ] for spk in spikes ])
134
- return spike_wfs * bit_volts
140
+ spike_wfs = np .dstack ([data [int (spk + wf_win [0 ]):int (spk + wf_win [- 1 ]), channel_ind ]
141
+ * channel_bit_volts
142
+ for spk in spikes ])
143
+ return spike_wfs
135
144
else : # if no spike found, return NaN of size (sample x channel x 1)
136
- return np .full ((len (range (* wf_win )), len (channel ), 1 ), np .nan )
145
+ return np .full ((len (range (* wf_win )), len (channel_ind ), 1 ), np .nan )
137
146
138
147
139
148
class SpikeGLXMeta :
@@ -177,7 +186,8 @@ def __init__(self, meta_filepath):
177
186
self .shankmap = self ._parse_shankmap (self .meta ['~snsShankMap' ]) if '~snsShankMap' in self .meta else None
178
187
self .imroTbl = self ._parse_imrotbl (self .meta ['~imroTbl' ]) if '~imroTbl' in self .meta else None
179
188
180
- self ._recording_channels = None
189
+ # Channels being recorded, exclude Sync channels - basically a 1-1 mapping to shankmap
190
+ self .recording_channels = np .arange (len (self .imroTbl ['data' ]))[self .get_recording_channels_indices (exclude_sync = True )]
181
191
182
192
@staticmethod
183
193
def _parse_chanmap (raw ):
@@ -208,6 +218,9 @@ def _parse_chanmap(raw):
208
218
@staticmethod
209
219
def _parse_shankmap (raw ):
210
220
"""
221
+ The shankmap contains details on the shank info
222
+ for each electrode sites of the sites being recorded only
223
+
211
224
https://github.com/billkarsh/SpikeGLX/blob/master/Markdown/UserManual.md#shank-map
212
225
Parse shank map header structure. Converts:
213
226
@@ -234,6 +247,10 @@ def _parse_shankmap(raw):
234
247
@staticmethod
235
248
def _parse_imrotbl (raw ):
236
249
"""
250
+ The imro table contains info for all electrode sites (no sync)
251
+ for a particular electrode configuration (all 384 sites)
252
+ Note: not all of these 384 sites are necessarily recorded
253
+
237
254
https://github.com/billkarsh/SpikeGLX/blob/master/Markdown/UserManual.md#imro-per-channel-settings
238
255
Parse imro tbl structure. Converts:
239
256
@@ -257,8 +274,17 @@ def _parse_imrotbl(raw):
257
274
258
275
return res
259
276
260
- @property
261
- def recording_channels (self ):
277
+ def get_recording_channels_indices (self , exclude_sync = False ):
278
+ """
279
+ The indices of recorded channels (in chanmap) with respect to the channels listed in the imro table
280
+ """
281
+ recorded_chns_ind = [int (v [0 ]) for k , v in self .chanmap .items ()
282
+ if k != 'shape' and (not k .startswith ('SY' ) if exclude_sync else True )]
283
+ orig_chns_ind = self .get_original_chans ()
284
+ _ , _ , chns_ind = np .intersect1d (orig_chns_ind , recorded_chns_ind , return_indices = True )
285
+ return chns_ind
286
+
287
+ def get_original_chans (self ):
262
288
"""
263
289
Because you can selectively save channels, the
264
290
ith channel in the file isn't necessarily the ith acquired channel.
@@ -267,22 +293,18 @@ def recording_channels(self):
267
293
Credit to https://billkarsh.github.io/SpikeGLX/Support/SpikeGLX_Datafile_Tools.zip
268
294
OriginalChans() function
269
295
"""
270
- if self ._recording_channels is None :
271
- if self .meta ['snsSaveChanSubset' ] == 'all' :
272
- # output = int32, 0 to nSavedChans - 1
273
- self ._recording_channels = np .arange (0 , int (self .meta ['nSavedChans' ]))
274
- else :
275
- # parse the snsSaveChanSubset string
276
- # split at commas
277
- chStrList = self .meta ['snsSaveChanSubset' ].split (sep = ',' )
278
- self ._recording_channels = np .arange (0 , 0 ) # creates an empty array of int32
279
- for sL in chStrList :
280
- currList = sL .split (sep = ':' )
281
- # each set of continuous channels specified by chan1:chan2 inclusive
282
- newChans = np .arange (int (currList [0 ]), int (currList [min (1 , len (currList ))]) + 1 )
283
-
284
- self ._recording_channels = np .append (self ._recording_channels , newChans )
285
- return self ._recording_channels
296
+ if self .meta ['snsSaveChanSubset' ] == 'all' :
297
+ # output = int32, 0 to nSavedChans - 1
298
+ channels = np .arange (0 , int (self .meta ['nSavedChans' ]))
299
+ else :
300
+ # parse the channel list self.meta['snsSaveChanSubset']
301
+ channels = np .arange (0 ) # empty array
302
+ for channel_range in self .meta ['snsSaveChanSubset' ].split (',' ):
303
+ # a block of contiguous channels specified as chan or chan1:chan2 inclusive
304
+ ix = [int (r ) for r in channel_range .split (':' )]
305
+ assert len (ix ) in (1 , 2 ), f"Invalid channel range spec '{ channel_range } '"
306
+ channels = np .append (np .r_ [ix [0 ]:ix [- 1 ] + 1 ])
307
+ return channels
286
308
287
309
288
310
# ============= HELPER FUNCTIONS =============
0 commit comments