@@ -37,7 +37,11 @@ def __init__(self, root_dir):
3737
3838 self .root_dir = pathlib .Path (root_dir )
3939
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+
4145 self .root_name = meta_filepath .name .replace ('.ap.meta' , '' )
4246
4347 @property
@@ -50,11 +54,11 @@ def apmeta(self):
5054 def ap_timeseries (self ):
5155 """
5256 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')
5459 """
5560 if self ._ap_timeseries is None :
5661 self ._ap_timeseries = self ._read_bin (self .root_dir / (self .root_name + '.ap.bin' ))
57- self ._ap_timeseries *= self .get_channel_bit_volts ('ap' )
5862 return self ._ap_timeseries
5963
6064 @property
@@ -67,16 +71,16 @@ def lfmeta(self):
6771 def lf_timeseries (self ):
6872 """
6973 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')
7176 """
7277 if self ._lf_timeseries is None :
7378 self ._lf_timeseries = self ._read_bin (self .root_dir / (self .root_name + '.lf.bin' ))
74- self ._lf_timeseries *= self .get_channel_bit_volts ('lf' )
7579 return self ._lf_timeseries
7680
7781 def get_channel_bit_volts (self , band = 'ap' ):
7882 """
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
8084 Following the steps specified in: https://billkarsh.github.io/SpikeGLX/Support/SpikeGLX_Datafile_Tools.zip
8185 dataVolts = dataInt * Vmax / Imax / gain
8286 """
@@ -86,11 +90,13 @@ def get_channel_bit_volts(self, band='ap'):
8690 imax = IMAX [self .apmeta .probe_model ]
8791 imroTbl_data = self .apmeta .imroTbl ['data' ]
8892 imroTbl_idx = 3
93+ chn_ind = self .apmeta .get_recording_channels_indices (exclude_sync = True )
8994
9095 elif band == 'lf' :
9196 imax = IMAX [self .lfmeta .probe_model ]
9297 imroTbl_data = self .lfmeta .imroTbl ['data' ]
9398 imroTbl_idx = 4
99+ chn_ind = self .lfmeta .get_recording_channels_indices (exclude_sync = True )
94100 else :
95101 raise ValueError (f'Unsupported band: { band } - Must be "ap" or "lf"' )
96102
@@ -102,25 +108,26 @@ def get_channel_bit_volts(self, band='ap'):
102108 # 3A, 3B1, 3B2 (NP 1.0)
103109 chn_gains = [c [imroTbl_idx ] for c in imroTbl_data ]
104110
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
106114
107115 def _read_bin (self , fname ):
108116 nchan = self .apmeta .meta ['nSavedChans' ]
109117 dtype = np .dtype ((np .int16 , nchan ))
110118 return np .memmap (fname , dtype , 'r' )
111119
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 )):
113121 """
114122 :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
116124 :param n_wf: number of spikes per unit to extract the waveforms
117125 :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)
120127 """
128+ channel_bit_volts = self .get_channel_bit_volts ('ap' )[channel_ind ]
121129
122130 data = self .ap_timeseries
123- channel_idx = [np .where (self .apmeta .recording_channels == chn )[0 ][0 ] for chn in channel ]
124131
125132 spikes = np .round (spikes * self .apmeta .meta ['imSampRate' ]).astype (int ) # convert to sample
126133 # 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
130137 spikes = spikes [:n_wf ]
131138 if len (spikes ) > 0 :
132139 # 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
135144 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 )
137146
138147
139148class SpikeGLXMeta :
@@ -177,7 +186,8 @@ def __init__(self, meta_filepath):
177186 self .shankmap = self ._parse_shankmap (self .meta ['~snsShankMap' ]) if '~snsShankMap' in self .meta else None
178187 self .imroTbl = self ._parse_imrotbl (self .meta ['~imroTbl' ]) if '~imroTbl' in self .meta else None
179188
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 )]
181191
182192 @staticmethod
183193 def _parse_chanmap (raw ):
@@ -208,6 +218,9 @@ def _parse_chanmap(raw):
208218 @staticmethod
209219 def _parse_shankmap (raw ):
210220 """
221+ The shankmap contains details on the shank info
222+ for each electrode sites of the sites being recorded only
223+
211224 https://github.com/billkarsh/SpikeGLX/blob/master/Markdown/UserManual.md#shank-map
212225 Parse shank map header structure. Converts:
213226
@@ -234,6 +247,10 @@ def _parse_shankmap(raw):
234247 @staticmethod
235248 def _parse_imrotbl (raw ):
236249 """
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+
237254 https://github.com/billkarsh/SpikeGLX/blob/master/Markdown/UserManual.md#imro-per-channel-settings
238255 Parse imro tbl structure. Converts:
239256
@@ -257,8 +274,17 @@ def _parse_imrotbl(raw):
257274
258275 return res
259276
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 ):
262288 """
263289 Because you can selectively save channels, the
264290 ith channel in the file isn't necessarily the ith acquired channel.
@@ -267,22 +293,18 @@ def recording_channels(self):
267293 Credit to https://billkarsh.github.io/SpikeGLX/Support/SpikeGLX_Datafile_Tools.zip
268294 OriginalChans() function
269295 """
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
286308
287309
288310# ============= HELPER FUNCTIONS =============
0 commit comments