def __init__(self, session_path, bpod_only=False, local=False): """ Loads and extracts the QC data for a given session path :param session_path: A str or Path to a Bpod session :param bpod_only: When True all data is extracted from Bpod instead of FPGA for ephys """ super().__init__(session_path, one=one, log=_logger) if local: dsets, out_files = ephys_fpga.extract_all(session_path, save=True) self.extractor = TaskQCExtractor(session_path, lazy=True, one=one) # Extract extra datasets required for QC self.extractor.data = dsets self.extractor.extract_data() # Aggregate and update Alyx QC fields self.run(update=False) else: self.load_data(bpod_only=bpod_only) self.compute() self.n_trials = self.extractor.data['intervals'].shape[0] self.wheel_data = { 're_pos': self.extractor.data.pop('wheel_position'), 're_ts': self.extractor.data.pop('wheel_timestamps') } # Print failed outcome, results, outcomes = self.compute_session_status() map = {k: [] for k in set(outcomes.values())} for k, v in outcomes.items(): map[v].append(k[6:]) for k, v in map.items(): if k == 'PASS': continue print(f'The following checks were labelled {k}:') print('\n'.join(v), '\n') # Make DataFrame from the trail level metrics def get_trial_level_failed(d): new_dict = { k[6:]: v for k, v in d.items() if isinstance(v, Sized) and len(v) == self.n_trials } return pd.DataFrame.from_dict(new_dict) self.frame = get_trial_level_failed(self.metrics) self.frame['intervals_0'] = self.extractor.data['intervals'][:, 0] self.frame['intervals_1'] = self.extractor.data['intervals'][:, 1] self.frame.insert(loc=0, column='trial_no', value=self.frame.index)
def load_data(self, bpod_only=False, download_data=True): """Extract the data from raw data files Extracts all the required task data from the raw data files. :param bpod_only: if True no data is extracted from the FPGA for ephys sessions :param download_data: if True, any missing raw data is downloaded via ONE. """ self.extractor = TaskQCExtractor( self.session_path, one=self.one, download_data=download_data, bpod_only=bpod_only)
def test_partial_extraction(self): ex = TaskQCExtractor(self.session_path, lazy=True, one=self.one, bpod_only=True) ex.extract_data() expected = [ 'contrastLeft', 'contrastRight', 'phase', 'position', 'probabilityLeft', 'quiescence', 'stimOn_times' ] expected_5up = [ 'contrast', 'errorCueTrigger_times', 'itiIn_times', 'stimFreezeTrigger_times', 'stimFreeze_times', 'stimOffTrigger_times', 'stimOff_times', 'stimOnTrigger_times' ] if version.ge(ex.settings['IBLRIG_VERSION_TAG'], '5.0.0'): expected += expected_5up self.assertTrue(set(expected).issubset(set(ex.data.keys())))
def test_download_data(self): """Test behavior when download_data flag is True """ # path = self.one.path_from_eid(self.eid_incomplete) # FIXME Returns None det = one.get_details(self.eid_incomplete) path = (Path(one._par.CACHE_DIR) / det['lab'] / 'Subjects' / det['subject'] / det['start_time'][:10] / str(det['number'])) ex = TaskQCExtractor(path, lazy=True, one=self.one, download_data=True) self.assertTrue(ex.lazy, 'Failed to set lazy flag') shutil.rmtree(self.session_path) # Remove downloaded data assert self.session_path.exists( ) is False, 'Failed to remove session folder' TaskQCExtractor(self.session_path, lazy=True, one=self.one, download_data=True) files = list(self.session_path.rglob('*.*')) expected = 6 # NB This session is missing raw ephys data and missing some datasets self.assertEqual(len(files), expected)
def _run(self): """ Extracts an iblrig training session """ trials, wheel, output_files = bpod_trials.extract_all( self.session_path, save=True) if trials is None: return None # Run the task QC # Compile task data for QC type = get_session_extractor_type(self.session_path) if type == 'habituation': qc = HabituationQC(self.session_path, one=self.one) qc.extractor = TaskQCExtractor(self.session_path, one=self.one) else: # Update wheel data qc = TaskQC(self.session_path, one=self.one) qc.extractor = TaskQCExtractor(self.session_path, one=self.one) qc.extractor.wheel_encoding = 'X1' # Aggregate and update Alyx QC fields qc.run(update=True) return output_files
def _run(self): dsets, out_files = ephys_fpga.extract_all(self.session_path, save=True) self._behaviour_criterion() # Run the task QC qc = TaskQC(self.session_path, one=self.one, log=_logger) qc.extractor = TaskQCExtractor(self.session_path, lazy=True, one=qc.one) # Extract extra datasets required for QC qc.extractor.data = dsets qc.extractor.extract_data() # Aggregate and update Alyx QC fields qc.run(update=True) return out_files
def test_frame2ttl_flicker(self): from ibllib.io.extractors import ephys_fpga from ibllib.qc.task_metrics import TaskQC from ibllib.qc.task_extractors import TaskQCExtractor init_path = self.data_path.joinpath("ephys", "ephys_choice_world_task") session_path = init_path.joinpath("ibl_witten_27/2021-01-21/001") dsets, out_files = ephys_fpga.extract_all(session_path, save=True) # Run the task QC qc = TaskQC(session_path, one=None) qc.extractor = TaskQCExtractor(session_path, lazy=True, one=None) # Extr+act extra datasets required for QC qc.extractor.data = dsets qc.extractor.extract_data() # Aggregate and update Alyx QC fields _, myqc = qc.run(update=False) # from ibllib.misc import pprint # pprint(myqc) assert myqc['_task_stimOn_delays'] > 0.9 # 0.6176 assert myqc["_task_stimFreeze_delays"] > 0.9 assert myqc["_task_stimOff_delays"] > 0.9 assert myqc["_task_stimOff_itiIn_delays"] > 0.9 assert myqc["_task_stimOn_delays"] > 0.9 assert myqc["_task_stimOn_goCue_delays"] > 0.9
def test_extraction(self): ex = TaskQCExtractor(self.session_path, lazy=True, one=self.one, bpod_only=True, download_data=True) self.assertIsNone(ex.raw_data) # Test loading raw data ex.load_raw_data() self.assertIsNotNone(ex.raw_data, 'Failed to load raw data') self.assertIsNotNone(ex.settings, 'Failed to load task settings') self.assertIsNotNone(ex.frame_ttls, 'Failed to load BNC1') self.assertIsNotNone(ex.audio_ttls, 'Failed to load BNC2') # Test extraction ex.extract_data() expected = [ 'choice', 'contrastLeft', 'contrastRight', 'correct', 'errorCue_times', 'feedbackType', 'feedback_times', 'firstMovement_times', 'goCueTrigger_times', 'goCue_times', 'intervals', 'phase', 'position', 'probabilityLeft', 'quiescence', 'response_times', 'rewardVolume', 'stimOn_times', 'valveOpen_times', 'wheel_moves_intervals', 'wheel_moves_peak_amplitude', 'wheel_position', 'wheel_timestamps' ] expected_5up = [ 'errorCueTrigger_times', 'itiIn_times', 'stimFreezeTrigger_times', 'stimFreeze_times', 'stimOffTrigger_times', 'stimOff_times', 'stimOnTrigger_times' ] expected += expected_5up self.assertTrue( len(set(expected).difference(set(ex.data.keys()))) == 0) self.assertEqual('ephys', ex.type) self.assertEqual('X1', ex.wheel_encoding)
def _task_extraction_assertions(self, session_path): alf_path = session_path.joinpath('alf') shutil.rmtree(alf_path, ignore_errors=True) # try once without the sync pulses trials, out_files = ephys_fpga.FpgaTrials(session_path).extract( save=False) # then extract for real sync, chmap = ephys_fpga.get_main_probe_sync(session_path, bin_exists=False) trials, out_files = ephys_fpga.FpgaTrials(session_path).extract( save=True, sync=sync, chmap=chmap) # check that the output is complete for f in BPOD_FILES: self.assertTrue(alf_path.joinpath(f).exists()) # check that the output is complete for f in FPGA_FILES: self.assertTrue(alf_path.joinpath(f).exists()) # check dimensions after alf load alf_trials = alf.io.load_object(alf_path, 'trials') self.assertTrue(alf.io.check_dimensions(alf_trials) == 0) # go deeper and check the internal fpga trials structure consistency fpga_trials = ephys_fpga.extract_behaviour_sync(sync, chmap) # check dimensions self.assertEqual(alf.io.check_dimensions(fpga_trials), 0) # check that the stimOn < stimFreeze < stimOff self.assertTrue( np.all(fpga_trials['stimOn_times'][:-1] < fpga_trials['stimOff_times'][:-1])) self.assertTrue( np.all(fpga_trials['stimFreeze_times'][:-1] < fpga_trials['stimOff_times'][:-1])) # a trial is either an error-nogo or a reward self.assertTrue( np.all( np.isnan(fpga_trials['valveOpen_times'][:-1] * fpga_trials['errorCue_times'][:-1]))) self.assertTrue( np.all( np.logical_xor(np.isnan(fpga_trials['valveOpen_times'][:-1]), np.isnan(fpga_trials['errorCue_times'][:-1])))) # do the task qc # tqc_ephys.extractor.settings['PYBPOD_PROTOCOL'] from ibllib.qc.task_extractors import TaskQCExtractor ex = TaskQCExtractor(session_path, lazy=True, one=None, bpod_only=False) ex.data = fpga_trials ex.extract_data() from ibllib.qc.task_metrics import TaskQC # '/mnt/s0/Data/IntegrationTests/ephys/ephys_choice_world_task/CSP004/2019-11-27/001' tqc_ephys = TaskQC(session_path) tqc_ephys.extractor = ex _, res_ephys = tqc_ephys.run(bpod_only=False, download_data=False) tqc_bpod = TaskQC(session_path) _, res_bpod = tqc_bpod.run(bpod_only=True, download_data=False) for k in res_ephys: if k == "_task_response_feedback_delays": continue assert (np.abs(res_bpod[k] - res_ephys[k]) < .2) shutil.rmtree(alf_path, ignore_errors=True)
def load_data(self, bpod_only=False, download_data=True): self.extractor = TaskQCExtractor( self.session_path, one=self.one, download_data=download_data, bpod_only=bpod_only)
def _task_extraction_assertions(self, session_path): alf_path = session_path.joinpath('alf') shutil.rmtree(alf_path, ignore_errors=True) # this gets the full output ephys_fpga.extract_all(session_path, save=True, bin_exists=False) # check that the output is complete for f in BPOD_FILES: self.assertTrue(alf_path.joinpath(f).exists()) # check that the output is complete for f in FPGA_FILES: self.assertTrue(alf_path.joinpath(f).exists()) # check dimensions after alf load alf_trials = alf.io.load_object(alf_path, 'trials') self.assertTrue(alf.io.check_dimensions(alf_trials) == 0) # go deeper and check the internal fpga trials structure consistency sync, chmap = ephys_fpga.get_main_probe_sync(session_path, bin_exists=False) fpga_trials = ephys_fpga.extract_behaviour_sync(sync, chmap) # check dimensions self.assertEqual(alf.io.check_dimensions(fpga_trials), 0) # check that the stimOn < stimFreeze < stimOff self.assertTrue( np.all(fpga_trials['stimOn_times'][:-1] < fpga_trials['stimOff_times'][:-1])) self.assertTrue( np.all(fpga_trials['stimFreeze_times'][:-1] < fpga_trials['stimOff_times'][:-1])) # a trial is either an error-nogo or a reward self.assertTrue( np.all( np.isnan(fpga_trials['valveOpen_times'][:-1] * fpga_trials['errorCue_times'][:-1]))) self.assertTrue( np.all( np.logical_xor(np.isnan(fpga_trials['valveOpen_times'][:-1]), np.isnan(fpga_trials['errorCue_times'][:-1])))) # do the task qc # tqc_ephys.extractor.settings['PYBPOD_PROTOCOL'] from ibllib.qc.task_extractors import TaskQCExtractor ex = TaskQCExtractor(session_path, lazy=True, one=None, bpod_only=False) ex.data = fpga_trials ex.extract_data() from ibllib.qc.task_metrics import TaskQC # '/mnt/s0/Data/IntegrationTests/ephys/ephys_choice_world_task/CSP004/2019-11-27/001' tqc_ephys = TaskQC(session_path) tqc_ephys.extractor = ex _, res_ephys = tqc_ephys.run(bpod_only=False, download_data=False) tqc_bpod = TaskQC(session_path) _, res_bpod = tqc_bpod.run(bpod_only=True, download_data=False) # for a swift comparison using variable explorer # import pandas as pd # df = pd.DataFrame([[res_bpod[k], res_ephys[k]] for k in res_ephys], index=res_ephys.keys()) ok = True for k in res_ephys: if k == "_task_response_feedback_delays": continue if (np.abs(res_bpod[k] - res_ephys[k]) > .2): ok = False print(f"{k} bpod: {res_bpod[k]}, ephys: {res_ephys[k]}") assert ok shutil.rmtree(alf_path, ignore_errors=True)
class QcFrame(TaskQC): def __init__(self, session, bpod_only=False, local=False): """ Loads and extracts the QC data for a given session path :param session: A str or Path to a session, or a session eid :param bpod_only: When True all data is extracted from Bpod instead of FPGA for ephys """ super().__init__(session, one=one, log=_logger) if local: dsets, out_files = ephys_fpga.extract_all(session, save=True) self.extractor = TaskQCExtractor(session, lazy=True, one=one) # Extract extra datasets required for QC self.extractor.data = dsets self.extractor.extract_data() # Aggregate and update Alyx QC fields self.run(update=False) else: self.load_data(bpod_only=bpod_only) self.compute() self.n_trials = self.extractor.data['intervals'].shape[0] self.wheel_data = {'re_pos': self.extractor.data.pop('wheel_position'), 're_ts': self.extractor.data.pop('wheel_timestamps')} # Print failed outcome, results, outcomes = self.compute_session_status() map = {k: [] for k in set(outcomes.values())} for k, v in outcomes.items(): map[v].append(k[6:]) for k, v in map.items(): if k == 'PASS': continue print(f'The following checks were labelled {k}:') print('\n'.join(v), '\n') # Make DataFrame from the trail level metrics def get_trial_level_failed(d): new_dict = {k[6:]: v for k, v in d.items() if isinstance(v, Sized) and len(v) == self.n_trials} return pd.DataFrame.from_dict(new_dict) self.frame = get_trial_level_failed(self.metrics) self.frame['intervals_0'] = self.extractor.data['intervals'][:, 0] self.frame['intervals_1'] = self.extractor.data['intervals'][:, 1] self.frame.insert(loc=0, column='trial_no', value=self.frame.index) def create_plots(self, axes, wheel_axes=None, trial_events=None, color_map=None, linestyle=None): """ Plots the data for bnc1 (sound) and bnc2 (frame2ttl) :param axes: An axes handle on which to plot the TTL events :param wheel_axes: An axes handle on which to plot the wheel trace :param trial_events: A list of Bpod trial events to plot, e.g. ['stimFreeze_times'], if None, valve, sound and stimulus events are plotted :param color_map: A color map to use for the events, default is the tableau color map linestyle: A line style map to use for the events, default is random. :return: None """ color_map = color_map or TABLEAU_COLORS.keys() if trial_events is None: # Default trial events to plot as vertical lines trial_events = [ 'goCue_times', 'goCueTrigger_times', 'feedback_times', 'stimFreeze_times', 'stimOff_times', 'stimOn_times' ] plot_args = { 'ymin': 0, 'ymax': 4, 'linewidth': 2, 'ax': axes } bnc1 = self.extractor.frame_ttls bnc2 = self.extractor.audio_ttls trial_data = self.extractor.data plots.squares(bnc1['times'], bnc1['polarities'] * 0.4 + 1, ax=axes, color='k') plots.squares(bnc2['times'], bnc2['polarities'] * 0.4 + 2, ax=axes, color='k') linestyle = linestyle or random.choices(('-', '--', '-.', ':'), k=len(trial_events)) if self.extractor.bpod_ttls is not None: bpttls = self.extractor.bpod_ttls plots.squares(bpttls['times'], bpttls['polarities'] * 0.4 + 3, ax=axes, color='k') plot_args['ymax'] = 4 ylabels = ['', 'frame2ttl', 'sound', 'bpod', ''] else: plot_args['ymax'] = 3 ylabels = ['', 'frame2ttl', 'sound', ''] for event, c, l in zip(trial_events, cycle(color_map), linestyle): plots.vertical_lines(trial_data[event], label=event, color=c, linestyle=l, **plot_args) axes.legend(loc='upper left', fontsize='xx-small', bbox_to_anchor=(1, 0.5)) axes.set_yticklabels(ylabels) axes.set_yticks(list(range(plot_args['ymax'] + 1))) axes.set_ylim([0, plot_args['ymax']]) if wheel_axes: wheel_plot_args = { 'ax': wheel_axes, 'ymin': self.wheel_data['re_pos'].min(), 'ymax': self.wheel_data['re_pos'].max()} plot_args = {**plot_args, **wheel_plot_args} wheel_axes.plot(self.wheel_data['re_ts'], self.wheel_data['re_pos'], 'k-x') for event, c, ln in zip(trial_events, cycle(color_map), linestyle): plots.vertical_lines(trial_data[event], label=event, color=c, linestyle=ln, **plot_args)
def test_lazy_extract(self): ex = TaskQCExtractor(self.session_path, lazy=True, one=self.one) self.assertIsNone(ex.data)