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 diff --git a/brainbox/behavior/dlc.py b/brainbox/behavior/dlc.py index c3f48d32e..c7c42be92 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,13 @@ 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 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": "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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", "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..61f5525d4 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", @@ -33,12 +37,18 @@ "\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" - ], - "metadata": { - "collapsed": false - } + "\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).**" + ] } ], "metadata": { @@ -58,7 +68,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 00689c3da..3e4b8961a 100644 --- a/examples/exploring_data/data_download.ipynb +++ b/examples/exploring_data/data_download.ipynb @@ -1,9 +1,26 @@ { "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", + "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", "metadata": { - "collapsed": false + "collapsed": false, + "nbsphinx": "hidden" }, "source": [ "# Download the public datasets\n", @@ -21,10 +38,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", @@ -149,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", @@ -212,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", @@ -292,6 +303,11 @@ { "cell_type": "code", "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "# Load in all trials datasets\n", @@ -299,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", @@ -350,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", @@ -360,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", @@ -411,16 +420,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/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 80e2e4505..e5758864c 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\"" ] }, { @@ -217,9 +221,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/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 fbe95d3fb..572c8de12 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\"" ] }, { @@ -28,77 +32,103 @@ "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", + "\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": "markdown", - "id": "0bbad5e1", + "cell_type": "code", + "execution_count": null, + "id": "db13c1bab069f492", "metadata": {}, + "outputs": [], "source": [ - "## 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", + "from one.api import ONE\n", + "from brainbox.io.one import SpikeSortingLoader\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", + "one = ONE(base_url='https://openalyx.internationalbrainlab.org')\n", + "t0 = 100 # timepoint in recording to stream\n", "\n", - "Full information about the compression and tool in [mtscomp repository](https://github.com/int-brain-lab/mtscomp)" + "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", + "ssl = SpikeSortingLoader(pid=pid, one=one)\n", + "# 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", - "id": "bb97cb8f", - "metadata": {}, + "id": "541898a2492f2c14", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ - "## Loading" + "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": "b51ffc0f", + "id": "2d17b0f8-e95f-4841-987a-1c3a5a221d1f", "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)." + "## 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": "68605764", + "id": "e0222f30-0b8c-4dca-8984-daf3ec854b4a", "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", + "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", - "time0 = 100 # timepoint in recording to stream\n", - "time_win = 1 # number of seconds to stream\n", - "band = 'ap' # either 'ap' or 'lf'\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", - "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", + "# 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", - "# Important: remove sync channel from raw data, and transpose\n", - "raw = sr[tsel, :-sr.nsync].T" + "# 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" ] }, { "cell_type": "markdown", - "id": "d7a5103c", + "id": "70de65c7-c615-4568-87b0-46847d73daab", "metadata": {}, "source": [ "
\n", "Note:\n", + " \n", + "**Why the transpose and the slicing in `sr_lf[first:last, :-sr_lf.nsync].T` ?**\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", + "- 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 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", @@ -107,76 +137,79 @@ }, { "cell_type": "markdown", - "id": "eb72b4bb", - "metadata": {}, - "source": [ - "### Option 2: Download all of raw ephys data" - ] - }, - { - "cell_type": "markdown", - "id": "3c5984dc", + "id": "61cce550-62ff-4ee7-b90f-8ae0314daa1f", "metadata": {}, "source": [ - "
\n", - "Warning.\n", - "\n", - "The raw ephys data is very large and downloading will take a long period of time.\n", - "\n", - "\n", - "
" + "### Display the data with channel information around a task event" ] }, { "cell_type": "code", "execution_count": null, - "id": "60857f5f", - "metadata": { - "ibl_execute": false - }, + "id": "e5e1a3fd-9d94-4603-807d-956a66eca540", + "metadata": {}, "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", + "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", - "# Use spikeglx reader to read in the whole raw data\n", - "sr = spikeglx.Reader(bin_file)\n", - "print(sr.shape)" + "# 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": "0a8b24db", + "id": "d2decedd-0f58-41ea-823d-7664f475193b", "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)" + "
\n", + "Note:\n", + "\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": "edd9d729", - "metadata": {}, + "id": "d7dba84029780138", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "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)" + "### 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" ] }, { @@ -206,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)" ] }, { @@ -232,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" ] @@ -269,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('')" @@ -294,93 +327,128 @@ }, { "cell_type": "markdown", - "id": "604bd9dc", + "id": "bb97cb8f", "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)." + "## 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": [ + "### 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 - } + "### Option 2: Download all of raw ephys data" + ] }, { "cell_type": "markdown", + "id": "3c5984dc", + "metadata": {}, "source": [ - "### Using the `eid` and `probe` information" - ], - "metadata": { - "collapsed": false - } + "
\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 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])" - ], - "metadata": { - "collapsed": false - } + "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 - } + "## 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 - } + "## 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", @@ -399,9 +467,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": { @@ -413,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 b49b51e56..8b8c9eb9e 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\"" ] }, { @@ -101,6 +105,7 @@ "metadata": {}, "outputs": [], "source": [ + "%%capture\n", "# Load the first 10 video frames\n", "frames = vidio.get_video_frames_preload(url, range(10)) " ] @@ -132,6 +137,7 @@ "metadata": {}, "outputs": [], "source": [ + "%%capture\n", "video_body = one.load_dataset(eid, f'*{label}Camera.raw*', collection='raw_video_data')" ] }, @@ -199,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", @@ -212,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])" ] }, @@ -234,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" @@ -273,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": { @@ -287,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 7009b7855..44b659980 100644 --- a/examples/loading_data/loading_spike_waveforms.ipynb +++ b/examples/loading_data/loading_spike_waveforms.ipynb @@ -1,179 +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, 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": "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 5f22cc3d2..f711414a1 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,13 +63,18 @@ "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", + "one = ONE(base_url='https://openalyx.internationalbrainlab.org')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9e9bc2a0ebac970", + "metadata": {}, + "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)" ] @@ -86,7 +95,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)" ] @@ -277,9 +286,9 @@ ], "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 +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 398d32734..8292c9654 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\"" ] }, { @@ -44,42 +48,50 @@ }, { "cell_type": "markdown", - "source": [ - "## Loading a single session's trials\n" - ], + "id": "a5d358e035a91310", "metadata": { "collapsed": false - } + }, + "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" - } - } + ] }, { "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 - } + }, + "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", @@ -93,13 +105,7 @@ "probabilityLeft = sl.trials['probabilityLeft']\n", "# Find all of them using:\n", "sl.trials.keys()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] }, { "cell_type": "markdown", @@ -294,18 +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 - } + ] }, { "cell_type": "code", "execution_count": null, + "id": "cf17cf97a866b206", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "from brainbox.io.one import load_iti\n", @@ -313,13 +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" - } - } + ] }, { "cell_type": "markdown", @@ -352,9 +359,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/loading_data/loading_video_data.ipynb b/examples/loading_data/loading_video_data.ipynb index 02cd3ee7b..175d64a83 100644 --- a/examples/loading_data/loading_video_data.ipynb +++ b/examples/loading_data/loading_video_data.ipynb @@ -1,199 +1,218 @@ -{ - "cells": [ - { - "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 053f72d2a..d6307f0ef 100644 --- a/examples/loading_data/loading_wheel_data.ipynb +++ b/examples/loading_data/loading_wheel_data.ipynb @@ -1,172 +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, 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": "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 - } - }, - { - "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" - } - } - }, - { - "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 -} \ No newline at end of file +{ + "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/__init__.py b/ibllib/__init__.py index e0ae113ee..e736602e6 100644 --- a/ibllib/__init__.py +++ b/ibllib/__init__.py @@ -2,8 +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/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) 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/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/io/raw_data_loaders.py b/ibllib/io/raw_data_loaders.py index c1edd215b..ca9a83cca 100644 --- a/ibllib/io/raw_data_loaders.py +++ b/ibllib/io/raw_data_loaders.py @@ -1,7 +1,3 @@ -#!/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. 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 fc848af85..5134c4082 100644 --- a/ibllib/pipes/base_tasks.py +++ b/ibllib/pipes/base_tasks.py @@ -75,6 +75,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) @@ -90,9 +93,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 +160,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) @@ -125,6 +210,70 @@ 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:`extractor` 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. + """ + 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 and self.extractor): + raise ValueError('No trials data and/or extractor found') + return trials_data + class VideoTask(DynamicTask): diff --git a/ibllib/pipes/behavior_tasks.py b/ibllib/pipes/behavior_tasks.py index 1daf04813..001f3bfed 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 = self._assert_trials_data(trials_data) # 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 @@ -277,6 +274,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): @@ -299,39 +297,52 @@ 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, 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. + """ + 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, 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( @@ -400,9 +411,9 @@ def _behaviour_criterion(self, update=True, truncate_to_pass=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) @@ -412,20 +423,18 @@ 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, QC=None): + 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, 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 @@ -457,13 +466,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 @@ -488,10 +497,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) @@ -524,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/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/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/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')) diff --git a/ibllib/pipes/misc.py b/ibllib/pipes/misc.py index 37a761f03..a8c62fa03 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 @@ -16,6 +17,7 @@ import uuid import socket import traceback +import tempfile import spikeglx from iblutil.io import hashfile, params @@ -164,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): @@ -365,6 +379,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 +488,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..5a6de498f 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) @@ -385,7 +381,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/pipes/training_preprocessing.py b/ibllib/pipes/training_preprocessing.py index ad2172809..ffced634d 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.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=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 and trials_data: + # 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_metrics.py b/ibllib/qc/task_metrics.py index efe30f73c..cd67ef0ac 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): """ @@ -174,7 +177,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 +186,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 +429,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/qc/task_qc_viewer/README.md b/ibllib/qc/task_qc_viewer/README.md new file mode 100644 index 000000000..004a3d63d --- /dev/null +++ b/ibllib/qc/task_qc_viewer/README.md @@ -0,0 +1,43 @@ +# 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 `task_qc session-uuid` , example: +```sh +task_qc c9fec76e-7a20-4da4-93ad-04510a89473b +``` + +Or just using a local path (on a local server for example): +```sh +task_qc /mnt/s0/Subjects/KS022/2019-12-10/001 --local +``` + +## Usage: from ipython prompt +```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) +``` + +## 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#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 new file mode 100644 index 000000000..48155b270 --- /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 + +from ibllib.misc import 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=parent) + 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.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 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) + + 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..f8a2b33de --- /dev/null +++ b/ibllib/qc/task_qc_viewer/task_qc.py @@ -0,0 +1,321 @@ +import logging +import argparse +from itertools import cycle +import random +from collections.abc import Sized +from pathlib import Path + +import pandas as pd +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.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 + +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', 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): + """ + 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): + 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))) + 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() 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): + if event in trial_data: + plots.vertical_lines(trial_data[event], + label=event, color=c, linestyle=ln, **plot_args) + + +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. + 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 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 = Path(qc_or_session) + tasks = get_trials_tasks(session_path, one=None if local else one) + # 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) + + # 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 + + +def qc_gui_cli(): + """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) + + +if __name__ == '__main__': + qc_gui_cli() diff --git a/ibllib/tests/fixtures/utils.py b/ibllib/tests/fixtures/utils.py index f536875d0..0e2471829 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 @@ -81,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 @@ -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 new file mode 100644 index 000000000..a3b1d61b4 --- /dev/null +++ b/ibllib/tests/qc/test_task_qc_viewer.py @@ -0,0 +1,114 @@ +"""Tests for the ibllib.qc.task_qc_viewer package.""" +import os +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 + + +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.""" + 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, 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]])} + qc_mock.extractor.frame_ttls = qc_mock.extractor.audio_ttls = qc_mock.extractor.bpod_ttls = mock.MagicMock() + + 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] + 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() diff --git a/ibllib/tests/test_base_tasks.py b/ibllib/tests/test_base_tasks.py index f5014a162..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 @@ -9,6 +10,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 +93,69 @@ 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('') + 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('') + 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('') + 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)) + + 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() 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) 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: diff --git a/ibllib/tests/test_pipes.py b/ibllib/tests/test_pipes.py index cbe86462a..f46577d9b 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'}) @@ -452,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 @@ -551,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): 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): 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 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=[], )