From de4e8ba493e5ef90087087b72653cb882a242648 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Wed, 3 Jan 2024 14:52:48 +0200 Subject: [PATCH 01/25] A task qc viewer implemented in PyQT #692 --- ibllib/io/raw_data_loaders.py | 8 +- ibllib/pipes/base_tasks.py | 49 +++++ ibllib/pipes/behavior_tasks.py | 57 +++-- ibllib/pipes/ephys_preprocessing.py | 30 +-- ibllib/pipes/tasks.py | 6 +- ibllib/pipes/training_preprocessing.py | 47 +++-- ibllib/qc/task_extractors.py | 16 +- ibllib/qc/task_qc_viewer/README.md | 48 +++++ ibllib/qc/task_qc_viewer/ViewEphysQC.py | 184 +++++++++++++++++ ibllib/qc/task_qc_viewer/__init__.py | 1 + ibllib/qc/task_qc_viewer/task_qc.py | 264 ++++++++++++++++++++++++ 11 files changed, 626 insertions(+), 84 deletions(-) create mode 100644 ibllib/qc/task_qc_viewer/README.md create mode 100644 ibllib/qc/task_qc_viewer/ViewEphysQC.py create mode 100644 ibllib/qc/task_qc_viewer/__init__.py create mode 100644 ibllib/qc/task_qc_viewer/task_qc.py diff --git a/ibllib/io/raw_data_loaders.py b/ibllib/io/raw_data_loaders.py index 3adfd3127..d3539715a 100644 --- a/ibllib/io/raw_data_loaders.py +++ b/ibllib/io/raw_data_loaders.py @@ -1,11 +1,7 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- -# @Author: Niccolò Bonacchi, Miles Wells -# @Date: Monday, July 16th 2018, 1:28:46 pm """ -Raw Data Loader functions for PyBpod rig +Raw Data Loader functions for PyBpod rig. -Module contains one loader function per raw datafile +Module contains one loader function per raw datafile. """ import re import json diff --git a/ibllib/pipes/base_tasks.py b/ibllib/pipes/base_tasks.py index fc848af85..ac3ea1ed5 100644 --- a/ibllib/pipes/base_tasks.py +++ b/ibllib/pipes/base_tasks.py @@ -1,6 +1,7 @@ """Abstract base classes for dynamic pipeline tasks.""" import logging from pathlib import Path +from abc import abstractmethod from packaging import version from one.webclient import no_cache @@ -75,6 +76,9 @@ def read_params_file(self): class BehaviourTask(DynamicTask): + extractor = None + """ibllib.io.extractors.base.BaseBpodExtractor: A trials extractor object.""" + def __init__(self, session_path, **kwargs): super().__init__(session_path, **kwargs) @@ -125,6 +129,51 @@ def _spacer_support(settings): ver = v(settings.get('IBLRIG_VERSION') or '100.0.0') return ver not in (v('100.0.0'), v('8.0.0')) and ver >= v('7.1.0') + def extract_behaviour(self, save=True): + """Extract trials data. + + This is an abstract method called by `_run` and `run_qc` methods. Subclasses should return + the extracted trials data and a list of output files. This method should also save the + trials extractor object to the :prop:`extarctor` property for use by `run_qc`. + + Parameters + ---------- + save : bool + Whether to save the extracted data as ALF datasets. + + Returns + ------- + dict + A dictionary of trials data. + list of pathlib.Path + A list of output file paths if save == true. + """ + return None, None + + def run_qc(self, trials_data=None, update=True): + """Run task QC. + + Subclass method should return the QC object. This just validates the trials_data is not + None. + + Parameters + ---------- + trials_data : dict + A dictionary of extracted trials data. The output of :meth:`extract_behaviour`. + update : bool + If true, update Alyx with the QC outcome. + + Returns + ------- + ibllib.qc.task_metrics.TaskQC + A TaskQC object replete with task data and computed metrics. + """ + if not self.extractor or trials_data is None: + trials_data, _ = self.extract_behaviour(save=False) + if not trials_data: + raise ValueError('No trials data found') + return trials_data + class VideoTask(DynamicTask): diff --git a/ibllib/pipes/behavior_tasks.py b/ibllib/pipes/behavior_tasks.py index 5e7e5d829..3a1ce8c5e 100644 --- a/ibllib/pipes/behavior_tasks.py +++ b/ibllib/pipes/behavior_tasks.py @@ -75,7 +75,7 @@ def _run(self, update=True, save=True): """ Extracts an iblrig training session """ - trials, output_files = self._extract_behaviour(save=save) + trials, output_files = self.extract_behaviour(save=save) if trials is None: return None @@ -83,19 +83,16 @@ def _run(self, update=True, save=True): return output_files # Run the task QC - self._run_qc(trials, update=update) + self.run_qc(trials, update=update) return output_files - def _extract_behaviour(self, **kwargs): + def extract_behaviour(self, **kwargs): self.extractor = get_bpod_extractor(self.session_path, task_collection=self.collection) self.extractor.default_path = self.output_collection return self.extractor.extract(task_collection=self.collection, **kwargs) - def _run_qc(self, trials_data=None, update=True): - if not self.extractor or trials_data is None: - trials_data, _ = self._extract_behaviour(save=False) - if not trials_data: - raise ValueError('No trials data found') + def run_qc(self, trials_data=None, update=True): + trials_data = super().run_qc(trials_data, update=False) # validate trials data # Compile task data for QC qc = HabituationQC(self.session_path, one=self.one) @@ -130,10 +127,10 @@ def signature(self): ('*.meta', self.sync_collection, True)] return signature - def _extract_behaviour(self, save=True, **kwargs): + def extract_behaviour(self, save=True, **kwargs): """Extract the habituationChoiceWorld trial data using NI DAQ clock.""" # Extract Bpod trials - bpod_trials, _ = super()._extract_behaviour(save=False, **kwargs) + bpod_trials, _ = super().extract_behaviour(save=False, **kwargs) # Sync Bpod trials to FPGA sync, chmap = get_sync_and_chn_map(self.session_path, self.sync_collection) @@ -146,13 +143,13 @@ def _extract_behaviour(self, save=True, **kwargs): task_collection=self.collection, protocol_number=self.protocol_number, **kwargs) return outputs, files - def _run_qc(self, trials_data=None, update=True, **_): + def run_qc(self, trials_data=None, update=True, **_): """Run and update QC. This adds the bpod TTLs to the QC object *after* the QC is run in the super call method. The raw Bpod TTLs are not used by the QC however they are used in the iblapps QC plot. """ - qc = super()._run_qc(trials_data=trials_data, update=update) + qc = super().run_qc(trials_data=trials_data, update=update) qc.extractor.bpod_ttls = self.extractor.bpod return qc @@ -299,30 +296,25 @@ def signature(self): return signature def _run(self, update=True, save=True): - """ - Extracts an iblrig training session - """ - trials, output_files = self._extract_behaviour(save=save) + """Extracts an iblrig training session.""" + trials, output_files = self.extract_behaviour(save=save) if trials is None: return None if self.one is None or self.one.offline: return output_files # Run the task QC - self._run_qc(trials) + self.run_qc(trials) return output_files - def _extract_behaviour(self, **kwargs): + def extract_behaviour(self, **kwargs): self.extractor = get_bpod_extractor(self.session_path, task_collection=self.collection) self.extractor.default_path = self.output_collection return self.extractor.extract(task_collection=self.collection, **kwargs) - def _run_qc(self, trials_data=None, update=True): - if not self.extractor or trials_data is None: - trials_data, _ = self._extract_behaviour(save=False) - if not trials_data: - raise ValueError('No trials data found') + def run_qc(self, trials_data=None, update=True): + trials_data = super().run_qc(trials_data, update=False) # validate trials data # Compile task data for QC qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one, @@ -391,9 +383,9 @@ def _behaviour_criterion(self, update=True): "sessions", eid, "extended_qc", {"behavior": int(good_enough)} ) - def _extract_behaviour(self, save=True, **kwargs): + def extract_behaviour(self, save=True, **kwargs): # Extract Bpod trials - bpod_trials, _ = super()._extract_behaviour(save=False, **kwargs) + bpod_trials, _ = super().extract_behaviour(save=False, **kwargs) # Sync Bpod trials to FPGA sync, chmap = get_sync_and_chn_map(self.session_path, self.sync_collection) @@ -403,11 +395,8 @@ def _extract_behaviour(self, save=True, **kwargs): task_collection=self.collection, protocol_number=self.protocol_number, **kwargs) return outputs, files - def _run_qc(self, trials_data=None, update=False, plot_qc=False): - if not self.extractor or trials_data is None: - trials_data, _ = self._extract_behaviour(save=False) - if not trials_data: - raise ValueError('No trials data found') + def run_qc(self, trials_data=None, update=False, plot_qc=False): + trials_data = super().run_qc(trials_data, update=False) # validate trials data # Compile task data for QC qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one, @@ -448,13 +437,13 @@ def _run_qc(self, trials_data=None, update=False, plot_qc=False): return qc def _run(self, update=True, plot_qc=True, save=True): - dsets, out_files = self._extract_behaviour(save=save) + dsets, out_files = self.extract_behaviour(save=save) if not self.one or self.one.offline: return out_files self._behaviour_criterion(update=update) - self._run_qc(dsets, update=update, plot_qc=plot_qc) + self.run_qc(dsets, update=update, plot_qc=plot_qc) return out_files @@ -479,10 +468,10 @@ def signature(self): for fn in filter(None, extractor.save_names)] return signature - def _extract_behaviour(self, save=True, **kwargs): + def extract_behaviour(self, save=True, **kwargs): """Extract the Bpod trials data and Timeline acquired signals.""" # First determine the extractor from the task protocol - bpod_trials, _ = ChoiceWorldTrialsBpod._extract_behaviour(self, save=False, **kwargs) + bpod_trials, _ = ChoiceWorldTrialsBpod.extract_behaviour(self, save=False, **kwargs) # Sync Bpod trials to DAQ self.extractor = TimelineTrials(self.session_path, bpod_trials=bpod_trials, bpod_extractor=self.extractor) diff --git a/ibllib/pipes/ephys_preprocessing.py b/ibllib/pipes/ephys_preprocessing.py index 7ea845d18..9cfaa22e3 100644 --- a/ibllib/pipes/ephys_preprocessing.py +++ b/ibllib/pipes/ephys_preprocessing.py @@ -693,24 +693,23 @@ def _behaviour_criterion(self): "sessions", eid, "extended_qc", {"behavior": int(good_enough)} ) - def _extract_behaviour(self): + def extract_behaviour(self, save=True): dsets, out_files, self.extractor = ephys_fpga.extract_all( - self.session_path, save=True, return_extractor=True) + self.session_path, save=save, return_extractor=True) return dsets, out_files - def _run(self, plot_qc=True): - dsets, out_files = self._extract_behaviour() - - if not self.one or self.one.offline: - return out_files + def run_qc(self, trials_data=None, update=True, plot_qc=False): + if trials_data is None: + trials_data, _ = self.extract_behaviour(save=False) + if not trials_data: + raise ValueError('No trials data found') - 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 = qc.extractor.rename_data(dsets) + qc.extractor.data = qc.extractor.rename_data(trials_data) wheel_ts_bpod = self.extractor.bpod2fpga(self.extractor.bpod_trials['wheel_timestamps']) qc.extractor.data['wheel_timestamps_bpod'] = wheel_ts_bpod qc.extractor.data['wheel_position_bpod'] = self.extractor.bpod_trials['wheel_position'] @@ -721,7 +720,7 @@ def _run(self, plot_qc=True): qc.extractor.bpod_ttls = self.extractor.bpod # Aggregate and update Alyx QC fields - qc.run(update=True) + qc.run(update=update) if plot_qc: _logger.info("Creating Trials QC plots") @@ -734,7 +733,14 @@ def _run(self, plot_qc=True): _logger.error('Could not create Trials QC Plot') _logger.error(traceback.format_exc()) self.status = -1 + return qc + + def _run(self, plot_qc=True): + dsets, out_files = self.extract_behaviour() + if self.one and not self.one.offline: + self._behaviour_criterion() + self.run_qc(trials_data=dsets, update=True, plot_qc=plot_qc) return out_files def get_signatures(self, **kwargs): @@ -761,8 +767,8 @@ class LaserTrialsLegacy(EphysTrials): This is legacy because personal project extractors should be in a separate repository. """ - def _extract_behaviour(self): - dsets, out_files = super()._extract_behaviour() + def extract_behaviour(self): + dsets, out_files = super().extract_behaviour() # Re-extract the laser datasets as the above default extractor discards them from ibllib.io.extractors import opto_trials diff --git a/ibllib/pipes/tasks.py b/ibllib/pipes/tasks.py index 670e2e8fa..00b324fc3 100644 --- a/ibllib/pipes/tasks.py +++ b/ibllib/pipes/tasks.py @@ -295,11 +295,7 @@ def _run(self, overwrite=False): """ def setUp(self, **kwargs): - """ - Setup method to get the data handler and ensure all data is available locally to run task - :param kwargs: - :return: - """ + """Get the data handler and ensure all data is available locally to run task.""" if self.location == 'server': self.get_signatures(**kwargs) diff --git a/ibllib/pipes/training_preprocessing.py b/ibllib/pipes/training_preprocessing.py index ad2172809..1cad5124e 100644 --- a/ibllib/pipes/training_preprocessing.py +++ b/ibllib/pipes/training_preprocessing.py @@ -47,27 +47,42 @@ class TrainingTrials(tasks.Task): ('*wheelMoves.peakAmplitude.npy', 'alf', True)] } - def _run(self): - """ - Extracts an iblrig training session - """ - trials, wheel, output_files = bpod_trials.extract_all(self.session_path, save=True) + def extract_behaviour(self, save=True): + """Extracts an iblrig training session.""" + trials, wheel, output_files = bpod_trials.extract_all(self.session_path, save=save) if trials is None: - return None - if self.one is None or self.one.offline: - return output_files - # Run the task QC + return None, None + if wheel is not None: + trials.update(wheel) + return trials, output_files + + def run_qc(self, trials_data=None, update=True): + if trials_data is None: + trials_data, _ = self.extract_behaviour(save=False) + if not trials_data: + raise ValueError('No trials data found') + # Compile task data for QC - type = get_session_extractor_type(self.session_path) - if type == 'habituation': + extractor_type = get_session_extractor_type(self.session_path) + if extractor_type == 'habituation': qc = HabituationQC(self.session_path, one=self.one) - qc.extractor = TaskQCExtractor(self.session_path, one=self.one) - else: # Update wheel data + else: qc = TaskQC(self.session_path, one=self.one) - qc.extractor = TaskQCExtractor(self.session_path, one=self.one) - qc.extractor.wheel_encoding = 'X1' + qc.extractor = TaskQCExtractor(self.session_path, one=self.one, lazy=True) + qc.extractor.type = extractor_type + qc.data = qc.extractor.rename_data(trials_data) + qc.extractor.load_raw_data() # re-loads raw data and populates various properties # Aggregate and update Alyx QC fields - qc.run(update=True) + qc.run(update=update) + + return qc + + def _run(self, **_): + """Extracts an iblrig training session and runs QC.""" + trials_data, output_files = self.extract_behaviour() + if self.one and not self.one.offline: + # Run the task QC + self.run_qc(trials_data) return output_files diff --git a/ibllib/qc/task_extractors.py b/ibllib/qc/task_extractors.py index 5f5269710..96e8940f7 100644 --- a/ibllib/qc/task_extractors.py +++ b/ibllib/qc/task_extractors.py @@ -118,14 +118,12 @@ def _ensure_required_data(self): ) def load_raw_data(self): - """ - Loads the TTLs, raw task data and task settings - :return: - """ + """Loads the TTLs, raw task data and task settings.""" self.log.info(f'Loading raw data from {self.session_path}') self.type = self.type or get_session_extractor_type(self.session_path, task_collection=self.task_collection) # Finds the sync type when it isn't explicitly set, if ephys we assume nidq otherwise bpod self.sync_type = self.sync_type or 'nidq' if self.type == 'ephys' else 'bpod' + self.wheel_encoding = 'X4' if (self.sync_type != 'bpod' and not self.bpod_only) else 'X1' self.settings, self.raw_data = raw.load_bpod(self.session_path, task_collection=self.task_collection) # Fetch the TTLs for the photodiode and audio @@ -147,22 +145,18 @@ def channel_events(name): self.frame_ttls, self.audio_ttls, self.bpod_ttls = ttls def extract_data(self): - """Extracts and loads behaviour data for QC + """Extracts and loads behaviour data for QC. + NB: partial extraction when bpod_only attribute is False requires intervals and intervals_bpod to be assigned to the data attribute before calling this function. - :return: """ warnings.warn('The TaskQCExtractor.extract_data will be removed in the future, ' 'use dynamic pipeline behaviour tasks instead.', DeprecationWarning) self.log.info(f'Extracting session: {self.session_path}') - self.type = self.type or get_session_extractor_type(self.session_path, task_collection=self.task_collection) - # Finds the sync type when it isn't explicitly set, if ephys we assume nidq otherwise bpod - self.sync_type = self.sync_type or 'nidq' if self.type == 'ephys' else 'bpod' - - self.wheel_encoding = 'X4' if (self.sync_type != 'bpod' and not self.bpod_only) else 'X1' if not self.raw_data: self.load_raw_data() + # Run extractors if self.sync_type != 'bpod' and not self.bpod_only: data, _ = ephys_fpga.extract_all(self.session_path, save=False, task_collection=self.task_collection) diff --git a/ibllib/qc/task_qc_viewer/README.md b/ibllib/qc/task_qc_viewer/README.md new file mode 100644 index 000000000..d9f9b930f --- /dev/null +++ b/ibllib/qc/task_qc_viewer/README.md @@ -0,0 +1,48 @@ +# Task QC Viewer +This will download the TTL pulses and data collected on Bpod and/or FPGA and plot the results +alongside an interactive table. +The UUID is the session id. + +## Usage: command line + +Launch the Viewer by typing `python task_qc.py session_UUID` , example: +``` +python task_qc.py c9fec76e-7a20-4da4-93ad-04510a89473b +# or with ipython +ipython task_qc.py -- c9fec76e-7a20-4da4-93ad-04510a89473b +``` + +Or just using a local path (on a local server for example): +``` +python task_qc.py /mnt/s0/Subjects/KS022/2019-12-10/001 --local +# or with ipython +ipython task_qc.py -- /mnt/s0/Subjects/KS022/2019-12-10/001 --local +``` + +## Usage: from ipython prompt +``` python +from iblapps.task_qc_viewer.task_qc import show_session_task_qc +session_path = "/datadisk/Data/IntegrationTests/ephys/choice_world_init/KS022/2019-12-10/001" +show_session_task_qc(session_path, local=True) +``` + +## Plots +1) Sync pulse display: +- TTL sync pulses (as recorded on the Bpod or FPGA for ephys sessions) for some key apparatus (i +.e. frame2TTL, audio signal). TTL pulse trains are displayed in black (time on x-axis, voltage on y-axis), offset by an increment of 1 each time (e.g. audio signal is on line 3, cf legend). +- trial event types, vertical lines (marked in different colours) + +2) Wheel display: +- the wheel position in radians +- trial event types, vertical lines (marked in different colours) + +3) Interactive table: +Each row is a trial entry. Each column is a trial event + +When double-clicking on any field of that table, the Sync pulse display time (x-) axis is adjusted so as to visualise the corresponding trial selected. + +### What to look for +Tests are defined in the SINGLE METRICS section of ibllib/qc/task_metrics.py: https://github.com/int-brain-lab/ibllib/blob/master/ibllib/qc/task_metrics.py#L148-L149 + +### Exit +Close the GUI window containing the interactive table to exit. diff --git a/ibllib/qc/task_qc_viewer/ViewEphysQC.py b/ibllib/qc/task_qc_viewer/ViewEphysQC.py new file mode 100644 index 000000000..301490a06 --- /dev/null +++ b/ibllib/qc/task_qc_viewer/ViewEphysQC.py @@ -0,0 +1,184 @@ +"""An interactive PyQT QC data frame.""" +import logging + +from PyQt5 import QtCore, QtWidgets +from matplotlib.figure import Figure +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT +import pandas as pd +import numpy as np + +import qt as qt + +_logger = logging.getLogger(__name__) + + +class DataFrameModel(QtCore.QAbstractTableModel): + DtypeRole = QtCore.Qt.UserRole + 1000 + ValueRole = QtCore.Qt.UserRole + 1001 + + def __init__(self, df=pd.DataFrame(), parent=None): + super(DataFrameModel, self).__init__(parent) + self._dataframe = df + + def setDataFrame(self, dataframe): + self.beginResetModel() + self._dataframe = dataframe.copy() + self.endResetModel() + + def dataFrame(self): + return self._dataframe + + dataFrame = QtCore.pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame) + + @QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str) + def headerData(self, section: int, orientation: QtCore.Qt.Orientation, + role: int = QtCore.Qt.DisplayRole): + if role == QtCore.Qt.DisplayRole: + if orientation == QtCore.Qt.Horizontal: + return self._dataframe.columns[section] + else: + return str(self._dataframe.index[section]) + return QtCore.QVariant() + + def rowCount(self, parent=QtCore.QModelIndex()): + if parent.isValid(): + return 0 + return len(self._dataframe.index) + + def columnCount(self, parent=QtCore.QModelIndex()): + if parent.isValid(): + return 0 + return self._dataframe.columns.size + + def data(self, index, role=QtCore.Qt.DisplayRole): + if (not index.isValid() or not (0 <= index.row() < self.rowCount() and + 0 <= index.column() < self.columnCount())): + return QtCore.QVariant() + row = self._dataframe.index[index.row()] + col = self._dataframe.columns[index.column()] + dt = self._dataframe[col].dtype + + val = self._dataframe.iloc[row][col] + if role == QtCore.Qt.DisplayRole: + return str(val) + elif role == DataFrameModel.ValueRole: + return val + if role == DataFrameModel.DtypeRole: + return dt + return QtCore.QVariant() + + def roleNames(self): + roles = { + QtCore.Qt.DisplayRole: b'display', + DataFrameModel.DtypeRole: b'dtype', + DataFrameModel.ValueRole: b'value' + } + return roles + + def sort(self, col, order): + """ + Sort table by given column number. + + :param col: the column number selected (between 0 and self._dataframe.columns.size) + :param order: the order to be sorted, 0 is descending; 1, ascending + :return: + """ + self.layoutAboutToBeChanged.emit() + col_name = self._dataframe.columns.values[col] + # print('sorting by ' + col_name) + self._dataframe.sort_values(by=col_name, ascending=not order, inplace=True) + self._dataframe.reset_index(inplace=True, drop=True) + self.layoutChanged.emit() + + +class PlotCanvas(FigureCanvasQTAgg): + + def __init__(self, parent=None, width=5, height=4, dpi=100, wheel=None): + fig = Figure(figsize=(width, height), dpi=dpi) + + FigureCanvasQTAgg.__init__(self, fig) + self.setParent(parent) + + FigureCanvasQTAgg.setSizePolicy( + self, + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + if wheel: + self.ax, self.ax2 = fig.subplots( + 2, 1, gridspec_kw={'height_ratios': [2, 1]}, sharex=True) + else: + self.ax = fig.add_subplot(111) + self.draw() + + +class PlotWindow(QtWidgets.QWidget): + def __init__(self, parent=None, wheel=None): + QtWidgets.QWidget.__init__(self, parent=None) + self.canvas = PlotCanvas(wheel=wheel) + self.vbl = QtWidgets.QVBoxLayout() # Set box for plotting + self.vbl.addWidget(self.canvas) + self.setLayout(self.vbl) + self.vbl.addWidget(NavigationToolbar2QT(self.canvas, self)) + + +class GraphWindow(QtWidgets.QWidget): + def __init__(self, parent=None, wheel=None): + QtWidgets.QWidget.__init__(self, parent=None) + vLayout = QtWidgets.QVBoxLayout(self) + hLayout = QtWidgets.QHBoxLayout() + self.pathLE = QtWidgets.QLineEdit(self) + hLayout.addWidget(self.pathLE) + self.loadBtn = QtWidgets.QPushButton("Select File", self) + hLayout.addWidget(self.loadBtn) + vLayout.addLayout(hLayout) + self.pandasTv = QtWidgets.QTableView(self) + vLayout.addWidget(self.pandasTv) + self.loadBtn.clicked.connect(self.loadFile) + self.pandasTv.setSortingEnabled(True) + self.pandasTv.doubleClicked.connect(self.tv_double_clicked) + self.wplot = PlotWindow(wheel=wheel) + self.wplot.show() + self.wheel = wheel + + def loadFile(self): + fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open File", "", + "CSV Files (*.csv)") + self.pathLE.setText(fileName) + df = pd.read_csv(fileName) + self.update_df(df) + + def update_df(self, df): + model = DataFrameModel(df) + self.pandasTv.setModel(model) + self.wplot.canvas.draw() + + def tv_double_clicked(self): + df = self.pandasTv.model()._dataframe + ind = self.pandasTv.currentIndex() + start = df.loc[ind.row()]['intervals_0'] + finish = df.loc[ind.row()]['intervals_1'] + dt = finish - start + if self.wheel: + idx = np.searchsorted(self.wheel['re_ts'], np.array([start - dt / 10, + finish + dt / 10])) + period = self.wheel['re_pos'][idx[0]:idx[1]] + if period.size == 0: + _logger.warning('No wheel data during trial #%i', ind.row()) + else: + min_val, max_val = np.min(period), np.max(period) + self.wplot.canvas.ax2.set_ylim(min_val - 1, max_val + 1) + self.wplot.canvas.ax2.set_xlim(start - dt / 10, finish + dt / 10) + self.wplot.canvas.ax.set_xlim(start - dt / 10, finish + dt / 10) + + self.wplot.canvas.draw() + + +def viewqc(qc=None, title=None, wheel=None): + qt.create_app() + qcw = GraphWindow(wheel=wheel) + qcw.setWindowTitle(title) + if qc is not None: + qcw.update_df(qc) + qcw.show() + return qcw diff --git a/ibllib/qc/task_qc_viewer/__init__.py b/ibllib/qc/task_qc_viewer/__init__.py new file mode 100644 index 000000000..205cab90a --- /dev/null +++ b/ibllib/qc/task_qc_viewer/__init__.py @@ -0,0 +1 @@ +"""Interactive task QC viewer.""" diff --git a/ibllib/qc/task_qc_viewer/task_qc.py b/ibllib/qc/task_qc_viewer/task_qc.py new file mode 100644 index 000000000..9bb6c1781 --- /dev/null +++ b/ibllib/qc/task_qc_viewer/task_qc.py @@ -0,0 +1,264 @@ +import logging +import argparse +from itertools import cycle +import random +from collections.abc import Sized + +import pandas as pd +import qt as qt +from matplotlib.colors import TABLEAU_COLORS +from one.api import ONE +from one.alf.spec import is_session_path + +import ibllib.plots as plots +from ibllib.qc.task_metrics import TaskQC +from ibllib.pipes.dynamic_pipeline import get_trials_tasks + +from . import ViewEphysQC + +EVENT_MAP = {'goCue_times': ['#2ca02c', 'solid'], # green + 'goCueTrigger_times': ['#2ca02c', 'dotted'], # green + 'errorCue_times': ['#d62728', 'solid'], # red + 'errorCueTrigger_times': ['#d62728', 'dotted'], # red + 'valveOpen_times': ['#17becf', 'solid'], # cyan + 'stimFreeze_times': ['#0000ff', 'solid'], # blue + 'stimFreezeTrigger_times': ['#0000ff', 'dotted'], # blue + 'stimOff_times': ['#9400d3', 'solid'], # dark violet + 'stimOffTrigger_times': ['#9400d3', 'dotted'], # dark violet + 'stimOn_times': ['#e377c2', 'solid'], # pink + 'stimOnTrigger_times': ['#e377c2', 'dotted'], # pink + 'response_times': ['#8c564b', 'solid'], # brown + } +cm = [EVENT_MAP[k][0] for k in EVENT_MAP] +ls = [EVENT_MAP[k][1] for k in EVENT_MAP] +CRITICAL_CHECKS = ( + 'check_audio_pre_trial', + 'check_correct_trial_event_sequence', + 'check_error_trial_event_sequence', + 'check_n_trial_events', + 'check_response_feedback_delays', + 'check_response_stimFreeze_delays', + 'check_reward_volume_set', + 'check_reward_volumes', + 'check_stimOn_goCue_delays', + 'check_stimulus_move_before_goCue', + 'check_wheel_move_before_feedback', + 'check_wheel_freeze_during_quiescence' +) + + +_logger = logging.getLogger(__name__) + + +class QcFrame: + + qc = None + """ibllib.qc.task_metrics.TaskQC: A TaskQC object containing extracted data""" + + frame = None + """pandas.DataFrame: A table of failing trial-level QC metrics.""" + + def __init__(self, qc): + """ + An interactive display of task QC data. + + Parameters + ---------- + qc : ibllib.qc.task_metrics.TaskQC + A TaskQC object containing extracted data for plotting. + """ + assert qc.extractor and qc.metrics, 'Please run QC before passing to QcFrame' + self.qc = qc + + # Print failed + outcome, results, outcomes = self.qc.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') + + print('The following *critical* checks did not pass:') + critical_checks = [f'_{x.replace("check", "task")}' for x in CRITICAL_CHECKS] + for k, v in outcomes.items(): + if v != 'PASS' and k in critical_checks: + print(k[6:]) + + # 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.qc.metrics) + self.frame['intervals_0'] = self.qc.extractor.data['intervals'][:, 0] + self.frame['intervals_1'] = self.qc.extractor.data['intervals'][:, 1] + self.frame.insert(loc=0, column='trial_no', value=self.frame.index) + + @property + def n_trials(self): + return self.qc.extractor.data['intervals'].shape[0] + + def get_wheel_data(self): + return {'re_pos': self.qc.extractor.data.get('wheel_position'), + 're_ts': self.qc.extractor.data.get('wheel_timestamps')} + + 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', + ('stimCenter_times' + if 'stimCenter_times' in self.qc.extractor.data + else 'stimFreeze_times'), # handle habituationChoiceWorld exception + 'stimOff_times', + 'stimOn_times' + ] + + plot_args = { + 'ymin': 0, + 'ymax': 4, + 'linewidth': 2, + 'ax': axes + } + + bnc1 = self.qc.extractor.frame_ttls + bnc2 = self.qc.extractor.audio_ttls + trial_data = self.qc.extractor.data + + if bnc1['times'].size: + plots.squares(bnc1['times'], bnc1['polarities'] * 0.4 + 1, ax=axes, color='k') + if bnc2['times'].size: + plots.squares(bnc2['times'], bnc2['polarities'] * 0.4 + 2, ax=axes, color='k') + linestyle = linestyle or random.choices(('-', '--', '-.', ':'), k=len(trial_events)) + + if self.qc.extractor.bpod_ttls is not None: + bpttls = self.qc.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_yticks(list(range(plot_args['ymax'] + 1))) + axes.set_yticklabels(ylabels) + axes.set_ylim([0, plot_args['ymax']]) + + if wheel_axes: + wheel_data = self.get_wheel_data() + wheel_plot_args = { + 'ax': wheel_axes, + 'ymin': wheel_data['re_pos'].min(), + 'ymax': wheel_data['re_pos'].max()} + plot_args = {**plot_args, **wheel_plot_args} + + wheel_axes.plot(wheel_data['re_ts'], 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 show_session_task_qc(qc_or_session=None, bpod_only=False, local=False, one=None): + """ + Displays the task QC for a given session. + + Parameters + ---------- + qc_or_session : str, pathlib.Path, ibllib.qc.task_metrics.TaskQC, QcFrame + An experiment ID, session path, or TaskQC object. + bpod_only : bool + If true, display Bpod extracted events instead of data from the DAQ. !!NOT IMPLEMENTED!! + local : bool + If true, asserts all data local (i.e. do not attempt to download missing datasets). + one : one.api.One + An instance of ONE. + + Returns + ------- + QcFrame + The QcFrame object. + """ + if bpod_only is True: + raise NotImplementedError + if isinstance(qc_or_session, QcFrame): + qc = qc_or_session + elif isinstance(qc_or_session, TaskQC): + qc = QcFrame(qc_or_session) + else: # assumed to be eid or session path + one = one or ONE(mode='local' if local else 'auto') + if not is_session_path(qc_or_session): + eid = one.to_eid(qc_or_session) + session_path = one.eid2path(eid) + else: + session_path = qc_or_session + tasks = get_trials_tasks(session_path, one=None if local else one) + # TODO Param to choose which task to pick if more than one + task = next(t for t in tasks if 'passive' not in t.name.lower()) + task.location = 'server' if local else 'remote' # affects whether missing data are downloaded + task.setUp() + if local: # currently setUp does not raise on missing data + task.assert_expected_inputs(raise_error=True) + task_qc = task.run_qc(update=False) + qc = QcFrame(task_qc) + + # Handle trial event names in habituationChoiceWorld + events = EVENT_MAP.keys() + if 'stimCenter_times' in qc.qc.extractor.data: + events = map(lambda x: x.replace('stimFreeze', 'stimCenter'), events) + + # Run QC and plot + w = ViewEphysQC.viewqc(wheel=qc.get_wheel_data) + qc.create_plots(w.wplot.canvas.ax, + wheel_axes=w.wplot.canvas.ax2, + trial_events=list(events), + color_map=cm, + linestyle=ls) + # Update table and callbacks + w.update_df(qc.frame) + qt.run_app() + return qc + + +if __name__ == '__main__': + """Run TaskQC viewer with wheel data. + + For information on the QC checks see the QC Flags & failures document: + https://docs.google.com/document/d/1X-ypFEIxqwX6lU9pig4V_zrcR5lITpd8UJQWzW9I9zI/edit# + + Examples + -------- + >>> ipython task_qc.py c9fec76e-7a20-4da4-93ad-04510a89473b + >>> ipython task_qc.py ./KS022/2019-12-10/001 --local + """ + # Parse parameters + parser = argparse.ArgumentParser(description='Quick viewer to see the behaviour data from' + 'choice world sessions.') + parser.add_argument('session', help='session uuid') + parser.add_argument('--bpod', action='store_true', help='run QC on Bpod data only (no FPGA)') + parser.add_argument('--local', action='store_true', help='run from disk location (lab server') + args = parser.parse_args() # returns data from the options specified (echo) + + show_session_task_qc(qc_or_session=args.session, bpod_only=args.bpod, local=args.local) From 4b660c91cddf03313e98ff954fb0706971c55f30 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Thu, 4 Jan 2024 12:49:30 +0200 Subject: [PATCH 02/25] task qc viewer tests --- ibllib/misc/qt.py | 45 +++++++++++ ibllib/pipes/base_tasks.py | 1 - ibllib/qc/task_metrics.py | 3 + ibllib/qc/task_qc_viewer/ViewEphysQC.py | 2 +- ibllib/qc/task_qc_viewer/task_qc.py | 86 ++++++++++++++++---- ibllib/tests/qc/test_task_qc_viewer.py | 102 ++++++++++++++++++++++++ 6 files changed, 221 insertions(+), 18 deletions(-) create mode 100644 ibllib/misc/qt.py create mode 100644 ibllib/tests/qc/test_task_qc_viewer.py diff --git a/ibllib/misc/qt.py b/ibllib/misc/qt.py new file mode 100644 index 000000000..5ea154735 --- /dev/null +++ b/ibllib/misc/qt.py @@ -0,0 +1,45 @@ +"""PyQt5 helper functions.""" +import logging +import sys +from functools import wraps + +from PyQt5 import QtWidgets + +_logger = logging.getLogger(__name__) + + +def get_main_window(): + """Get the Main window of a QT application.""" + app = QtWidgets.QApplication.instance() + return [w for w in app.topLevelWidgets() if isinstance(w, QtWidgets.QMainWindow)][0] + + +def create_app(): + """Create a Qt application.""" + global QT_APP + QT_APP = QtWidgets.QApplication.instance() + if QT_APP is None: # pragma: no cover + QT_APP = QtWidgets.QApplication(sys.argv) + return QT_APP + + +def require_qt(func): + """Function decorator to specify that a function requires a Qt application. + + Use this decorator to specify that a function needs a running Qt application before it can run. + An error is raised if that is not the case. + """ + @wraps(func) + def wrapped(*args, **kwargs): + if not QtWidgets.QApplication.instance(): + _logger.warning('Creating a Qt application.') + create_app() + return func(*args, **kwargs) + return wrapped + + +@require_qt +def run_app(): # pragma: no cover + """Run the Qt application.""" + global QT_APP + return QT_APP.exit(QT_APP.exec_()) diff --git a/ibllib/pipes/base_tasks.py b/ibllib/pipes/base_tasks.py index ac3ea1ed5..035da032a 100644 --- a/ibllib/pipes/base_tasks.py +++ b/ibllib/pipes/base_tasks.py @@ -1,7 +1,6 @@ """Abstract base classes for dynamic pipeline tasks.""" import logging from pathlib import Path -from abc import abstractmethod from packaging import version from one.webclient import no_cache diff --git a/ibllib/qc/task_metrics.py b/ibllib/qc/task_metrics.py index efe30f73c..d9f0c5c07 100644 --- a/ibllib/qc/task_metrics.py +++ b/ibllib/qc/task_metrics.py @@ -87,6 +87,9 @@ class TaskQC(base.QC): criteria['_task_iti_delays'] = {'NOT_SET': 0} criteria['_task_passed_trial_checks'] = {'NOT_SET': 0} + extractor = None + """ibllib.qc.task_extractors.TaskQCExtractor: A task extractor object containing raw and extracted data.""" + @staticmethod def _thresholding(qc_value, thresholds=None): """ diff --git a/ibllib/qc/task_qc_viewer/ViewEphysQC.py b/ibllib/qc/task_qc_viewer/ViewEphysQC.py index 301490a06..7151d87d6 100644 --- a/ibllib/qc/task_qc_viewer/ViewEphysQC.py +++ b/ibllib/qc/task_qc_viewer/ViewEphysQC.py @@ -7,7 +7,7 @@ import pandas as pd import numpy as np -import qt as qt +from ibllib.misc import qt _logger = logging.getLogger(__name__) diff --git a/ibllib/qc/task_qc_viewer/task_qc.py b/ibllib/qc/task_qc_viewer/task_qc.py index 9bb6c1781..32ec8722f 100644 --- a/ibllib/qc/task_qc_viewer/task_qc.py +++ b/ibllib/qc/task_qc_viewer/task_qc.py @@ -3,16 +3,21 @@ from itertools import cycle import random from collections.abc import Sized +from pathlib import Path import pandas as pd -import qt as qt +import numpy as np from matplotlib.colors import TABLEAU_COLORS from one.api import ONE from one.alf.spec import is_session_path import ibllib.plots as plots +from ibllib.misc import qt from ibllib.qc.task_metrics import TaskQC from ibllib.pipes.dynamic_pipeline import get_trials_tasks +from ibllib.pipes.base_tasks import BehaviourTask +from ibllib.pipes.behavior_tasks import HabituationTrialsBpod, ChoiceWorldTrialsBpod +from ibllib.pipes.training_preprocessing import TrainingTrials from . import ViewEphysQC @@ -103,8 +108,8 @@ def n_trials(self): return self.qc.extractor.data['intervals'].shape[0] def get_wheel_data(self): - return {'re_pos': self.qc.extractor.data.get('wheel_position'), - 're_ts': self.qc.extractor.data.get('wheel_timestamps')} + return {'re_pos': self.qc.extractor.data.get('wheel_position', np.array([])), + 're_ts': self.qc.extractor.data.get('wheel_timestamps', np.array([]))} def create_plots(self, axes, wheel_axes=None, trial_events=None, color_map=None, linestyle=None): @@ -160,7 +165,8 @@ def create_plots(self, axes, 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) + if event in trial_data: + 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_yticks(list(range(plot_args['ymax'] + 1))) @@ -171,38 +177,69 @@ def create_plots(self, axes, wheel_data = self.get_wheel_data() wheel_plot_args = { 'ax': wheel_axes, - 'ymin': wheel_data['re_pos'].min(), - 'ymax': wheel_data['re_pos'].max()} + 'ymin': wheel_data['re_pos'].min() if wheel_data['re_pos'].size else 0, + 'ymax': wheel_data['re_pos'].max() if wheel_data['re_pos'].size else 1} plot_args = {**plot_args, **wheel_plot_args} wheel_axes.plot(wheel_data['re_ts'], 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) + if event in trial_data: + plots.vertical_lines(trial_data[event], + label=event, color=c, linestyle=ln, **plot_args) -def show_session_task_qc(qc_or_session=None, bpod_only=False, local=False, one=None): +def get_bpod_trials_task(task): + """ + Return the correct trials task for extracting only the Bpod trials. + + Parameters + ---------- + task : ibllib.pipes.tasks.Task + A pipeline task from which to derive the Bpod trials task. + + Returns + ------- + ibllib.pipes.tasks.Task + A Bpod choice world trials task instance. + """ + if isinstance(task, TrainingTrials) or task.__class__ in (ChoiceWorldTrialsBpod, HabituationTrialsBpod): + pass # do nothing; already Bpod only + elif isinstance(task, BehaviourTask): + # A dynamic pipeline task + trials_class = HabituationTrialsBpod if 'habituation' in task.protocol else ChoiceWorldTrialsBpod + task = trials_class(task.session_path, + collection=task.collection, protocol_number=task.protocol_number, + protocol=task.protocol, one=task.one) + else: # A legacy pipeline task (should be EphysTrials as there are no other options) + task = TrainingTrials(task.session_path, one=task.one) + return task + + +def show_session_task_qc(qc_or_session=None, bpod_only=False, local=False, one=None, protocol_number=None): """ Displays the task QC for a given session. + NB: For this to work, all behaviour trials task classes must implement a `run_qc` method. + Parameters ---------- qc_or_session : str, pathlib.Path, ibllib.qc.task_metrics.TaskQC, QcFrame An experiment ID, session path, or TaskQC object. bpod_only : bool - If true, display Bpod extracted events instead of data from the DAQ. !!NOT IMPLEMENTED!! + If true, display Bpod extracted events instead of data from the DAQ. local : bool If true, asserts all data local (i.e. do not attempt to download missing datasets). one : one.api.One An instance of ONE. + protocol_number : int + If not None, displays the QC for the protocol number provided. Argument is ignored if + `qc_or_session` is a TaskQC object or QcFrame instance. Returns ------- QcFrame The QcFrame object. """ - if bpod_only is True: - raise NotImplementedError if isinstance(qc_or_session, QcFrame): qc = qc_or_session elif isinstance(qc_or_session, TaskQC): @@ -213,14 +250,31 @@ def show_session_task_qc(qc_or_session=None, bpod_only=False, local=False, one=N eid = one.to_eid(qc_or_session) session_path = one.eid2path(eid) else: - session_path = qc_or_session + session_path = Path(qc_or_session) tasks = get_trials_tasks(session_path, one=None if local else one) - # TODO Param to choose which task to pick if more than one - task = next(t for t in tasks if 'passive' not in t.name.lower()) + # Get the correct task and ensure not passive + if protocol_number is None: + if not (task := next((t for t in tasks if 'passive' not in t.name.lower()), None)): + raise ValueError('No non-passive behaviour tasks found for session ' + '/'.join(session_path.parts[-3:])) + elif not isinstance(protocol_number, int) or protocol_number < 0: + raise TypeError('Protocol number must be a positive integer') + elif protocol_number > len(tasks) - 1: + raise ValueError('Invalid protocol number') + else: + task = tasks[protocol_number] + if 'passive' in task.name.lower(): + raise ValueError('QC display not supported for passive protocols') + # If Bpod only and not a dynamic pipeline Bpod behaviour task OR legacy TrainingTrials task + if bpod_only and 'bpod' not in task.name.lower(): + # Use the dynamic pipeline Bpod behaviour task instead (should work with legacy pipeline too) + task = get_bpod_trials_task(task) + _logger.debug('Using %s task', task.name) + # Ensure required data are present task.location = 'server' if local else 'remote' # affects whether missing data are downloaded task.setUp() if local: # currently setUp does not raise on missing data task.assert_expected_inputs(raise_error=True) + # Compute the QC and build the frame task_qc = task.run_qc(update=False) qc = QcFrame(task_qc) @@ -230,7 +284,7 @@ def show_session_task_qc(qc_or_session=None, bpod_only=False, local=False, one=N events = map(lambda x: x.replace('stimFreeze', 'stimCenter'), events) # Run QC and plot - w = ViewEphysQC.viewqc(wheel=qc.get_wheel_data) + w = ViewEphysQC.viewqc(wheel=qc.get_wheel_data()) qc.create_plots(w.wplot.canvas.ax, wheel_axes=w.wplot.canvas.ax2, trial_events=list(events), diff --git a/ibllib/tests/qc/test_task_qc_viewer.py b/ibllib/tests/qc/test_task_qc_viewer.py new file mode 100644 index 000000000..370cb9144 --- /dev/null +++ b/ibllib/tests/qc/test_task_qc_viewer.py @@ -0,0 +1,102 @@ +"""Tests for the ibllib.qc.task_qc_viewer package.""" +import unittest +from unittest import mock + +from one.api import ONE +import numpy as np + +from ibllib.pipes.ephys_preprocessing import EphysTrials +from ibllib.pipes.training_preprocessing import TrainingTrials +from ibllib.pipes.behavior_tasks import HabituationTrialsBpod, ChoiceWorldTrialsNidq, ChoiceWorldTrialsBpod, PassiveTask +from ibllib.qc.task_qc_viewer.task_qc import get_bpod_trials_task, show_session_task_qc, QcFrame +from ibllib.qc.task_metrics import TaskQC +from ibllib.tests import TEST_DB + + +class TestTaskQC(unittest.TestCase): + """Tests for ibllib.qc.task_qc_viewer.task_qc module.""" + + def setUp(self): + self.one = ONE(**TEST_DB, mode='local') + + def test_get_bpod_trials_task(self): + """Test get_bpod_trials_task function.""" + task = TrainingTrials('foo/bar', one=self.one) + bpod_task = get_bpod_trials_task(task) + self.assertIs(task, bpod_task) + + task = HabituationTrialsBpod('foo/bar', one=self.one, + protocol_number=0, protocol='habituationChoiceWorld', collection='raw_task_data_00') + bpod_task = get_bpod_trials_task(task) + self.assertIs(task, bpod_task) + + task = ChoiceWorldTrialsNidq('foo/bar', one=self.one, + protocol_number=2, protocol='ephysChoiceWorld', collection='raw_task_data_02') + bpod_task = get_bpod_trials_task(task) + self.assertIs(bpod_task.__class__, ChoiceWorldTrialsBpod) + self.assertEqual(bpod_task.protocol_number, 2) + self.assertEqual(bpod_task.protocol, 'ephysChoiceWorld') + self.assertEqual(bpod_task.collection, 'raw_task_data_02') + self.assertIs(bpod_task.one, self.one) + + task = EphysTrials('foo/bar', one=self.one) + bpod_task = get_bpod_trials_task(task) + self.assertIsInstance(bpod_task, TrainingTrials) + + @mock.patch('ibllib.qc.task_qc_viewer.task_qc.qt.run_app') + @mock.patch('ibllib.qc.task_qc_viewer.task_qc.get_trials_tasks') + def test_show_session_task_qc(self, trials_tasks_mock, run_app_mock): + """Test show_session_task_qc function.""" + trials_tasks_mock.return_value = [] + session_path = 'foo/bar/subject/2023-01-01/001' + self.assertRaises(ValueError, show_session_task_qc, session_path, one=self.one) + self.assertRaises(TypeError, show_session_task_qc, session_path, one=self.one, protocol_number=-2) + self.assertRaises(ValueError, show_session_task_qc, session_path, one=self.one, protocol_number=1) + + passive_task = PassiveTask('foo/bar', protocol='_iblrig_passiveChoiceWorld', protocol_number=0) + trials_tasks_mock.return_value = [passive_task] + self.assertRaises(ValueError, show_session_task_qc, session_path, one=self.one, protocol_number=0) + self.assertRaises(ValueError, show_session_task_qc, session_path, one=self.one) + + # Set up QC mock + qc_mock = mock.Mock(spec=TaskQC) + qc_mock.metrics = {'foo': .7} + qc_mock.compute_session_status.return_value = ('Fail', qc_mock.metrics, {'foo': 'FAIL'}) + qc_mock.extractor.data = {'intervals': np.array([[0, 1]])} + qc_mock.extractor.frame_ttls = qc_mock.extractor.audio_ttls = qc_mock.extractor.bpod_ttls = mock.MagicMock() + + active_task = mock.Mock(spec=ChoiceWorldTrialsNidq) + active_task.run_qc.return_value = qc_mock + active_task.name = 'Trials_activeChoiceWorld_01' + trials_tasks_mock.return_value = [passive_task, active_task] + qc = show_session_task_qc(session_path, one=self.one) + + self.assertIsInstance(qc, QcFrame) + self.assertIsInstance(qc.qc, TaskQC) + self.assertCountEqual(qc.get_wheel_data(), ('re_ts', 're_pos')) + active_task.run_qc.assert_called_once_with(update=False) + self.assertEqual('remote', active_task.location) + active_task.setUp.assert_called_once() + active_task.assert_expected_inputs.assert_not_called() + run_app_mock.assert_called_once() + + active_task.reset_mock(return_value=False) + with mock.patch('ibllib.qc.task_qc_viewer.task_qc.get_bpod_trials_task', return_value=active_task) as \ + get_bpod_trials_task_mock: + show_session_task_qc(session_path, one=self.one, local=True, bpod_only=True) + # Should be called in bpod_only mode + get_bpod_trials_task_mock.assert_called_once_with(active_task) + # Should be called in local mode + active_task.assert_expected_inputs.assert_called_once_with(raise_error=True) + + # If QcFrame instance passed, should use this and return it + self.assertIs(show_session_task_qc(qc, one=self.one), qc) + # If passing TaskQC object, should not call trials_tasks_mock + trials_tasks_mock.reset_mock() + show_session_task_qc(qc_mock, one=self.one) + self.assertIsInstance(qc, QcFrame) + trials_tasks_mock.assert_not_called() + + +if __name__ == '__main__': + unittest.main() From 956b1edbdf7b9f1bcdfeaf3333318afeafdd5305 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Wed, 31 Jan 2024 17:57:23 +0200 Subject: [PATCH 03/25] _get_checks method of TaskQC for overloading in subclass; get_bpodqc_metrics_frame moved from function to TaskQC method; QC kwarg for behaviour task run_qc method allows passing of custom TaskQC class; deprecated videopc params functions in ibllib.pipes.misc; raise in BehaviourTask.get_protocol when task protocol ambiguous --- ibllib/io/extractors/base.py | 28 +++++--- ibllib/io/extractors/training_trials.py | 4 +- ibllib/pipes/base_tasks.py | 84 +++++++++++++++++++++- ibllib/pipes/behavior_tasks.py | 40 ++++++++--- ibllib/pipes/misc.py | 6 ++ ibllib/pipes/tasks.py | 2 +- ibllib/qc/task_metrics.py | 93 ++++++++++++++----------- ibllib/tests/test_base_tasks.py | 39 +++++++++++ 8 files changed, 231 insertions(+), 65 deletions(-) diff --git a/ibllib/io/extractors/base.py b/ibllib/io/extractors/base.py index 1dd6417e1..1b3717a89 100644 --- a/ibllib/io/extractors/base.py +++ b/ibllib/io/extractors/base.py @@ -17,13 +17,16 @@ class BaseExtractor(abc.ABC): """ - Base extractor class + Base extractor class. + Writing an extractor checklist: - - on the child class, overload the _extract method - - this method should output one or several numpy.arrays or dataframe with a consistent shape - - save_names is a list or a string of filenames, there should be one per dataset - - set save_names to None for a dataset that doesn't need saving (could be set dynamically - in the _extract method) + + - on the child class, overload the _extract method + - this method should output one or several numpy.arrays or dataframe with a consistent shape + - save_names is a list or a string of filenames, there should be one per dataset + - set save_names to None for a dataset that doesn't need saving (could be set dynamically in + the _extract method) + :param session_path: Absolute path of session folder :type session_path: str/Path """ @@ -122,10 +125,11 @@ def _extract(self): class BaseBpodTrialsExtractor(BaseExtractor): """ - Base (abstract) extractor class for bpod jsonable data set - Wrps the _extract private method + Base (abstract) extractor class for bpod jsonable data set. - :param session_path: Absolute path of session folder + Wraps the _extract private method. + + :param session_path: Absolute path of session folder. :type session_path: str :param bpod_trials :param settings @@ -159,6 +163,12 @@ def extract(self, bpod_trials=None, settings=None, **kwargs): self.settings["IBLRIG_VERSION"] = "100.0.0" return super(BaseBpodTrialsExtractor, self).extract(**kwargs) + @property + def alf_path(self): + """pathlib.Path: The full task collection filepath.""" + if self.session_path: + return self.session_path.joinpath(self.task_collection or '').absolute() + def run_extractor_classes(classes, session_path=None, **kwargs): """ diff --git a/ibllib/io/extractors/training_trials.py b/ibllib/io/extractors/training_trials.py index d3ca1447d..477942fd9 100644 --- a/ibllib/io/extractors/training_trials.py +++ b/ibllib/io/extractors/training_trials.py @@ -570,9 +570,9 @@ def get_stimOn_times_ge5(session_path, data=False, task_collection='raw_behavior @staticmethod def get_stimOn_times_lt5(session_path, data=False, task_collection='raw_behavior_data'): """ - Find the time of the statemachine command to turn on hte stim + Find the time of the statemachine command to turn on the stim (state stim_on start or rotary_encoder_event2) - Find the next frame change from the photodiodeafter that TS. + Find the next frame change from the photodiode after that TS. Screen is not displaying anything until then. (Frame changes are in BNC1High and BNC1Low) """ diff --git a/ibllib/pipes/base_tasks.py b/ibllib/pipes/base_tasks.py index fc848af85..c2c74a693 100644 --- a/ibllib/pipes/base_tasks.py +++ b/ibllib/pipes/base_tasks.py @@ -90,9 +90,66 @@ def __init__(self, session_path, **kwargs): self.output_collection += f'/task_{self.protocol_number:02}' def get_protocol(self, protocol=None, task_collection=None): - return protocol if protocol else sess_params.get_task_protocol(self.session_params, task_collection) + """ + Return the task protocol name. + + This returns the task protocol based on the task collection. If `protocol` is not None, this + acts as an identity function. If both `task_collection` and `protocol` are None, returns + the protocol defined in the experiment description file only if a single protocol was run. + If the `task_collection` is not None, the associated protocol name is returned. + + + Parameters + ---------- + protocol : str + A task protocol name. If not None, the same value is returned. + task_collection : str + The task collection whose protocol name to return. May be None if only one protocol run. + + Returns + ------- + str, None + The task protocol name, or None, if no protocol found. + + Raises + ------ + ValueError + For session with multiple task protocols, a task collection must be passed. + """ + if protocol: + return protocol + protocol = sess_params.get_task_protocol(self.session_params, task_collection) or None + if isinstance(protocol, set): + if len(protocol) == 1: + protocol = next(iter(protocol)) + else: + raise ValueError('Multiple task protocols for session. Task collection must be explicitly defined.') + return protocol def get_task_collection(self, collection=None): + """ + Return the task collection. + + If `collection` is not None, this acts as an identity function. Otherwise loads it from + the experiment description if only one protocol was run. + + Parameters + ---------- + collection : str + A task collection. If not None, the same value is returned. + + Returns + ------- + str, None + The task collection, or None if no task protocols were run. + + Raises + ------ + AssertionError + Raised if multiple protocols were run and collection is None, or if experiment + description file is improperly formatted. + + """ if not collection: collection = sess_params.get_task_collection(self.session_params) # If inferring the collection from the experiment description, assert only one returned @@ -100,6 +157,31 @@ def get_task_collection(self, collection=None): return collection def get_protocol_number(self, number=None, task_protocol=None): + """ + Return the task protocol number. + + Numbering starts from 0. If the 'protocol_number' field is missing from the experiment + description, None is returned. If `task_protocol` is None, the first protocol number if n + protocols == 1, otherwise returns None. + + NB: :func:`ibllib.pipes.dynamic_pipeline.make_pipeline` will determine the protocol number + from the order of the tasks in the experiment description if the task collection follows + the pattern 'raw_task_data_XX'. If the task protocol does not follow this pattern, the + experiment description file should explicitly define the number with the 'protocol_number' + field. + + Parameters + ---------- + number : int + The protocol number. If not None, the same value is returned. + task_protocol : str + The task protocol name. + + Returns + ------- + int, None + The task protocol number, if defined. + """ if number is None: # Do not use "if not number" as that will return True if number is 0 number = sess_params.get_task_protocol_number(self.session_params, task_protocol) # If inferring the number from the experiment description, assert only one returned (or something went wrong) diff --git a/ibllib/pipes/behavior_tasks.py b/ibllib/pipes/behavior_tasks.py index 1daf04813..4ed3700b3 100644 --- a/ibllib/pipes/behavior_tasks.py +++ b/ibllib/pipes/behavior_tasks.py @@ -277,6 +277,7 @@ class ChoiceWorldTrialsBpod(base_tasks.BehaviourTask): priority = 90 job_size = 'small' extractor = None + """ibllib.io.extractors.base.BaseBpodTrialsExtractor: An instance of the Bpod trials extractor.""" @property def signature(self): @@ -318,7 +319,24 @@ def _extract_behaviour(self, **kwargs): self.extractor.default_path = self.output_collection return self.extractor.extract(task_collection=self.collection, **kwargs) - def _run_qc(self, trials_data=None, update=True): + def _run_qc(self, trials_data=None, update=True, QC=None): + """ + Run the task QC. + + Parameters + ---------- + trials_data : dict + The complete extracted task data. + update : bool + If True, updates the session QC fields on Alyx. + QC : ibllib.qc.task_metrics.TaskQC + An optional QC class to instantiate. + + Returns + ------- + ibllib.qc.task_metrics.TaskQC + The task QC object. + """ if not self.extractor or trials_data is None: trials_data, _ = self._extract_behaviour(save=False) if not trials_data: @@ -328,10 +346,11 @@ def _run_qc(self, trials_data=None, update=True): qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one, sync_type=self.sync, task_collection=self.collection) qc_extractor.data = qc_extractor.rename_data(trials_data) - if type(self.extractor).__name__ == 'HabituationTrials': - qc = HabituationQC(self.session_path, one=self.one, log=_logger) - else: - qc = TaskQC(self.session_path, one=self.one, log=_logger) + if not QC: + QC = HabituationQC if type(self.extractor).__name__ == 'HabituationTrials' else TaskQC + _logger.debug('Running QC with %s.%s', QC.__module__, QC.__name__) + qc = QC(self.session_path, one=self.one, log=_logger) + if QC is not HabituationQC: qc_extractor.wheel_encoding = 'X1' qc_extractor.settings = self.extractor.settings qc_extractor.frame_ttls, qc_extractor.audio_ttls = load_bpod_fronts( @@ -412,7 +431,7 @@ def _extract_behaviour(self, save=True, **kwargs): task_collection=self.collection, protocol_number=self.protocol_number, **kwargs) return outputs, files - def _run_qc(self, trials_data=None, update=False, plot_qc=False): + def _run_qc(self, trials_data=None, update=False, plot_qc=False, QC=None): if not self.extractor or trials_data is None: trials_data, _ = self._extract_behaviour(save=False) if not trials_data: @@ -422,10 +441,11 @@ def _run_qc(self, trials_data=None, update=False, plot_qc=False): qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one, sync_type=self.sync, task_collection=self.collection) qc_extractor.data = qc_extractor.rename_data(trials_data.copy()) - if type(self.extractor).__name__ == 'HabituationTrials': - qc = HabituationQC(self.session_path, one=self.one, log=_logger) - else: - qc = TaskQC(self.session_path, one=self.one, log=_logger) + if not QC: + QC = HabituationQC if type(self.extractor).__name__ == 'HabituationTrials' else TaskQC + _logger.debug('Running QC with %s.%s', QC.__module__, QC.__name__) + qc = QC(self.session_path, one=self.one, log=_logger) + if QC is not HabituationQC: # Add Bpod wheel data wheel_ts_bpod = self.extractor.bpod2fpga(self.extractor.bpod_trials['wheel_timestamps']) qc_extractor.data['wheel_timestamps_bpod'] = wheel_ts_bpod diff --git a/ibllib/pipes/misc.py b/ibllib/pipes/misc.py index 37a761f03..e5108b027 100644 --- a/ibllib/pipes/misc.py +++ b/ibllib/pipes/misc.py @@ -9,6 +9,7 @@ import sys import time import logging +import warnings from functools import wraps from pathlib import Path from typing import Union, List, Callable, Any @@ -365,6 +366,8 @@ def load_params_dict(params_fname: str) -> dict: def load_videopc_params(): + """(DEPRECATED) This will be removed in favour of iblrigv8 functions.""" + warnings.warn('load_videopc_params will be removed in favour of iblrigv8', FutureWarning) if not load_params_dict("videopc_params"): create_videopc_params() return load_params_dict("videopc_params") @@ -472,6 +475,9 @@ def create_basic_transfer_params(param_str='transfer_params', local_data_path=No def create_videopc_params(force=False, silent=False): + """(DEPRECATED) This will be removed in favour of iblrigv8 functions.""" + url = 'https://github.com/int-brain-lab/iblrig/blob/videopc/docs/source/video.rst' + warnings.warn(f'create_videopc_params is deprecated, see {url}', DeprecationWarning) if Path(params.getfile("videopc_params")).exists() and not force: print(f"{params.getfile('videopc_params')} exists already, exiting...") print(Path(params.getfile("videopc_params")).exists()) diff --git a/ibllib/pipes/tasks.py b/ibllib/pipes/tasks.py index 670e2e8fa..e49082947 100644 --- a/ibllib/pipes/tasks.py +++ b/ibllib/pipes/tasks.py @@ -385,7 +385,7 @@ def assert_expected(self, expected_files, silent=False): everything_is_fine = True files = [] for expected_file in expected_files: - actual_files = list(Path(self.session_path).rglob(str(Path(expected_file[1]).joinpath(expected_file[0])))) + actual_files = list(Path(self.session_path).rglob(str(Path(*filter(None, reversed(expected_file[:2])))))) if len(actual_files) == 0 and expected_file[2]: everything_is_fine = False if not silent: diff --git a/ibllib/qc/task_metrics.py b/ibllib/qc/task_metrics.py index efe30f73c..dde8d0298 100644 --- a/ibllib/qc/task_metrics.py +++ b/ibllib/qc/task_metrics.py @@ -174,7 +174,7 @@ def compute(self, **kwargs): self.criteria['_task_passed_trial_checks'] = {'NOT_SET': 0} self.log.info(f'Session {self.session_path}: Running QC on behavior data...') - self.metrics, self.passed = get_bpodqc_metrics_frame( + self.get_bpodqc_metrics_frame( self.extractor.data, wheel_gain=self.extractor.settings['STIM_GAIN'], # The wheel gain photodiode=self.extractor.frame_ttls, @@ -183,7 +183,56 @@ def compute(self, **kwargs): min_qt=self.extractor.settings.get('QUIESCENT_PERIOD') or 0.2, audio_output=self.extractor.settings.get('device_sound', {}).get('OUTPUT', 'unknown') ) - return + + def _get_checks(self): + """ + Find all methods that begin with 'check_'. + + Returns + ------- + Dict[str, function] + A map of QC check function names and the corresponding functions that return `metric` + (any), `passed` (bool). + """ + def is_metric(x): + return isfunction(x) and x.__name__.startswith('check_') + + return dict(getmembers(sys.modules[__name__], is_metric)) + + def get_bpodqc_metrics_frame(self, data, **kwargs): + """ + Evaluates all the QC metric functions in this module (those starting with 'check') and + returns the results. The optional kwargs listed below are passed to each QC metric function. + :param data: dict of extracted task data + :param re_encoding: the encoding of the wheel data, X1, X2 or X4 + :param enc_res: the rotary encoder resolution + :param wheel_gain: the STIM_GAIN task parameter + :param photodiode: the fronts from Bpod's BNC1 input or FPGA frame2ttl channel + :param audio: the fronts from Bpod's BNC2 input FPGA audio sync channel + :param min_qt: the QUIESCENT_PERIOD task parameter + :return metrics: dict of checks and their QC metrics + :return passed: dict of checks and a float array of which samples passed + """ + + # Find all methods that begin with 'check_' + checks = self._get_checks() + prefix = '_task_' # Extended QC fields will start with this + # Method 'check_foobar' stored with key '_task_foobar' in metrics map + qc_metrics_map = {prefix + k[6:]: fn(data, **kwargs) for k, fn in checks.items()} + + # Split metrics and passed frames + self.metrics = {} + self.passed = {} + for k in qc_metrics_map: + self.metrics[k], self.passed[k] = qc_metrics_map[k] + + # Add a check for trial level pass: did a given trial pass all checks? + n_trials = data['intervals'].shape[0] + # Trial-level checks return an array the length that equals the number of trials + trial_level_passed = [m for m in self.passed.values() if isinstance(m, Sized) and len(m) == n_trials] + name = prefix + 'passed_trial_checks' + self.metrics[name] = reduce(np.logical_and, trial_level_passed or (None, None)) + self.passed[name] = self.metrics[name].astype(float) if trial_level_passed else None def run(self, update=False, namespace='task', **kwargs): """ @@ -377,46 +426,6 @@ def compute(self, download_data=None, **kwargs): self.metrics, self.passed = (metrics, passed) -def get_bpodqc_metrics_frame(data, **kwargs): - """ - Evaluates all the QC metric functions in this module (those starting with 'check') and - returns the results. The optional kwargs listed below are passed to each QC metric function. - :param data: dict of extracted task data - :param re_encoding: the encoding of the wheel data, X1, X2 or X4 - :param enc_res: the rotary encoder resolution - :param wheel_gain: the STIM_GAIN task parameter - :param photodiode: the fronts from Bpod's BNC1 input or FPGA frame2ttl channel - :param audio: the fronts from Bpod's BNC2 input FPGA audio sync channel - :param min_qt: the QUIESCENT_PERIOD task parameter - :return metrics: dict of checks and their QC metrics - :return passed: dict of checks and a float array of which samples passed - """ - def is_metric(x): - return isfunction(x) and x.__name__.startswith('check_') - # Find all methods that begin with 'check_' - checks = getmembers(sys.modules[__name__], is_metric) - prefix = '_task_' # Extended QC fields will start with this - # Method 'check_foobar' stored with key '_task_foobar' in metrics map - qc_metrics_map = {prefix + k[6:]: fn(data, **kwargs) for k, fn in checks} - - # Split metrics and passed frames - metrics = {} - passed = {} - for k in qc_metrics_map: - metrics[k], passed[k] = qc_metrics_map[k] - - # Add a check for trial level pass: did a given trial pass all checks? - n_trials = data['intervals'].shape[0] - # Trial-level checks return an array the length that equals the number of trials - trial_level_passed = [m for m in passed.values() - if isinstance(m, Sized) and len(m) == n_trials] - name = prefix + 'passed_trial_checks' - metrics[name] = reduce(np.logical_and, trial_level_passed or (None, None)) - passed[name] = metrics[name].astype(float) if trial_level_passed else None - - return metrics, passed - - # SINGLE METRICS # ---------------------------------------------------------------------------- # diff --git a/ibllib/tests/test_base_tasks.py b/ibllib/tests/test_base_tasks.py index f5014a162..229b48813 100644 --- a/ibllib/tests/test_base_tasks.py +++ b/ibllib/tests/test_base_tasks.py @@ -9,6 +9,7 @@ from one.registration import RegistrationClient from ibllib.pipes import base_tasks +from ibllib.pipes.behavior_tasks import ChoiceWorldTrialsBpod from ibllib.tests import TEST_DB @@ -91,6 +92,44 @@ def test_spacer_support(self) -> None: with self.subTest(version): self.assertIs(spacer_support(), expected) + def test_get_task_collection(self) -> None: + """Test for BehaviourTask.get_task_collection method.""" + params = {'tasks': [{'fooChoiceWorld': {'collection': 'raw_task_data_00'}}]} + task = ChoiceWorldTrialsBpod('foo/bar') + self.assertIsNone(task.get_task_collection()) + task.session_params = params + self.assertEqual('raw_task_data_00', task.get_task_collection()) + params['tasks'].append({'barChoiceWorld': {'collection': 'raw_task_data_01'}}) + self.assertRaises(AssertionError, task.get_task_collection) + self.assertEqual('raw_task_data_02', task.get_task_collection('raw_task_data_02')) + + def test_get_protocol(self) -> None: + """Test for BehaviourTask.get_protocol method.""" + task = ChoiceWorldTrialsBpod('foo/bar') + self.assertIsNone(task.get_protocol()) + self.assertEqual('foobar', task.get_protocol(protocol='foobar')) + task.session_params = {'tasks': [{'fooChoiceWorld': {'collection': 'raw_task_data_00'}}]} + self.assertEqual('fooChoiceWorld', task.get_protocol()) + task.session_params['tasks'].append({'barChoiceWorld': {'collection': 'raw_task_data_01'}}) + self.assertRaises(ValueError, task.get_protocol) + self.assertEqual('barChoiceWorld', task.get_protocol(task_collection='raw_task_data_01')) + self.assertIsNone(task.get_protocol(task_collection='raw_behavior_data')) + + def test_get_protocol_number(self) -> None: + """Test for BehaviourTask.get_protocol_number method.""" + params = {'tasks': [ + {'fooChoiceWorld': {'collection': 'raw_task_data_00', 'protocol_number': 0}}, + {'barChoiceWorld': {'collection': 'raw_task_data_01', 'protocol_number': 1}} + ]} + task = ChoiceWorldTrialsBpod('foo/bar') + self.assertIsNone(task.get_protocol_number()) + self.assertRaises(AssertionError, task.get_protocol_number, number='foo') + self.assertEqual(1, task.get_protocol_number(number=1)) + task.session_params = params + self.assertEqual(1, task.get_protocol_number()) + for i, proc in enumerate(('fooChoiceWorld', 'barChoiceWorld')): + self.assertEqual(i, task.get_protocol_number(task_protocol=proc)) + if __name__ == '__main__': unittest.main() From 77334d0d2494240b63f6d294d8476246ab3d781f Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Wed, 31 Jan 2024 18:16:12 +0200 Subject: [PATCH 04/25] Remove deprecated globus module --- ibllib/io/globus.py | 127 ---------------------------------------- ibllib/tests/test_io.py | 53 +---------------- 2 files changed, 1 insertion(+), 179 deletions(-) delete mode 100644 ibllib/io/globus.py diff --git a/ibllib/io/globus.py b/ibllib/io/globus.py deleted file mode 100644 index 492847866..000000000 --- a/ibllib/io/globus.py +++ /dev/null @@ -1,127 +0,0 @@ -"""(DEPRECATED) Globus SDK utility functions. - -This has been deprecated in favour of the one.remote.globus module. -""" -import re -import sys -import os -from pathlib import Path -import warnings -import traceback -import logging - -import globus_sdk as globus -from iblutil.io import params - - -for line in traceback.format_stack(): - print(line.strip()) - -msg = 'ibllib.io.globus has been deprecated. Use one.remote.globus instead. See stack above' -warnings.warn(msg, DeprecationWarning) -logging.getLogger(__name__).warning(msg) - - -def as_globus_path(path): - """ - (DEPRECATED) Convert a path into one suitable for the Globus TransferClient. - - NB: If using tilda in path, the home folder of your Globus Connect instance must be the same as - the OS home dir. - - :param path: A path str or Path instance - :return: A formatted path string - - Examples: - # A Windows path - >>> as_globus_path('E:\\FlatIron\\integration') - >>> '/E/FlatIron/integration' - - # A relative POSIX path - >>> as_globus_path('../data/integration') - >>> '/mnt/data/integration' - - # A globus path - >>> as_globus_path('/E/FlatIron/integration') - >>> '/E/FlatIron/integration' - TODO Remove in favour of one.remote.globus.as_globus_path - """ - msg = 'ibllib.io.globus.as_globus_path has been deprecated. Use one.remote.globus.as_globus_path instead.' - warnings.warn(msg, DeprecationWarning) - - path = str(path) - if ( - re.match(r'/[A-Z]($|/)', path) - if sys.platform in ('win32', 'cygwin') - else Path(path).is_absolute() - ): - return path - path = Path(path).resolve() - if path.drive: - path = '/' + str(path.as_posix().replace(':', '', 1)) - return str(path) - - -def _login(globus_client_id, refresh_tokens=False): - # TODO Import from one.remove.globus - client = globus.NativeAppAuthClient(globus_client_id) - client.oauth2_start_flow(refresh_tokens=refresh_tokens) - - authorize_url = client.oauth2_get_authorize_url() - print('Please go to this URL and login: {0}'.format(authorize_url)) - auth_code = input( - 'Please enter the code you get after login here: ').strip() - - token_response = client.oauth2_exchange_code_for_tokens(auth_code) - globus_transfer_data = token_response.by_resource_server['transfer.api.globus.org'] - - token = dict(refresh_token=globus_transfer_data['refresh_token'], - access_token=globus_transfer_data['access_token'], - expires_at_seconds=globus_transfer_data['expires_at_seconds'], - ) - return token - - -def login(globus_client_id): - msg = 'ibllib.io.globus.login has been deprecated. Use one.remote.globus.Globus instead.' - warnings.warn(msg, DeprecationWarning) - - token = _login(globus_client_id, refresh_tokens=False) - authorizer = globus.AccessTokenAuthorizer(token['access_token']) - tc = globus.TransferClient(authorizer=authorizer) - return tc - - -def setup(globus_client_id, str_app='globus/default'): - msg = 'ibllib.io.globus.setup has been deprecated. Use one.remote.globus.Globus instead.' - warnings.warn(msg, DeprecationWarning) - # Lookup and manage consents there - # https://auth.globus.org/v2/web/consents - gtok = _login(globus_client_id, refresh_tokens=True) - params.write(str_app, gtok) - - -def login_auto(globus_client_id, str_app='globus/default'): - msg = 'ibllib.io.globus.login_auto has been deprecated. Use one.remote.globus.Globus instead.' - warnings.warn(msg, DeprecationWarning) - token = params.read(str_app, {}) - required_fields = {'refresh_token', 'access_token', 'expires_at_seconds'} - if not (token and required_fields.issubset(token.as_dict())): - raise ValueError("Token file doesn't exist, run ibllib.io.globus.setup first") - client = globus.NativeAppAuthClient(globus_client_id) - client.oauth2_start_flow(refresh_tokens=True) - authorizer = globus.RefreshTokenAuthorizer(token.refresh_token, client) - return globus.TransferClient(authorizer=authorizer) - - -def get_local_endpoint(): - msg = 'ibllib.io.globus.get_local_endpoint has been deprecated. Use one.remote.globus.get_local_endpoint_id instead.' - warnings.warn(msg, DeprecationWarning) - - if sys.platform == 'win32' or sys.platform == 'cygwin': - id_path = Path(os.environ['LOCALAPPDATA']).joinpath("Globus Connect") - else: - id_path = Path.home().joinpath(".globusonline", "lta") - with open(id_path / "client-id.txt", 'r') as fid: - globus_id = fid.read() - return globus_id.strip() diff --git a/ibllib/tests/test_io.py b/ibllib/tests/test_io.py index eff04c862..d08b645e7 100644 --- a/ibllib/tests/test_io.py +++ b/ibllib/tests/test_io.py @@ -4,10 +4,8 @@ import uuid import tempfile from pathlib import Path -import sys import logging import json -from datetime import datetime import numpy as np from one.api import ONE @@ -15,7 +13,7 @@ import yaml from ibllib.tests import TEST_DB -from ibllib.io import flags, misc, globus, video, session_params +from ibllib.io import flags, misc, video, session_params import ibllib.io.raw_data_loaders as raw import ibllib.io.raw_daq_loaders as raw_daq @@ -369,55 +367,6 @@ def test_delete_empty_folders(self): self.assertTrue(all([x == y for x, y in zip(pos, pos_expected)])) -class TestsGlobus(unittest.TestCase): - def setUp(self): - self.patcher = patch.multiple('globus_sdk', - NativeAppAuthClient=unittest.mock.DEFAULT, - RefreshTokenAuthorizer=unittest.mock.DEFAULT, - TransferClient=unittest.mock.DEFAULT) - self.patcher.start() - self.addCleanup(self.patcher.stop) - - def test_as_globus_path(self): - assert datetime.now() < datetime(2024, 1, 30), 'remove deprecated module' - # A Windows path - if sys.platform == 'win32': - # "/E/FlatIron/integration" - actual = globus.as_globus_path('E:\\FlatIron\\integration') - self.assertTrue(actual.startswith('/E/')) - # A relative POSIX path - actual = globus.as_globus_path('/mnt/foo/../data/integration') - expected = '/mnt/data/integration' # "/C/mnt/data/integration - self.assertTrue(actual.endswith(expected)) - - # A globus path - actual = globus.as_globus_path('/E/FlatIron/integration') - expected = '/E/FlatIron/integration' - self.assertEqual(expected, actual) - - @unittest.mock.patch('iblutil.io.params.read') - def test_login_auto(self, mock_params): - assert datetime.now() < datetime(2024, 1, 30), 'remove deprecated module' - client_id = 'h3u2ier' - # Test ValueError thrown with incorrect parameters - mock_params.return_value = None # No parameters saved - with self.assertRaises(ValueError): - globus.login_auto(client_id) - # mock_params.assert_called_with('globus/default') - - pars = params.from_dict({'access_token': '7r3hj89', 'expires_at_seconds': '2020-09-10'}) - mock_params.return_value = pars # Incomplete parameter object - with self.assertRaises(ValueError): - globus.login_auto(client_id) - - # Complete parameter object - mock_params.return_value = pars.set('refresh_token', '37yh4') - gtc = globus.login_auto(client_id) - self.assertIsInstance(gtc, unittest.mock.Mock) - mock, _ = self.patcher.get_original() - mock.assert_called_once_with(client_id) - - class TestVideo(unittest.TestCase): @classmethod def setUpClass(cls) -> None: From 660f98e4fec653bcfbe77da1b9da2a7bee1a5618 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Wed, 31 Jan 2024 18:36:27 +0200 Subject: [PATCH 05/25] Revert "Issue #688 - short circuit push builds if commit already associated with" This reverts commit 669b37be0b1e3b3c5df27d0f6761d3c4536b6b76. --- .github/workflows/ibllib_ci.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/ibllib_ci.yml b/.github/workflows/ibllib_ci.yml index ec50517f4..20564820d 100644 --- a/.github/workflows/ibllib_ci.yml +++ b/.github/workflows/ibllib_ci.yml @@ -10,28 +10,9 @@ on: branches: [ master, develop ] jobs: - detect-outstanding-prs: # Don't run builds for push events if associated with PR - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - outputs: - abort: ${{ steps.debounce.outputs.abort }} - steps: - - name: Debounce - if: github.event_name == 'push' - id: debounce - run: | - pr_branches=$(gh pr list --json headRefName --repo $GITHUB_REPOSITORY) - if [[ $(echo "$pr_branches" | jq -r --arg GITHUB_REF '.[].headRefName | select(. == $GITHUB_REF)') ]]; then - echo "This push is associated with a pull request. Skipping the job." - echo "abort=true" >> "$GITHUB_OUTPUT" - fi - build: name: build (${{ matrix.python-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} - needs: debounce - if: needs.debounce.outputs.abort != 'true' strategy: fail-fast: false # Whether to stop execution of other instances max-parallel: 2 From 9a22dfaf985dee4f2cb62910bcb315032db64174 Mon Sep 17 00:00:00 2001 From: olivier Date: Thu, 1 Feb 2024 11:29:07 +0000 Subject: [PATCH 06/25] LFP / AP loader documentation --- .../loading_data/loading_raw_ephys_data.ipynb | 186 +++++++++++++++--- 1 file changed, 162 insertions(+), 24 deletions(-) diff --git a/examples/loading_data/loading_raw_ephys_data.ipynb b/examples/loading_data/loading_raw_ephys_data.ipynb index fbe95d3fb..1526e0984 100644 --- a/examples/loading_data/loading_raw_ephys_data.ipynb +++ b/examples/loading_data/loading_raw_ephys_data.ipynb @@ -28,15 +28,158 @@ "id": "2de85fd2", "metadata": {}, "source": [ - "Raw electrophysiology data recorded using spikeglx and compressed using mtscomp" + "Raw electrophysiology data recorded using spikeglx and compressed using [mtscomp](https://github.com/int-brain-lab/mtscomp)\n", + "The recommended way to load raw AP or LF band data for analysis is by using the `SpikeSortingLoader`.\n", + "\n", + "This will gather all the relevant meta-data for a given probe and the histology reconstructed channel locations in the brain. \n", + "\n", + "## AP and LF band streaming examples\n", + "We start by instantiating a spike sorting loader object and reading in the histology information by loading the channels table." ] }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from one.api import ONE\n", + "from brainbox.io.one import SpikeSortingLoader\n", + "\n", + "one = ONE(base_url='https://openalyx.internationalbrainlab.org')\n", + "t0 = 100 # timepoint in recording to stream\n", + "\n", + "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", + "ssl = SpikeSortingLoader(pid=pid, one=one)\n", + "channels = ssl.load_channels()" + ], + "metadata": { + "collapsed": false + }, + "id": "db13c1bab069f492", + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Here we stream one second of raw AP data around the timepoint of interest" + ], + "metadata": { + "collapsed": false + }, + "id": "541898a2492f2c14" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "sr_ap = ssl.raw_electrophysiology(band='ap', stream=True)\n", + "# Important: remove sync channel from raw data, and transpose to get a [n_channels, n_samples] array\n", + "first, last = (int(t0 * sr_ap.fs), int((t0 + 1) * sr_ap.fs))\n", + "raw_ap = sr_ap[first:last, :-sr_ap.nsync].T" + ], + "metadata": { + "collapsed": false + }, + "id": "139ece4af85da17b", + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Here we do the same for 5 seconds of LF data" + ], + "metadata": { + "collapsed": false + }, + "id": "ab8a76eccb43a5b4" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "sr_lf = ssl.raw_electrophysiology(band='lf', stream=True)\n", + "# Important: remove sync channel from raw data, and transpose to get a [n_channels, n_samples] array\n", + "first, last = (int(t0 * sr_lf.fs), int((t0 + 5) * sr_lf.fs))\n", + "raw_lf = sr_lf[first:last, :-sr_lf.nsync].T" + ], + "metadata": { + "collapsed": false + }, + "id": "a477781f1d46136d", + "execution_count": null + }, { "cell_type": "markdown", - "id": "0bbad5e1", + "source": [ + "Let's have a look at the raw data alongside channels information. Here we apply a filter to the raw data to remove the DC component for display." + ], + "metadata": { + "collapsed": false + }, + "id": "87728453c5d54ffb" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import scipy.signal\n", + "from brainbox.ephys_plots import plot_brain_regions\n", + "from ibllib.plots import Density\n", + "\n", + "sos_ap = scipy.signal.butter(3, 300 / sr_ap.fs /2, btype='highpass', output='sos') # 300 Hz high pass AP band\n", + "sos_lf = scipy.signal.butter(3, 2 / sr_lf.fs /2, btype='highpass', output='sos') # 2 Hz high pass LF band\n", + "filtered_ap = scipy.signal.sosfiltfilt(sos_ap, raw_ap)\n", + "filtered_lf = scipy.signal.sosfiltfilt(sos_lf, raw_lf)\n", + "\n", + "# displays the AP band over LFP band\n", + "fig, axs = plt.subplots(2, 2, gridspec_kw={'width_ratios': [.95, .05]}, figsize=(18, 12))\n", + "Density(- filtered_ap[:, 12000:15000], fs=sr_ap.fs, taxis=1, ax=axs[0, 0])\n", + "plot_brain_regions(channels[\"atlas_id\"], channel_depths=channels[\"axial_um\"], ax = axs[0, 1], display=True)\n", + "Density(- filtered_lf[:, 9000:15000], fs=sr_lf.fs, taxis=1, ax=axs[1, 0])\n", + "plot_brain_regions(channels[\"atlas_id\"], channel_depths=channels[\"axial_um\"], ax = axs[1, 1], display=True)\n" + ], + "metadata": { + "collapsed": false + }, + "id": "1bb95de26299907f", + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "### Downloading the raw data\n", + "\n", + "
\n", + "Warning.\n", + "\n", + "The raw ephys data is very large and downloading will take a long period of time and fill up your hard drive pretty fast.\n", + "\n", + "
\n", + "\n", + "When accessing the raw electrophysiology method of the spike sorting loader, turning the streaming mode off will download the full\n", + "file if it is not already present in the cache.\n", + "\n", + "We recommend setting the path of your `ONE` instance to make sure you control the destination path of the downloaded data.\n", + "\n", + "```python\n", + "PATH_CACHE = Path(\"/path_to_raw_data_drive/openalyx\")\n", + "one = ONE(base_url=\"https://openalyx.internationalbrainlab.org\", cache_dir=PATH_CACHE)\n", + "sr_ap = ssl.raw_electrophysiology(band='ap', stream=False) # sr_ap is a spikeglx.Reader object that uses memmap\n", + "```\n" + ], + "metadata": { + "collapsed": false + }, + "id": "d7dba84029780138" + }, + { + "cell_type": "markdown", + "id": "bb97cb8f", "metadata": {}, "source": [ - "## Relevant datasets\n", + "## Low level loading and downloading functions\n", + "\n", + "### Relevant datasets\n", "The raw data comprises 3 files:\n", "* `\\_spikeglx_ephysData*.cbin` the compressed raw binary\n", "* `\\_spikeglx_ephysData*.meta` the metadata file from spikeglx\n", @@ -47,14 +190,6 @@ "Full information about the compression and tool in [mtscomp repository](https://github.com/int-brain-lab/mtscomp)" ] }, - { - "cell_type": "markdown", - "id": "bb97cb8f", - "metadata": {}, - "source": [ - "## Loading" - ] - }, { "cell_type": "markdown", "id": "b51ffc0f", @@ -78,16 +213,14 @@ "\n", "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", "\n", - "time0 = 100 # timepoint in recording to stream\n", - "time_win = 1 # number of seconds to stream\n", + "t0 = 100 # timepoint in recording to stream\n", "band = 'ap' # either 'ap' or 'lf'\n", "\n", "sr = Streamer(pid=pid, one=one, remove_cached=False, typ=band)\n", - "s0 = time0 * sr.fs\n", - "tsel = slice(int(s0), int(s0) + int(time_win * sr.fs))\n", + "first, last = (int(t0 * sr.fs), int((t0 + 1) * sr.fs))\n", "\n", - "# Important: remove sync channel from raw data, and transpose\n", - "raw = sr[tsel, :-sr.nsync].T" + "# Important: remove sync channel from raw data, and transpose to get a [n_channels, n_samples] array\n", + "raw = sr[first:last, :-sr.nsync].T" ] }, { @@ -333,7 +466,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "11ea47563a9ede81" }, { "cell_type": "markdown", @@ -342,7 +476,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "c0ef9d3ce4f29b0" }, { "cell_type": "code", @@ -358,7 +493,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "2a77bb9df8795525" }, { "cell_type": "markdown", @@ -367,7 +503,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "92be0a74a5d6bfa3" }, { "cell_type": "code", @@ -380,7 +517,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "c27ce7e75852cd95" }, { "cell_type": "markdown", @@ -399,9 +537,9 @@ "metadata": { "celltoolbar": "Edit Metadata", "kernelspec": { - "display_name": "Python [conda env:iblenv] *", + "name": "python3", "language": "python", - "name": "conda-env-iblenv-py" + "display_name": "Python 3 (ipykernel)" }, "language_info": { "codemirror_mode": { From 3442fe726efd1b6f22cd9f056b835ce772f13ebf Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Thu, 1 Feb 2024 14:47:51 +0200 Subject: [PATCH 07/25] Mock QT app --- ibllib/tests/qc/test_task_qc_viewer.py | 12 ++++++++++++ ibllib/tests/test_ephys.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ibllib/tests/qc/test_task_qc_viewer.py b/ibllib/tests/qc/test_task_qc_viewer.py index 370cb9144..6f198ffc3 100644 --- a/ibllib/tests/qc/test_task_qc_viewer.py +++ b/ibllib/tests/qc/test_task_qc_viewer.py @@ -1,4 +1,5 @@ """Tests for the ibllib.qc.task_qc_viewer package.""" +import os import unittest from unittest import mock @@ -13,11 +14,22 @@ from ibllib.tests import TEST_DB +MOCK_QT = os.environ.get('IBL_MOCK_QT', True) +"""bool: If true, do not run the QT application.""" + + class TestTaskQC(unittest.TestCase): """Tests for ibllib.qc.task_qc_viewer.task_qc module.""" def setUp(self): self.one = ONE(**TEST_DB, mode='local') + """Some testing environments do not have the correct QT libraries. It is difficult to + ensure Qt is installed correctly as Anaconda, OpenCV, and system QT installations can + disrupt the lib paths. If MOCK_QT is true, the QC application is never run.""" + if MOCK_QT: + qt_mock = mock.patch('ibllib.qc.task_qc_viewer.ViewEphysQC.viewqc') + qt_mock.start() + self.addCleanup(qt_mock.stop) def test_get_bpod_trials_task(self): """Test get_bpod_trials_task function.""" diff --git a/ibllib/tests/test_ephys.py b/ibllib/tests/test_ephys.py index 69267a347..5e024c6f9 100644 --- a/ibllib/tests/test_ephys.py +++ b/ibllib/tests/test_ephys.py @@ -229,5 +229,5 @@ def test_channel_detections(self): # ephys_bad_channels(data.T, 30000, labels, xfeats) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main(exit=False, verbosity=2) From c4cfaac0669811912b8e850bc983298dbe5a9204 Mon Sep 17 00:00:00 2001 From: olivier Date: Thu, 1 Feb 2024 13:30:04 +0000 Subject: [PATCH 08/25] try explicit openalyx one to avoid time out error --- examples/loading_data/loading_spikesorting_data.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/loading_data/loading_spikesorting_data.ipynb b/examples/loading_data/loading_spikesorting_data.ipynb index 5f22cc3d2..2905ec663 100644 --- a/examples/loading_data/loading_spikesorting_data.ipynb +++ b/examples/loading_data/loading_spikesorting_data.ipynb @@ -61,7 +61,7 @@ "from brainbox.io.one import SpikeSortingLoader\n", "from iblatlas.atlas import AllenAtlas\n", "\n", - "one = ONE()\n", + "one = ONE(base_url='https://openalyx.internationalbrainlab.org')\n", "ba = AllenAtlas()\n", "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd' \n", "\n", From 63832a630746db69140dd87f721cdee9d09b3a24 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Thu, 1 Feb 2024 16:08:01 +0200 Subject: [PATCH 09/25] Fix superclass confusion --- ibllib/pipes/base_tasks.py | 23 ++++++++++++++++-- ibllib/pipes/behavior_tasks.py | 6 ++--- ibllib/tests/qc/test_task_qc_viewer.py | 2 +- ibllib/tests/test_base_tasks.py | 32 +++++++++++++++++++++++--- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/ibllib/pipes/base_tasks.py b/ibllib/pipes/base_tasks.py index 2bcfbd277..5134c4082 100644 --- a/ibllib/pipes/base_tasks.py +++ b/ibllib/pipes/base_tasks.py @@ -249,10 +249,29 @@ def run_qc(self, trials_data=None, update=True): ibllib.qc.task_metrics.TaskQC A TaskQC object replete with task data and computed metrics. """ + self._assert_trials_data(trials_data) + return None + + def _assert_trials_data(self, trials_data=None): + """Check trials data available. + + Called by :meth:`run_qc`, this extracts the trial data if `trials_data` is None, and raises + if :meth:`extract_behaviour` returns None. + + Parameters + ---------- + trials_data : dict, None + A dictionary of extracted trials data or None. + + Returns + ------- + trials_data : dict + A dictionary of extracted trials data. The output of :meth:`extract_behaviour`. + """ if not self.extractor or trials_data is None: trials_data, _ = self.extract_behaviour(save=False) - if not trials_data: - raise ValueError('No trials data found') + if not (trials_data and self.extractor): + raise ValueError('No trials data and/or extractor found') return trials_data diff --git a/ibllib/pipes/behavior_tasks.py b/ibllib/pipes/behavior_tasks.py index ed55eb330..b9eb8479a 100644 --- a/ibllib/pipes/behavior_tasks.py +++ b/ibllib/pipes/behavior_tasks.py @@ -92,7 +92,7 @@ def extract_behaviour(self, **kwargs): return self.extractor.extract(task_collection=self.collection, **kwargs) def run_qc(self, trials_data=None, update=True): - trials_data = super().run_qc(trials_data, update=False) # validate trials data + trials_data = self._assert_trials_data(trials_data) # validate trials data # Compile task data for QC qc = HabituationQC(self.session_path, one=self.one) @@ -332,7 +332,7 @@ def run_qc(self, trials_data=None, update=True, QC=None): ibllib.qc.task_metrics.TaskQC The task QC object. """ - trials_data = super().run_qc(trials_data, update=False) # validate trials data + trials_data = self._assert_trials_data(trials_data) # validate trials data # Compile task data for QC qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one, @@ -424,7 +424,7 @@ def extract_behaviour(self, save=True, **kwargs): return outputs, files def run_qc(self, trials_data=None, update=False, plot_qc=False, QC=None): - trials_data = super().run_qc(trials_data, update=False) # validate trials data + trials_data = self._assert_trials_data(trials_data) # validate trials data # Compile task data for QC qc_extractor = TaskQCExtractor(self.session_path, lazy=True, sync_collection=self.sync_collection, one=self.one, diff --git a/ibllib/tests/qc/test_task_qc_viewer.py b/ibllib/tests/qc/test_task_qc_viewer.py index 6f198ffc3..0001bd4ad 100644 --- a/ibllib/tests/qc/test_task_qc_viewer.py +++ b/ibllib/tests/qc/test_task_qc_viewer.py @@ -71,7 +71,7 @@ def test_show_session_task_qc(self, trials_tasks_mock, run_app_mock): self.assertRaises(ValueError, show_session_task_qc, session_path, one=self.one) # Set up QC mock - qc_mock = mock.Mock(spec=TaskQC) + qc_mock = mock.Mock(spec=TaskQC, unsafe=True) qc_mock.metrics = {'foo': .7} qc_mock.compute_session_status.return_value = ('Fail', qc_mock.metrics, {'foo': 'FAIL'}) qc_mock.extractor.data = {'intervals': np.array([[0, 1]])} diff --git a/ibllib/tests/test_base_tasks.py b/ibllib/tests/test_base_tasks.py index 229b48813..a6dab7edf 100644 --- a/ibllib/tests/test_base_tasks.py +++ b/ibllib/tests/test_base_tasks.py @@ -1,4 +1,5 @@ import unittest +from unittest.mock import patch import tempfile from pathlib import Path from functools import partial @@ -95,7 +96,7 @@ def test_spacer_support(self) -> None: def test_get_task_collection(self) -> None: """Test for BehaviourTask.get_task_collection method.""" params = {'tasks': [{'fooChoiceWorld': {'collection': 'raw_task_data_00'}}]} - task = ChoiceWorldTrialsBpod('foo/bar') + task = ChoiceWorldTrialsBpod('') self.assertIsNone(task.get_task_collection()) task.session_params = params self.assertEqual('raw_task_data_00', task.get_task_collection()) @@ -105,7 +106,7 @@ def test_get_task_collection(self) -> None: def test_get_protocol(self) -> None: """Test for BehaviourTask.get_protocol method.""" - task = ChoiceWorldTrialsBpod('foo/bar') + task = ChoiceWorldTrialsBpod('') self.assertIsNone(task.get_protocol()) self.assertEqual('foobar', task.get_protocol(protocol='foobar')) task.session_params = {'tasks': [{'fooChoiceWorld': {'collection': 'raw_task_data_00'}}]} @@ -121,7 +122,7 @@ def test_get_protocol_number(self) -> None: {'fooChoiceWorld': {'collection': 'raw_task_data_00', 'protocol_number': 0}}, {'barChoiceWorld': {'collection': 'raw_task_data_01', 'protocol_number': 1}} ]} - task = ChoiceWorldTrialsBpod('foo/bar') + task = ChoiceWorldTrialsBpod('') self.assertIsNone(task.get_protocol_number()) self.assertRaises(AssertionError, task.get_protocol_number, number='foo') self.assertEqual(1, task.get_protocol_number(number=1)) @@ -130,6 +131,31 @@ def test_get_protocol_number(self) -> None: for i, proc in enumerate(('fooChoiceWorld', 'barChoiceWorld')): self.assertEqual(i, task.get_protocol_number(task_protocol=proc)) + def test_assert_trials_data(self): + """Test for BehaviourTask._assert_trials_data method.""" + task = ChoiceWorldTrialsBpod('') + trials_data = {'foo': [1, 2, 3]} + + def _set(**_): + task.extractor = True # set extractor attribute + return trials_data, None + + with patch.object(task, 'extract_behaviour', side_effect=_set) as mock: + # Trials data but no extractor + self.assertEqual(trials_data, task._assert_trials_data(trials_data)) + mock.assert_called_with(save=False) + with patch.object(task, 'extract_behaviour', return_value=(trials_data, None)) as mock: + # Extractor but no trials data + self.assertEqual(trials_data, task._assert_trials_data(None)) + mock.assert_called_with(save=False) + # Returns no trials + mock.return_value = (None, None) + self.assertRaises(ValueError, task._assert_trials_data) + with patch.object(task, 'extract_behaviour', return_value=(trials_data, None)) as mock: + # Both extractor and trials data + self.assertEqual(trials_data, task._assert_trials_data(trials_data)) + mock.assert_not_called() + if __name__ == '__main__': unittest.main() From ce5406c8773b34172889acae6edc55663f105370 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Thu, 1 Feb 2024 17:15:08 +0200 Subject: [PATCH 10/25] register_new_session test util --- ibllib/tests/fixtures/utils.py | 40 ++++++++++++++++++++++++ ibllib/tests/qc/test_alignment_qc.py | 19 +++-------- ibllib/tests/qc/test_base_qc.py | 8 ++--- ibllib/tests/qc/test_critical_reasons.py | 10 ++---- ibllib/tests/qc/test_task_qc_viewer.py | 2 +- ibllib/tests/test_pipes.py | 6 +--- ibllib/tests/test_plots.py | 7 ++--- 7 files changed, 55 insertions(+), 37 deletions(-) diff --git a/ibllib/tests/fixtures/utils.py b/ibllib/tests/fixtures/utils.py index f536875d0..08b78197e 100644 --- a/ibllib/tests/fixtures/utils.py +++ b/ibllib/tests/fixtures/utils.py @@ -3,10 +3,17 @@ # @Author: Niccolò Bonacchi # @Date: Friday, October 9th 2020, 12:02:56 pm import json +import random +import string +import logging from pathlib import Path +from one.registration import RegistrationClient + from ibllib.io import session_params +_logger = logging.getLogger(__name__) + def create_fake_session_folder( root_data_path, lab="fakelab", mouse="fakemouse", date="1900-01-01", num="001", increment=True @@ -374,3 +381,36 @@ def create_fake_ephys_recording_bad_passive_transfer_sessions( populate_task_settings(fpath, passive_settings) return session_path, passive_session_path + + +def register_new_session(one, subject=None, date=None): + """ + Register a new test session. + + NB: This creates the session path on disk, using `one.cache_dir`. + + Parameters + ---------- + one : one.api.OneAlyx + An instance of ONE. + subject : str + The subject name. If None, a new random subject is created. + date : str + An ISO date string. If None, a random one is created. + + Returns + ------- + pathlib.Path + New local session path. + uuid.UUID + The experiment UUID. + """ + if not date: + date = f'20{random.randint(0, 99):02}-{random.randint(1, 12):02}-{random.randint(1, 28):02}' + if not subject: + subject = ''.join(random.choices(string.ascii_letters, k=10)) + one.alyx.rest('subjects', 'create', data={'lab': 'mainenlab', 'nickname': subject}) + + session_path, eid = RegistrationClient(one).create_new_session(subject, date=str(date)[:10]) + _logger.debug('Registered session %s with eid %s', session_path, eid) + return session_path, eid diff --git a/ibllib/tests/qc/test_alignment_qc.py b/ibllib/tests/qc/test_alignment_qc.py index 30df45de4..8c222f209 100644 --- a/ibllib/tests/qc/test_alignment_qc.py +++ b/ibllib/tests/qc/test_alignment_qc.py @@ -7,17 +7,16 @@ import copy import random import string -import datetime from one.api import ONE from neuropixel import trace_header from ibllib.tests import TEST_DB +from ibllib.tests.fixtures.utils import register_new_session from iblatlas.atlas import AllenAtlas from ibllib.pipes.misc import create_alyx_probe_insertions from ibllib.qc.alignment_qc import AlignmentQC from ibllib.pipes.histology import register_track, register_chronic_track -from one.registration import RegistrationClient EPHYS_SESSION = 'b1c968ad-4874-468d-b2e4-5ffa9b9964e9' one = ONE(**TEST_DB) @@ -34,11 +33,9 @@ class TestTracingQc(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - rng = np.random.default_rng() probe = [''.join(random.choices(string.ascii_letters, k=5)), ''.join(random.choices(string.ascii_letters, k=5))] - date = str(datetime.date(2019, rng.integers(1, 12), rng.integers(1, 28))) - _, eid = RegistrationClient(one).create_new_session('ZM_1150', date=date) + _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) # Currently the task protocol of a session must contain 'ephys' in order to create an insertion! one.alyx.rest('sessions', 'partial_update', id=cls.eid, data={'task_protocol': 'ephys'}) @@ -75,13 +72,11 @@ def tearDownClass(cls) -> None: class TestChronicTracingQC(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - rng = np.random.default_rng() probe = ''.join(random.choices(string.ascii_letters, k=5)) serial = ''.join(random.choices(string.ascii_letters, k=10)) # Make a chronic insertions - date = str(datetime.date(2019, rng.integers(1, 12), rng.integers(1, 28))) - _, eid = RegistrationClient(one).create_new_session('ZM_1150', date=date) + _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) # Currently the task protocol of a session must contain 'ephys' in order to create an insertion! one.alyx.rest('sessions', 'partial_update', id=cls.eid, data={'task_protocol': 'ephys'}) @@ -142,7 +137,6 @@ class TestAlignmentQcExisting(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - rng = np.random.default_rng() data = np.load(Path(Path(__file__).parent.parent. joinpath('fixtures', 'qc', 'data_alignmentqc_existing.npz')), allow_pickle=True) @@ -155,8 +149,7 @@ def setUpClass(cls) -> None: insertion = data['insertion'].tolist() insertion['name'] = ''.join(random.choices(string.ascii_letters, k=5)) insertion['json'] = {'xyz_picks': cls.xyz_picks} - date = str(datetime.date(2019, rng.integers(1, 12), rng.integers(1, 28))) - _, eid = RegistrationClient(one).create_new_session('ZM_1150', date=date) + _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) # Currently the task protocol of a session must contain 'ephys' in order to create an insertion! one.alyx.rest('sessions', 'partial_update', id=cls.eid, data={'task_protocol': 'ephys'}) @@ -265,7 +258,6 @@ class TestAlignmentQcManual(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - rng = np.random.default_rng() fixture_path = Path(__file__).parent.parent.joinpath('fixtures', 'qc') data = np.load(fixture_path / 'data_alignmentqc_manual.npz', allow_pickle=True) cls.xyz_picks = (data['xyz_picks'] * 1e6).tolist() @@ -277,8 +269,7 @@ def setUpClass(cls) -> None: insertion['name'] = ''.join(random.choices(string.ascii_letters, k=5)) insertion['json'] = {'xyz_picks': cls.xyz_picks} - date = str(datetime.date(2018, rng.integers(1, 12), rng.integers(1, 28))) - _, eid = RegistrationClient(one).create_new_session('ZM_1150', date=date) + _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) insertion['session'] = cls.eid probe_insertion = one.alyx.rest('insertions', 'create', data=insertion) diff --git a/ibllib/tests/qc/test_base_qc.py b/ibllib/tests/qc/test_base_qc.py index b1eb5b6a4..acf671179 100644 --- a/ibllib/tests/qc/test_base_qc.py +++ b/ibllib/tests/qc/test_base_qc.py @@ -1,13 +1,12 @@ import unittest from unittest import mock -import random import numpy as np +from one.api import ONE from ibllib.tests import TEST_DB from ibllib.qc.base import QC -from one.api import ONE -from one.registration import RegistrationClient +from ibllib.tests.fixtures.utils import register_new_session one = ONE(**TEST_DB) @@ -20,8 +19,7 @@ class TestQC(unittest.TestCase): @classmethod def setUpClass(cls): - date = f'20{random.randint(0, 30):02}-{random.randint(1, 12):02}-{random.randint(1, 28):02}' - _, eid = RegistrationClient(one).create_new_session('ZM_1150', date=date) + _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) def setUp(self) -> None: diff --git a/ibllib/tests/qc/test_critical_reasons.py b/ibllib/tests/qc/test_critical_reasons.py index 038eaf52f..31f9ef732 100644 --- a/ibllib/tests/qc/test_critical_reasons.py +++ b/ibllib/tests/qc/test_critical_reasons.py @@ -3,14 +3,12 @@ import json import random import string -import datetime -import numpy as np import requests from one.api import ONE -from one.registration import RegistrationClient from ibllib.tests import TEST_DB +from ibllib.tests.fixtures.utils import register_new_session import ibllib.qc.critical_reasons as usrpmt one = ONE(**TEST_DB) @@ -28,12 +26,10 @@ def mock_input(prompt): class TestUserPmtSess(unittest.TestCase): def setUp(self) -> None: - rng = np.random.default_rng() # Make sure tests use correct session ID one.alyx.clear_rest_cache() # Create new session on database with a random date to avoid race conditions - date = str(datetime.date(2022, rng.integers(1, 12), rng.integers(1, 28))) - _, eid = RegistrationClient(one).create_new_session('ZM_1150', date=date) + _, eid = register_new_session(one, subject='ZM_1150') eid = str(eid) # Currently the task protocol of a session must contain 'ephys' in order to create an insertion! one.alyx.rest('sessions', 'partial_update', id=eid, data={'task_protocol': 'ephys'}) @@ -159,7 +155,7 @@ def tearDown(self) -> None: class TestSignOffNote(unittest.TestCase): def setUp(self) -> None: - path, eid = RegistrationClient(one).create_new_session('ZM_1743') + path, eid = register_new_session(one, subject='ZM_1743') self.eid = str(eid) self.sign_off_keys = ['biasedChoiceWorld_00', 'passiveChoiceWorld_01'] data = {'sign_off_checklist': dict.fromkeys(map(lambda x: f'{x}', self.sign_off_keys)), diff --git a/ibllib/tests/qc/test_task_qc_viewer.py b/ibllib/tests/qc/test_task_qc_viewer.py index 0001bd4ad..a3b1d61b4 100644 --- a/ibllib/tests/qc/test_task_qc_viewer.py +++ b/ibllib/tests/qc/test_task_qc_viewer.py @@ -77,7 +77,7 @@ def test_show_session_task_qc(self, trials_tasks_mock, run_app_mock): qc_mock.extractor.data = {'intervals': np.array([[0, 1]])} qc_mock.extractor.frame_ttls = qc_mock.extractor.audio_ttls = qc_mock.extractor.bpod_ttls = mock.MagicMock() - active_task = mock.Mock(spec=ChoiceWorldTrialsNidq) + active_task = mock.Mock(spec=ChoiceWorldTrialsNidq, unsafe=True) active_task.run_qc.return_value = qc_mock active_task.name = 'Trials_activeChoiceWorld_01' trials_tasks_mock.return_value = [passive_task, active_task] diff --git a/ibllib/tests/test_pipes.py b/ibllib/tests/test_pipes.py index cbe86462a..f6e1c2c30 100644 --- a/ibllib/tests/test_pipes.py +++ b/ibllib/tests/test_pipes.py @@ -8,8 +8,6 @@ from pathlib import Path from unittest import mock from functools import partial -import numpy as np -import datetime import random import string from uuid import uuid4 @@ -289,9 +287,7 @@ def test_create_alyx_probe_insertions(self): # Connect to test DB one = ONE(**TEST_DB) # Create new session on database with a random date to avoid race conditions - date = str(datetime.date(2022, np.random.randint(1, 12), np.random.randint(1, 28))) - from one.registration import RegistrationClient - _, eid = RegistrationClient(one).create_new_session('ZM_1150', date=date) + _, eid = fu.register_new_session(one, subject='ZM_1150') eid = str(eid) # Currently the task protocol of a session must contain 'ephys' in order to create an insertion! one.alyx.rest('sessions', 'partial_update', id=eid, data={'task_protocol': 'ephys'}) diff --git a/ibllib/tests/test_plots.py b/ibllib/tests/test_plots.py index 04cf89b99..0b620564a 100644 --- a/ibllib/tests/test_plots.py +++ b/ibllib/tests/test_plots.py @@ -5,13 +5,12 @@ from pathlib import Path from PIL import Image from urllib.parse import urlparse -import datetime -import numpy as np from one.api import ONE from one.webclient import http_download_file from ibllib.tests import TEST_DB +from ibllib.tests.fixtures.utils import register_new_session from ibllib.plots.snapshot import Snapshot from ibllib.plots.figures import dlc_qc_plot @@ -34,9 +33,7 @@ def setUpClass(cls): cls.notes = [] # make a new test session - date = str(datetime.date(2018, np.random.randint(1, 12), np.random.randint(1, 28))) - from one.registration import RegistrationClient - _, eid = RegistrationClient(cls.one).create_new_session('ZM_1150', date=date) + _, eid = register_new_session(cls.one, subject='ZM_1150') cls.eid = str(eid) def _get_image(self, url): From cf6b95f45cbac0c788cbb9c583532f260e67b8e4 Mon Sep 17 00:00:00 2001 From: owinter Date: Thu, 1 Feb 2024 19:49:40 +0000 Subject: [PATCH 11/25] add credentials and split cells to trace timout --- examples/exploring_data/data_download.ipynb | 6 +++--- .../loading_spikesorting_data.ipynb | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/examples/exploring_data/data_download.ipynb b/examples/exploring_data/data_download.ipynb index 00689c3da..26de4b822 100644 --- a/examples/exploring_data/data_download.ipynb +++ b/examples/exploring_data/data_download.ipynb @@ -21,10 +21,10 @@ "source": [ "## Installation\n", "### Environment\n", - "To use IBL data you will need a python environment with python > 3.7. To create a new environment from scratch you can install [anaconda](https://www.anaconda.com/products/distribution#download-section) and follow the instructions below to create a new python environment (more information can also be found [here](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html))\n", + "To use IBL data you will need a python environment with python > 3.8. To create a new environment from scratch you can install [anaconda](https://www.anaconda.com/products/distribution#download-section) and follow the instructions below to create a new python environment (more information can also be found [here](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html))\n", "\n", "```\n", - "conda create --name ibl python=3.9\n", + "conda create --name ibl python=3.11\n", "```\n", "Make sure to always activate this environment before installing or working with the IBL data\n", "```\n", @@ -423,4 +423,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} \ No newline at end of file +} diff --git a/examples/loading_data/loading_spikesorting_data.ipynb b/examples/loading_data/loading_spikesorting_data.ipynb index 2905ec663..9c5bfbe61 100644 --- a/examples/loading_data/loading_spikesorting_data.ipynb +++ b/examples/loading_data/loading_spikesorting_data.ipynb @@ -59,16 +59,22 @@ "source": [ "from one.api import ONE\n", "from brainbox.io.one import SpikeSortingLoader\n", - "from iblatlas.atlas import AllenAtlas\n", - "\n", - "one = ONE(base_url='https://openalyx.internationalbrainlab.org')\n", - "ba = AllenAtlas()\n", + "one = ONE(base_url='https://openalyx.internationalbrainlab.org', password='international')" + ] + }, + { + "cell_type": "code", + "outputs": [], + "source": [ "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd' \n", - "\n", - "sl = SpikeSortingLoader(pid=pid, one=one, atlas=ba)\n", + "sl = SpikeSortingLoader(pid=pid, one=one)\n", "spikes, clusters, channels = sl.load_spike_sorting()\n", "clusters = sl.merge_clusters(spikes, clusters, channels)" - ] + ], + "metadata": { + "collapsed": false + }, + "id": "a9e9bc2a0ebac970" }, { "cell_type": "markdown", From 9bb8b6b167d964ec33ecc21bc39c88f3291a22eb Mon Sep 17 00:00:00 2001 From: owinter Date: Thu, 1 Feb 2024 20:53:43 +0000 Subject: [PATCH 12/25] silence tqdm on top cells --- .../loading_data/loading_raw_ephys_data.ipynb | 8 ++++++-- .../loading_spikesorting_data.ipynb | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/loading_data/loading_raw_ephys_data.ipynb b/examples/loading_data/loading_raw_ephys_data.ipynb index 1526e0984..667a578ca 100644 --- a/examples/loading_data/loading_raw_ephys_data.ipynb +++ b/examples/loading_data/loading_raw_ephys_data.ipynb @@ -17,10 +17,14 @@ }, "outputs": [], "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", "import logging\n", + "import os\n", + "\n", "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" ] }, { diff --git a/examples/loading_data/loading_spikesorting_data.ipynb b/examples/loading_data/loading_spikesorting_data.ipynb index 9c5bfbe61..f36c4619f 100644 --- a/examples/loading_data/loading_spikesorting_data.ipynb +++ b/examples/loading_data/loading_spikesorting_data.ipynb @@ -17,10 +17,14 @@ }, "outputs": [], "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", "import logging\n", + "import os\n", + "\n", "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" ] }, { @@ -59,7 +63,7 @@ "source": [ "from one.api import ONE\n", "from brainbox.io.one import SpikeSortingLoader\n", - "one = ONE(base_url='https://openalyx.internationalbrainlab.org', password='international')" + "one = ONE(base_url='https://openalyx.internationalbrainlab.org')" ] }, { @@ -74,7 +78,8 @@ "metadata": { "collapsed": false }, - "id": "a9e9bc2a0ebac970" + "id": "a9e9bc2a0ebac970", + "execution_count": null }, { "cell_type": "markdown", @@ -92,7 +97,7 @@ "outputs": [], "source": [ "eid, pname = one.pid2eid(pid)\n", - "sl = SpikeSortingLoader(eid=eid, pname=pname, one=one, atlas=ba)\n", + "sl = SpikeSortingLoader(eid=eid, pname=pname, one=one)\n", "spikes, clusters, channels = sl.load_spike_sorting()\n", "clusters = sl.merge_clusters(spikes, clusters, channels)" ] @@ -283,9 +288,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python [conda env:iblenv] *", + "name": "python3", "language": "python", - "name": "conda-env-iblenv-py" + "display_name": "Python 3 (ipykernel)" }, "language_info": { "codemirror_mode": { From 8623f89acdbc4be97a990f6b7e6f01841af9bab5 Mon Sep 17 00:00:00 2001 From: olivier Date: Fri, 2 Feb 2024 11:23:30 +0000 Subject: [PATCH 13/25] documentation: suppress tqdm outputs and hide cells --- examples/exploring_data/data_download.ipynb | 18 ++++++++++++ .../loading_data/loading_ephys_data.ipynb | 10 +++++-- .../loading_data/loading_raw_video_data.ipynb | 8 ++++-- .../loading_spike_waveforms.ipynb | 8 ++++-- .../loading_data/loading_trials_data.ipynb | 28 +++++++++++++------ .../loading_data/loading_video_data.ipynb | 19 +++++++++++++ .../loading_data/loading_wheel_data.ipynb | 16 +++++++---- 7 files changed, 86 insertions(+), 21 deletions(-) diff --git a/examples/exploring_data/data_download.ipynb b/examples/exploring_data/data_download.ipynb index 26de4b822..c9eeb9f99 100644 --- a/examples/exploring_data/data_download.ipynb +++ b/examples/exploring_data/data_download.ipynb @@ -1,8 +1,26 @@ { "cells": [ + { + "cell_type": "code", + "outputs": [], + "source": [ + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", + "import logging\n", + "import os\n", + "\n", + "logger = logging.getLogger('ibllib')\n", + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" + ], + "metadata": { + "collapsed": false + } + }, { "cell_type": "markdown", "metadata": { + "nbsphinx": "hidden", "collapsed": false }, "source": [ diff --git a/examples/loading_data/loading_ephys_data.ipynb b/examples/loading_data/loading_ephys_data.ipynb index 80e2e4505..dfdf43533 100644 --- a/examples/loading_data/loading_ephys_data.ipynb +++ b/examples/loading_data/loading_ephys_data.ipynb @@ -17,10 +17,14 @@ }, "outputs": [], "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", "import logging\n", + "import os\n", + "\n", "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" ] }, { @@ -222,4 +226,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/examples/loading_data/loading_raw_video_data.ipynb b/examples/loading_data/loading_raw_video_data.ipynb index b49b51e56..3d3204c28 100644 --- a/examples/loading_data/loading_raw_video_data.ipynb +++ b/examples/loading_data/loading_raw_video_data.ipynb @@ -17,10 +17,14 @@ }, "outputs": [], "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", "import logging\n", + "import os\n", + "\n", "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" ] }, { diff --git a/examples/loading_data/loading_spike_waveforms.ipynb b/examples/loading_data/loading_spike_waveforms.ipynb index 7009b7855..407fd1eeb 100644 --- a/examples/loading_data/loading_spike_waveforms.ipynb +++ b/examples/loading_data/loading_spike_waveforms.ipynb @@ -17,10 +17,14 @@ }, "outputs": [], "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", "import logging\n", + "import os\n", + "\n", "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" ] }, { diff --git a/examples/loading_data/loading_trials_data.ipynb b/examples/loading_data/loading_trials_data.ipynb index 398d32734..4b85dd0e2 100644 --- a/examples/loading_data/loading_trials_data.ipynb +++ b/examples/loading_data/loading_trials_data.ipynb @@ -17,10 +17,14 @@ }, "outputs": [], "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", "import logging\n", + "import os\n", + "\n", "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" ] }, { @@ -49,7 +53,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "a5d358e035a91310" }, { "cell_type": "code", @@ -66,7 +71,8 @@ "pycharm": { "name": "#%%\n" } - } + }, + "id": "e5688df9114dd1cc" }, { "cell_type": "markdown", @@ -75,7 +81,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "d6c98a81f5426445" }, { "cell_type": "code", @@ -99,7 +106,8 @@ "pycharm": { "name": "#%%\n" } - } + }, + "id": "a323e20fb2fe5db3" }, { "cell_type": "markdown", @@ -301,7 +309,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "55ad2e5d71ac301" }, { "cell_type": "code", @@ -319,7 +328,8 @@ "pycharm": { "name": "#%%\n" } - } + }, + "id": "cf17cf97a866b206" }, { "cell_type": "markdown", @@ -357,4 +367,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/examples/loading_data/loading_video_data.ipynb b/examples/loading_data/loading_video_data.ipynb index 02cd3ee7b..1c0b12a16 100644 --- a/examples/loading_data/loading_video_data.ipynb +++ b/examples/loading_data/loading_video_data.ipynb @@ -1,5 +1,24 @@ { "cells": [ + { + "cell_type": "code", + "outputs": [], + "source": [ + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", + "import logging\n", + "import os\n", + "\n", + "logger = logging.getLogger('ibllib')\n", + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" + ], + "metadata": { + "nbsphinx": "hidden", + "collapsed": false + }, + "id": "96289b087c51ad66" + }, { "cell_type": "markdown", "id": "b730e49f", diff --git a/examples/loading_data/loading_wheel_data.ipynb b/examples/loading_data/loading_wheel_data.ipynb index 053f72d2a..1a591d07d 100644 --- a/examples/loading_data/loading_wheel_data.ipynb +++ b/examples/loading_data/loading_wheel_data.ipynb @@ -17,10 +17,14 @@ }, "outputs": [], "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", "import logging\n", + "import os\n", + "\n", "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" ] }, { @@ -100,7 +104,8 @@ ], "metadata": { "collapsed": false - } + }, + "id": "5a947733bf0b16f0" }, { "cell_type": "code", @@ -116,7 +121,8 @@ "pycharm": { "name": "#%%\n" } - } + }, + "id": "7bf6131b343ffe21" }, { "cell_type": "markdown", @@ -169,4 +175,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From 92c409400c738790c0506c50118d7b624fbc77e7 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Fri, 2 Feb 2024 18:17:05 +0200 Subject: [PATCH 14/25] Correct assignment of data in legacy training QC task --- ibllib/pipes/training_preprocessing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ibllib/pipes/training_preprocessing.py b/ibllib/pipes/training_preprocessing.py index 1cad5124e..ffced634d 100644 --- a/ibllib/pipes/training_preprocessing.py +++ b/ibllib/pipes/training_preprocessing.py @@ -70,7 +70,7 @@ def run_qc(self, trials_data=None, update=True): qc = TaskQC(self.session_path, one=self.one) qc.extractor = TaskQCExtractor(self.session_path, one=self.one, lazy=True) qc.extractor.type = extractor_type - qc.data = qc.extractor.rename_data(trials_data) + qc.extractor.data = qc.extractor.rename_data(trials_data) qc.extractor.load_raw_data() # re-loads raw data and populates various properties # Aggregate and update Alyx QC fields qc.run(update=update) @@ -80,7 +80,7 @@ def run_qc(self, trials_data=None, update=True): def _run(self, **_): """Extracts an iblrig training session and runs QC.""" trials_data, output_files = self.extract_behaviour() - if self.one and not self.one.offline: + if self.one and not self.one.offline and trials_data: # Run the task QC self.run_qc(trials_data) return output_files From 68027d4d9a1287f3f75c667fb9c314f8f03b3c43 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Mon, 5 Feb 2024 12:49:52 +0200 Subject: [PATCH 15/25] Update QC viewer readme --- ibllib/qc/task_qc_viewer/README.md | 17 ++++++++--------- ibllib/qc/task_qc_viewer/ViewEphysQC.py | 14 +++++++------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/ibllib/qc/task_qc_viewer/README.md b/ibllib/qc/task_qc_viewer/README.md index d9f9b930f..7c118ff5d 100644 --- a/ibllib/qc/task_qc_viewer/README.md +++ b/ibllib/qc/task_qc_viewer/README.md @@ -1,28 +1,27 @@ # Task QC Viewer This will download the TTL pulses and data collected on Bpod and/or FPGA and plot the results -alongside an interactive table. -The UUID is the session id. +alongside an interactive table. The UUID is the session id. ## Usage: command line -Launch the Viewer by typing `python task_qc.py session_UUID` , example: -``` +Launch the Viewer by typing `python task_qc.py session-uuid` , example: +```sh python task_qc.py c9fec76e-7a20-4da4-93ad-04510a89473b # or with ipython ipython task_qc.py -- c9fec76e-7a20-4da4-93ad-04510a89473b ``` Or just using a local path (on a local server for example): -``` +```sh python task_qc.py /mnt/s0/Subjects/KS022/2019-12-10/001 --local # or with ipython ipython task_qc.py -- /mnt/s0/Subjects/KS022/2019-12-10/001 --local ``` ## Usage: from ipython prompt -``` python -from iblapps.task_qc_viewer.task_qc import show_session_task_qc -session_path = "/datadisk/Data/IntegrationTests/ephys/choice_world_init/KS022/2019-12-10/001" +```python +from ibllib.qc.task_qc_viewer.task_qc import show_session_task_qc +session_path = r"/datadisk/Data/IntegrationTests/ephys/choice_world_init/KS022/2019-12-10/001" show_session_task_qc(session_path, local=True) ``` @@ -42,7 +41,7 @@ Each row is a trial entry. Each column is a trial event When double-clicking on any field of that table, the Sync pulse display time (x-) axis is adjusted so as to visualise the corresponding trial selected. ### What to look for -Tests are defined in the SINGLE METRICS section of ibllib/qc/task_metrics.py: https://github.com/int-brain-lab/ibllib/blob/master/ibllib/qc/task_metrics.py#L148-L149 +Tests are defined in the SINGLE METRICS section of ibllib/qc/task_metrics.py: https://github.com/int-brain-lab/ibllib/blob/master/ibllib/qc/task_metrics.py#L420 ### Exit Close the GUI window containing the interactive table to exit. diff --git a/ibllib/qc/task_qc_viewer/ViewEphysQC.py b/ibllib/qc/task_qc_viewer/ViewEphysQC.py index 7151d87d6..48155b270 100644 --- a/ibllib/qc/task_qc_viewer/ViewEphysQC.py +++ b/ibllib/qc/task_qc_viewer/ViewEphysQC.py @@ -124,7 +124,7 @@ def __init__(self, parent=None, wheel=None): class GraphWindow(QtWidgets.QWidget): def __init__(self, parent=None, wheel=None): - QtWidgets.QWidget.__init__(self, parent=None) + QtWidgets.QWidget.__init__(self, parent=parent) vLayout = QtWidgets.QVBoxLayout(self) hLayout = QtWidgets.QHBoxLayout() self.pathLE = QtWidgets.QLineEdit(self) @@ -134,16 +134,16 @@ def __init__(self, parent=None, wheel=None): vLayout.addLayout(hLayout) self.pandasTv = QtWidgets.QTableView(self) vLayout.addWidget(self.pandasTv) - self.loadBtn.clicked.connect(self.loadFile) + self.loadBtn.clicked.connect(self.load_file) self.pandasTv.setSortingEnabled(True) self.pandasTv.doubleClicked.connect(self.tv_double_clicked) self.wplot = PlotWindow(wheel=wheel) self.wplot.show() self.wheel = wheel - def loadFile(self): - fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open File", "", - "CSV Files (*.csv)") + def load_file(self): + fileName, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "Open File", "", "CSV Files (*.csv)") self.pathLE.setText(fileName) df = pd.read_csv(fileName) self.update_df(df) @@ -160,8 +160,8 @@ def tv_double_clicked(self): finish = df.loc[ind.row()]['intervals_1'] dt = finish - start if self.wheel: - idx = np.searchsorted(self.wheel['re_ts'], np.array([start - dt / 10, - finish + dt / 10])) + idx = np.searchsorted( + self.wheel['re_ts'], np.array([start - dt / 10, finish + dt / 10])) period = self.wheel['re_pos'][idx[0]:idx[1]] if period.size == 0: _logger.warning('No wheel data during trial #%i', ind.row()) From 1bb417a858d5315065c45b3b4e5d001485294f46 Mon Sep 17 00:00:00 2001 From: olivier Date: Tue, 6 Feb 2024 09:17:23 +0000 Subject: [PATCH 16/25] prepare pandas3: proper assignation method to avoid warnings --- brainbox/behavior/dlc.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/brainbox/behavior/dlc.py b/brainbox/behavior/dlc.py index c3f48d32e..57279fd13 100644 --- a/brainbox/behavior/dlc.py +++ b/brainbox/behavior/dlc.py @@ -56,9 +56,8 @@ def likelihood_threshold(dlc, threshold=0.9): features = np.unique(['_'.join(x.split('_')[:-1]) for x in dlc.keys()]) for feat in features: nan_fill = dlc[f'{feat}_likelihood'] < threshold - dlc[f'{feat}_x'][nan_fill] = np.nan - dlc[f'{feat}_y'][nan_fill] = np.nan - + dlc.loc[f'{feat}_x', nan_fill] = np.nan + dlc.loc[f'{feat}_y', nan_fill] = np.nan return dlc From 7e5dabd41de895742ac4c53c04628a169ae536df Mon Sep 17 00:00:00 2001 From: olivier Date: Tue, 6 Feb 2024 12:50:58 +0000 Subject: [PATCH 17/25] documentation: raw data loading --- brainbox/behavior/dlc.py | 4 +- brainbox/examples/docs_wheel_moves.ipynb | 207 ++----- .../data_release/data_release_behavior.ipynb | 13 +- .../data_release_brainwidemap.ipynb | 11 +- .../data_release_repro_ephys.ipynb | 4 +- ...data_release_spikesorting_benchmarks.ipynb | 4 +- examples/exploring_data/data_download.ipynb | 97 ++-- examples/exploring_data/data_structure.ipynb | 8 +- .../loading_data/loading_ephys_data.ipynb | 2 +- .../loading_multi_photon_imaging_data.ipynb | 258 ++++----- .../loading_data/loading_passive_data.ipynb | 516 +++++++++--------- .../loading_data/loading_raw_audio_data.ipynb | 336 ++++++------ .../loading_data/loading_raw_ephys_data.ipynb | 462 +++++++--------- .../loading_raw_mesoscope_data.ipynb | 47 +- .../loading_data/loading_raw_video_data.ipynb | 12 +- .../loading_spike_waveforms.ipynb | 367 ++++++------- .../loading_spikesorting_data.ipynb | 16 +- .../loading_data/loading_trials_data.ipynb | 73 ++- .../loading_data/loading_video_data.ipynb | 436 +++++++-------- .../loading_data/loading_wheel_data.ipynb | 355 ++++++------ ibllib/atlas/genes.py | 4 +- 21 files changed, 1515 insertions(+), 1717 deletions(-) diff --git a/brainbox/behavior/dlc.py b/brainbox/behavior/dlc.py index 57279fd13..130ef9df8 100644 --- a/brainbox/behavior/dlc.py +++ b/brainbox/behavior/dlc.py @@ -56,8 +56,8 @@ def likelihood_threshold(dlc, threshold=0.9): features = np.unique(['_'.join(x.split('_')[:-1]) for x in dlc.keys()]) for feat in features: nan_fill = dlc[f'{feat}_likelihood'] < threshold - dlc.loc[f'{feat}_x', nan_fill] = np.nan - dlc.loc[f'{feat}_y', nan_fill] = np.nan + dlc.loc[nan_fill, f'{feat}_x'] = np.nan + dlc.loc[nan_fill, f'{feat}_y'] = np.nan return dlc diff --git a/brainbox/examples/docs_wheel_moves.ipynb b/brainbox/examples/docs_wheel_moves.ipynb index 8024e782d..6c8c78535 100644 --- a/brainbox/examples/docs_wheel_moves.ipynb +++ b/brainbox/examples/docs_wheel_moves.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -33,17 +33,9 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2020-09-21_1_SWC_043\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", "print(one.eid2ref(eid, as_dict=False))" @@ -77,17 +69,9 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The wheel diameter is 6.2 cm and the number of ticks is 4096 per revolution\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "device_info = ('The wheel diameter is {} cm and the number of ticks is {} per revolution'\n", " .format(wh.WHEEL_DIAMETER, wh.ENC_RES))\n", @@ -108,22 +92,9 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "wheel.position: \n", - " [ 1.53398079e-03 -0.00000000e+00 -1.53398079e-03 ... -4.52088682e+02\n", - " -4.52090216e+02 -4.52091750e+02]\n", - "wheel.timestamps: \n", - " [2.64973500e-02 3.13635300e-02 3.42632400e-02 ... 4.29549951e+03\n", - " 4.29570042e+03 4.29584134e+03]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "wheel = one.load_object(eid, 'wheel', collection='alf')\n", "\n", @@ -145,7 +116,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -170,18 +141,9 @@ }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "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\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "pos, t = wh.interpolate_position(wheel.timestamps, wheel.position)\n", "sec = 5 # Number of seconds to plot\n", @@ -211,7 +173,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -222,18 +184,9 @@ }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "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\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "# Detect wheel movements for the first 5 seconds\n", "mask = t < (t[0] + sec)\n", @@ -254,17 +207,9 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The wheel must be turned ~0.3 rad to move the stimulus to threshold\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "threshold_deg = 35 # visual degrees\n", "gain = 4 # deg / mm\n", @@ -289,7 +234,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -311,18 +256,9 @@ }, { "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYcAAAD3CAYAAAD2S5gLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAA/FElEQVR4nO3deVxU5f4H8M/swzAsyiaEimAiooi5hEVpBZmmWd1crqERtxS7ds2FRJNwAxWhumkXza7LLyuXXLpmi7ZomZg7JpuCJiAqm8AMMAszz++PkdEJVBiGM8P4fb9evZw5M+c8369j853nOed5Do8xxkAIIYTchm/tAAghhNgeKg6EEEKaoOJACCGkCSoOhBBCmqDiQAghpAmhtQMwx5kzZyCRSIzP1Wq1yXN7YY952WNOAOVl6y6WKQEA/h5yAPaT1+1akpNarUZoaGiLjtchi4NEIkFQUJDxeU5Ojslze2GPedljTgDlZesW/ZIBANj2uCGXjppXSVU9AMDH1aHJay3JKScnp8VtdcjiQAgh96NZ284AALZNG9rubVFxIITYvTeffNDaIVgEl3lQcSCE2L3wB92tHYJFcJkHXa1ECLF7WSXVyCqptnYYbVZYUYfCijpO2qLiQAixe0v2ZmPJ3mxrh9FmcV9mIu7LTE7aomElQgjpIGZF9uKsLSoOhBDSQYT5u3HW1n01rKTXM2w/XoTiG9yM2RFCiCUVlClRcHNCX3u7r3oO1fVaLN6bBa2eYeZTD2La4/4QCu6r+kgI6cAW7PoDAM1zsLhOjmIcmD0MSftysOr7POzPvo60cf3R01Nu7dAIIe3o7WcCrR2CRXCZh1nFQaFQIC4uDkqlElqtFvHx8RgwYACOHDmC1NRUCIVCDB06FLNmzTLZr66uDnPmzEF1dTUcHBywatUqdO7cGWfOnEFSUhIEAgHCw8MxY8YMiyTXHB9XB3z08kN4JrMECV+dw7Mf/oq5TwciJrwHBHxeu7VLCLGegd07WzsEi+AyD7PGVDZu3IiwsDBs2bIFy5cvx5IlSwAAKSkpSElJwbZt23Ds2DHk5eWZ7Ld9+3YEBwfj888/x7PPPov//Oc/AIDExESkpaXhiy++QGZmJrKystqY1r2N6e+D/bMex2MPeiDpmxy8mH4EedcU7d4uIYR7Jy9X4uTlSmuH0WZ51xScfU+Z1XOIjo6GWCwGAOh0OuNKgEFBQaiqqoJWq4VarYZAIGiyn06nAwCUlJTA3d0dSqUSGo0G3bp1AwCEh4cjIyMDwcHBZifVUp5OUqyfMhBfn72KxP9lYczqw1g4OghThvq1e9uEEO6kfGf4ocrFWH17evercwBs5JzDjh07sHnzZpNtycnJCAkJQVlZGeLi4rBgwQIAQGBgIGJjY+Hq6orAwED4+/s3OZ5AIMCUKVNw/vx5bNy4EUqlEnL5rTF/R0dHFBUV3TUmtVptsrqgSqVq1WqDf9VTDHw02hvv/1aGd7/KwpkLxfjHoM7g86w7zNTWvGyRPeYEUF62rq7OcIViYy4dNa+/95ECaH51VYvnxMyUm5vLRo0axQ4ePMgYY6y6upqFhYWxa9euMcYYW7lyJVu/fv0d98/Pz2dPPfUUUygUbOTIkcbtmzZtYp988sld287Ozr7rc3M16PQs8atzrPu8r9mMz08xlbbBIsc1l6XysiX2mBNjlJetG7/2CBu/9ojxub3kdbuW5NSavM0655Cfn4+ZM2ciLS0Nw4YNAwBIpVLIZDLIZDIAgKenJ2pqakz2W7duHfbs2QMAkMlkEAgEkMvlEIlEKCwsBGMMhw8fxqBBg9pQ7swn4POQOKYP5j3TG3szSxCz6TgUKq1VYiGEkL/ico0os845pKWlQaPRICkpCQAgl8uRnp6O+Ph4xMTEQCKRwMnJCStWrAAAxMTEYO3atfjb3/6GefPmYefOndDpdEhOTgYALF68GHPnzoVOp0N4eDj69+9vofRaj8fjYfrwAHg4STBv51lM/PgoNscMgbvcvu4aRQjpeBrXh7KJcw7NSU9Pb3Z7ZGQkIiMjm2zfsGEDAMDd3R3//e9/m7weGhqK7du3mxNKu3lpoC/c5GJM33ISEz8+ih3ThqKTo9jaYRFCzPDumD7WDsEiuMyDpgffxROBntj06hAUVtbhtf87AZVWZ+2QCCFmCPZxQbCPi7XDaDMu86DicA9h/m74YEIoThXewIzPT0PToLd2SISQVjp8oRyHL5RbO4w2yyyqQmZRFSdtUXFogVH9vLHkuWD8kHMdb3x2EuoG6kEQ0pGs/ukCVv90wdphtFnyNzlI/oabS3Dvq7WV2mLyUD+Ax0PCnnOY+n8nsW7yQEhFgnvuRwghlrJkbF/O2qKeQytMDuuOFS/2w6HzZZjx+WlodTTERAjhTmAXJwR2ceKkLSoOrTRxSDcsHWsYYpq7IxM6PbN2SISQ+wSXa0TRsJIZJg/1g1Ktw8rvciETC5H8Ql/wrLzUBiHE/nG5RhQVBzNNHx4ApVqLj34ugEwswMJng6hAEGKjkl/sZ+0QLILLPKg4tMHcpwNRq9bhv4cvQSjgIf6Z3lQgCLFBAR72cUMvLvOg4tAGPJ5hLaYGvR7rDl2EkM/D3KcDqUAQYmN+yL4OAIjo42XlSNrm6MUKAIb5V+2NikMb8Xg8LHmuL3R64KOfCyDg8zE7spe1wyKE3Gb9rxcBdPzi8P6B8wDonEOHwefzkPR8X+j0enz44wUIeDzMjHjQ2mERQuzMqpe4W5SUioOF8Pk8rHgxBDo98P4P51GnbaBzEIQQi+rmJuOsLSoOFsTn87DqpRDIxAKsO3QR1XVaJL3QDwI+FQhCSNs1rg8V/qB7u7dFxcHC+HwelowNhqtMhNU/5aNGpcX7E0IhEdJSG4SQtmlcH4qKQwfF4/Ew5+lAuDiIsGxfDhSqE0iPGgi5hP66CbGG9yeEWjsEi+AyD1o+ox299pg/Vr0UgiMFFRi75jDySxXWDomQ+5KPqwN8XB2sHUabcZkHFYd2Nm5QV2z5x8Oortdi7Jrf8O0fV60dEiH3nb2ZJdibWWLtMNrsYF4pDuaVctIWFQcODA1ww9dvPoZeXZww/bNTWP5tDhpoRVdCOLPl6GVsOXrZ2mG0WfrBAqQfLOCkLRoE50gXFym2Tg3Dkr3ZWHfoIv4orsYHE0Lh6Sy1dmiEkA5i9aQBnLVFPQcOSYQCJL3QDykvheDE5Rt46r1D2HL0MvS07DchpAU8naTwdOLmByUVBysYP6grvpv5GPr6uGDhnnMYty4DBWVKa4dFCLFxP2RfN64T1d6oOFiJv4ccn7/+MFLH9ceF6wqM+vevWP/LRbp5ECHkjtb/etG4TlR7o3MOVsTj8fDSQF88/qA7Fuw+h6RvcvDNuatY9VJ/9PS0jyWGCbEF6VEDrR2CRXCZB/UcbICnsxTrpwzEvyeG4lJ5LUZ9+CtSvsuFUqOzdmiE2IXOjmJ0dhRbO4w24zIP6jnYCB6Ph7GhD2BogBuS9uXgPwcL8KmEj39VyzB5aHdIRbT8hi3S6RmuVtdDo6PhQFu240QRAMO8o47su3OGeVLP9PVu97aoONgYTycp/j1xAF5/zB/v7jyJpG9ysOG3S/jXUw/ipYG+EAmos2crLpXXYubW0zhbXA0+D/BzL0UvTyf06uKEXl5yBHo5wc/dkT4zG/DlyWIAHb84bPztTwBUHO5rfR9wQVKkN26IPLBqfx7m7/oD6w4V4K2IXng2xJu+cKzsy5PFePercxAJ+FgwqjcuFV/DDZ0U568rsD/7GhqvK3AQCfDG8ABMHeZPiy+SNlv/yiDO2jKrOCgUCsTFxUGpVEKr1SI+Ph4DBgzAkSNHkJqaCqFQiKFDh2LWrFkm+9XV1WHOnDmorq6Gg4MDVq1ahc6dO2P//v1ISUmBt7ehGr755psYMmRI27OzA4/0dMeuADf8mFOK1P15eGvbGSR/k4NJD3fDpCHdaBKdFXz0cz5WfZ+Hh3t0xgcTQ+Ht4oCcHA2CgoIAACqtDgVlSkOhyLqOtAPnsefMFSS/0A8Pc3B7R2K/nKUiztoyqzhs3LgRYWFhiI6OxsWLFzFnzhzs3r0bKSkpSE1NRUBAACZNmoS8vDwEBgYa99u+fTuCg4MxY8YM7Nq1C//5z3+wcOFCZGVlIS4uDiNGjLBYYvaEx+Mhoo8XnuztiYPnS7H5yGV88MMFrPkpHyP7eePVR/3wULdO1g7zvrDpt0tY9X0exob64L3xoc3eq0MqEiDYxwXBPi54YYAvfs4rRcKec5jw8VGMG+iLBaOC0MkOTo4S7jWuDzWmv0+7t2VWcYiOjoZYbPjHrdPpIJFIAABBQUGoqqqCVquFWq2GQCBosp9OZ7gCp6SkBO7uhjXJs7KykJOTg82bNyMkJARz586FUHjn0NRqNXJycozPVSqVyXN70Vxe3gDihzphSrAUX+fVYH/2NezNLMGgBxwQ1b8TAj1suyfRkT+r/RcUeP9IGYZ2leG1fhKcz8s1vna3vLoAWD3KC59n3sCuU8X4/lwJXhvkhogAeZvuFKjRMeSWqXD2Wj3OXVeBAXCRCOAqFcDFwfCnq1QAFyn/5p8CyMX8VrXZkT+v29XV1QGAMZeOmtfHPxmKQ09xdZPXLJ0TjzF218ssduzYgc2bN5tsS05ORkhICMrKyvD6669jwYIFGDJkCDZt2oQ1a9bA1dUVgYGBWL16Nfj8pmPjU6ZMwfnz57Fx40YEBQVh48aNiIiIgK+vLxITE9GrVy9ERUXdMaacnBxjF7655/aiJXnVqhuw5ehlrD1UgBt1WjzV2xPThwdgYPdONnmL0o76WX15shhvf5mJR3u645NXBjU5f9DSvHKv1eCd3edw8vINhPl3RtIL/RDgYTqnRd2gQ2WtBhVKDcqVapQrNahQqlFRa3jeuD2/VAl1gx58HtDHxxkOIgEqlBpU1GpQXa9ttn0hn4fOjmK4ySVwl4vhdvNxZ0fxzecSeDgZ/nOTi3HxwvkO+Xn9Vf3Ny8IdxIbPraP+O/xrHrdrSU6tyfuexeFO8vLyMHv2bLz99tsYNmwYampqMGLECOzZswdeXl5ISUlB586d8dprrzW7f0FBAaZNm4YffvgBNTU1cHZ2BgAcOnQI33//PZKTk+/YNhWHppTqBmw+8ic+/uUiquu1CPF1QfQjfhjZ17vZf0jW0hE/q42/XcLivdkI7+mO9VMGmf0/ZiO9nmHr8SKs+DYHKq0ejz3ojhqVFhVKDcqUaihUDc3uJxby4SE3fGm7OYrRw12OoQFuGNKjM1wcTMeiNQ163Ki7VUwqGwtLraHQGJ5rUFFreL3uDnNq5GI+urjK4CE3FIzOjmLIxAI4iARwEAsgEwshEwsgFQkgEwv+8lgIh5vvFQtt6wKKjvjv8F4sXRzMGlbKz8/HzJkz8cEHH6B3794AAKlUCplMBpnMcANsT09PVFZWmuy3bt06eHl54fnnn4dMJoNAIABjDM899xy2bt2KLl26ICMjA8HBweaEdV+TS4T45xM98eqjfth56go2Hr6E2dszsXDPOYwI7oKxoT4I7+kOIV3l1GKMMfz7xwv44IcLGBHshQ//PsAiVxzx+TxMergbIvt4YcW3ufjjShXcHCXo4+MMd7nE+GveTW74Ne8ul8BNLoGjWNDi3qBYyIeXsxReLbxgoV6jQ0WtoZdSrlCjTKlGuUKN84XX0CCSoUyhxtniKlTUaqDS6qBt5bwOIZ9nLBQysQAON4vKrSJj+lgmFkIqEsBRLIC7XAJvVyl8XBzgKhOZ1SP+NONPAMDkoX6t3teW7D5tuCT3hQG+7d6WWcUhLS0NGo0GSUlJAAC5XI709HTEx8cjJiYGEokETk5OWLFiBQAgJiYGa9euxd/+9jfMmzcPO3fuhE6nQ3JyMng8HpYtW4YZM2ZAKpUiICAA48ePt1yG9xmZWIjJYd3x8pBu+P1SJb46cwX7/riK3aevwF0uxugQH4wN9UFoV1ebHHayFZoGPZZ8nYUtRwvx0kBfrHixn8ULq4eTBGnj+1v0mOZyEAvgK5bBt5PMZHtOTkOzvzS1Oj3qNDrUa3So1+pQp2lAvUZn2KbV/eVxA+puPldpdX953IBypdq4T71Ghzqt7o5rjDmIBMZC4e0ihberA7ycJXC8WUykIr6xyDiIDL0YB7EA/8ssAZ/H6/DFYesxw2Q+LoqD2cNK1kTDSq2jbtDh59wyfHXmCn7MLYWmQQ8/NxmeDfHGs/18EOTtxFmhuD0nxhhyripwXaEy/I/u4gBnqdDqRSuzqArL9mXj+J83MG2YP+aN6A1+M1cl3Y7+DVoOYwwanR4qjR61mgaUKtS4WlWPkmoVSqrqcbW6HiVVKlytrkepQo3WfIOJhYbiIeTp4SyTmhYUkQDSxh7MzaIibXws4hufG3o0QvT2doK7XNJ+fxHN0N68SVhz85xsYliJdCwSoQDP9O2CZ/p2QXW9Ft+fu4avMq8g/WABPvq5AD3cHTGqXxc82dsLoV1dm70805LKFGrsOFmEHSeKcam81uQ1uUQIbxep8V65Pjcfe7tK8cDNbZaYAKjXMyjUDVCotFCoGqBQNaCwsg6f/34ZpwqrIJcI8e+JoRgb+kCb2yKtw+PxIBEKIBEK4CITwcfVAaFdXZt9r6ZBj8paza2eh9bQI1Fpb/VgVFod1v96EXo9MLq/D1RaHa6WVUAiczK+v16jQ3W99ua+epPj3Y2fmwwPdeuEh7p3wsDundC7S/v+0OJy8isVh/uMi4MI4wd3xfjBXVGuVOP7rGv45o+rxkLhKhPh8Qc9ENHHC8MDPSw66aZeo8NnmTew8/OfUa/VYYhfZ8QO80dPTzmuVatRUlWPkup6w59VKmSVVKNcqTE5hkTIR29vZ/R7wBn9HnBBLy8nNOgZFCotauoNX/Y1N7/sb33x3yoAjY+VmoZmf3H6ucmQOKYPXhroCycOJxwR84iFfHRxufd5la/PGtYkih9pOEfa0l/QjDGoG/TGQtFYNGrqtfjjSjVOXr6BXy6UYdfpKwCA/r4uiBvRG+EPurchqzvjco0oKg73MXe5BC8/3B0vP9wd1XVa/Jpfhp9zy3DofCn+l1kCkYCHoQHuGNvfByP6doFcYt4/F61Ojz2nr+C9A+dxtVqFUf26YHZkYIuWJVdpdbh2czjhSlU9zl9X4I8r1fjqdAm2HC28434iAQ9OUhGcpEI43/zTz11m3OYkFcFZKrztsQiuMhH6eDvfcwiJ3D94PJ5xKOmv00wf6WkoAIwxFFXW49CFMqT/nI+o//6OSQ93Q9LzfS3ei+ByjSgqDgQA4CITYXSID0aH+ECnZzhTdAP7s65j3x9XMWdHJt7Z8wci+3TB86E+eLyXR4u6tyqtDrtOXUH6oXwUVdYjxNcFs4d2wrjhD7U4LqlIAD93R/i5O5ps1+sZLlfWIb9UCamIf9uXvqEYSIStm+xF7Nu2aUPb7dg8Hg/d3GSY7NYd4wb6Im1/Htb/egleTlLMjHjQom21Zx5/RcWBNCHg8zCwe2cM7N4Z8SN741ThDew5XYKvz5Zgb2YJOslEeDbEG8+HPoCHunVCmVKNgjIlLpbVGv+8WK5E8Y16MAb07+qKRWOC8WRvT+Tm5t47gBbg83no4e6IHn8pGoRYk1QkwIJRQShXarD6pwuI7OOFPj7O1g7LLFQcyF3xeLcKxbtj+uCX82XYc6YEX54sxpajhRAL+NDcvIICMFxq6O/hiNCunfDiAF8M6dEZjwS40a94YlUf/1IAAJj6eEC7t8Xj8ZA4pg9+vVCGeTvPYvcbj1jsMugvjhmGUv8+pJtFjnc3VBxIi4kEfDwV5IWngrygVDfg+3PXkH21Bt3dZPB3l8PfwxFdnKU0Zk9szo85pQC4KQ4A4CoTY9FzwZjx+WlsOXoZ0Y/2sMhxvz5rWFuJigOxWXKJEH8b6Iu/WTsQQmzUs/288X9+l/HJ4UuYPNTPIpeIf/ZamAUiaxlaS4EQQtoBj8dDTLgfim/U46fcUmuH02pUHAghpJ08FeQFN0cx9py5YpHjfZrxp3GdqPZGxYEQYvca5ypwTSTg49kQb/yQfR0KVfPLqLfGDzml+CGHm14InXMghNi9zTHWu+3w2NAH8H8Zl7E/6zr+NrBtC+ZxmQf1HAghpB091M0Vvp0c8NXNW3x2FFQcCCF278MfL+DDHy9YpW0ej4fRIT74Lb8cSnXzN3JqqQ2HL2HD4UsWiuzuqDgQQuzeb/nl+C2/3GrtP/agO3R6hmOXKtp0nCMF5ThSwE0edM6BEELa2cDunSAW8vFbfgWe7O1l9nE+eWWwBaO6O+o5EEJIO5OKBBjs18mqvZfWouJACCEceCTAHbnXFChXqs0+xse/FBjXiWpvVBwIIXavk0yMTjKxVWN49Ob9HzIKzD/vcOpyFU5drrJQRHdH5xwIIXZv7eSB1g4B/R5wgZNUiCMF5RjT38esY3CZB/UcCCGEAwI+D2H+bvgtv21XLHGFigMhxO6t/C4XK7+zzI2m2uKRADcUVtbhanW9Wfv/52A+/nMw38JRNY+GlQghdu/U5RvWDgEAEOLrAgDIulIDbxeHVu+fXVJj6ZDuiIoDIYRwpHcXZ/B4QFZJDSL6tH6+w5pJLb//elvRsBIhhHDEUSJED3dHZJVUWzuUe6LiQAghHAr2cUGWmcNDXK4RRcWBEGL3vF2k8HaRWjsMAECwjzOuVNWjqk7T6n0vlilxsUzZDlE1ZdY5B4VCgbi4OCiVSmi1WsTHx2PAgAE4cuQIUlNTIRQKMXToUMyaNavZ/QsKCjB+/HgcOXIEEokEZ86cQVJSEgQCAcLDwzFjxow2JUUIIbf7YOIAa4dgFOzjDMBwcvmRmxPjWorLPMzqOWzcuBFhYWHYsmULli9fjiVLlgAAUlJSkJKSgm3btuHYsWPIy8trsq9SqcTKlSshFt+arZiYmIi0tDR88cUXyMzMRFZWlpnpEEKIbQv2uXnFEodXHpnDrOIQHR2NiRMnAgB0Oh0kEgkAICgoCFVVVdBqtVCr1RAITG/LxxhDQkICZs+eDQcHw2VcSqUSGo0G3bp1A4/HQ3h4ODIyMtqSEyGEmFi8NwuL99rGj87OjmJ4u0jNOin93v48vLe/6Y/u9nDPYaUdO3Zg8+bNJtuSk5MREhKCsrIyxMXFYcGCBQCAwMBAxMbGwtXVFYGBgfD39zfZb82aNRg2bBh69+5t3KZUKiGXy43PHR0dUVRUdNeY1Go1cnJyjM9VKpXJc3thj3nZY04A5WXrTuRfAwDk5Bh+D1s7r25OfJy6VNbqGHIuG+4fnZOjb/KaxXNiZsrNzWWjRo1iBw8eZIwxVl1dzcLCwti1a9cYY4ytXLmSrV+/3mSfiIgIFhUVxaKioljfvn3ZpEmTmEKhYCNHjjS+Z9OmTeyTTz65a9vZ2dl3fW4v7DEve8yJMcrL1o1fe4SNX3vE+NzaeaXtz2M94r9mdeoGix2zJTm1Jm+zTkjn5+dj5syZ+OCDD4y9AKlUCplMBplMBgDw9PREZWWlyX4HDhwwPn7yySexYcMGSCQSiEQiFBYWomvXrjh8+DCdkCaE2LU+3s7QMyDnWg0e6tbJ2uE0y6zikJaWBo1Gg6SkJACAXC5Heno64uPjERMTA4lEAicnJ6xYsQIAEBMTg7Vr15qchL7d4sWLMXfuXOh0OoSHh6N///5mpkMIIbav8YqlrJLWFYfG9aHmPdP7Hu9sO7OKQ3p6erPbIyMjERkZ2WT7hg0bmmz76aefjI9DQ0Oxfft2c0IhhJB78vdwtHYIJnw7OUAuESL/uqJV+5kzN8JctLYSIcTuLX8xxNohmODxePBzl+FSRV2r9uMyD5ohTQghVuDn5og/y2utHcYdUXEghNi9+bvOYv6us9YOw0QPd0cU36iDpqHpZal3krQvG0n7stsxqltoWIkQYvcultneL/Qe7o7QM6Cwsg49PeX33gGAStvyQtJWVBwIIcQK/NwNJ8n/LK9tcXFY+nzf9gzJBA0rEUKIFfRwu1kcKmyvVwNQcSCEEKvo5CiGi4MIl1pxUprLNaJoWIkQYvf63Jx0Zmv83B1ttudAxYEQYvcSxwRbO4Rm9XCT4fifN1r8fi7zoGElQgixEj93R5RU10Ol1Vk7lCaoOBBC7N5bW0/jra2nrR1GEz3cHcEYcLmFM6UT9pxDwp5z7RyVARUHQojdu1qtwtVqlbXDaKLHzctZW3pSWiriQyri5mubzjkQQoiVGOc6tPCk9DvP9mnPcExQz4EQQqzEWSqCm6PYJtdYouJACCFW5Ofu2OJhJS7XiKJhJUKI3Xuou23ebQ0wrM56OL+sRe91lTV/w7T2QMWBEGL3uLhzmrm6dnZAqUINTYMeYuHdB3O4zIOGlQghxIp8XBzAGHC9xraupqLiQAixe7GfnkTspyetHUazfFwdAABXqurv+d65OzIxd0dme4cEgIaVCCH3gRsc3nu5tbxdpQCAq9X3Lg4+LtL2DseIigMhhFiRj4uh51BSde9hpdlPB7Z3OEY0rEQIIVbkIBagk0zUomElLlFxIIQQK/NxdcDVFhQHLteIomElQojde7Snu7VDuCsfVwcUtmDxPX+Plt1O1BKoOBBC7N6/nnrQ2iHclY+LFEcvVtzzfVzmQcNKhBBiZT6uDlCoGlCj0lo7FCMqDoQQu/fKhmN4ZcMxa4dxR9435zpcvccVSzM+P4UZn5/iIiQaViKE2D9bvNPa7R64OdehpLoegV2c7vg+Lu+FbVbPQaFQIDY2FlFRUZgwYQJOnzacPT9y5AhefPFFjB8/Hu+///4d9y8oKMDAgQOhVqsBAPv370dERAQmT56MyZMn49gx263whBBiaY2zpEvuccXSG8N74o3hPbkIybyew8aNGxEWFobo6GhcvHgRc+bMwe7du5GSkoLU1FQEBARg0qRJyMvLQ2Cg6aQNpVKJlStXQiy+tbpgVlYW4uLiMGLEiLZlQwghHZCnkxQCPu+ew0pcMqvnEB0djYkTJwIAdDodJBIJACAoKAhVVVXQarVQq9UQCAQm+zHGkJCQgNmzZ8PBwcG4PSsrCzt37sSkSZOwYsUKNDQ0mJsPIYR0OAI+D12cpffsOXC5RtQ9ew47duzA5s2bTbYlJycjJCQEZWVliIuLw4IFCwAAgYGBiI2NhaurKwIDA+Hv72+y35o1azBs2DD07m267Oyjjz6KiIgI+Pr6IjExEVu3bkVUVNQdY1Kr1cjJyTE+V6lUJs/thT3mZY85AZSXrevb2fBnYy62mJeLWI8LJRV3jesBqWGNqObeY/GcmJlyc3PZqFGj2MGDBxljjFVXV7OwsDB27do1xhhjK1euZOvXrzfZJyIigkVFRbGoqCjWt29fNmnSJOO+jQ4ePMjmz59/17azs7Pv+txe2GNe9pgTY5RXR2OLeb35+Sn22MqfzN6/JTm1Jm+zzjnk5+dj5syZ+OCDD4y9AKlUCplMBplMBgDw9PREZWWlyX4HDhwwPn7yySexYcMGMMbw3HPPYevWrejSpQsyMjIQHBxsbq0jhJAOycfVAd+euwq9noHP51k7HPNOSKelpUGj0SApKQkAIJfLkZ6ejvj4eMTExEAikcDJyQkrVqwAAMTExGDt2rUmJ6Eb8Xg8LFu2DDNmzIBUKkVAQADGjx/fhpQIIcTUhHUZAIBt04ZaOZI783GVQqtjKK9Vw9Op+aW5X9t8HADwySuD2z0es4pDenp6s9sjIyMRGRnZZPuGDRuabPvpp5+Mj8PDwxEeHm5OKIQQYhduX7r7TsXhkQDu1oiiSXCEEGIDGm/6U1JVj9Curs2+Jya8B2fx0PIZhBBiAx5o4UQ4rlBxIIQQG+DiIIKDSHDXO8JxuUYUDSsRQuze6BBva4dwTzweDz6u0rveSzoiyJOzeKg4EELs3uShftYOoUV8XB3uOqzEZR40rEQIsXv1Gh3qNba9MitguGLpio2sr0TFgRBi96I3HkP0Rttf7dnH1QHlSjXUDc0Xspc/OYqXPznKSSw0rEQIITai8XLWa9UqdHdzbPL66BAfzmKh4kAIITbi1uWszReHvw/pxlksNKxECCE2oqU3/eECFQdCCLER3i63Zkk3Z8K6DOM6Ue2NhpUIIXbvpYG+1g6hRaQiAdwcxSipbv6KJS7zoOJACLF74wZ1tXYILeZ9l4lwXOZBw0qEELtXWatBZa3G2mG0SBdnKa7doeeg1emh1ek5iYOKAyHE7k3fchLTt3Bz7+W28nKW4npN88Uh6pPfEfXJ75zEQcNKhBBiQ7o4S3GjTguVVgepSGDy2sQh3A0rUXEghBAb4nXziqXSGjW6uclMXnthAHcnpGlYiRBCbEgX55uzpJsZWuJyjSgqDoQQYkO6uNy5OHC5RhQNKxFC7F5UWHdrh9BiXjd7DtebuWKJyzyoOBBC7N6Y/twtWNdWzlIhHESCZnsOXOZBw0qEELtXUlVvE+sVtQSPx0MXF2mzxaFGpUWNSstJHNRzIITYvVnbzgAAtk0bat1AWsjLWdLssNLrm08A4CYPKg6EEGJjujhLceLyjSbbX33Uj7MYqDgQQoiN8XKRorRGDcYYeDyecfszfb05i4HOORBCiI3p4iyFRqdvsh4Ul2tEUXEghBAbc6eJcFyuEWXWsJJCoUBcXByUSiW0Wi3i4+MxYMAAHDlyBKmpqRAKhRg6dChmzZplsh9jDI8//jj8/PwAAKGhoZgzZw7OnDmDpKQkCAQChIeHY8aMGW1OjBBCGr3+mL+1Q2iVxiU0rteoEOzjYtzOZR5mFYeNGzciLCwM0dHRuHjxIubMmYPdu3cjJSUFqampCAgIwKRJk5CXl4fAwEDjfoWFhQgODsbatWtNjpeYmIjVq1eja9eumDp1KrKyshAcHNy2zAgh5KaIPl7WDqFVjBPhatQm27nMw6ziEB0dDbFYDADQ6XSQSCQAgKCgIFRVVUGr1UKtVkMgMF1RMCsrC9evX8fkyZMhlUoxf/58eHp6QqPRoFs3w42zw8PDkZGRQcWBEGIxBWVKAECAh9zKkbSMh9zwnVr6l+JQqjAMM3k6Sds9hnsWhx07dmDz5s0m25KTkxESEoKysjLExcVhwYIFAIDAwEDExsbC1dUVgYGB8Pc37QJ5eHhg6tSpGDlyJE6cOIG4uDh89NFHkMtvfWCOjo4oKiq6a0xqtRo5OTnG5yqVyuS5vbDHvOwxJ4DysnVvf1cCAEh5xjDDuCPk5SLlI6/wKnJyGozb/prH7Syd0z2Lw7hx4zBu3Lgm2/Py8jB79my8/fbbGDJkCGpqarBu3Trs27cPXl5eSElJwYYNG/Daa68Z9+nbt6+xNzFo0CBcv34djo6OqK2tNb6ntrYWzs7Od41JIpEgKCjI+DwnJ8fkub2wx7zsMSeA8rJ1sl+qAMCYS0fIy9u1DBqBg0mcc/huAICgQM8m729JTq0pHmZdrZSfn4+ZM2ciLS0Nw4YNAwBIpVLIZDLIZIb1xz09PVFTU2Oy35o1a4y9kNzcXPj4+MDJyQkikQiFhYVgjOHw4cMYNGiQOWERQojd8HKWolRhOqw0PNATw5spDO3BrHMOaWlp0Gg0SEpKAgDI5XKkp6cjPj4eMTExkEgkcHJywooVKwAAMTExWLt2LaZOnYq4uDgcOnQIAoEAy5cvBwAsXrwYc+fOhU6nQ3h4OPr372+h9AghpGPycpYg95rpD+zG9aF8XB3avX2zikN6enqz2yMjIxEZGdlk+4YNGwAAYrEYH3/8cZPXQ0NDsX37dnNCIYQQu+TlLEWZQg2dnkHAN8yS5nKNKFo+gxBi99588kFrh9Bqnk4S6BlQoVTD8+alrVzmQcWBEGL3wh90t3YIrdZYEEoVt4oDl3nQ8hmEELuXVVKNrJJqa4fRKrcmwt1aQqOwog6FFXWctE/FgRBi95bszcaSvdnWDqNVPJ0ME+FunyUd92Um4r7M5KR9GlYihBAb5HGzODTOigaAWZG9OGufigMhhNggkYAPd7nYpOcQ5u/GWfs0rEQIITbK00mK0tvOORSUKY3rRLU36jkQQoiN8nSW4Pptw0oLdv0BgOY5EEKIRbz9TOC932SDvJykyC65NUuayzyoOBBC7N7A7p2tHYJZvJwlKFeq0aDTQyjgc5oHnXMghNi9k5crcfJypbXDaDVPZ6lhlvTN+0bnXVMg75qCk7apOBBC7F7Kd3lI+S7P2mG0WuNch8ab/rz71Tm8+9U5TtqmYSVCCLFRt8+S7gcXLBjF3T0oqDgQQoiNMhaHm1cs9e/qylnbNKxECCE2yl0uBo93awkNLteIouJACCE2Sijgw81RgrKbPQcu14iiYSVCiN17d0wfa4dgNi9nibHnwGUeVBwIIXYv2MfF2iGYzctZaly2m8s8aFiJEGL3Dl8ox+EL5dYOwyyeTrd6DplFVcgsquKkXeo5EELs3uqfLgDouHeEq6g1zJJO/iYHAK2t1CparRbFxcVQqVT3fnMHodVqkZOTY+0wLKotOUmlUvj6+kIkElk4KkJsl5ezBIwB5UoNlozty1m7dlMciouL4eTkBD8/P/B4PGuHYxH19fVwcHCwdhgWZW5OjDFUVFSguLgYPXr0aIfICLFNnk63JsLRPAczqFQquLm52U1hIKZ4PB7c3NzsqmdISEt4OTfeEU7N6RpRdtNzAECFwc7R50vuR7cvofHJrxcB0DkHQgixiOQX+1k7BLO5OYrB5wGlNSpO87CbYSXSOmq1Gjt27OCsvePHjyM3N5ez9gi5XYCHHAEecmuHYRahgA83ueFyVi7zsNuew4R1GU22jQ7xxuShfqjX6BC98ViT118a6Itxg7qislaD6VtOmrzGRTeOS2VlZdixYwfGjRvHSXs7d+7EqFGj0L17d07aI+R2P2RfBwBE9PGyciTm8XKWoFShwtGLFQCAMH+3dm/TbosD13bt2oWff/4ZKpUKZWVlmDJlCn788UdcuHABb7/9NiIiIvC///0Pmzdvhlgshp+fH5YsWYJZs2ZhypQpGDJkCM6ePYv09HR8+OGHSExMxKVLlwAAb731Fh5++GGMGTMGgwYNwvnz59GjRw+4ubnhxIkTEIvF+Pjjj6FSqfDOO+/gxo0bAICFCxciMDAQTz/9NB566CFcunQJbm5uWL16NdauXYv8/HysWbMGM2bMMOaRnZ2NpUuXQiAQQCKRYOnSpdDr9ZgzZw66dOmCoqIi9OvXD4sXL8bJkyexcuVKCIVCODs7IzU1FRKJBImJibh8+TL0ej3eeustODo64tdff0VWVhb+/e9/w9/f3yqfEbl/rb85Vt9hi4OTFFerVXj/wHkAHP1YZWaoqalh06ZNYy+//DIbP348O3XqFGOMsd9++4298MILbNy4cey9995rsp9er2fh4eEsKiqKRUVFsdTUVMYYY99//z176qmnjNt///33u7afnZ3d5Plft3Ft586d7NVXX2WMMfb111+zl156ien1epaRkcGmT5/OKisrWUREBFMoFIwxxpKSktinn37KDh48yOLj4xljjC1atIj99NNP7LPPPmMpKSmsrq6OVVZWslGjRjHGGHviiSfYiRMnGGOMjRgxgh08eJAxxtjLL7/MsrOzWUpKCvvss88YY4xdunSJTZw4kTHGWO/evVlJSQljjLEJEyaw06dPs6KiIjZu3LgmebzwwgvGv8sDBw6wN998kxUVFbEhQ4YwhULBGhoa2PDhw1lpaSlbsWIF+/jjj5lOp2MHDhxgV65cMcbOGDOJfd68eezQoUOsrq6uTX/P1v6c78RW42ore8lr/NojbPzaI8bnHS2v+J2ZbODS/exyeS27XF7b7HtaklNr8jar57Bx40aEhYUhOjoaFy9exJw5c7B7926kpKQgNTUVAQEBmDRpEvLy8hAYeOuG2IWFhQgODsbatWtNjpeVlYW4uDiMGDGibZXOyoKCDDficHJyQkBAAHg8HlxcXKBWq1FUVISePXtCLjeMFw4ePBiHDx/GpEmTsGrVKlRVVeHEiRNYuHAhli5dipMnT+L06dMQCARoaGgw9gaCg4MBAM7OzggICDA+VqvVOH/+PI4ePYpvv/0WAFBTY7gxeadOneDt7Q0A8Pb2hlqtvmMOpaWlxjwGDx6MtLQ0AEC3bt2MsXt4eECtViM2NhZr167FK6+8Ai8vL4SEhOD8+fM4efIkzp49CwAmsRNCzOPpJEVFrQberlKIBNycKjarOERHR0MsFgMAdDodJBLDdbhBQUGoqqqCVquFWq2GQCAw2S8rKwvXr1/H5MmTIZVKMX/+fPj7+yMrKws5OTnYvHkzQkJCMHfuXAiFHW/E626XWvr6+qKgoAB1dXWQyWQ4duwYevToAT6fj2eeeQaLFi1CREQEBAIB/P390aVLF7zyyivg8XhIT0+Hi4vLPdvw9/fHc889hzFjxqCiosJ4wrm5ffh8PvR6fZPtnp6eyM3NRe/evXH8+HH4+fnd8Rh79+7FCy+8gHnz5mHdunXYvn27MfbY2FioVCpj7DweD4yxu/79EUKa5+UsBWPAN39chZujhJNlQO75Dbxjxw5s3rzZZFtycjJCQkJQVlaGuLg4LFiwAAAQGBiI2NhYuLq6IjAwsMnYsoeHB6ZOnYqRI0fixIkTiIuLw86dO/Hoo48iIiICvr6+SExMxNatWxEVFXXHmNRqtckSDCqVCgKBAPX19a1K3pI0Gg0aGhpQX19v8lilUkGv18PBwQHTpk1DVFQU+Hw+unbtin/+85+or6/Hs88+i9GjR+Orr75CfX09xo4diyVLliAmJga1tbUYP3481Go19Ho96uvrodfrodfroVKpUF9fD51OB7VajejoaCxatAhffPEFamtrERsbi/r6ejDGjH83je+VyWRQq9VYvnw53nrrLWMeCQkJWLx4MRhjEAgEWLRokTGHxmM0tt2rVy+8/fbbkMlkEAqFePfdd+Hh4YElS5Zg0qRJUCqVxtj79OmDVatWYcWKFcYejzlsdUkRlUplk3G1lb3kVVdXBwDGXDpaXprqWgDAh/uzIRPx4faMT5P3WDynFg9A/UVubi4bNWqUcdy7urqahYWFsWvXrjHGGFu5ciVbv369yT51dXVMrVYbnz/66KNMr9ez6upq47aDBw+y+fPn37VtWzzn0B7aOj5vi+icQ8diL3lduVHHrty49W+vo+V1tqiKdZ/3Nfv898smedzO0ucczBq8ys/Px8yZM5GWloZhw4YBMCyKJpPJIJPJABiGJxrHvButWbPG2AvJzc2Fj4+h+j333HO4du0aACAjI8M4rk4IIZbg4+oAH9eOu05Z4xIaDXrGWR5mDeynpaVBo9EgKSkJACCXy5Geno74+HjExMRAIpHAyckJK1asAADExMRg7dq1mDp1KuLi4nDo0CEIBAIsX74cPB4Py5Ytw4wZMyCVShEQEIDx48dbLkNCyH1vb2YJAGBM/6bDMR2Bm1wCPg84fqkCXTs5YHigZ7u3aVZxSE9Pb3Z7ZGQkIiMjm2zfsGEDABivx/+r8PBwhIeHmxMKIYTc05ajlwF03OIg4PPg4SRBRkEFrteobbc4EEII4ZankxRyqRD/nhjKSXu0thIhhHQAXs4SVNVpjfd3aG9UHAghpAPwdJaiuLLOuE5Ue6PicB/bsmULAMO6UKmpqRY77pNPPnnXWdi3mzx5MgoKCky2FRQUYPLkyRaLhxB74OkkgULdgI9/uchJe3ZbHCasy8COE0UAAK1OjwnrMrD7dDEAoF6jw4R1GcYrGGpUWkxYl4Hvzl0FAFTWajBhXYaxQpcq7PPuY3e6sIAQe5MeNRDpUQOtHUabNN70Z9FzfThpj05IW8iuXbuwc+dO6PV6/Otf/0JVVRU2bdoEPp+PgQMHYu7cuc2uYrp//378+OOPUCqVuHHjBv75z39ixIgR+O233/Dee+/BwcEBrq6uSE5ORk5ODtavXw+RSITi4mKMGjUK06dPx/79+7F+/XoIhUI88MADSElJQW1tbbMrtDZKT09HdXU1Fi1ahJCQEGRmZiImJgaVlZX4+9//jgkTJmD06NHw8/ODWCzG4sWLmz1efHw8CgsLoVar8Y9//AOjRo0CACxatAjFxYZivGbNGshkMixYsAB//vknAODVV181vhcwrOk0d+5cMMbg4eHBxUdG7iOdHcXWDqHNGuc6qBuaLnvTLlo8Xc6G2OIM6Z07d7LY2FjGGGM3btxgI0eONM4Gnjt3Ljt8+HCzq5ju3LmTRUdHM51Ox8rKytjw4cOZRqNhTzzxBLt06RJjjLFNmzaxFStWsKNHj7KRI0cyrVbLamtr2UMPPcQYY+zNN99kX3/9NWOMsd27d7Pq6uo7rtB6u0ceecQYe3R0NNPr9ayoqIiNHDmSMWZYBTYrK4sxxpo9nkKhYMOHD2cVFRWsoqKC/e9//zPud/z4ccaYYTXWffv2sU8//ZQlJSWxuro6plAoWGRkJKuoqGBRUVEsPz+frVixgm3bto0xxti+fftYVFRUs3/P1v6c78RW42ore8lr+/FCtv14ofF5R8zrj2LDLOnkfc3HbhMzpEnzevToAcCw+mxlZSWmTp1qHFMvKipCbGwsKisr8corr+C7774zLi44ePBg8Pl8uLu7w9nZGeXl5ZDL5fDy8jK+fuHCBQBAr169IBQKIZPJIJUaupnz58/H8ePHERUVhVOnToHP5+P8+fPYuXMnJk+ejISEhCaz1f+qT58+4PF48PDwgEp1axitMafmjieXy5GQkICEhATMmjULGo3GuF/fvn0BAO7u7lCpVCgoKMDgwYMBGCZNBgQEoKioyPj+CxcuICQkBADw0EMPmfkJENK8L08W48uTxdYOo008b/Ycvs+6xkl7NKxkQXy+odb6+vrC29sbGzZsgEgkwq5duxAUFNTsKqY+Pj7IysoCAJSXl0OpVMLT0xNKpRJlZWXo1q0bjh07dtfVUbdt24Y333wTbm5uePfdd3HgwIE7rtB6O3bbKql3Wu21MafmjldaWoqsrCx89NFHUKvVGDZsGMaOHdvs8QICAnDixAmEh4dDqVTi/Pnz8PX1Nb7u7++P06dPo3fv3vjjjz9a+ldOyH3DzdEwSzqSoxsWUXFoB507d0Z0dDQmT54MnU6HBx54ACNHjoRGo0F8fDxkMhlEIhGWLFmC48ePo7y8HK+88goUCgUSExMhEAiwbNkyzJkzBwKBAC4uLli+fLmx9/BXISEhePXVV+Hq6gpHR0cMHz4cw4cPxzvvvIPt27dDqVSa3O2tUUBAAObOnYtHHnnknjnFxsY2OZ6HhwfKysrw/PPPQyaTISYm5o5LrY8fPx4JCQmIjo6GVqvFjBkz4OZ261aHM2fOxKxZs/DNN9+YFA1CiIGAz4OnkxTV9VpO2uMx1vEW2c/JyTHekKbxOQCTbR3Frl27cPHiRcydO7fJa/X19XBw6LiLhTWnrTn99bO3FbYaV1vZS16N95RvvL1mR81r4Z4/0FkmxuynA5u81pKcWpM39RwIIaSDuHBdyVlbVBys7MUXX7R2CITYvU2vDrF2CBbBZR52VRwYY3e9jSbp2DrgCCixEQ5iwb3f1AFwmYfdXMoqlUpRUVFBXyB2ijGGiooK4+W7hLTGpxl/4tOMP60dRpvtPl1sXOmhvdlNz8HX1xfFxcUoKyuzdigWo9VqIRKJrB2GRbUlJ6lUSlcyEbN8fdawNM7koX7WDaSNth4zzA16YUD7/39gN8VBJBIZJ2zZi456RcXd2GNOhHBly2sPc9aW3RQHQgixdyIBd2cC7OacAyGE2LsdJ4qMq023NyoOhBDSQXC5RlSHnCF95swZSCQSa4dBCCEdilqtRmhoaIve2yGLAyGEkPZFw0qEEEKaoOJACCGkCSoOhBBCmqDiQAghpAkqDoQQQpqg4kAIIaQJm1w+Q6fTYeHChbh06RIEAgGWL18OjUaDhIQEMMbQu3dvJCQkQCAQYPv27di6dSuEQiGmT5+OJ554wuRYOTk5WLp0KQQCAcRiMVauXAl3d/cOn1ejvXv3YsuWLdi2bRvH2RhYMqeKigosXLgQNTU10Ol0SElJQbdu3Tp8Xjk5Ocbbv/r5+SEpKcl4b25bzgsAKisrMXHiROzdu7fJ3KLLly8jPj4ePB4PDz74IBITE62SlyVz6qjfF8Dd82rUqu8LZoMOHDjA4uPjGWOMHT16lMXGxrLp06ezY8eOMcYYmzdvHtu/fz8rLS1lo0ePZmq1mtXU1Bgf3+7ll19m2dnZjDHGvvjiC5acnMxtMrexZF6MMZadnc2mTJnCxo0bx2ket7NkTvPmzWP79u1jjDGWkZHBfv75Z05zuZ0l83rjjTfYwYMHGWOMzZ49m/3444/cJnOblubFGGO//PILGzt2LBswYABTqVRNjjVt2jR29OhRxhhjCQkJxv24ZsmcOuL3BWP3zoux1n9f2OSwUkREBJYuXQoAKCkpgbu7O1avXo3BgwdDo9GgrKwMbm5uOHv2LAYMGACxWAwnJyd069YNubm5Jsd67733jKuA6nQ6q86stmReN27cQGpqKhYsWGCNVIwsmdOpU6dw/fp1REdHY+/evRgyxHp377JkXkFBQaiqqgJjDLW1tRAKrddhb2leAMDn87Fx40a4uro2e6ysrCzjZ/T444/jyJEjnOTwV5bMqSN+XwD3zsuc7wubLA4AIBQKMW/ePCxduhQjRoyAQCDAlStXMHr0aNy4cQM9evSAUqmEk5OTcR9HR0colab3WPX09ARg+OLZsmULoqOjuUyjCUvkpdPp8M4772DBggVwdHS0RhomLPVZXblyBc7Ozti0aRO8vb2xfv16rlMxYam8GoeSRo4ciYqKCjz8MHfLLjenJXkBwKOPPopOnTrd8TjstjsvOjo6QqFQcBJ/cyyVU0f8vgDunpfZ3xdt7vu0s9LSUjZ8+HBWW1tr3LZ9+3b29ttvsx9++IElJiYat7/xxhvs7NmzTY6xb98+Nnr0aFZYWMhFyC3SlrwyMzPZqFGjWFRUFBs3bhwbMGAAW7ZsGZfhN6utn9UjjzzCKisrGWOMZWVlsddee42TuO+lrXmFhYWx8+fPM8YY27JlC1u0aBEncd/L3fK63RNPPNHsUMVjjz1mfHzgwAG2ePHi9gu2hdqaE2Md7/vids3lZe73hU32HPbs2YN169YBABwcHMDj8TBjxgz8+eefAAy/Uvh8PkJCQnDy5Emo1WooFAoUFBSgV69eJsf66quvsGXLFnz66afo2rUr16mYsFReISEh2LdvHz799FO899576NmzJ9555x1rpGTRz2rgwIE4dOgQAOD48ePo2bMnp7nczpJ5ubi4QC6XAzD8Mq2pqeE0l9u1NK+W6NOnD37//XcAwC+//IJBgwa1S8z3YsmcOuL3xb2Y+31hkwvv1dXVYf78+SgvL0dDQwNef/11dO7cGSkpKRCJRHBwcMCyZcvg6emJ7du3Y9u2bWCMYdq0aRgxYgTy8/OxZcsWJCQkYOjQofD29oazszMAYPDgwfjXv/7VofNatGiR8ZjFxcWYPXs2tm/f3uFzunLlChYuXIj6+nrI5XKkpaXBxcWlw+d14sQJpKamQigUQiQSYenSpVa73Wlr8mr05JNP4ttvv4VEIjHJ69KlS0hISIBWq4W/vz+WLVtmvHKmI+bUkb8v7paXud8XNlkcCCGEWJdNDisRQgixLioOhBBCmqDiQAghpAkqDoQQQpqg4kAIIaQJKg6EEEKaoOJACCGkif8HkNGhYBDjNCoAAAAASUVORK5CYII=\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "trial_data = one.load_object(eid, 'trials', collection='alf')\n", "ts = wh.get_movement_onset(wheel_moves.intervals, trial_data.response_times)\n", @@ -356,17 +292,9 @@ }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Session 2020-09-19_1_CSH_ZAD_029\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "eid = 'c7bd79c9-c47e-4ea5-aea3-74dda991b48e'\n", "print('Session ' + one.eid2ref(eid, as_dict=False))\n", @@ -381,18 +309,9 @@ }, { "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "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\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "trial_data = one.load_object(eid, 'trials', collection='alf')\n", "\n", @@ -424,18 +343,9 @@ }, { "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001B[36m2023-07-10 12:11:44.796 INFO [training_wheel.py:343] minimum quiescent period assumed to be 200ms\u001B[0m\n", - "\u001B[1;33m2023-07-10 12:11:44.831 WARNING [training_wheel.py:367] no reliable goCue/Feedback times (both needed) for 114 trials\u001B[0m\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "wheel_moves = one.load_object(eid, 'wheelMoves', collection='alf')\n", "firstMove_times, is_final_movement, ids = extract_first_movement_times(wheel_moves, trial_data)" @@ -451,18 +361,9 @@ }, { "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "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\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "n = 569\n", "on, off = wheel_moves['intervals'][n,]\n", @@ -490,18 +391,9 @@ }, { "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "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\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "n = 403 # trial number\n", "start, end = trial_data['intervals'][n,] # trial intervals\n", @@ -535,18 +427,9 @@ }, { "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": "
", - "image/png": "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\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "n_trials = 3 # Number of trials to plot\n", "# Randomly select the trials to plot\n", @@ -600,9 +483,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/examples/data_release/data_release_behavior.ipynb b/examples/data_release/data_release_behavior.ipynb index a3364dcd7..b06a6cddb 100644 --- a/examples/data_release/data_release_behavior.ipynb +++ b/examples/data_release/data_release_behavior.ipynb @@ -16,6 +16,10 @@ }, { "cell_type": "markdown", + "id": "dd157e91", + "metadata": { + "collapsed": false + }, "source": [ "## Overview of the Data\n", "We have released behavioral data throughout learning from our standardized training pipeline, implemented across 9 labs in 7 institutions. Users can download behavioral data from mice throughout their training, and analyse the transition from novice to expert behavior unfold. The behavioral data is associated with 198 mice up until 2020-03-23, as used in [The International Brain Laboratory et al. 2020](https://elifesciences.org/articles/63711). This dataset contains notably information on the sensory stimuli presented to the mouse, as well as mouse decisions and response times.\n", @@ -35,10 +39,7 @@ "Note:\n", "\n", "* The tag associated to this release is `2021_Q1_IBL_et_al_Behaviour`" - ], - "metadata": { - "collapsed": false - } + ] } ], "metadata": { @@ -58,9 +59,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/examples/data_release/data_release_brainwidemap.ipynb b/examples/data_release/data_release_brainwidemap.ipynb index 001fed7cf..47d90b225 100644 --- a/examples/data_release/data_release_brainwidemap.ipynb +++ b/examples/data_release/data_release_brainwidemap.ipynb @@ -16,6 +16,10 @@ }, { "cell_type": "markdown", + "id": "e9be5894", + "metadata": { + "collapsed": false + }, "source": [ "## Overview of the Data\n", "We have released data from 459 Neuropixel recording sessions, which encompass 699 probe insertions, obtained in 139 subjects performing the IBL task across 12 different laboratories. As output of spike-sorting, there are 376730 units; of which 45085 are considered to be of good quality. In total, 138 brain regions were recorded in sufficient numbers for inclusion in IBL’s analyses [(IBL et al. 2023)](https://www.biorxiv.org/content/10.1101/2023.07.04.547681v2).\n", @@ -35,10 +39,7 @@ "\n", "## Receive updates on the data\n", "To receive a notification that we released new datasets, please fill up [this form](https://forms.gle/9ex2vL1JwV4QXnf98)\n" - ], - "metadata": { - "collapsed": false - } + ] } ], "metadata": { @@ -58,7 +59,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/data_release/data_release_repro_ephys.ipynb b/examples/data_release/data_release_repro_ephys.ipynb index bda041508..10c032155 100644 --- a/examples/data_release/data_release_repro_ephys.ipynb +++ b/examples/data_release/data_release_repro_ephys.ipynb @@ -57,9 +57,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/examples/data_release/data_release_spikesorting_benchmarks.ipynb b/examples/data_release/data_release_spikesorting_benchmarks.ipynb index d67d72e2d..6325fb80a 100644 --- a/examples/data_release/data_release_spikesorting_benchmarks.ipynb +++ b/examples/data_release/data_release_spikesorting_benchmarks.ipynb @@ -89,9 +89,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/examples/exploring_data/data_download.ipynb b/examples/exploring_data/data_download.ipynb index c9eeb9f99..3e4b8961a 100644 --- a/examples/exploring_data/data_download.ipynb +++ b/examples/exploring_data/data_download.ipynb @@ -2,6 +2,8 @@ "cells": [ { "cell_type": "code", + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", @@ -12,16 +14,13 @@ "logger.setLevel(logging.CRITICAL)\n", "\n", "os.environ[\"TQDM_DISABLE\"] = \"1\"" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", "metadata": { - "nbsphinx": "hidden", - "collapsed": false + "collapsed": false, + "nbsphinx": "hidden" }, "source": [ "# Download the public datasets\n", @@ -167,54 +166,51 @@ { "cell_type": "code", "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "# Find sessions that have spikes.times datasets\n", "sessions_with_spikes = one.search(project='brainwide', data='spikes.times')" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "[Click here](https://int-brain-lab.github.io/ONE/notebooks/one_search/one_search.html) for a complete guide to searching using ONE.\n", "\n", "\n", "### Find data associated with a release or publication\n", "Datasets are often associated to a publication, and are tagged as such to facilitate reproducibility of analysis. You can list all tags and their associated publications like this:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "# List and print all tags in the public database\n", "tags = {t['name']: t['description'] for t in one.alyx.rest('tags', 'list') if t['public']}\n", "for key, value in tags.items():\n", " print(f\"{key}\\n{value}\\n\")" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", - "source": [ - "You can use the tag to restrict your searches to a specific data release and as a filter when browsing the public database:" - ], "metadata": { "collapsed": false - } + }, + "source": [ + "You can use the tag to restrict your searches to a specific data release and as a filter when browsing the public database:" + ] }, { "cell_type": "code", "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "# Note that tags are associated with datasets originally\n", @@ -230,10 +226,7 @@ "# To return to the full cache containing an index of all IBL experiments\n", "ONE.cache_clear()\n", "one = ONE(base_url='https://openalyx.internationalbrainlab.org')" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", @@ -310,6 +303,11 @@ { "cell_type": "code", "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "# Load in all trials datasets\n", @@ -317,22 +315,16 @@ "\n", "# Load in a single wheel dataset\n", "wheel_times = one.load_dataset(eid, '_ibl_wheel.timestamps.npy')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] }, { "cell_type": "markdown", - "source": [ - "Examples for loading different objects can be found in the following tutorials [here](https://int-brain-lab.github.io/iblenv/loading_examples.html)." - ], "metadata": { "collapsed": false - } + }, + "source": [ + "Examples for loading different objects can be found in the following tutorials [here](https://int-brain-lab.github.io/iblenv/loading_examples.html)." + ] }, { "cell_type": "markdown", @@ -368,6 +360,13 @@ }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], "source": [ "# List details of all sessions (returns a list of dictionaries)\n", "_, det = one.search(details=True)\n", @@ -378,27 +377,19 @@ "\n", "# Searching for RS sessions with specific lab name\n", "sessions_lab = one.search(data='spikes', lab=lab_name)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "You can also get this list, using [one.alyx.rest](https://int-brain-lab.github.io/ONE/notebooks/one_advanced/one_advanced.html), however it is a little slower." - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "You can also get this list, using [one.alyx.rest](https://int-brain-lab.github.io/ONE/notebooks/one_advanced/one_advanced.html), however it is a little slower." + ] }, { "cell_type": "code", @@ -429,14 +420,14 @@ "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/exploring_data/data_structure.ipynb b/examples/exploring_data/data_structure.ipynb index a3bd7dc93..0ccf43ff5 100644 --- a/examples/exploring_data/data_structure.ipynb +++ b/examples/exploring_data/data_structure.ipynb @@ -270,16 +270,16 @@ "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.11.6" } }, "nbformat": 4, "nbformat_minor": 0 -} \ No newline at end of file +} diff --git a/examples/loading_data/loading_ephys_data.ipynb b/examples/loading_data/loading_ephys_data.ipynb index dfdf43533..e5758864c 100644 --- a/examples/loading_data/loading_ephys_data.ipynb +++ b/examples/loading_data/loading_ephys_data.ipynb @@ -221,7 +221,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/loading_data/loading_multi_photon_imaging_data.ipynb b/examples/loading_data/loading_multi_photon_imaging_data.ipynb index d80aa5452..e6561c24e 100644 --- a/examples/loading_data/loading_multi_photon_imaging_data.ipynb +++ b/examples/loading_data/loading_multi_photon_imaging_data.ipynb @@ -2,20 +2,23 @@ "cells": [ { "cell_type": "markdown", - "source": [ - "# Loading Multi-photon Calcium Imaging Data\n", - "\n", - "Cellular Calcium activity recorded using a multi-photon imaging." - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "# Loading Multi-photon Calcium Imaging Data\n", + "\n", + "Cellular Calcium activity recorded using a multi-photon imaging." + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## Relevant ALF objects\n", "* mpci\n", @@ -33,13 +36,13 @@ "Sessions that contain any form of imaging data have an 'Imaging' procedure. This includes sessions\n", "photometry, mesoscope, 2P, and widefield data. To further filter by imaging modality you can query\n", "the imaging type associated with a session's field of view." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "# Find mesoscope imaging sessions\n", @@ -52,13 +55,16 @@ "query = 'field_of_view__imaging_type__name,mesoscope'\n", "eids = one.search(procedures='Imaging', django=query, query_type='remote')\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "Sessions can be further filtered by brain region. You can filter with by Allen atlas name, acronym\n", "or ID, for example:\n", @@ -66,66 +72,63 @@ "* `atlas_name='Primary visual area'`\n", "* `atlas_acronym='VISp'`\n", "* `atlas_id=385`" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "# Find mesoscope imaging sessions in V1, layer 2/3\n", "query = 'field_of_view__imaging_type__name,mesoscope'\n", "eids = one.search(procedures='Imaging', django=query, query_type='remote', atlas_acronym='VISp2/3')\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", - "source": [ - "The 'details' flag will return the session details, including a `field_of_view` field which contains\n", - "a list of each field of view and its location. All preprocessed mpci imaging data is in `alf/FOV_XX`\n", - "where XX is the field of view number. The `FOV_XX` corresponds to a field of view name in Alyx." - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "The 'details' flag will return the session details, including a `field_of_view` field which contains\n", + "a list of each field of view and its location. All preprocessed mpci imaging data is in `alf/FOV_XX`\n", + "where XX is the field of view number. The `FOV_XX` corresponds to a field of view name in Alyx." + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "eids, det = one.search(procedures='Imaging', django=query, query_type='remote', atlas_acronym='VISp2/3', details=True)\n", "FOVs = det[0]['field_of_view']\n", "print(FOVs[0])\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "The ibllib AllenAtlas class allows you to search brain region descendents and ancestors in order to\n", "find the IDs of brain regions at a certain granularity." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "# Search brain areas by name using Alyx\n", @@ -144,41 +147,41 @@ "# Show all descendents of primary visual area (i.e. all layers)\n", "atlas.regions.descendants(V1_id)\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", - "source": [ - "For more information see \"[Working with ibllib atlas](../atlas_working_with_ibllib_atlas.html)\"." - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "For more information see \"[Working with ibllib atlas](../atlas_working_with_ibllib_atlas.html)\"." + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## Loading imaging data for a given field of view\n", "\n", "For mesoscope sessions there are likely more than one field of view, not all of which cover the\n", "area of interest. For mesoscope sessions it's therefore more useful to search by field of view instead.\n", "Each field of view returned contains a session eid for loading data with." - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "# Search for all mesoscope fields of view containing V1\n", @@ -190,51 +193,51 @@ "eid = 'a5550a8e-2484-4539-b7f0-8e5f829d0ba7'\n", "FOVs = one.alyx.rest('fields-of-view', 'list', imaging_type='mesoscope', atlas_id=187, session=eid)\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## Loading imaging stacks\n", "For mesoscope sessions the same region may be acquired at multiple depths. The plane at each depth\n", "is considered a separate field of view and are related to one another through the stack object.\n", "If a field of view was acquired as part of a stack, the `stack` field will contain an ID. You can\n", "find all fields of view in a given stack by querying the 'imaging-stack' endpoint:" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "stack = one.alyx.rest('imaging-stack', 'read', id=FOVs[0]['stack'])\n", "FOVs = stack['slices']\n", "print('There were %i fields of view in stack %s' % (len(FOVs), stack['id']))\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", - "source": [ - "### List the number of fields of view (FOVs) recorded during a session" - ], "metadata": { "collapsed": false - } + }, + "source": [ + "### List the number of fields of view (FOVs) recorded during a session" + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "from one.api import ONE\n", @@ -245,22 +248,22 @@ "fovs = sorted(map(lambda x: int(x[-2:]), fov_folders))\n", "nFOV = len(fovs)\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", - "source": [ - "## Loading ROI activity for a single session" - ], "metadata": { "collapsed": false - } + }, + "source": [ + "## Loading ROI activity for a single session" + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "# Loading ROI activity for a single FOV\n", @@ -272,29 +275,29 @@ "print(all_ROI_data.keys())\n", "print(all_ROI_data.FOV_00.keys())\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "### Get the brain location of an ROI\n", "The brain location of each ROI are first estimated using the surgical coordinates of the imaging window.\n", "These datasets have an '_estimate' in the name. After histological alignment, datasets are created\n", "without '_estimate' in the name. The histologically aligned locations are most accurate and should be\n", "used where available." - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "roi = 0 # The ROI index to lookup\n", @@ -304,13 +307,16 @@ "atlas_id = ROI_data_00['mpciROI'][key][roi]\n", "print(f'ROI {roi} was located in {atlas.regions.id2acronym(atlas_id)}')\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## Loading times\n", "Timestamps for each frame are in seconds from session start and represent the time when frame acquisition started.\n", @@ -318,16 +324,13 @@ "in configuarations such as dual plane mode). Thus there is a fixed time offset between regions of interest.\n", "The offset can be found in the mpciStack.timeshift.npy dataset and depending on its shape, may be per voxel or per\n", "scan line." - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "frame_times = ROI_data_00['mpci']['times']\n", @@ -343,61 +346,58 @@ "plt.plot(roi_times[roi], roi_signal[roi])\n", "plt.xlabel('Timestamps / s'), plt.ylabel('ROI activity / photodetector units')\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", - "source": [ - "### Search for sessions with multi-depth fields of view (imaging stacks)" - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "### Search for sessions with multi-depth fields of view (imaging stacks)" + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "query = 'field_of_view__stack__isnull,False'\n", "eids, det = one.search(procedures='Imaging', django=query, query_type='remote', details=True)\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", - "source": [ - "### Search sessions with GCaMP mice\n", - "..." - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "### Search sessions with GCaMP mice\n", + "..." + ] }, { "cell_type": "markdown", - "source": [ - "## More details\n", - "* [Description of mesoscope datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.nvzaz0fozs8h)\n", - "* [Loading raw mesoscope data](./loading_raw_mesoscope_data.ipynb)" - ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "## More details\n", + "* [Description of mesoscope datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.nvzaz0fozs8h)\n", + "* [Loading raw mesoscope data](./loading_raw_mesoscope_data.ipynb)" + ] } ], "metadata": { @@ -409,14 +409,14 @@ "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/loading_data/loading_passive_data.ipynb b/examples/loading_data/loading_passive_data.ipynb index 8c49a186b..5d3e03114 100644 --- a/examples/loading_data/loading_passive_data.ipynb +++ b/examples/loading_data/loading_passive_data.ipynb @@ -1,258 +1,258 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "5683982d", - "metadata": {}, - "source": [ - "# Loading Passive Data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6b2485da", - "metadata": { - "nbsphinx": "hidden" - }, - "outputs": [], - "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", - "import logging\n", - "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" - ] - }, - { - "cell_type": "markdown", - "id": "16345774", - "metadata": {}, - "source": [ - "Passive stimuli related events. The passive protocol is split into three sections\n", - "1. Spontaneous activity (SP)\n", - "2. Receptive Field Mapping (RFM)\n", - "3. Task replay (TR)" - ] - }, - { - "cell_type": "markdown", - "id": "8d62c890", - "metadata": {}, - "source": [ - "## Relevant datasets\n", - "* passivePeriods.intervalsTable.csv (SP)\n", - "* passiveRFM.times.npy (RFM)\n", - "* \\_iblrig_RFMapStim.raw.bin (RFM)\n", - "* passiveGabor.table.csv (TR - visual)\n", - "* passiveStims.table.csv (TR - auditory)\n" - ] - }, - { - "cell_type": "markdown", - "id": "bc23fdf7", - "metadata": {}, - "source": [ - "## Loading" - ] - }, - { - "cell_type": "markdown", - "id": "9103084d", - "metadata": {}, - "source": [ - "### Loading spontaneous activity" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2b807296", - "metadata": { - "ibl_execute": false - }, - "outputs": [], - "source": [ - "from one.api import ONE\n", - "one = ONE()\n", - "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", - "\n", - "passive_times = one.load_dataset(eid, '*passivePeriods*', collection='alf')\n", - "SP_times = passive_times['spontaneousActivity']" - ] - }, - { - "cell_type": "markdown", - "id": "203d23c1", - "metadata": {}, - "source": [ - "### Loading recpetive field mapping" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "811e3533", - "metadata": { - "ibl_execute": false - }, - "outputs": [], - "source": [ - "from brainbox.io.one import load_passive_rfmap\n", - "\n", - "RFMap = load_passive_rfmap(eid, one=one)" - ] - }, - { - "cell_type": "markdown", - "id": "5b6bf3fb", - "metadata": {}, - "source": [ - "### Loading task replay" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c65f1ca8", - "metadata": { - "ibl_execute": false - }, - "outputs": [], - "source": [ - "# Load visual stimulus task replay events\n", - "visual_TR = one.load_dataset(eid, '*passiveGabor*', collection='alf')\n", - "\n", - "# Load auditory stimulus task replay events\n", - "auditory_TR = one.load_dataset(eid, '*passiveStims*', collection='alf')" - ] - }, - { - "cell_type": "markdown", - "id": "bef6702e", - "metadata": {}, - "source": [ - "## More details\n", - "* [Description of passive datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.81i06nkedtbe)\n", - "* [Decsription of passive protocol](https://docs.google.com/document/d/1PkN_-jWXBLAWbONWXVa2JZh3D9tfurNGsXh422dUxMo/edit#heading=h.fiffmd82uci7)" - ] - }, - { - "cell_type": "markdown", - "id": "4e9dd4b9", - "metadata": {}, - "source": [ - "## Useful modules\n", - "* [brainbox.io.one](https://int-brain-lab.github.io/iblenv/_autosummary/brainbox.io.one.html#brainbox.io.one.load_passive_rfmap)\n", - "* [brainbox.task.passive](https://int-brain-lab.github.io/iblenv/_autosummary/brainbox.task.passive.html)\n", - "* [ibllib.io.extractors.extract_passive](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.io.extractors.ephys_passive.html#module-ibllib.io.extractors.ephys_passive)" - ] - }, - { - "cell_type": "markdown", - "id": "4ad23565", - "metadata": {}, - "source": [ - "## Exploring passive data" - ] - }, - { - "cell_type": "markdown", - "id": "92df091a", - "metadata": {}, - "source": [ - "### Example 1: Compute firing rate for each cluster during spontaneous activity" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7552f7c5", - "metadata": { - "ibl_execute": false - }, - "outputs": [], - "source": [ - "# Find first probe insertion for session\n", - "pid = one.alyx.rest('insertions', 'list', session=eid)[0]['id']\n", - "\n", - "from brainbox.io.one import SpikeSortingLoader\n", - "from iblatlas.atlas import AllenAtlas\n", - "import numpy as np\n", - "ba = AllenAtlas()\n", - "\n", - "# Load in spikesorting\n", - "sl = SpikeSortingLoader(pid=pid, one=one, atlas=ba)\n", - "spikes, clusters, channels = sl.load_spike_sorting()\n", - "clusters = sl.merge_clusters(spikes, clusters, channels)\n", - "\n", - "# Find spike times during spontaneous activity\n", - "SP_idx = np.bitwise_and(spikes['times'] >= SP_times[0], spikes['times'] <= SP_times[1])\n", - "\n", - "# Count the number of clusters during SP time period and compute firing rate\n", - "from brainbox.population.decode import get_spike_counts_in_bins\n", - "counts, cluster_ids = get_spike_counts_in_bins(spikes['times'][SP_idx], spikes['clusters'][SP_idx], \n", - " np.c_[SP_times[0], SP_times[1]])\n", - "fr = counts / (SP_times[1] - SP_times[0])" - ] - }, - { - "cell_type": "markdown", - "id": "4942328f", - "metadata": {}, - "source": [ - "### Example 2: Find RFM stimulus positions and timepoints" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eebdc9af", - "metadata": { - "ibl_execute": false - }, - "outputs": [], - "source": [ - "# Find out at what times each voxel on the screen was turned 'on' (grey to white) or turned 'off' (grey to black)\n", - "from brainbox.task.passive import get_on_off_times_and_positions\n", - "\n", - "RF_frame_times, RF_frame_pos, RF_frame_stim = get_on_off_times_and_positions(RFMap)\n", - "\n", - "# Find times where pixel at location x=1, y=4 on display was turned 'on'\n", - "pixel_idx = np.bitwise_and(RF_frame_pos[:, 0] == 1, RF_frame_pos[:, 1] == 4)\n", - "stim_on_frames = RF_frame_stim['on'][pixel_idx]\n", - "stim_on_times = RF_frame_times[stim_on_frames[0][0]]" - ] - }, - { - "cell_type": "markdown", - "id": "ae7b6c15", - "metadata": {}, - "source": [ - "## Other relevant examples\n", - "* COMING SOON" - ] - } - ], - "metadata": { - "celltoolbar": "Edit Metadata", - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5683982d", + "metadata": {}, + "source": [ + "# Loading Passive Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b2485da", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Turn off logging, this is a hidden cell on docs page\n", + "import logging\n", + "logger = logging.getLogger('ibllib')\n", + "logger.setLevel(logging.CRITICAL)" + ] + }, + { + "cell_type": "markdown", + "id": "16345774", + "metadata": {}, + "source": [ + "Passive stimuli related events. The passive protocol is split into three sections\n", + "1. Spontaneous activity (SP)\n", + "2. Receptive Field Mapping (RFM)\n", + "3. Task replay (TR)" + ] + }, + { + "cell_type": "markdown", + "id": "8d62c890", + "metadata": {}, + "source": [ + "## Relevant datasets\n", + "* passivePeriods.intervalsTable.csv (SP)\n", + "* passiveRFM.times.npy (RFM)\n", + "* \\_iblrig_RFMapStim.raw.bin (RFM)\n", + "* passiveGabor.table.csv (TR - visual)\n", + "* passiveStims.table.csv (TR - auditory)\n" + ] + }, + { + "cell_type": "markdown", + "id": "bc23fdf7", + "metadata": {}, + "source": [ + "## Loading" + ] + }, + { + "cell_type": "markdown", + "id": "9103084d", + "metadata": {}, + "source": [ + "### Loading spontaneous activity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b807296", + "metadata": { + "ibl_execute": false + }, + "outputs": [], + "source": [ + "from one.api import ONE\n", + "one = ONE()\n", + "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", + "\n", + "passive_times = one.load_dataset(eid, '*passivePeriods*', collection='alf')\n", + "SP_times = passive_times['spontaneousActivity']" + ] + }, + { + "cell_type": "markdown", + "id": "203d23c1", + "metadata": {}, + "source": [ + "### Loading recpetive field mapping" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "811e3533", + "metadata": { + "ibl_execute": false + }, + "outputs": [], + "source": [ + "from brainbox.io.one import load_passive_rfmap\n", + "\n", + "RFMap = load_passive_rfmap(eid, one=one)" + ] + }, + { + "cell_type": "markdown", + "id": "5b6bf3fb", + "metadata": {}, + "source": [ + "### Loading task replay" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c65f1ca8", + "metadata": { + "ibl_execute": false + }, + "outputs": [], + "source": [ + "# Load visual stimulus task replay events\n", + "visual_TR = one.load_dataset(eid, '*passiveGabor*', collection='alf')\n", + "\n", + "# Load auditory stimulus task replay events\n", + "auditory_TR = one.load_dataset(eid, '*passiveStims*', collection='alf')" + ] + }, + { + "cell_type": "markdown", + "id": "bef6702e", + "metadata": {}, + "source": [ + "## More details\n", + "* [Description of passive datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.81i06nkedtbe)\n", + "* [Decsription of passive protocol](https://docs.google.com/document/d/1PkN_-jWXBLAWbONWXVa2JZh3D9tfurNGsXh422dUxMo/edit#heading=h.fiffmd82uci7)" + ] + }, + { + "cell_type": "markdown", + "id": "4e9dd4b9", + "metadata": {}, + "source": [ + "## Useful modules\n", + "* [brainbox.io.one](https://int-brain-lab.github.io/iblenv/_autosummary/brainbox.io.one.html#brainbox.io.one.load_passive_rfmap)\n", + "* [brainbox.task.passive](https://int-brain-lab.github.io/iblenv/_autosummary/brainbox.task.passive.html)\n", + "* [ibllib.io.extractors.extract_passive](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.io.extractors.ephys_passive.html#module-ibllib.io.extractors.ephys_passive)" + ] + }, + { + "cell_type": "markdown", + "id": "4ad23565", + "metadata": {}, + "source": [ + "## Exploring passive data" + ] + }, + { + "cell_type": "markdown", + "id": "92df091a", + "metadata": {}, + "source": [ + "### Example 1: Compute firing rate for each cluster during spontaneous activity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7552f7c5", + "metadata": { + "ibl_execute": false + }, + "outputs": [], + "source": [ + "# Find first probe insertion for session\n", + "pid = one.alyx.rest('insertions', 'list', session=eid)[0]['id']\n", + "\n", + "from brainbox.io.one import SpikeSortingLoader\n", + "from iblatlas.atlas import AllenAtlas\n", + "import numpy as np\n", + "ba = AllenAtlas()\n", + "\n", + "# Load in spikesorting\n", + "sl = SpikeSortingLoader(pid=pid, one=one, atlas=ba)\n", + "spikes, clusters, channels = sl.load_spike_sorting()\n", + "clusters = sl.merge_clusters(spikes, clusters, channels)\n", + "\n", + "# Find spike times during spontaneous activity\n", + "SP_idx = np.bitwise_and(spikes['times'] >= SP_times[0], spikes['times'] <= SP_times[1])\n", + "\n", + "# Count the number of clusters during SP time period and compute firing rate\n", + "from brainbox.population.decode import get_spike_counts_in_bins\n", + "counts, cluster_ids = get_spike_counts_in_bins(spikes['times'][SP_idx], spikes['clusters'][SP_idx], \n", + " np.c_[SP_times[0], SP_times[1]])\n", + "fr = counts / (SP_times[1] - SP_times[0])" + ] + }, + { + "cell_type": "markdown", + "id": "4942328f", + "metadata": {}, + "source": [ + "### Example 2: Find RFM stimulus positions and timepoints" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eebdc9af", + "metadata": { + "ibl_execute": false + }, + "outputs": [], + "source": [ + "# Find out at what times each voxel on the screen was turned 'on' (grey to white) or turned 'off' (grey to black)\n", + "from brainbox.task.passive import get_on_off_times_and_positions\n", + "\n", + "RF_frame_times, RF_frame_pos, RF_frame_stim = get_on_off_times_and_positions(RFMap)\n", + "\n", + "# Find times where pixel at location x=1, y=4 on display was turned 'on'\n", + "pixel_idx = np.bitwise_and(RF_frame_pos[:, 0] == 1, RF_frame_pos[:, 1] == 4)\n", + "stim_on_frames = RF_frame_stim['on'][pixel_idx]\n", + "stim_on_times = RF_frame_times[stim_on_frames[0][0]]" + ] + }, + { + "cell_type": "markdown", + "id": "ae7b6c15", + "metadata": {}, + "source": [ + "## Other relevant examples\n", + "* COMING SOON" + ] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python [conda env:iblenv] *", + "language": "python", + "name": "conda-env-iblenv-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/loading_data/loading_raw_audio_data.ipynb b/examples/loading_data/loading_raw_audio_data.ipynb index cbd88aa03..497e5a9c8 100644 --- a/examples/loading_data/loading_raw_audio_data.ipynb +++ b/examples/loading_data/loading_raw_audio_data.ipynb @@ -1,168 +1,168 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "5683982d", - "metadata": {}, - "source": [ - "# Loading Raw Audio Data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6b2485da", - "metadata": { - "nbsphinx": "hidden" - }, - "outputs": [], - "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", - "import logging\n", - "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" - ] - }, - { - "cell_type": "markdown", - "id": "16345774", - "metadata": {}, - "source": [ - "The audio file is saved from the microphone. It is useful to look at it to plot a spectrogram and confirm the sounds played during the task are indeed audible." - ] - }, - { - "cell_type": "markdown", - "id": "8d62c890", - "metadata": {}, - "source": [ - "## Relevant datasets\n", - "* _iblrig_micData.raw.flac\n" - ] - }, - { - "cell_type": "markdown", - "id": "bc23fdf7", - "metadata": {}, - "source": [ - "## Loading" - ] - }, - { - "cell_type": "markdown", - "id": "9103084d", - "metadata": {}, - "source": [ - "### Loading raw audio file" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2b807296", - "metadata": { - "ibl_execute": false - }, - "outputs": [], - "source": [ - "from one.api import ONE\n", - "import soundfile as sf\n", - "\n", - "one = ONE()\n", - "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", - "\n", - "# -- Get raw data\n", - "filename = one.load_dataset(eid, '_iblrig_micData.raw.flac', download_only=True)\n", - "with open(filename, 'rb') as f:\n", - " wav, fs = sf.read(f)" - ] - }, - { - "cell_type": "markdown", - "id": "203d23c1", - "metadata": {}, - "source": [ - "## Plot the spectrogram" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "811e3533", - "metadata": { - "ibl_execute": false - }, - "outputs": [], - "source": [ - "from ibllib.io.extractors.training_audio import welchogram\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "# -- Compute spectrogram over first 2 minutes\n", - "t_idx = 120 * fs\n", - "tscale, fscale, W, detect = welchogram(fs, wav[:t_idx])\n", - "\n", - "# -- Put data into single variable\n", - "TF = {}\n", - "\n", - "TF['power'] = W.astype(np.single)\n", - "TF['frequencies'] = fscale[None, :].astype(np.single)\n", - "TF['onset_times'] = detect\n", - "TF['times_mic'] = tscale[:, None].astype(np.single)\n", - "\n", - "# # -- Plot spectrogram\n", - "tlims = TF['times_mic'][[0, -1]].flatten()\n", - "flims = TF['frequencies'][0, [0, -1]].flatten()\n", - "fig = plt.figure(figsize=[16, 7])\n", - "ax = plt.axes()\n", - "im = ax.imshow(20 * np.log10(TF['power'].T), aspect='auto', cmap=plt.get_cmap('magma'),\n", - " extent=np.concatenate((tlims, flims)),\n", - " origin='lower')\n", - "ax.set_xlabel(r'Time (s)')\n", - "ax.set_ylabel(r'Frequency (Hz)')\n", - "plt.colorbar(im)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "bef6702e", - "metadata": {}, - "source": [ - "## More details\n", - "* [Description of audio datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.n61f0vdcplxp)" - ] - }, - { - "cell_type": "markdown", - "id": "4e9dd4b9", - "metadata": {}, - "source": [ - "## Useful modules\n", - "* [ibllib.io.extractors.training_audio](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.io.extractors.training_audio.html#module-ibllib.io.extractors.training_audio)" - ] - } - ], - "metadata": { - "celltoolbar": "Edit Metadata", - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5683982d", + "metadata": {}, + "source": [ + "# Loading Raw Audio Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b2485da", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Turn off logging, this is a hidden cell on docs page\n", + "import logging\n", + "logger = logging.getLogger('ibllib')\n", + "logger.setLevel(logging.CRITICAL)" + ] + }, + { + "cell_type": "markdown", + "id": "16345774", + "metadata": {}, + "source": [ + "The audio file is saved from the microphone. It is useful to look at it to plot a spectrogram and confirm the sounds played during the task are indeed audible." + ] + }, + { + "cell_type": "markdown", + "id": "8d62c890", + "metadata": {}, + "source": [ + "## Relevant datasets\n", + "* _iblrig_micData.raw.flac\n" + ] + }, + { + "cell_type": "markdown", + "id": "bc23fdf7", + "metadata": {}, + "source": [ + "## Loading" + ] + }, + { + "cell_type": "markdown", + "id": "9103084d", + "metadata": {}, + "source": [ + "### Loading raw audio file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b807296", + "metadata": { + "ibl_execute": false + }, + "outputs": [], + "source": [ + "from one.api import ONE\n", + "import soundfile as sf\n", + "\n", + "one = ONE()\n", + "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", + "\n", + "# -- Get raw data\n", + "filename = one.load_dataset(eid, '_iblrig_micData.raw.flac', download_only=True)\n", + "with open(filename, 'rb') as f:\n", + " wav, fs = sf.read(f)" + ] + }, + { + "cell_type": "markdown", + "id": "203d23c1", + "metadata": {}, + "source": [ + "## Plot the spectrogram" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "811e3533", + "metadata": { + "ibl_execute": false + }, + "outputs": [], + "source": [ + "from ibllib.io.extractors.training_audio import welchogram\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# -- Compute spectrogram over first 2 minutes\n", + "t_idx = 120 * fs\n", + "tscale, fscale, W, detect = welchogram(fs, wav[:t_idx])\n", + "\n", + "# -- Put data into single variable\n", + "TF = {}\n", + "\n", + "TF['power'] = W.astype(np.single)\n", + "TF['frequencies'] = fscale[None, :].astype(np.single)\n", + "TF['onset_times'] = detect\n", + "TF['times_mic'] = tscale[:, None].astype(np.single)\n", + "\n", + "# # -- Plot spectrogram\n", + "tlims = TF['times_mic'][[0, -1]].flatten()\n", + "flims = TF['frequencies'][0, [0, -1]].flatten()\n", + "fig = plt.figure(figsize=[16, 7])\n", + "ax = plt.axes()\n", + "im = ax.imshow(20 * np.log10(TF['power'].T), aspect='auto', cmap=plt.get_cmap('magma'),\n", + " extent=np.concatenate((tlims, flims)),\n", + " origin='lower')\n", + "ax.set_xlabel(r'Time (s)')\n", + "ax.set_ylabel(r'Frequency (Hz)')\n", + "plt.colorbar(im)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "bef6702e", + "metadata": {}, + "source": [ + "## More details\n", + "* [Description of audio datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.n61f0vdcplxp)" + ] + }, + { + "cell_type": "markdown", + "id": "4e9dd4b9", + "metadata": {}, + "source": [ + "## Useful modules\n", + "* [ibllib.io.extractors.training_audio](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.io.extractors.training_audio.html#module-ibllib.io.extractors.training_audio)" + ] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python [conda env:iblenv] *", + "language": "python", + "name": "conda-env-iblenv-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/loading_data/loading_raw_ephys_data.ipynb b/examples/loading_data/loading_raw_ephys_data.ipynb index 667a578ca..572c8de12 100644 --- a/examples/loading_data/loading_raw_ephys_data.ipynb +++ b/examples/loading_data/loading_raw_ephys_data.ipynb @@ -38,11 +38,16 @@ "This will gather all the relevant meta-data for a given probe and the histology reconstructed channel locations in the brain. \n", "\n", "## AP and LF band streaming examples\n", + "\n", + "### Get the raw data streamers and the meta-data\n", "We start by instantiating a spike sorting loader object and reading in the histology information by loading the channels table." ] }, { "cell_type": "code", + "execution_count": null, + "id": "db13c1bab069f492", + "metadata": {}, "outputs": [], "source": [ "from one.api import ONE\n", @@ -53,267 +58,158 @@ "\n", "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", "ssl = SpikeSortingLoader(pid=pid, one=one)\n", - "channels = ssl.load_channels()" - ], - "metadata": { - "collapsed": false - }, - "id": "db13c1bab069f492", - "execution_count": null + "# The channels information is contained in a dict table / dataframe\n", + "channels = ssl.load_channels()\n", + "\n", + "# Get AP and LFP spikeglx.Reader objects\n", + "sr_lf = ssl.raw_electrophysiology(band=\"lf\", stream=True)\n", + "sr_ap = ssl.raw_electrophysiology(band=\"ap\", stream=True)\n" + ] }, { "cell_type": "markdown", - "source": [ - "Here we stream one second of raw AP data around the timepoint of interest" - ], + "id": "541898a2492f2c14", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "id": "541898a2492f2c14" - }, - { - "cell_type": "code", - "outputs": [], "source": [ - "sr_ap = ssl.raw_electrophysiology(band='ap', stream=True)\n", - "# Important: remove sync channel from raw data, and transpose to get a [n_channels, n_samples] array\n", - "first, last = (int(t0 * sr_ap.fs), int((t0 + 1) * sr_ap.fs))\n", - "raw_ap = sr_ap[first:last, :-sr_ap.nsync].T" - ], - "metadata": { - "collapsed": false - }, - "id": "139ece4af85da17b", - "execution_count": null + "Here we stream one second of raw AP data around the timepoint of interest and 5 seconds of data for the raw LF data" + ] }, { "cell_type": "markdown", + "id": "2d17b0f8-e95f-4841-987a-1c3a5a221d1f", + "metadata": {}, "source": [ - "Here we do the same for 5 seconds of LF data" - ], - "metadata": { - "collapsed": false - }, - "id": "ab8a76eccb43a5b4" + "## Synchronisation\n", + "Each probe has its own internal clock and report to the main clock of the experiment. When loading the raw data, there is a sample to experiment clock operation necessary to align the raw data.\n", + "\n", + "### Streaming data around a task event" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "e0222f30-0b8c-4dca-8984-daf3ec854b4a", + "metadata": {}, "outputs": [], "source": [ - "sr_lf = ssl.raw_electrophysiology(band='lf', stream=True)\n", - "# Important: remove sync channel from raw data, and transpose to get a [n_channels, n_samples] array\n", - "first, last = (int(t0 * sr_lf.fs), int((t0 + 5) * sr_lf.fs))\n", + "stimOn_times = one.load_object(ssl.eid, 'trials', collection='alf')['stimOn_times']\n", + "event_no = 100\n", + "# timepoint in recording to stream, as per the experiment main clock \n", + "t_event = stimOn_times[event_no]\n", + "\n", + "# corresponding sample in the AP data\n", + "s_event = int(ssl.samples2times(stimOn_times[event_no], direction='reverse'))\n", + "print(f'raw AP band sample for event at time {t_event}: {s_event}')\n", + "\n", + "# get the AP data surrounding samples\n", + "window_secs_ap = [-0.05, 0.05] # we'll look at 100ms before the event and 200ms after the event for AP\n", + "first, last = (int(window_secs_ap[0] * sr_ap.fs) + s_event, int(window_secs_ap[1] * sr_ap.fs + s_event))\n", + "raw_ap = sr_ap[first:last, :-sr_ap.nsync].T\n", + "\n", + "# get the LF data surrounding samples\n", + "window_secs_ap = [-0.750, 0.750] # we'll look at 100ms before the event and 200ms after the event\n", + "sample_lf = s_event // 12 # NB: for neuropixel probes this is always 12\n", + "first, last = (int(window_secs_ap[0] * sr_lf.fs) + sample_lf, int(window_secs_ap[1] * sr_lf.fs + sample_lf))\n", "raw_lf = sr_lf[first:last, :-sr_lf.nsync].T" - ], - "metadata": { - "collapsed": false - }, - "id": "a477781f1d46136d", - "execution_count": null - }, - { - "cell_type": "markdown", - "source": [ - "Let's have a look at the raw data alongside channels information. Here we apply a filter to the raw data to remove the DC component for display." - ], - "metadata": { - "collapsed": false - }, - "id": "87728453c5d54ffb" - }, - { - "cell_type": "code", - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import scipy.signal\n", - "from brainbox.ephys_plots import plot_brain_regions\n", - "from ibllib.plots import Density\n", - "\n", - "sos_ap = scipy.signal.butter(3, 300 / sr_ap.fs /2, btype='highpass', output='sos') # 300 Hz high pass AP band\n", - "sos_lf = scipy.signal.butter(3, 2 / sr_lf.fs /2, btype='highpass', output='sos') # 2 Hz high pass LF band\n", - "filtered_ap = scipy.signal.sosfiltfilt(sos_ap, raw_ap)\n", - "filtered_lf = scipy.signal.sosfiltfilt(sos_lf, raw_lf)\n", - "\n", - "# displays the AP band over LFP band\n", - "fig, axs = plt.subplots(2, 2, gridspec_kw={'width_ratios': [.95, .05]}, figsize=(18, 12))\n", - "Density(- filtered_ap[:, 12000:15000], fs=sr_ap.fs, taxis=1, ax=axs[0, 0])\n", - "plot_brain_regions(channels[\"atlas_id\"], channel_depths=channels[\"axial_um\"], ax = axs[0, 1], display=True)\n", - "Density(- filtered_lf[:, 9000:15000], fs=sr_lf.fs, taxis=1, ax=axs[1, 0])\n", - "plot_brain_regions(channels[\"atlas_id\"], channel_depths=channels[\"axial_um\"], ax = axs[1, 1], display=True)\n" - ], - "metadata": { - "collapsed": false - }, - "id": "1bb95de26299907f", - "execution_count": null - }, - { - "cell_type": "markdown", - "source": [ - "### Downloading the raw data\n", - "\n", - "
\n", - "Warning.\n", - "\n", - "The raw ephys data is very large and downloading will take a long period of time and fill up your hard drive pretty fast.\n", - "\n", - "
\n", - "\n", - "When accessing the raw electrophysiology method of the spike sorting loader, turning the streaming mode off will download the full\n", - "file if it is not already present in the cache.\n", - "\n", - "We recommend setting the path of your `ONE` instance to make sure you control the destination path of the downloaded data.\n", - "\n", - "```python\n", - "PATH_CACHE = Path(\"/path_to_raw_data_drive/openalyx\")\n", - "one = ONE(base_url=\"https://openalyx.internationalbrainlab.org\", cache_dir=PATH_CACHE)\n", - "sr_ap = ssl.raw_electrophysiology(band='ap', stream=False) # sr_ap is a spikeglx.Reader object that uses memmap\n", - "```\n" - ], - "metadata": { - "collapsed": false - }, - "id": "d7dba84029780138" + ] }, { "cell_type": "markdown", - "id": "bb97cb8f", + "id": "70de65c7-c615-4568-87b0-46847d73daab", "metadata": {}, "source": [ - "## Low level loading and downloading functions\n", + "
\n", + "Note:\n", + " \n", + "**Why the transpose and the slicing in `sr_lf[first:last, :-sr_lf.nsync].T` ?**\n", "\n", - "### Relevant datasets\n", - "The raw data comprises 3 files:\n", - "* `\\_spikeglx_ephysData*.cbin` the compressed raw binary\n", - "* `\\_spikeglx_ephysData*.meta` the metadata file from spikeglx\n", - "* `\\_spikeglx_ephysData*.ch` the compression header containing chunks address in the file\n", + "- we transpose (`.T`) our internal representation of the `raw` data. On disk by experimental necessity, the data is sorted by time sample first, channel second; this is not desirable for pre-processing as time samples are not contiguous.This is why our internal representation for the raw data snippets (i.e. dimensions used when working with such data) is `[number of channels, number of samples]`, in Python c-ordering, the time samples are contiguous in memory.\n", "\n", - "The raw data is compressed with a lossless compression algorithm in chunks of 1 second each. This allows to retrieve parts of the data without having to uncompress the whole file. We recommend using the `spikeglx.Reader` module from [ibl-neuropixel repository](https://github.com/int-brain-lab/ibl-neuropixel)\n", + "- the raw data will contain the synching channels (i.e. the voltage information contained on the analog and digital DAQ channels, that mark events in the task notably). You need to remove them before wanting to use solely the raw ephys data (e.g. for plotting or exploring).\n", "\n", - "Full information about the compression and tool in [mtscomp repository](https://github.com/int-brain-lab/mtscomp)" + "
" ] }, { "cell_type": "markdown", - "id": "b51ffc0f", + "id": "61cce550-62ff-4ee7-b90f-8ae0314daa1f", "metadata": {}, "source": [ - "### Option 1: Stream snippets of raw ephys data\n", - "This is a useful option if you are interested to perform analysis on a chunk of data of smaller duration than the whole recording, as it will take less time to download. Data snippets can be loaded in chunks of 1-second, i.e. you can load at minimum 1 second of raw data, and any multiplier of such chunk length (for example 4 or 92 seconds)." + "### Display the data with channel information around a task event" ] }, { "cell_type": "code", "execution_count": null, - "id": "68605764", + "id": "e5e1a3fd-9d94-4603-807d-956a66eca540", "metadata": {}, "outputs": [], "source": [ - "from one.api import ONE\n", - "from brainbox.io.spikeglx import Streamer\n", - "\n", - "one = ONE()\n", - "\n", - "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", - "\n", - "t0 = 100 # timepoint in recording to stream\n", - "band = 'ap' # either 'ap' or 'lf'\n", - "\n", - "sr = Streamer(pid=pid, one=one, remove_cached=False, typ=band)\n", - "first, last = (int(t0 * sr.fs), int((t0 + 1) * sr.fs))\n", + "import matplotlib.pyplot as plt\n", + "import scipy.signal\n", + "from brainbox.ephys_plots import plot_brain_regions\n", + "from ibllib.plots import Density\n", + "sos_ap = scipy.signal.butter(3, 300 / sr_ap.fs /2, btype='highpass', output='sos') # 300 Hz high pass AP band\n", + "sos_lf = scipy.signal.butter(3, 2 / sr_lf.fs /2, btype='highpass', output='sos') # 2 Hz high pass LF band\n", + "filtered_ap = scipy.signal.sosfiltfilt(sos_ap, raw_ap)\n", + "filtered_lf = scipy.signal.sosfiltfilt(sos_lf, raw_lf)\n", "\n", - "# Important: remove sync channel from raw data, and transpose to get a [n_channels, n_samples] array\n", - "raw = sr[first:last, :-sr.nsync].T" + "# displays the AP band and LFP band around this stim_on event\n", + "fig, axs = plt.subplots(2, 2, gridspec_kw={'width_ratios': [.95, .05]}, figsize=(18, 12))\n", + "Density(- filtered_ap, fs=sr_ap.fs, taxis=1, ax=axs[0, 0])\n", + "plot_brain_regions(channels[\"atlas_id\"], channel_depths=channels[\"axial_um\"], ax = axs[0, 1], display=True)\n", + "Density(- filtered_lf, fs=sr_lf.fs, taxis=1, ax=axs[1, 0])\n", + "plot_brain_regions(channels[\"atlas_id\"], channel_depths=channels[\"axial_um\"], ax = axs[1, 1], display=True)" ] }, { "cell_type": "markdown", - "id": "d7a5103c", + "id": "d2decedd-0f58-41ea-823d-7664f475193b", "metadata": {}, "source": [ "
\n", "Note:\n", "\n", - "- the transpose (`.T`) for internal representation of the `raw` data. On disk, the data is sorted by time sample first, channel second; this is not desirable for pre-processing as time samples are not contiguous.This is why our internal representation for the raw data (i.e. dimensions used when working with such data) is `[number of channels, number of samples]`, in Python c-ordering, the time samples are contiguous in memory.\n", - "\n", - "- the raw data will contain the synching channels (i.e. the voltage information contained on the analog and digital DAQ channels, that mark events in the task notably). You need to remove them before wanting to use solely the raw ephys data (e.g. for plotting or exploring).\n", + "If you plan on computing time aligned averages on many events, it will be much more efficient to download the raw data files once and for all instead of using the streaming cache. This way you have full control over the disk space usage and the bulky data retention policy.\n", "\n", + "The following example shows hot to instantiate the same objects as above with a full downloaded file instead of streaming.\n", "
" ] }, { "cell_type": "markdown", - "id": "eb72b4bb", - "metadata": {}, - "source": [ - "### Option 2: Download all of raw ephys data" - ] - }, - { - "cell_type": "markdown", - "id": "3c5984dc", - "metadata": {}, + "id": "d7dba84029780138", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ + "### Downloading the raw data\n", + "\n", "
\n", "Warning.\n", "\n", - "The raw ephys data is very large and downloading will take a long period of time.\n", - "\n", - "\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "60857f5f", - "metadata": { - "ibl_execute": false - }, - "outputs": [], - "source": [ - "from one.api import ONE\n", - "import spikeglx\n", - "one = ONE()\n", + "The raw ephys data is very large and downloading will take a long period of time and fill up your hard drive pretty fast.\n", "\n", - "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", - "eid, probe = one.pid2eid(pid)\n", + "\n", "\n", - "band = 'ap' # either 'ap','lf'\n", + "When accessing the raw electrophysiology method of the spike sorting loader, turning the streaming mode off will download the full\n", + "file if it is not already present in the cache.\n", "\n", - "# Find the relevant datasets and download them\n", - "dsets = one.list_datasets(eid, collection=f'raw_ephys_data/{probe}', filename='*.lf.*')\n", - "data_files, _ = one.load_datasets(eid, dsets, download_only=True)\n", - "bin_file = next(df for df in data_files if df.suffix == '.cbin')\n", + "We recommend setting the path of your `ONE` instance to make sure you control the destination path of the downloaded data.\n", "\n", - "# Use spikeglx reader to read in the whole raw data\n", - "sr = spikeglx.Reader(bin_file)\n", - "print(sr.shape)" - ] - }, - { - "cell_type": "markdown", - "id": "0a8b24db", - "metadata": {}, - "source": [ - "## More details\n", - "* [Details of raw ap datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.ms0y69xbzova)\n", - "* [Details of raw lfp datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.nct1c3j9tedk)\n", - "* [Details of mtscomp compression algorithm](https://github.com/int-brain-lab/mtscomp#readme)\n", - "* [Spikesorting white paper](https://figshare.com/articles/online_resource/Spike_sorting_pipeline_for_the_International_Brain_Laboratory/19705522)" - ] - }, - { - "cell_type": "markdown", - "id": "edd9d729", - "metadata": {}, - "source": [ - "## Useful modules\n", - "* [ibllib.io.spikeglx](https://int-brain-lab.github.io/ibl-neuropixel/_autosummary/spikeglx.html)\n", - "* [ibllib.voltage.dsp](https://int-brain-lab.github.io/ibl-neuropixel/_autosummary/neurodsp.voltage.html)\n", - "* [brainbox.io.spikeglx.stream](https://int-brain-lab.github.io/iblenv/_autosummary/brainbox.io.spikeglx.html#brainbox.io.spikeglx.stream)\n", - "* [viewephys](https://github.com/oliche/viewephys) to visualise raw data snippets (Note: this package is not within `ibllib` but standalone)" + "```python\n", + "PATH_CACHE = Path(\"/path_to_raw_data_drive/openalyx\")\n", + "one = ONE(base_url=\"https://openalyx.internationalbrainlab.org\", cache_dir=PATH_CACHE)\n", + "sr_ap = ssl.raw_electrophysiology(band='ap', stream=False) # sr_ap is a spikeglx.Reader object that uses memmap\n", + "```\n" ] }, { @@ -343,7 +239,7 @@ "from neurodsp.voltage import destripe\n", "# Reminder : If not done before, remove first the sync channel from raw data\n", "# Apply destriping algorithm to data\n", - "destriped = destripe(raw, fs=sr.fs)" + "destriped = destripe(raw_ap, fs=sr_ap.fs)" ] }, { @@ -369,7 +265,7 @@ "source": [ "%gui qt\n", "from viewephys.gui import viewephys\n", - "v_raw = viewephys(raw, fs=sr.fs)\n", + "v_raw = viewephys(raw_ap, fs=sr.fs)\n", "v_des = viewephys(destriped, fs=sr.fs)\n", "# You will then be able to zoom in, adjust the gain etc - see README for details" ] @@ -406,24 +302,24 @@ "MAX_X = -MIN_X\n", "\n", "# Shorten and transpose the data for plotting\n", - "X = destriped[:, :int(DISPLAY_TIME * sr.fs)].T\n", - "Xs = X[SAMPLE_SKIP:].T # Remove artifact at begining\n", - "Tplot = Xs.shape[1]/sr.fs\n", + "X = destriped[:, :int(DISPLAY_TIME * sr_ap.fs)].T\n", + "Xs = X[SAMPLE_SKIP:].T # Remove apodization at begining\n", + "Tplot = Xs.shape[1] / sr_ap.fs\n", "\n", - "X_raw = raw[:, :int(DISPLAY_TIME * sr.fs)].T\n", - "Xs_raw = X_raw[SAMPLE_SKIP:].T # Remove artifact at begining\n", + "X_raw = raw_ap[:, :int(DISPLAY_TIME * sr_ap.fs)].T\n", + "Xs_raw = X_raw[SAMPLE_SKIP:].T # Remove apodization at begining\n", "\n", "# Plot\n", "fig, axs = plt.subplots(nrows=1, ncols=2)\n", "\n", "i_plt = 0\n", - "d0 = Density(-Xs_raw, fs=sr.fs, taxis=1, ax=axs[i_plt], vmin=MIN_X, vmax=MAX_X, cmap='Greys')\n", + "d0 = Density(-Xs_raw, fs=sr_ap.fs, taxis=1, ax=axs[i_plt], vmin=MIN_X, vmax=MAX_X, cmap='Greys')\n", "axs[i_plt].title.set_text('Raw ephys data')\n", "axs[i_plt].set_xlim((0, Tplot * 1e3))\n", "axs[i_plt].set_ylabel('Channels')\n", "\n", "i_plt = 1\n", - "d1 = Density(-Xs, fs=sr.fs, taxis=1, ax=axs[i_plt], vmin=MIN_X, vmax=MAX_X, cmap='Greys')\n", + "d1 = Density(-Xs, fs=sr_ap.fs, taxis=1, ax=axs[i_plt], vmin=MIN_X, vmax=MAX_X, cmap='Greys')\n", "axs[i_plt].title.set_text('Destriped ephys data')\n", "axs[i_plt].set_xlim((0, Tplot * 1e3))\n", "axs[i_plt].set_ylabel('')" @@ -431,98 +327,128 @@ }, { "cell_type": "markdown", - "id": "604bd9dc", + "id": "bb97cb8f", + "metadata": {}, + "source": [ + "## Low level loading and downloading functions\n", + "\n", + "### Relevant datasets\n", + "The raw data comprises 3 files:\n", + "* `\\_spikeglx_ephysData*.cbin` the compressed raw binary\n", + "* `\\_spikeglx_ephysData*.meta` the metadata file from spikeglx\n", + "* `\\_spikeglx_ephysData*.ch` the compression header containing chunks address in the file\n", + "\n", + "The raw data is compressed with a lossless compression algorithm in chunks of 1 second each. This allows to retrieve parts of the data without having to uncompress the whole file. We recommend using the `spikeglx.Reader` module from [ibl-neuropixel repository](https://github.com/int-brain-lab/ibl-neuropixel)\n", + "\n", + "Full information about the compression and tool in [mtscomp repository](https://github.com/int-brain-lab/mtscomp)" + ] + }, + { + "cell_type": "markdown", + "id": "b51ffc0f", "metadata": {}, "source": [ - "### Example 2: Stream LFP data around task event\n", - "The example downloads a 1-second snippet of raw LF data ; all that needs setting as parameters are the `time0` (the time of the even of interest), the `band` (LFP), and the duration `time_win` (1 second)." + "### Option 1: Stream snippets of raw ephys data\n", + "This is a useful option if you are interested to perform analysis on a chunk of data of smaller duration than the whole recording, as it will take less time to download. Data snippets can be loaded in chunks of 1-second, i.e. you can load at minimum 1 second of raw data, and any multiplier of such chunk length (for example 4 or 92 seconds)." ] }, { "cell_type": "code", "execution_count": null, - "id": "591a1a8a", + "id": "68605764", "metadata": {}, "outputs": [], "source": [ - "eid, probe = one.pid2eid(pid)\n", - "stimOn_times = one.load_object(eid, 'trials', collection='alf')['stimOn_times']\n", - "event_no = 100\n", + "from one.api import ONE\n", + "from brainbox.io.spikeglx import Streamer\n", "\n", - "# Get the 1s of LFP data around time point of interest\n", - "time0 = stimOn_times[event_no] # timepoint in recording to stream\n", - "time_win = 1 # number of seconds to stream\n", - "band = 'lf' # either 'ap' or 'lf'\n", + "one = ONE()\n", + "\n", + "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", + "\n", + "t0 = 100 # timepoint in recording to stream\n", + "band = 'ap' # either 'ap' or 'lf'\n", "\n", "sr = Streamer(pid=pid, one=one, remove_cached=False, typ=band)\n", - "s0 = time0 * sr.fs\n", - "tsel = slice(int(s0), int(s0) + int(time_win * sr.fs))\n", - "# remove sync channel from raw data\n", - "raw = sr[tsel, :-sr.nsync].T\n", - "# apply destriping algorithm to data\n", - "destriped = destripe(raw, fs=sr.fs)" + "first, last = (int(t0 * sr.fs), int((t0 + 1) * sr.fs))\n", + "\n", + "# Important: remove sync channel from raw data, and transpose to get a [n_channels, n_samples] array\n", + "raw = sr[first:last, :-sr.nsync].T" ] }, { "cell_type": "markdown", + "id": "eb72b4bb", + "metadata": {}, "source": [ - "## Get the probe geometry" - ], - "metadata": { - "collapsed": false - }, - "id": "11ea47563a9ede81" + "### Option 2: Download all of raw ephys data" + ] }, { "cell_type": "markdown", + "id": "3c5984dc", + "metadata": {}, "source": [ - "### Using the `eid` and `probe` information" - ], - "metadata": { - "collapsed": false - }, - "id": "c0ef9d3ce4f29b0" + "
\n", + "Warning.\n", + "\n", + "The raw ephys data is very large and downloading will take a long period of time.\n", + "\n", + "\n", + "
" + ] }, { "cell_type": "code", "execution_count": null, - "outputs": [], - "source": [ - "from brainbox.io.one import load_channel_locations\n", - "channels = load_channel_locations(eid, probe)\n", - "print(channels[probe].keys())\n", - "# Use the axial and lateral coordinates ; Print example first 4 channels\n", - "print(channels[probe][\"axial_um\"][0:4])\n", - "print(channels[probe][\"lateral_um\"][0:4])" - ], + "id": "60857f5f", "metadata": { - "collapsed": false + "ibl_execute": false }, - "id": "2a77bb9df8795525" + "outputs": [], + "source": [ + "from one.api import ONE\n", + "import spikeglx\n", + "one = ONE()\n", + "\n", + "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", + "eid, probe = one.pid2eid(pid)\n", + "\n", + "band = 'ap' # either 'ap','lf'\n", + "\n", + "# Find the relevant datasets and download them\n", + "dsets = one.list_datasets(eid, collection=f'raw_ephys_data/{probe}', filename='*.lf.*')\n", + "data_files, _ = one.load_datasets(eid, dsets, download_only=True)\n", + "bin_file = next(df for df in data_files if df.suffix == '.cbin')\n", + "\n", + "# Use spikeglx reader to read in the whole raw data\n", + "sr = spikeglx.Reader(bin_file)\n", + "print(sr.shape)" + ] }, { "cell_type": "markdown", + "id": "0a8b24db", + "metadata": {}, "source": [ - "### Using the reader and the `.cbin` file" - ], - "metadata": { - "collapsed": false - }, - "id": "92be0a74a5d6bfa3" + "## More details\n", + "* [Details of raw ap datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.ms0y69xbzova)\n", + "* [Details of raw lfp datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.nct1c3j9tedk)\n", + "* [Details of mtscomp compression algorithm](https://github.com/int-brain-lab/mtscomp#readme)\n", + "* [Spikesorting white paper](https://figshare.com/articles/online_resource/Spike_sorting_pipeline_for_the_International_Brain_Laboratory/19705522)" + ] }, { - "cell_type": "code", - "execution_count": null, - "outputs": [], + "cell_type": "markdown", + "id": "edd9d729", + "metadata": {}, "source": [ - "# You would have loaded the bin file as per the loading example above\n", - "# sr = spikeglx.Reader(bin_file)\n", - "sr.geometry" - ], - "metadata": { - "collapsed": false - }, - "id": "c27ce7e75852cd95" + "## Useful modules\n", + "* [ibllib.io.spikeglx](https://int-brain-lab.github.io/ibl-neuropixel/_autosummary/spikeglx.html)\n", + "* [ibllib.voltage.dsp](https://int-brain-lab.github.io/ibl-neuropixel/_autosummary/neurodsp.voltage.html)\n", + "* [brainbox.io.spikeglx.stream](https://int-brain-lab.github.io/iblenv/_autosummary/brainbox.io.spikeglx.html#brainbox.io.spikeglx.stream)\n", + "* [viewephys](https://github.com/oliche/viewephys) to visualise raw data snippets (Note: this package is not within `ibllib` but standalone)" + ] }, { "cell_type": "markdown", @@ -541,9 +467,9 @@ "metadata": { "celltoolbar": "Edit Metadata", "kernelspec": { - "name": "python3", + "display_name": "Python 3 (ipykernel)", "language": "python", - "display_name": "Python 3 (ipykernel)" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -555,7 +481,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/loading_data/loading_raw_mesoscope_data.ipynb b/examples/loading_data/loading_raw_mesoscope_data.ipynb index 816b2c8b2..6d89e0694 100644 --- a/examples/loading_data/loading_raw_mesoscope_data.ipynb +++ b/examples/loading_data/loading_raw_mesoscope_data.ipynb @@ -21,6 +21,9 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "```python\n", "from one.api import ONE\n", @@ -39,27 +42,29 @@ "import suite2p.gui\n", "suite2p.gui.run(statfile=dst_dir / 'stat.npy')\n", "```" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, - "outputs": [], - "source": [ - "# Downloading the raw images" - ], "metadata": { - "collapsed": false, "pycharm": { "name": "#%%\n" } - } + }, + "outputs": [], + "source": [ + "# Downloading the raw images" + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## Suite2P output vs ALF datasets\n", "Below is a table compareing the raw output of Suite2P with the ALF datasets available through ONE.\n", @@ -72,24 +77,18 @@ "| **ops.npy** (badframes) [nFrames] | **mpci.badFrames.npy** [nFrames] |\n", "| **iscell.npy** [nROIs, 2] | **mpciROIs.included.npy** [nROIs] |\n", "| **stat.npy** (med) [nROIs, 3] | **mpciROIs.stackPos.npy** [nROIs, 3] |" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## More details\n", "* [Description of mesoscope datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.nvzaz0fozs8h)\n", "* [Loading multi-photon imaging data](./loading_multi_photon_imaging_data.ipynb)\n" - ], - "metadata": { - "collapsed": false - } + ] } ], "metadata": { @@ -101,14 +100,14 @@ "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/loading_data/loading_raw_video_data.ipynb b/examples/loading_data/loading_raw_video_data.ipynb index 3d3204c28..8b8c9eb9e 100644 --- a/examples/loading_data/loading_raw_video_data.ipynb +++ b/examples/loading_data/loading_raw_video_data.ipynb @@ -105,6 +105,7 @@ "metadata": {}, "outputs": [], "source": [ + "%%capture\n", "# Load the first 10 video frames\n", "frames = vidio.get_video_frames_preload(url, range(10)) " ] @@ -136,6 +137,7 @@ "metadata": {}, "outputs": [], "source": [ + "%%capture\n", "video_body = one.load_dataset(eid, f'*{label}Camera.raw*', collection='raw_video_data')" ] }, @@ -203,6 +205,7 @@ "metadata": {}, "outputs": [], "source": [ + "%%capture\n", "# The preload function will by default pre-allocate the memory before loading the frames,\n", "# and will return the frames as a numpy array of the shape (l, h, w, 3), where l = the number of\n", "# frame indices given. The indices must be an iterable of positive integers. Because the videos\n", @@ -216,7 +219,6 @@ "# \n", "# A warning is printed if fetching a frame fails. The affected frames will be returned as zeros\n", "# or None if `as_list` is True.\n", - "\n", "frames = vidio.get_video_frames_preload(url, range(10), mask=np.s_[:, :, 0])" ] }, @@ -238,7 +240,7 @@ "outputs": [], "source": [ "from ibllib.qc.camera import CameraQC\n", - "qc = CameraQC(one.eid2path(eid), 'body', download_data=True)\n", + "qc = CameraQC(one.eid2path(eid), 'body', download_data=True, one=one)\n", "outcome, extended = qc.run()\n", "print(f'video QC = {outcome}')\n", "extended" @@ -277,9 +279,9 @@ "metadata": { "celltoolbar": "Edit Metadata", "kernelspec": { - "display_name": "Python [conda env:iblenv] *", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-iblenv-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -291,7 +293,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/loading_data/loading_spike_waveforms.ipynb b/examples/loading_data/loading_spike_waveforms.ipynb index 407fd1eeb..44b659980 100644 --- a/examples/loading_data/loading_spike_waveforms.ipynb +++ b/examples/loading_data/loading_spike_waveforms.ipynb @@ -1,183 +1,184 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "f73e02ee", - "metadata": {}, - "source": [ - "# Loading Spike Waveforms" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ea70eb4a", - "metadata": { - "nbsphinx": "hidden" - }, - "outputs": [], - "source": [ - "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", - "import logging\n", - "import os\n", - "\n", - "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)\n", - "\n", - "os.environ[\"TQDM_DISABLE\"] = \"1\"" - ] - }, - { - "cell_type": "markdown", - "id": "64cec921", - "metadata": {}, - "source": [ - "Sample of spike waveforms extracted during spike sorting" - ] - }, - { - "cell_type": "markdown", - "id": "dca47f09", - "metadata": {}, - "source": [ - "## Relevant Alf objects\n", - "* \\_phy_spikes_subset" - ] - }, - { - "cell_type": "markdown", - "id": "eb34d848", - "metadata": {}, - "source": [ - "## Loading" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c5d32232", - "metadata": {}, - "outputs": [], - "source": [ - "from one.api import ONE\n", - "from brainbox.io.one import SpikeSortingLoader\n", - "from iblatlas.atlas import AllenAtlas\n", - "\n", - "one = ONE()\n", - "ba = AllenAtlas()\n", - "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd' \n", - "\n", - "# Load in the spikesorting\n", - "sl = SpikeSortingLoader(pid=pid, one=one, atlas=ba)\n", - "spikes, clusters, channels = sl.load_spike_sorting()\n", - "clusters = sl.merge_clusters(spikes, clusters, channels)\n", - "\n", - "# Load the spike waveforms\n", - "spike_wfs = one.load_object(sl.eid, '_phy_spikes_subset', collection=sl.collection)" - ] - }, - { - "cell_type": "markdown", - "id": "327a23e7", - "metadata": {}, - "source": [ - "## More details\n", - "* [Description of datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.vcop4lz26gs9)" - ] - }, - { - "cell_type": "markdown", - "id": "257fb8b8", - "metadata": {}, - "source": [ - "## Useful modules\n", - "* COMING SOON" - ] - }, - { - "cell_type": "markdown", - "id": "157bf219", - "metadata": {}, - "source": [ - "## Exploring sample waveforms" - ] - }, - { - "cell_type": "markdown", - "id": "a617f8fb", - "metadata": {}, - "source": [ - "### Example 1: Finding the cluster ID for each sample waveform" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1ac805b6", - "metadata": {}, - "outputs": [], - "source": [ - "# Find the cluster id for each sample waveform\n", - "wf_clusterIDs = spikes['clusters'][spike_wfs['spikes']]" - ] - }, - { - "cell_type": "markdown", - "id": "baf9eb11", - "metadata": {}, - "source": [ - "### Example 2: Compute average waveform for cluster" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3d8a729c", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# define cluster of interest\n", - "clustID = 2\n", - "\n", - "# Find waveforms for this cluster\n", - "wf_idx = np.where(wf_clusterIDs == clustID)[0]\n", - "wfs = spike_wfs['waveforms'][wf_idx, :, :]\n", - "\n", - "# Compute average waveform on channel with max signal (chn_index 0)\n", - "wf_avg_chn_max = np.mean(wfs[:, :, 0], axis=0)" - ] - }, - { - "cell_type": "markdown", - "id": "a20b24ea", - "metadata": {}, - "source": [ - "## Other relevant examples\n", - "* COMING SOON" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f73e02ee", + "metadata": {}, + "source": [ + "# Loading Spike Waveforms" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea70eb4a", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", + "import logging\n", + "import os\n", + "\n", + "logger = logging.getLogger('ibllib')\n", + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" + ] + }, + { + "cell_type": "markdown", + "id": "64cec921", + "metadata": {}, + "source": [ + "Sample of spike waveforms extracted during spike sorting" + ] + }, + { + "cell_type": "markdown", + "id": "dca47f09", + "metadata": {}, + "source": [ + "## Relevant Alf objects\n", + "* \\_phy_spikes_subset" + ] + }, + { + "cell_type": "markdown", + "id": "eb34d848", + "metadata": {}, + "source": [ + "## Loading" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5d32232", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "from one.api import ONE\n", + "from brainbox.io.one import SpikeSortingLoader\n", + "from iblatlas.atlas import AllenAtlas\n", + "\n", + "one = ONE()\n", + "ba = AllenAtlas()\n", + "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd' \n", + "\n", + "# Load in the spikesorting\n", + "sl = SpikeSortingLoader(pid=pid, one=one, atlas=ba)\n", + "spikes, clusters, channels = sl.load_spike_sorting()\n", + "clusters = sl.merge_clusters(spikes, clusters, channels)\n", + "\n", + "# Load the spike waveforms\n", + "spike_wfs = one.load_object(sl.eid, '_phy_spikes_subset', collection=sl.collection)" + ] + }, + { + "cell_type": "markdown", + "id": "327a23e7", + "metadata": {}, + "source": [ + "## More details\n", + "* [Description of datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.vcop4lz26gs9)" + ] + }, + { + "cell_type": "markdown", + "id": "257fb8b8", + "metadata": {}, + "source": [ + "## Useful modules\n", + "* COMING SOON" + ] + }, + { + "cell_type": "markdown", + "id": "157bf219", + "metadata": {}, + "source": [ + "## Exploring sample waveforms" + ] + }, + { + "cell_type": "markdown", + "id": "a617f8fb", + "metadata": {}, + "source": [ + "### Example 1: Finding the cluster ID for each sample waveform" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ac805b6", + "metadata": {}, + "outputs": [], + "source": [ + "# Find the cluster id for each sample waveform\n", + "wf_clusterIDs = spikes['clusters'][spike_wfs['spikes']]" + ] + }, + { + "cell_type": "markdown", + "id": "baf9eb11", + "metadata": {}, + "source": [ + "### Example 2: Compute average waveform for cluster" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d8a729c", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "# define cluster of interest\n", + "clustID = 2\n", + "\n", + "# Find waveforms for this cluster\n", + "wf_idx = np.where(wf_clusterIDs == clustID)[0]\n", + "wfs = spike_wfs['waveforms'][wf_idx, :, :]\n", + "\n", + "# Compute average waveform on channel with max signal (chn_index 0)\n", + "wf_avg_chn_max = np.mean(wfs[:, :, 0], axis=0)" + ] + }, + { + "cell_type": "markdown", + "id": "a20b24ea", + "metadata": {}, + "source": [ + "## Other relevant examples\n", + "* COMING SOON" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/loading_data/loading_spikesorting_data.ipynb b/examples/loading_data/loading_spikesorting_data.ipynb index f36c4619f..f711414a1 100644 --- a/examples/loading_data/loading_spikesorting_data.ipynb +++ b/examples/loading_data/loading_spikesorting_data.ipynb @@ -68,18 +68,16 @@ }, { "cell_type": "code", + "execution_count": null, + "id": "a9e9bc2a0ebac970", + "metadata": {}, "outputs": [], "source": [ "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd' \n", "sl = SpikeSortingLoader(pid=pid, one=one)\n", "spikes, clusters, channels = sl.load_spike_sorting()\n", "clusters = sl.merge_clusters(spikes, clusters, channels)" - ], - "metadata": { - "collapsed": false - }, - "id": "a9e9bc2a0ebac970", - "execution_count": null + ] }, { "cell_type": "markdown", @@ -288,9 +286,9 @@ ], "metadata": { "kernelspec": { - "name": "python3", + "display_name": "Python 3 (ipykernel)", "language": "python", - "display_name": "Python 3 (ipykernel)" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -302,7 +300,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/loading_data/loading_trials_data.ipynb b/examples/loading_data/loading_trials_data.ipynb index 4b85dd0e2..8292c9654 100644 --- a/examples/loading_data/loading_trials_data.ipynb +++ b/examples/loading_data/loading_trials_data.ipynb @@ -48,45 +48,50 @@ }, { "cell_type": "markdown", - "source": [ - "## Loading a single session's trials\n" - ], + "id": "a5d358e035a91310", "metadata": { "collapsed": false }, - "id": "a5d358e035a91310" + "source": [ + "## Loading a single session's trials\n" + ] }, { "cell_type": "code", "execution_count": null, + "id": "e5688df9114dd1cc", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "from one.api import ONE\n", "one = ONE()\n", "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", "trials = one.load_object(eid, 'trials', collection='alf')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "id": "e5688df9114dd1cc" + ] }, { "cell_type": "markdown", - "source": [ - "For combining trials data with various recording modalities for a given session, the `SessionLoader` class is more convenient:" - ], + "id": "d6c98a81f5426445", "metadata": { "collapsed": false }, - "id": "d6c98a81f5426445" + "source": [ + "For combining trials data with various recording modalities for a given session, the `SessionLoader` class is more convenient:" + ] }, { "cell_type": "code", "execution_count": null, + "id": "a323e20fb2fe5db3", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "from brainbox.io.one import SessionLoader\n", @@ -100,14 +105,7 @@ "probabilityLeft = sl.trials['probabilityLeft']\n", "# Find all of them using:\n", "sl.trials.keys()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "id": "a323e20fb2fe5db3" + ] }, { "cell_type": "markdown", @@ -302,19 +300,25 @@ }, { "cell_type": "markdown", + "id": "55ad2e5d71ac301", + "metadata": { + "collapsed": false + }, "source": [ "### Example 5: Computing the inter-trial interval (ITI)\n", "The ITI is the period of open-loop grey screen commencing at stimulus off and lasting until the\n", "quiescent period at the start of the following trial." - ], - "metadata": { - "collapsed": false - }, - "id": "55ad2e5d71ac301" + ] }, { "cell_type": "code", "execution_count": null, + "id": "cf17cf97a866b206", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "from brainbox.io.one import load_iti\n", @@ -322,14 +326,7 @@ "trials = one.load_object(eid, 'trials')\n", "trials['iti'] = load_iti(trials)\n", "print(trials.to_df().iloc[:5, -5:])" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "id": "cf17cf97a866b206" + ] }, { "cell_type": "markdown", @@ -362,7 +359,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.6" } }, "nbformat": 4, diff --git a/examples/loading_data/loading_video_data.ipynb b/examples/loading_data/loading_video_data.ipynb index 1c0b12a16..175d64a83 100644 --- a/examples/loading_data/loading_video_data.ipynb +++ b/examples/loading_data/loading_video_data.ipynb @@ -1,218 +1,218 @@ -{ - "cells": [ - { - "cell_type": "code", - "outputs": [], - "source": [ - "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", - "import logging\n", - "import os\n", - "\n", - "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)\n", - "\n", - "os.environ[\"TQDM_DISABLE\"] = \"1\"" - ], - "metadata": { - "nbsphinx": "hidden", - "collapsed": false - }, - "id": "96289b087c51ad66" - }, - { - "cell_type": "markdown", - "id": "b730e49f", - "metadata": {}, - "source": [ - "# Loading Video Data" - ] - }, - { - "cell_type": "markdown", - "id": "95f87066", - "metadata": {}, - "source": [ - "Extracted DLC features and motion energy from raw video data" - ] - }, - { - "cell_type": "markdown", - "id": "7629947f", - "metadata": {}, - "source": [ - "## Relevant Alf objects\n", - "* bodyCamera\n", - "* leftCamera\n", - "* rightCamera\n", - "* licks\n", - "* ROIMotionEnergy" - ] - }, - { - "cell_type": "markdown", - "id": "50db510d", - "metadata": {}, - "source": [ - "## Loading" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a6d2a83e", - "metadata": {}, - "outputs": [], - "source": [ - "from one.api import ONE\n", - "\n", - "one = ONE()\n", - "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", - "\n", - "label = 'right' # 'left', 'right' or 'body'\n", - "\n", - "video_features = one.load_object(eid, f'{label}Camera', collection='alf')" - ] - }, - { - "cell_type": "markdown", - "id": "48aa068e", - "metadata": {}, - "source": [ - "## More details\n", - "* [Description of camera datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.yjwa7dpoipz)\n", - "* [Description of DLC pipeline in IBL](https://github.com/int-brain-lab/iblvideo#readme)\n", - "* [Description of DLC QC metrics](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.qc.dlc.html)\n", - "* [IBL video white paper](https://docs.google.com/document/u/1/d/e/2PACX-1vS2777bCbDmMre-wyeDr4t0jC-0YsV_uLtYkfS3h9zTwgC7qeMk-GUqxPqcY7ylH17I1Vo1nIuuj26L/pub)" - ] - }, - { - "cell_type": "markdown", - "id": "d8b4a8e8", - "metadata": {}, - "source": [ - "## Useful modules\n", - "* [brainbox.behavior.dlc](https://int-brain-lab.github.io/iblenv/_autosummary/brainbox.behavior.dlc.html)\n", - "* [ibllib.qc.dlc](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.qc.dlc.html)" - ] - }, - { - "cell_type": "markdown", - "id": "a7a88103", - "metadata": {}, - "source": [ - "## Exploring video data" - ] - }, - { - "cell_type": "markdown", - "id": "8c09b09e", - "metadata": {}, - "source": [ - "### Example 1: Filtering dlc features by likelihood threshold" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "de72d811", - "metadata": {}, - "outputs": [], - "source": [ - "# Set values with likelihood below chosen threshold to NaN\n", - "from brainbox.behavior.dlc import likelihood_threshold\n", - "\n", - "dlc = likelihood_threshold(video_features['dlc'], threshold=0.9)" - ] - }, - { - "cell_type": "markdown", - "id": "bd5a739e", - "metadata": {}, - "source": [ - "### Example 2: Compute speed of dlc feature" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "523a1745", - "metadata": {}, - "outputs": [], - "source": [ - "from brainbox.behavior.dlc import get_speed\n", - "\n", - "# Compute the speed of the right paw\n", - "feature = 'paw_r'\n", - "dlc_times = video_features['times']\n", - "paw_r_speed = get_speed(dlc, dlc_times, label, feature=feature)" - ] - }, - { - "cell_type": "markdown", - "id": "fc8a5f0f", - "metadata": {}, - "source": [ - "### Example 3: Plot raster of lick times around feedback event" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e37c1536", - "metadata": { - "nbsphinx": "hidden" - }, - "outputs": [], - "source": [ - "# Turn off logging, this is a hidden cell on docs page\n", - "import logging\n", - "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "22a0772e", - "metadata": {}, - "outputs": [], - "source": [ - "licks = one.load_object(eid, 'licks', collection='alf')\n", - "trials = one.load_object(eid, 'trials', collection='alf')\n", - "\n", - "from brainbox.behavior.dlc import plot_lick_raster\n", - "fig = plot_lick_raster(licks['times'], trials.to_df())" - ] - }, - { - "cell_type": "markdown", - "id": "8690f9f8", - "metadata": {}, - "source": [ - "## Other relevant examples\n", - "* COMING SOON" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "96289b087c51ad66", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", + "import logging\n", + "import os\n", + "\n", + "logger = logging.getLogger('ibllib')\n", + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" + ] + }, + { + "cell_type": "markdown", + "id": "b730e49f", + "metadata": {}, + "source": [ + "# Loading Video Data" + ] + }, + { + "cell_type": "markdown", + "id": "95f87066", + "metadata": {}, + "source": [ + "Extracted DLC features and motion energy from raw video data" + ] + }, + { + "cell_type": "markdown", + "id": "7629947f", + "metadata": {}, + "source": [ + "## Relevant Alf objects\n", + "* bodyCamera\n", + "* leftCamera\n", + "* rightCamera\n", + "* licks\n", + "* ROIMotionEnergy" + ] + }, + { + "cell_type": "markdown", + "id": "50db510d", + "metadata": {}, + "source": [ + "## Loading" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6d2a83e", + "metadata": {}, + "outputs": [], + "source": [ + "from one.api import ONE\n", + "\n", + "one = ONE()\n", + "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", + "\n", + "label = 'right' # 'left', 'right' or 'body'\n", + "\n", + "video_features = one.load_object(eid, f'{label}Camera', collection='alf')" + ] + }, + { + "cell_type": "markdown", + "id": "48aa068e", + "metadata": {}, + "source": [ + "## More details\n", + "* [Description of camera datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.yjwa7dpoipz)\n", + "* [Description of DLC pipeline in IBL](https://github.com/int-brain-lab/iblvideo#readme)\n", + "* [Description of DLC QC metrics](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.qc.dlc.html)\n", + "* [IBL video white paper](https://docs.google.com/document/u/1/d/e/2PACX-1vS2777bCbDmMre-wyeDr4t0jC-0YsV_uLtYkfS3h9zTwgC7qeMk-GUqxPqcY7ylH17I1Vo1nIuuj26L/pub)" + ] + }, + { + "cell_type": "markdown", + "id": "d8b4a8e8", + "metadata": {}, + "source": [ + "## Useful modules\n", + "* [brainbox.behavior.dlc](https://int-brain-lab.github.io/iblenv/_autosummary/brainbox.behavior.dlc.html)\n", + "* [ibllib.qc.dlc](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.qc.dlc.html)" + ] + }, + { + "cell_type": "markdown", + "id": "a7a88103", + "metadata": {}, + "source": [ + "## Exploring video data" + ] + }, + { + "cell_type": "markdown", + "id": "8c09b09e", + "metadata": {}, + "source": [ + "### Example 1: Filtering dlc features by likelihood threshold" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de72d811", + "metadata": {}, + "outputs": [], + "source": [ + "# Set values with likelihood below chosen threshold to NaN\n", + "from brainbox.behavior.dlc import likelihood_threshold\n", + "\n", + "dlc = likelihood_threshold(video_features['dlc'], threshold=0.9)" + ] + }, + { + "cell_type": "markdown", + "id": "bd5a739e", + "metadata": {}, + "source": [ + "### Example 2: Compute speed of dlc feature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "523a1745", + "metadata": {}, + "outputs": [], + "source": [ + "from brainbox.behavior.dlc import get_speed\n", + "\n", + "# Compute the speed of the right paw\n", + "feature = 'paw_r'\n", + "dlc_times = video_features['times']\n", + "paw_r_speed = get_speed(dlc, dlc_times, label, feature=feature)" + ] + }, + { + "cell_type": "markdown", + "id": "fc8a5f0f", + "metadata": {}, + "source": [ + "### Example 3: Plot raster of lick times around feedback event" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e37c1536", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Turn off logging, this is a hidden cell on docs page\n", + "import logging\n", + "logger = logging.getLogger('ibllib')\n", + "logger.setLevel(logging.CRITICAL)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22a0772e", + "metadata": {}, + "outputs": [], + "source": [ + "licks = one.load_object(eid, 'licks', collection='alf')\n", + "trials = one.load_object(eid, 'trials', collection='alf')\n", + "\n", + "from brainbox.behavior.dlc import plot_lick_raster\n", + "fig = plot_lick_raster(licks['times'], trials.to_df())" + ] + }, + { + "cell_type": "markdown", + "id": "8690f9f8", + "metadata": {}, + "source": [ + "## Other relevant examples\n", + "* COMING SOON" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:iblenv] *", + "language": "python", + "name": "conda-env-iblenv-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/loading_data/loading_wheel_data.ipynb b/examples/loading_data/loading_wheel_data.ipynb index 1a591d07d..d6307f0ef 100644 --- a/examples/loading_data/loading_wheel_data.ipynb +++ b/examples/loading_data/loading_wheel_data.ipynb @@ -1,178 +1,177 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "72bb4faa", - "metadata": {}, - "source": [ - "# Loading Wheel Data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "41f0fec2", - "metadata": { - "nbsphinx": "hidden" - }, - "outputs": [], - "source": [ - "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", - "import logging\n", - "import os\n", - "\n", - "logger = logging.getLogger('ibllib')\n", - "logger.setLevel(logging.CRITICAL)\n", - "\n", - "os.environ[\"TQDM_DISABLE\"] = \"1\"" - ] - }, - { - "cell_type": "markdown", - "id": "5996744e", - "metadata": {}, - "source": [ - "Wheel data recorded during task" - ] - }, - { - "cell_type": "markdown", - "id": "4bafefa8", - "metadata": {}, - "source": [ - "## Relevant Alf objects\n", - "* wheel\n", - "* wheelMoves" - ] - }, - { - "cell_type": "markdown", - "id": "5c04be5e", - "metadata": {}, - "source": [ - "## Loading" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e8100a7", - "metadata": {}, - "outputs": [], - "source": [ - "from one.api import ONE\n", - "one = ONE()\n", - "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", - "\n", - "wheel = one.load_object(eid, 'wheel', collection='alf')\n", - "wheelMoves = one.load_object(eid, 'wheelMoves', collection='alf')" - ] - }, - { - "cell_type": "markdown", - "id": "08106755", - "metadata": {}, - "source": [ - "## More details\n", - "* [Description of wheel datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.hnjqyfnroyya)\n", - "* [Working with wheel data](./docs_wheel_moves.html)" - ] - }, - { - "cell_type": "markdown", - "id": "357a860b", - "metadata": {}, - "source": [ - "## Useful modules and functions\n", - "* [brainbox.behavior.wheel](../_autosummary/brainbox.behavior.wheel.html)\n", - "* [brainbox.io.one.load_wheel_reaction_times](../_autosummary/brainbox.io.one.html#brainbox.io.one.load_wheel_reaction_times)\n", - "* [ibllib.qc.task_metrics](../_autosummary/ibllib.qc.task_metrics.html)" - ] - }, - { - "cell_type": "markdown", - "id": "86a02336", - "metadata": {}, - "source": [ - "## Exploring wheel data" - ] - }, - { - "cell_type": "markdown", - "source": [ - "### Example 3: Find linearly interpolated wheel position" - ], - "metadata": { - "collapsed": false - }, - "id": "5a947733bf0b16f0" - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "from brainbox.behavior.wheel import interpolate_position\n", - "Fs = 1000\n", - "wh_pos_lin, wh_ts_lin = interpolate_position(wheel['timestamps'], wheel['position'], freq=Fs)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "id": "7bf6131b343ffe21" - }, - { - "cell_type": "markdown", - "id": "5a4b3e83", - "metadata": {}, - "source": [ - "### Example 2: Extract wheel velocity" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7a487944", - "metadata": {}, - "outputs": [], - "source": [ - "from brainbox.behavior.wheel import velocity_filtered\n", - "\n", - "wh_velocity, wh_acc = velocity_filtered(wh_pos_lin, Fs)\n" - ] - }, - { - "cell_type": "markdown", - "id": "9765d47c", - "metadata": {}, - "source": [ - "## Other relevant examples\n", - "* [Working with wheel data](./docs_wheel_moves.html)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "72bb4faa", + "metadata": {}, + "source": [ + "# Loading Wheel Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41f0fec2", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Turn off logging and disable tqdm this is a hidden cell on docs page\n", + "import logging\n", + "import os\n", + "\n", + "logger = logging.getLogger('ibllib')\n", + "logger.setLevel(logging.CRITICAL)\n", + "\n", + "os.environ[\"TQDM_DISABLE\"] = \"1\"" + ] + }, + { + "cell_type": "markdown", + "id": "5996744e", + "metadata": {}, + "source": [ + "Wheel data recorded during task" + ] + }, + { + "cell_type": "markdown", + "id": "4bafefa8", + "metadata": {}, + "source": [ + "## Relevant Alf objects\n", + "* wheel\n", + "* wheelMoves" + ] + }, + { + "cell_type": "markdown", + "id": "5c04be5e", + "metadata": {}, + "source": [ + "## Loading" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e8100a7", + "metadata": {}, + "outputs": [], + "source": [ + "from one.api import ONE\n", + "one = ONE()\n", + "eid = '4ecb5d24-f5cc-402c-be28-9d0f7cb14b3a'\n", + "\n", + "wheel = one.load_object(eid, 'wheel', collection='alf')\n", + "wheelMoves = one.load_object(eid, 'wheelMoves', collection='alf')" + ] + }, + { + "cell_type": "markdown", + "id": "08106755", + "metadata": {}, + "source": [ + "## More details\n", + "* [Description of wheel datasets](https://docs.google.com/document/d/1OqIqqakPakHXRAwceYLwFY9gOrm8_P62XIfCTnHwstg/edit#heading=h.hnjqyfnroyya)\n", + "* [Working with wheel data](./docs_wheel_moves.html)" + ] + }, + { + "cell_type": "markdown", + "id": "357a860b", + "metadata": {}, + "source": [ + "## Useful modules and functions\n", + "* [brainbox.behavior.wheel](../_autosummary/brainbox.behavior.wheel.html)\n", + "* [brainbox.io.one.load_wheel_reaction_times](../_autosummary/brainbox.io.one.html#brainbox.io.one.load_wheel_reaction_times)\n", + "* [ibllib.qc.task_metrics](../_autosummary/ibllib.qc.task_metrics.html)" + ] + }, + { + "cell_type": "markdown", + "id": "86a02336", + "metadata": {}, + "source": [ + "## Exploring wheel data" + ] + }, + { + "cell_type": "markdown", + "id": "5a947733bf0b16f0", + "metadata": { + "collapsed": false + }, + "source": [ + "### Example 3: Find linearly interpolated wheel position" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bf6131b343ffe21", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "from brainbox.behavior.wheel import interpolate_position\n", + "Fs = 1000\n", + "wh_pos_lin, wh_ts_lin = interpolate_position(wheel['timestamps'], wheel['position'], freq=Fs)" + ] + }, + { + "cell_type": "markdown", + "id": "5a4b3e83", + "metadata": {}, + "source": [ + "### Example 2: Extract wheel velocity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a487944", + "metadata": {}, + "outputs": [], + "source": [ + "from brainbox.behavior.wheel import velocity_filtered\n", + "\n", + "wh_velocity, wh_acc = velocity_filtered(wh_pos_lin, Fs)\n" + ] + }, + { + "cell_type": "markdown", + "id": "9765d47c", + "metadata": {}, + "source": [ + "## Other relevant examples\n", + "* [Working with wheel data](./docs_wheel_moves.html)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:iblenv] *", + "language": "python", + "name": "conda-env-iblenv-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ibllib/atlas/genes.py b/ibllib/atlas/genes.py index fd4784cba..f9c8c4b5d 100644 --- a/ibllib/atlas/genes.py +++ b/ibllib/atlas/genes.py @@ -1,6 +1,6 @@ """Gene expression maps.""" -from iblatlas.genomics import genes +from iblatlas.genomics import agea from ibllib.atlas import deprecated_decorator @@ -15,4 +15,4 @@ def allen_gene_expression(filename='gene-expression.pqt', folder_cache=None): (nexperiments, ml, dv, ap). The spacing between slices is 200 um """ - return genes.allen_gene_expression(filename=filename, folder_cache=folder_cache) + return agea.load(filename=filename, folder_cache=folder_cache) From 5dc997198d9941a6a1193b66b96326713f9f6c2d Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Tue, 6 Feb 2024 15:36:00 +0200 Subject: [PATCH 18/25] Resolves #725 --- brainbox/behavior/dlc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/brainbox/behavior/dlc.py b/brainbox/behavior/dlc.py index c3f48d32e..10e4e6fe9 100644 --- a/brainbox/behavior/dlc.py +++ b/brainbox/behavior/dlc.py @@ -1,6 +1,4 @@ -""" -Set of functions to deal with dlc data -""" +"""Set of functions to deal with dlc data.""" import logging import pandas as pd import warnings @@ -48,7 +46,9 @@ def insert_idx(array, values): def likelihood_threshold(dlc, threshold=0.9): """ - Set dlc points with likelihood less than threshold to nan + Set dlc points with likelihood less than threshold to nan. + + FIXME Add unit test. :param dlc: dlc pqt object :param threshold: likelihood threshold :return: @@ -56,14 +56,14 @@ def likelihood_threshold(dlc, threshold=0.9): features = np.unique(['_'.join(x.split('_')[:-1]) for x in dlc.keys()]) for feat in features: nan_fill = dlc[f'{feat}_likelihood'] < threshold - dlc[f'{feat}_x'][nan_fill] = np.nan - dlc[f'{feat}_y'][nan_fill] = np.nan + dlc.loc[nan_fill, (f'{feat}_x', f'{feat}_y')] = np.nan return dlc def get_speed(dlc, dlc_t, camera, feature='paw_r'): """ + FIXME Document and add unit test! :param dlc: dlc pqt table :param dlc_t: dlc time points From b3923c5edadb963b78621a2534c790198f0a1f4c Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Tue, 6 Feb 2024 15:41:03 +0200 Subject: [PATCH 19/25] Use test db --- ibllib/pipes/behavior_tasks.py | 5 +++-- ibllib/pipes/local_server.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ibllib/pipes/behavior_tasks.py b/ibllib/pipes/behavior_tasks.py index b9eb8479a..001f3bfed 100644 --- a/ibllib/pipes/behavior_tasks.py +++ b/ibllib/pipes/behavior_tasks.py @@ -533,11 +533,12 @@ def signature(self): def _run(self, upload=True): """ - Extracts training status for subject + Extracts training status for subject. """ lab = get_lab(self.session_path, self.one.alyx) - if lab == 'cortexlab': + if lab == 'cortexlab' and 'cortexlab' in self.one.alyx.base_url: + _logger.info('Switching from cortexlab Alyx to IBL Alyx for training status queries.') one = ONE(base_url='https://alyx.internationalbrainlab.org') else: one = self.one diff --git a/ibllib/pipes/local_server.py b/ibllib/pipes/local_server.py index 6f1aab35b..42edc3b34 100644 --- a/ibllib/pipes/local_server.py +++ b/ibllib/pipes/local_server.py @@ -7,17 +7,18 @@ import time from datetime import datetime from pathlib import Path -import pkg_resources import re import subprocess import sys import traceback import importlib +import importlib.metadata from one.api import ONE from one.webclient import AlyxClient from one.remote.globus import get_lab_from_endpoint_id, get_local_endpoint_id +from ibllib import __version__ as ibllib_version from ibllib.io.extractors.base import get_pipeline, get_task_protocol, get_session_extractor_type from ibllib.pipes import tasks, training_preprocessing, ephys_preprocessing from ibllib.time import date2isostr @@ -75,8 +76,8 @@ def report_health(one): Get a few indicators and label the json field of the corresponding lab with them. """ status = {'python_version': sys.version, - 'ibllib_version': pkg_resources.get_distribution("ibllib").version, - 'phylib_version': pkg_resources.get_distribution("phylib").version, + 'ibllib_version': ibllib_version, + 'phylib_version': importlib.metadata.version('phylib'), 'local_time': date2isostr(datetime.now())} status.update(_get_volume_usage('/mnt/s0/Data', 'raid')) status.update(_get_volume_usage('/', 'system')) From 036b74e628922920e3a89d54ebe96b75687cccd2 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Mon, 12 Feb 2024 18:19:39 +0200 Subject: [PATCH 20/25] Task QC entry point --- ibllib/qc/task_qc_viewer/task_qc.py | 9 ++++++--- setup.py | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ibllib/qc/task_qc_viewer/task_qc.py b/ibllib/qc/task_qc_viewer/task_qc.py index 32ec8722f..f8a2b33de 100644 --- a/ibllib/qc/task_qc_viewer/task_qc.py +++ b/ibllib/qc/task_qc_viewer/task_qc.py @@ -14,13 +14,12 @@ import ibllib.plots as plots from ibllib.misc import qt from ibllib.qc.task_metrics import TaskQC +from ibllib.qc.task_qc_viewer import ViewEphysQC from ibllib.pipes.dynamic_pipeline import get_trials_tasks from ibllib.pipes.base_tasks import BehaviourTask from ibllib.pipes.behavior_tasks import HabituationTrialsBpod, ChoiceWorldTrialsBpod from ibllib.pipes.training_preprocessing import TrainingTrials -from . import ViewEphysQC - EVENT_MAP = {'goCue_times': ['#2ca02c', 'solid'], # green 'goCueTrigger_times': ['#2ca02c', 'dotted'], # green 'errorCue_times': ['#d62728', 'solid'], # red @@ -296,7 +295,7 @@ def show_session_task_qc(qc_or_session=None, bpod_only=False, local=False, one=N return qc -if __name__ == '__main__': +def qc_gui_cli(): """Run TaskQC viewer with wheel data. For information on the QC checks see the QC Flags & failures document: @@ -316,3 +315,7 @@ def show_session_task_qc(qc_or_session=None, bpod_only=False, local=False, one=N args = parser.parse_args() # returns data from the options specified (echo) show_session_task_qc(qc_or_session=args.session, bpod_only=args.bpod, local=args.local) + + +if __name__ == '__main__': + qc_gui_cli() diff --git a/setup.py b/setup.py index bf0ec0a3c..0d83836ae 100644 --- a/setup.py +++ b/setup.py @@ -54,5 +54,10 @@ def get_version(rel_path): include_package_data=True, # external packages as dependencies install_requires=require, + entry_points={ + 'console_scripts': [ + 'task_qc = ibllib.qc.task_qc_viewer.task_qc:qc_gui_cli', + ], + }, scripts=[], ) From b88b6955e00815da4afc3de26f7ec5de00c1f49b Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Mon, 12 Feb 2024 18:49:20 +0200 Subject: [PATCH 21/25] Download settings for legacy pipeline --- ibllib/pipes/dynamic_pipeline.py | 2 ++ ibllib/qc/task_qc_viewer/README.md | 10 +++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ibllib/pipes/dynamic_pipeline.py b/ibllib/pipes/dynamic_pipeline.py index ec4228256..9d6695cc8 100644 --- a/ibllib/pipes/dynamic_pipeline.py +++ b/ibllib/pipes/dynamic_pipeline.py @@ -467,6 +467,8 @@ def get_trials_tasks(session_path, one=None): tasks.append(t) else: # Otherwise default to old way of doing things + if one and one.to_eid(session_path): + one.load_dataset(session_path, '_iblrig_taskSettings.raw', collection='raw_behavior_data', download_only=True) pipeline = get_pipeline(session_path) if pipeline == 'training': from ibllib.pipes.training_preprocessing import TrainingTrials diff --git a/ibllib/qc/task_qc_viewer/README.md b/ibllib/qc/task_qc_viewer/README.md index 7c118ff5d..004a3d63d 100644 --- a/ibllib/qc/task_qc_viewer/README.md +++ b/ibllib/qc/task_qc_viewer/README.md @@ -4,18 +4,14 @@ alongside an interactive table. The UUID is the session id. ## Usage: command line -Launch the Viewer by typing `python task_qc.py session-uuid` , example: +Launch the Viewer by typing `task_qc session-uuid` , example: ```sh -python task_qc.py c9fec76e-7a20-4da4-93ad-04510a89473b -# or with ipython -ipython task_qc.py -- c9fec76e-7a20-4da4-93ad-04510a89473b +task_qc c9fec76e-7a20-4da4-93ad-04510a89473b ``` Or just using a local path (on a local server for example): ```sh -python task_qc.py /mnt/s0/Subjects/KS022/2019-12-10/001 --local -# or with ipython -ipython task_qc.py -- /mnt/s0/Subjects/KS022/2019-12-10/001 --local +task_qc /mnt/s0/Subjects/KS022/2019-12-10/001 --local ``` ## Usage: from ipython prompt From c9a04e65b64aef2adfedfd2486eb4d11d7f48b09 Mon Sep 17 00:00:00 2001 From: Gaelle Date: Thu, 15 Feb 2024 15:55:27 +0100 Subject: [PATCH 22/25] docs bw release --- .../data_release/data_release_brainwidemap.ipynb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/data_release/data_release_brainwidemap.ipynb b/examples/data_release/data_release_brainwidemap.ipynb index 47d90b225..61f5525d4 100644 --- a/examples/data_release/data_release_brainwidemap.ipynb +++ b/examples/data_release/data_release_brainwidemap.ipynb @@ -37,8 +37,17 @@ "\n", "* The tag associated to this release is `Brainwidemap`\n", "\n", - "## Receive updates on the data\n", - "To receive a notification that we released new datasets, please fill up [this form](https://forms.gle/9ex2vL1JwV4QXnf98)\n" + "\n", + "## Updates on the data\n", + "Note: The section [Overview of the Data](#overview-of-the-data) contains the latest numbers released.\n", + "\n", + "### Receive updates on the data\n", + "To receive a notification that we released new datasets, please fill up [this form](https://forms.gle/9ex2vL1JwV4QXnf98)\n", + "\n", + "### 15 February 2024\n", + "We have added data from an additional 105 recording sessions, which encompass 152 probe insertions, obtained in 24 subjects performing the IBL task. As output of spike-sorting, there are 81229 new units; of which 12319 are considered to be of good quality.\n", + "\n", + "We have also replaced and removed some video data. Application of additional quality control processes revealed that the video timestamps for some of the previously released data were incorrect. We corrected the video timestamps where possible (285 videos: 137 left, 148 right) and removed video data for which the correction was not possible (139 videos: 135 body, 3 left, 1 right). We also added 31 videos (16 left, 15 right). **We strongly recommend that all data analyses using video data be rerun with the data currently in the database (please be sure to clear your cache directory first).**" ] } ], From 3fa897a42f29a072dbd45a0e51c7794767718b75 Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Fri, 16 Feb 2024 14:07:51 +0200 Subject: [PATCH 23/25] backup_session function to tempdir --- ibllib/pipes/misc.py | 61 +++++++++++++++++++++++--------------- ibllib/tests/test_pipes.py | 48 ++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/ibllib/pipes/misc.py b/ibllib/pipes/misc.py index e5108b027..a8c62fa03 100644 --- a/ibllib/pipes/misc.py +++ b/ibllib/pipes/misc.py @@ -17,6 +17,7 @@ import uuid import socket import traceback +import tempfile import spikeglx from iblutil.io import hashfile, params @@ -165,36 +166,48 @@ def rename_session(session_path: str, new_subject=None, new_date=None, new_numbe return new_session_path -def backup_session(session_path): - """Used to move the contents of a session to a backup folder, likely before the folder is +def backup_session(folder_path, root=None, extra=''): + """ + Used to move the contents of a session to a backup folder, likely before the folder is removed. - :param session_path: A session path to be backed up - :return: True if directory was backed up or exits if something went wrong - :rtype: Bool + Parameters + ---------- + folder_path : str, pathlib.Path + The folder path to remove. + root : str, pathlib.Path + Copy folder tree relative to this. If None, copies from the session_path root. + extra : str, pathlib.Path + Extra folder parts to append to destination root path. + + Returns + ------- + pathlib.Path + The location of the backup data, if succeeded to copy. """ - bk_session_path = Path() - if Path(session_path).exists(): + if not root: + if session_path := get_session_path(folder_path): + root = session_path.parents[2] + else: + root = folder_path.parent + folder_path = Path(folder_path) + bk_path = Path(tempfile.gettempdir(), 'backup_sessions', extra, folder_path.relative_to(root)) + if folder_path.exists(): + if not folder_path.is_dir(): + log.error(f'The given folder path is not a directory: {folder_path}') + return try: - bk_session_path = Path(*session_path.parts[:-4]).joinpath( - "Subjects_backup_renamed_sessions", Path(*session_path.parts[-3:])) - Path(bk_session_path.parent).mkdir(parents=True) - print(f"Created path: {bk_session_path.parent}") - # shutil.copytree(session_path, bk_session_path, dirs_exist_ok=True) - shutil.copytree(session_path, bk_session_path) # python 3.7 compatibility - print(f"Copied contents from {session_path} to {bk_session_path}") - return True + log.debug(f'Created path: {bk_path.parent}') + bk_path = shutil.copytree(folder_path, bk_path) + log.info(f'Copied contents from {folder_path} to {bk_path}') + return bk_path except FileExistsError: - log.error(f"A backup session for the given path already exists: {bk_session_path}, " - f"manual intervention is necessary.") - raise - except shutil.Error: - log.error(f'Some kind of copy error occurred when moving files from {session_path} to ' - f'{bk_session_path}') - log.error(shutil.Error) + log.error(f'A backup session for the given path already exists: {bk_path}, ' + f'manual intervention is necessary.') + except shutil.Error as ex: + log.error('Failed to copy files from %s to %s: %s', folder_path, bk_path, ex) else: - log.error(f"The given session path does not exist: {session_path}") - return False + log.error('The given session path does not exist: %s', folder_path) def copy_with_check(src, dst, **kwargs): diff --git a/ibllib/tests/test_pipes.py b/ibllib/tests/test_pipes.py index f6e1c2c30..f46577d9b 100644 --- a/ibllib/tests/test_pipes.py +++ b/ibllib/tests/test_pipes.py @@ -448,6 +448,7 @@ def setUp(self): p.touch() # Create video data too fu.create_fake_raw_video_data_folder(self.session_path) + self.bk_root = Path(tempfile.gettempdir(), 'backup_sessions') # location of backup data def test_rdiff_install(self): if os.name == "nt": # remove executable if on windows @@ -547,19 +548,48 @@ def test_rsync_paths(self): def test_backup_session(self): # Test when backup path does NOT already exist - self.assertTrue(misc.backup_session(self.session_path)) + dst = misc.backup_session(self.session_path) + self.assertIsNotNone(dst) + expected = self.bk_root.joinpath(*self.session_path.parts[-3:]) + self.assertEqual(expected, dst) + self.assertEqual(len(list(self.session_path.rglob('*'))), len(list(dst.rglob('*')))) # Test when backup path does exist - bk_session_path = Path(*self.session_path.parts[:-4]).joinpath( - "Subjects_backup_renamed_sessions", Path(*self.session_path.parts[-3:])) - Path(bk_session_path.parent).mkdir(parents=True, exist_ok=True) - with self.assertRaises(FileExistsError): - misc.backup_session(self.session_path) - print(">>> Error messages regarding a 'backup session already exists' or a 'given session " - "path does not exist' is expected in this test. <<< ") + with self.assertLogs(misc.__name__, level='ERROR'): + self.assertIsNone(misc.backup_session(self.session_path)) # Test when a bad session path is given - self.assertFalse(misc.backup_session("a session path that does NOT exist")) + with self.assertLogs(misc.__name__, level='ERROR'): + self.assertIsNone(misc.backup_session(self.session_path.with_name('notexist'))) + + # Test unexpected copy error + shutil.rmtree(dst) + with mock.patch(misc.__name__ + '.shutil.copytree', side_effect=shutil.Error('foo msg')), \ + self.assertLogs(misc.__name__, level='ERROR') as log: + self.assertIsNone(misc.backup_session(self.session_path)) + self.assertIn('foo msg', log.records[-1].getMessage()) + + # Test invalid folder (not dir) + assert not dst.exists() + with self.assertLogs(misc.__name__, level='ERROR'): + file = next(self.session_path.rglob('*.mp4')) + self.assertIsNone(misc.backup_session(file)) + + # Test root kwarg + dst = misc.backup_session(self.session_path, root=self.session_path.parents[4]) + self.assertIsNotNone(dst) + expected = self.bk_root.joinpath(*self.session_path.parts[-5:]) + self.assertEqual(expected, dst) + + # Test extra kwarg + dst = misc.backup_session(self.session_path, extra='fake_remote') + self.assertIsNotNone(dst) + expected = self.bk_root.joinpath('fake_remote', *self.session_path.parts[-3:]) + self.assertEqual(expected, dst) + + def tearDown(self): + for folder in filter(lambda x: x.name.startswith('fake'), self.bk_root.iterdir()): + shutil.rmtree(folder, ignore_errors=True) class TestScanFixPassiveFiles(unittest.TestCase): From 3e0c32159a7e887bb5bc388514c9289441297d6d Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Fri, 16 Feb 2024 14:30:26 +0200 Subject: [PATCH 24/25] Release notes --- ibllib/__init__.py | 2 +- release_notes.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ibllib/__init__.py b/ibllib/__init__.py index aea0d234d..e736602e6 100644 --- a/ibllib/__init__.py +++ b/ibllib/__init__.py @@ -2,7 +2,7 @@ import logging import warnings -__version__ = '2.29.0' +__version__ = '2.30.0' warnings.filterwarnings('always', category=DeprecationWarning, module='ibllib') # if this becomes a full-blown library we should let the logging configuration to the discretion of the dev diff --git a/release_notes.md b/release_notes.md index 33bcc69f0..f3e2e321e 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,3 +1,12 @@ +## Release Notes 2.30 + +### features +- Task QC viewer +- Raw ephys data loading documentation + +### other +- Pandas 3.0 support + ## Release Notes 2.29 ### features From e40bb33ef14399cd04da420065c4e804ffd5554d Mon Sep 17 00:00:00 2001 From: Miles Wells Date: Fri, 16 Feb 2024 16:08:31 +0200 Subject: [PATCH 25/25] typo --- ibllib/tests/fixtures/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ibllib/tests/fixtures/utils.py b/ibllib/tests/fixtures/utils.py index 08b78197e..0e2471829 100644 --- a/ibllib/tests/fixtures/utils.py +++ b/ibllib/tests/fixtures/utils.py @@ -88,7 +88,7 @@ def populate_raw_spikeglx(session_path, Touch file tree to emulate files saved by SpikeGLX :param session_path: The raw ephys data path to place files :param model: Probe model file structure ('3A' or '3B') - :param legacy: If true, the emulate older SpikeGLX version where all files are saved + :param legacy: If true, emulate older SpikeGLX version where all files are saved into a single folder :param user_label: User may input any name into SpikeGLX and filenames will include this :param n_probes: Number of probe datafiles to touch