77
88import numpy as np
99import pandas as pd
10- from scipy import signal
10+ from scipy import signal , stats
1111from tqdm import tqdm
1212import one .alf .io as alfio
1313from iblutil .util import Bunch
@@ -105,7 +105,10 @@ def _compute_metrics_array(raw, fs, h):
105105 rms_pre_proc = dsp .rms (destripe )
106106 detections = spikes .detection (data = destripe .T , fs = fs , h = h , detect_threshold = SPIKE_THRESHOLD_UV * 1e-6 )
107107 spike_rate = np .bincount (detections .trace , minlength = raw .shape [0 ]).astype (np .float32 )
108- return rms_raw , rms_pre_proc , spike_rate
108+ channel_labels , _ = dsp .voltage .detect_bad_channels (raw , fs = fs )
109+ _ , psd = signal .welch (destripe , fs = fs , window = 'hanning' , nperseg = WELCH_WIN_LENGTH_SAMPLES ,
110+ detrend = 'constant' , return_onesided = True , scaling = 'density' , axis = - 1 )
111+ return rms_raw , rms_pre_proc , spike_rate , channel_labels , psd
109112
110113 def run (self , update : bool = False , overwrite : bool = True , stream : bool = None , ** kwargs ) -> (str , dict ):
111114 """
@@ -124,14 +127,18 @@ def run(self, update: bool = False, overwrite: bool = True, stream: bool = None,
124127 self .load_data ()
125128 qc_files = []
126129 # If ap meta file present, calculate median RMS per channel before and after destriping
127- # TODO: This should go a a separate function once we have a spikeglx.Streamer that behaves like the Reader
130+ # NB: ideally this should go a a separate function once we have a spikeglx.Streamer that behaves like the Reader
128131 if self .data .ap_meta :
129- rms_file = self .probe_path .joinpath ("_iblqc_ephysChannels.apRMS.npy" )
130- spike_rate_file = self .probe_path .joinpath ("_iblqc_ephysChannels.rawSpikeRates.npy" )
131- if all ([rms_file .exists (), spike_rate_file .exists ()]) and not overwrite :
132+ files = {'rms' : self .probe_path .joinpath ("_iblqc_ephysChannels.apRMS.npy" ),
133+ 'spike_rate' : self .probe_path .joinpath ("_iblqc_ephysChannels.rawSpikeRates.npy" ),
134+ 'channel_labels' : self .probe_path .joinpath ("_iblqc_ephysChannels.labels.npy" ),
135+ 'ap_freqs' : self .probe_path .joinpath ("_iblqc_ephysSpectralDensityAP.freqs.npy" ),
136+ 'ap_power' : self .probe_path .joinpath ("_iblqc_ephysSpectralDensityAP.power.npy" ),
137+ }
138+ if all ([files [k ].exists () for k in files ]) and not overwrite :
132139 _logger .warning (f'RMS map already exists for .ap data in { self .probe_path } , skipping. '
133140 f'Use overwrite option.' )
134- median_rms = np .load (rms_file )
141+ results = { k : np .load (files [ k ]) for k in files }
135142 else :
136143 rl = self .data .ap_meta .fileTimeSecs
137144 nsync = len (spikeglx ._get_sync_trace_indices_from_meta (self .data .ap_meta ))
@@ -145,14 +152,18 @@ def run(self, update: bool = False, overwrite: bool = True, stream: bool = None,
145152 raise ValueError ("Wrong Neuropixel channel mapping used - ABORT" )
146153 t0s = np .arange (TMIN , rl - SAMPLE_LENGTH , BATCHES_SPACING )
147154 all_rms = np .zeros ((2 , nc , t0s .shape [0 ]))
148- all_srs = np .zeros ((nc , t0s .shape [0 ]))
155+ all_srs , channel_ok = (np .zeros ((nc , t0s .shape [0 ])) for _ in range (2 ))
156+ psds = np .zeros ((nc , dsp .fscale (WELCH_WIN_LENGTH_SAMPLES , 1 , one_sided = True ).size ))
149157 # If the ap.bin file is not present locally, stream it
150158 if self .data .ap is None and self .stream is True :
151159 _logger .warning (f'Streaming .ap data to compute RMS samples for probe { self .pid } ' )
152160 for i , t0 in enumerate (tqdm (t0s )):
153161 sr , _ = sglx_streamer (self .pid , t0 = t0 , nsecs = 1 , one = self .one , remove_cached = True )
154162 raw = sr [:, :- nsync ].T
155- all_rms [0 , :, i ], all_rms [1 , :, i ], all_srs [:, i ] = self ._compute_metrics_array (raw , sr .fs , h )
163+ all_rms [0 , :, i ], all_rms [1 , :, i ], all_srs [:, i ], channel_ok [:, i ], psd = \
164+ self ._compute_metrics_array (raw , sr .fs , h )
165+ psds += psd
166+ fs = sr .fs
156167 elif self .data .ap is None and self .stream is not True :
157168 _logger .warning ('Raw .ap data is not available locally. Run with stream=True in order to stream '
158169 'data for calculating RMS samples.' )
@@ -161,23 +172,27 @@ def run(self, update: bool = False, overwrite: bool = True, stream: bool = None,
161172 for i , t0 in enumerate (t0s ):
162173 sl = slice (int (t0 * self .data .ap .fs ), int ((t0 + SAMPLE_LENGTH ) * self .data .ap .fs ))
163174 raw = self .data .ap [sl , :- nsync ].T
164- all_rms [0 , :, i ], all_rms [1 , :, i ], all_srs [:, i ] = self ._compute_metrics_array (raw , self .data .ap .fs , h )
175+ all_rms [0 , :, i ], all_rms [1 , :, i ], all_srs [:, i ], channel_ok [:, i ], psd = \
176+ self ._compute_metrics_array (raw , self .data .ap .fs , h )
177+ fs = self .data .ap .fs
178+ psds += psd
165179 # Calculate the median RMS across all samples per channel
166- median_rms = np .median (all_rms , axis = - 1 )
167- median_spike_rate = np .median (all_srs , axis = - 1 )
168- np .save (rms_file , median_rms )
169- np .save (spike_rate_file , median_spike_rate )
170- qc_files .extend ([rms_file , spike_rate_file ])
171-
180+ results = {'rms' : np .median (all_rms , axis = - 1 ),
181+ 'spike_rate' : np .median (all_srs , axis = - 1 ),
182+ 'channel_labels' : stats .mode (channel_ok , axis = 1 )[0 ],
183+ 'ap_freqs' : dsp .fscale (WELCH_WIN_LENGTH_SAMPLES , 1 / fs , one_sided = True ),
184+ 'ap_power' : psds .T / len (t0s ), # shape: (nfreqs, nchannels)
185+ }
186+ for k in files :
187+ np .save (files [k ], results [k ])
188+ qc_files .extend ([files [k ] for k in files ])
172189 for p in [10 , 90 ]:
173- self .metrics [f'apRms_p{ p } _raw' ] = np .format_float_scientific (np . percentile ( median_rms [ 0 , :], p ),
174- precision = 2 )
175- self .metrics [f'apRms_p{ p } _proc' ] = np .format_float_scientific (np . percentile ( median_rms [ 1 , :], p ),
176- precision = 2 )
190+ self .metrics [f'apRms_p{ p } _raw' ] = np .format_float_scientific (
191+ np . percentile ( results [ 'rms' ][ 0 , :], p ), precision = 2 )
192+ self .metrics [f'apRms_p{ p } _proc' ] = np .format_float_scientific (
193+ np . percentile ( results [ 'rms' ][ 1 , :], p ), precision = 2 )
177194 if update :
178195 self .update_extended_qc (self .metrics )
179- # self.update(outcome)
180-
181196 # If lf meta and bin file present, run the old qc on LF data
182197 if self .data .lf_meta and self .data .lf :
183198 qc_files .extend (extract_rmsmap (self .data .lf , out_folder = self .probe_path , overwrite = overwrite ))
0 commit comments