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