├── .codespellrc ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── codespell.yml │ ├── deploy_pypi.yml │ ├── run_tests.yml │ └── run_tutorials.yml ├── .gitignore ├── LICENSE.md ├── README.rst ├── docker ├── README.md ├── cpu.Dockerfile ├── generate_dockerfile_cpu.sh ├── generate_dockerfile_gpu.sh └── gpu-cu102.Dockerfile ├── setup.py ├── tutorials ├── Makefile ├── README.md ├── _config.yml ├── _toc.yml ├── merge_notebooks.py ├── notebooks │ ├── README.md │ ├── shortclips │ │ ├── 01_setup_colab.ipynb │ │ ├── 02_download_shortclips.ipynb │ │ ├── 03_compute_explainable_variance.ipynb │ │ ├── 04_understand_ridge_regression.ipynb │ │ ├── 05_fit_wordnet_model.ipynb │ │ ├── 06_visualize_hemodynamic_response.ipynb │ │ ├── 07_extract_motion_energy.ipynb │ │ ├── 08_fit_motion_energy_model.ipynb │ │ ├── 09_fit_banded_ridge_model.ipynb │ │ ├── README.md │ │ ├── vem_tutorials_merged_for_colab.ipynb │ │ └── vem_tutorials_merged_for_colab_model_fitting.ipynb │ └── vim2 │ │ ├── 00_download_vim2.ipynb │ │ ├── 01_extract_motion_energy.ipynb │ │ ├── 02_plot_ridge_model.ipynb │ │ └── README.md ├── pages │ ├── index.md │ ├── references.md │ ├── voxelwise_modeling.md │ └── voxelwise_package.rst └── static │ ├── colab.png │ ├── custom.css │ ├── download.png │ ├── flatmap.png │ ├── moten.png │ └── references.bib └── voxelwise_tutorials ├── __init__.py ├── delayer.py ├── delays_toy.py ├── io.py ├── progress_bar.py ├── regression_toy.py ├── tests ├── __init__.py ├── test_delayer.py ├── test_delays_toy.py ├── test_io.py ├── test_mappers.py ├── test_model.py ├── test_regression_toy.py └── test_utils.py ├── utils.py ├── viz.py └── wordnet.py /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | skip = .git,*.pdf,*.svg,*.css,.codespellrc,build,_build,_auto_examples 3 | check-hidden = true 4 | ignore-regex = ^\s*"image/\S+": ".* 5 | # ignore-words-list = 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # *.ipynb -diff 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codespell 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | codespell: 15 | name: Check for spelling errors 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Codespell 22 | uses: codespell-project/actions-codespell@v2 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to PyPI 2 | # Deploy to PyPI if the __version__ variable in voxelwise_tutorials/__init__.py 3 | # is larger than the latest version on PyPI. 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | # trigger workflow only on commits that change __init__.py 11 | - 'voxelwise_tutorials/__init__.py' 12 | 13 | jobs: 14 | deploy-pypi: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v5 19 | 20 | - name: Get versions 21 | # Compare the latest version on PyPI, and the current version 22 | run: | 23 | python -m pip install --upgrade -q pip 24 | pip index versions voxelwise_tutorials 25 | LATEST=$(pip index versions voxelwise_tutorials | grep 'voxelwise_tutorials' |awk '{print $2}' | tr -d '(' | tr -d ')') 26 | CURRENT=$(cat voxelwise_tutorials/__init__.py | grep "__version__" | awk '{print $3}' | tr -d "'" | tr -d '"') 27 | EQUAL=$([ "$CURRENT" = "$LATEST" ] && echo 1 || echo 0) 28 | echo "LATEST=$LATEST" >> $GITHUB_ENV 29 | echo "CURRENT=$CURRENT" >> $GITHUB_ENV 30 | echo "EQUAL=$EQUAL" >> $GITHUB_ENV 31 | 32 | - name: Print versions 33 | run: | 34 | echo ${{ env.LATEST }} 35 | echo ${{ env.CURRENT }} 36 | echo ${{ env.EQUAL }} 37 | 38 | - name: Install pypa/build 39 | run: >- 40 | python -m 41 | pip install 42 | build 43 | 44 | - name: Build a source tarball 45 | run: >- 46 | python -m 47 | build 48 | --sdist 49 | --outdir dist/ 50 | 51 | - name: Publish 52 | if: ${{ env.EQUAL == 0 }} 53 | uses: pypa/gh-action-pypi-publish@release/v1 54 | with: 55 | password: ${{ secrets.PYPI_PASSWORD }} -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | run-tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 15 | max-parallel: 5 16 | fail-fast: false 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - uses: actions/cache@v4 26 | with: 27 | path: ~/.cache/pip 28 | key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} 29 | restore-keys: | 30 | ${{ runner.os }}-pip- 31 | 32 | - uses: actions/cache@v4 33 | with: 34 | path: ~/voxelwise_tutorials_data/shortclips 35 | key: shortclips-dataset 36 | 37 | - name: Install dependencies 38 | run: | 39 | pip install -U setuptools 40 | pip install -U wheel 41 | # install himalaya from source to get early testing 42 | pip install git+https://github.com/gallantlab/himalaya.git 43 | pip install -e ."[github]" 44 | # use neurodebian installer for travis 45 | bash <(wget -q -O- http://neuro.debian.net/_files/neurodebian-travis.sh) 46 | sudo apt-get update -qq 47 | sudo apt-get install git-annex-standalone 48 | 49 | - name: Lint with flake8 50 | run: | 51 | pip install -q flake8 52 | # stop the build if there are Python syntax errors or undefined names 53 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 54 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 55 | flake8 . --count --exit-zero --ignore=E402,C901 --max-line-length=127 --statistics 56 | 57 | - name: Config git-annex 58 | run: | 59 | # add some git config for git-annex 60 | git config --global user.email "github-actions@example.com" 61 | git config --global user.name "Github Actions" 62 | 63 | - name: Test with pytest 64 | run: | 65 | # run the tests 66 | pip install -q pytest pytest-cov codecov 67 | pytest --cov=./ 68 | 69 | - name: Upload coverage to Codecov 70 | uses: codecov/codecov-action@v5 71 | with: 72 | env_vars: OS,PYTHON 73 | fail_ci_if_error: true 74 | token: ${{ secrets.CODECOV_TOKEN }} 75 | verbose: false 76 | -------------------------------------------------------------------------------- /.github/workflows/run_tutorials.yml: -------------------------------------------------------------------------------- 1 | name: Tutorials 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | run-tutorials: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 15 | max-parallel: 5 16 | fail-fast: true 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - uses: actions/cache@v4 26 | with: 27 | path: ~/.cache/pip 28 | key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} 29 | restore-keys: | 30 | ${{ runner.os }}-pip- 31 | 32 | - uses: actions/cache@v4 33 | with: 34 | path: ~/voxelwise_tutorials_data/shortclips 35 | key: shortclips-dataset 36 | 37 | - name: Install dependencies 38 | run: | 39 | pip install -U setuptools 40 | pip install -U wheel 41 | # install himalaya from source to get early testing 42 | pip install git+https://github.com/gallantlab/himalaya.git 43 | pip install -e ."[docs,github]" 44 | # use neurodebian installer for travis 45 | bash <(wget -q -O- http://neuro.debian.net/_files/neurodebian-travis.sh) 46 | sudo apt-get update -qq 47 | sudo apt-get install git-annex-standalone 48 | 49 | - name: Config git-annex 50 | run: | 51 | # add some git config for git-annex 52 | git config --global user.email "github-actions@example.com" 53 | git config --global user.name "Github Actions" 54 | 55 | - name: Run some tutorials 56 | run: | 57 | # run tutorials with a shortcut option to skip model fitting 58 | HIMALAYA_SKIP_FIT=True jupyter-book build tutorials 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | *.so 4 | 5 | .pytest_cache 6 | __pycache__ 7 | .vscode/ 8 | .idea/ 9 | build/ 10 | dist/ 11 | 12 | # Documentation build 13 | doc/_build/ 14 | doc/_auto_examples/ 15 | tutorials/_build 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, the voxelwise tutorials developers 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | Voxelwise Encoding Model tutorials 3 | ================================== 4 | 5 | |Github| |Python| |License| |Build| |Build Tutorials| |Downloads| 6 | 7 | Welcome to the Voxelwise Encoding Model tutorials, brought to you by the 8 | `Gallant Lab `_. 9 | 10 | Paper 11 | ===== 12 | 13 | If you use these tutorials for your work, consider citing the corresponding paper: 14 | 15 | Dupré la Tour, T., Visconti di Oleggio Castello, M., & Gallant, J. L. (2025). 16 | The Voxelwise Encoding Model framework: A tutorial introduction to fitting encoding models to fMRI data. 17 | *Imaging Neuroscience*. https://doi.org/10.1162/imag_a_00575 18 | 19 | 20 | Tutorials 21 | ========= 22 | 23 | This repository contains tutorials describing how to use the Voxelwise Encoding Model 24 | (VEM) framework. `VEM 25 | `_ is 26 | a framework to perform functional magnetic resonance imaging (fMRI) data 27 | analysis, fitting encoding models at the voxel level. 28 | 29 | To explore these tutorials, one can: 30 | 31 | - Read the rendered examples in the tutorials 32 | `website `_ (recommended). 33 | - Run the merged notebook in 34 | `Colab `_. 35 | - Run the Jupyter notebooks (`tutorials/notebooks `_ directory) locally. 36 | 37 | The tutorials are best explored in order, starting with the "shortclips" 38 | tutorial. The "vim2" tutorial is optional and redundant with the "shortclips" one. 39 | 40 | 41 | Dockerfiles 42 | =========== 43 | 44 | This repository contains Dockerfiles to run the tutorials locally. Please see the 45 | instructions in the `docker `_ directory. 46 | 47 | 48 | Helper Python package 49 | ===================== 50 | 51 | To run the tutorials, this repository contains a small Python package 52 | called ``voxelwise_tutorials``, with useful functions to download the 53 | data sets, load the files, process the data, and visualize the results. 54 | 55 | Installation 56 | ------------ 57 | 58 | To install the ``voxelwise_tutorials`` package, run: 59 | 60 | .. code-block:: bash 61 | 62 | pip install voxelwise_tutorials 63 | 64 | 65 | To also download the tutorial scripts and notebooks, clone the repository via: 66 | 67 | .. code-block:: bash 68 | 69 | git clone https://github.com/gallantlab/voxelwise_tutorials.git 70 | cd voxelwise_tutorials 71 | pip install . 72 | 73 | 74 | Developers can also install the package in editable mode via: 75 | 76 | .. code-block:: bash 77 | 78 | pip install --editable . 79 | 80 | 81 | Requirements 82 | ------------ 83 | 84 | The tutorials are not compatible with Windows. 85 | If you are using Windows, we recommend running the tutorials on Google Colab or 86 | in the provided Docker containers. 87 | 88 | `git-annex `_ is required to download the 89 | data sets. Please follow the instructions in the 90 | `git-annex documentation `_ to install 91 | it on your system. 92 | 93 | The tutorials and the package ``voxelwise_tutorials`` require Python 3.9 or higher. 94 | 95 | The package ``voxelwise_tutorials`` has the following Python dependencies: 96 | `numpy `_, 97 | `scipy `_, 98 | `h5py `_, 99 | `scikit-learn `_, 100 | `matplotlib `_, 101 | `networkx `_, 102 | `nltk `_, 103 | `pycortex `_, 104 | `himalaya `_, 105 | `pymoten `_, 106 | `datalad `_. 107 | 108 | 109 | .. |Github| image:: https://img.shields.io/badge/github-voxelwise_tutorials-blue 110 | :target: https://github.com/gallantlab/voxelwise_tutorials 111 | 112 | .. |Python| image:: https://img.shields.io/badge/python-3.9%2B-blue 113 | :target: https://www.python.org/downloads/release/python-390 114 | 115 | .. |License| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg 116 | :target: https://opensource.org/licenses/BSD-3-Clause 117 | 118 | .. |Build| image:: https://github.com/gallantlab/voxelwise_tutorials/actions/workflows/run_tests.yml/badge.svg 119 | :target: https://github.com/gallantlab/voxelwise_tutorials/actions/workflows/run_tests.yml 120 | 121 | .. |Build Tutorials| image:: https://github.com/gallantlab/voxelwise_tutorials/actions/workflows/run_tutorials.yml/badge.svg 122 | :target: https://github.com/gallantlab/voxelwise_tutorials/actions/workflows/run_tutorials.yml 123 | 124 | .. |Downloads| image:: https://pepy.tech/badge/voxelwise_tutorials 125 | :target: https://pepy.tech/project/voxelwise_tutorials 126 | 127 | 128 | Cite as 129 | ======= 130 | 131 | If you use one of our packages in your work (``voxelwise_tutorials`` [1]_, 132 | ``himalaya`` [2]_, ``pycortex`` [3]_, or ``pymoten`` [4]_), please cite the 133 | corresponding publications: 134 | 135 | .. [1] Dupré la Tour, T., Visconti di Oleggio Castello, M., & Gallant, J. L. (2025). 136 | The Voxelwise Encoding Model framework: A tutorial introduction to fitting encoding models to fMRI data. 137 | Imaging Neuroscience. https://doi.org/10.1162/imag_a_00575 138 | 139 | .. [2] Dupré la Tour, T., Eickenberg, M., Nunez-Elizalde, A.O., & Gallant, J. L. (2022). 140 | Feature-space selection with banded ridge regression. NeuroImage. 141 | https://doi.org/10.1016/j.neuroimage.2022.119728 142 | 143 | .. [3] Gao, J. S., Huth, A. G., Lescroart, M. D., & Gallant, J. L. (2015). 144 | Pycortex: an interactive surface visualizer for fMRI. Frontiers in 145 | neuroinformatics, 23. https://doi.org/10.3389/fninf.2015.00023 146 | 147 | .. [4] Nunez-Elizalde, A.O., Deniz, F., Dupré la Tour, T., Visconti di Oleggio 148 | Castello, M., and Gallant, J.L. (2021). pymoten: scientific python package 149 | for computing motion energy features from video. Zenodo. 150 | https://doi.org/10.5281/zenodo.6349625 151 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Using the Dockerfiles 2 | 3 | Although the easiest way to run the tutorials is to use Google Colab, we also provide Dockerfiles for those who prefer to run the tutorials locally. 4 | 5 | We provide two versions of the Dockerfile: one for CPU and one for GPU. You should use the CPU version if you do not have a compatible Nvidia GPU or if you prefer to run the tutorials on a CPU. The GPU version is recommended for those with a compatible Nvidia GPU, as it will significantly speed up the fitting of voxelwise encoding models. 6 | 7 | ## CPU Version 8 | The CPU version of the Dockerfile is designed to run on any machine with a CPU. It does not require any special hardware or drivers, making it suitable for a wide range of environments. Note, however, that the CPU version will be significantly slower when fitting voxelwise encoding models compared to the GPU version. 9 | 10 | ### Prerequisites 11 | - Install Docker on your machine. You can find instructions for your operating system [here](https://docs.docker.com/get-docker/). 12 | 13 | ### Build the Docker image 14 | To build the CPU version, run the following command from the current `voxelwise_tutorials/docker` directory: 15 | 16 | ```bash 17 | docker build --tag voxelwise_tutorials --file cpu.Dockerfile . 18 | ``` 19 | 20 | This will create a Docker image named `voxelwise_tutorials` based on the `cpu.Dockerfile`. 21 | 22 | ### Run the Docker container 23 | To run the CPU version, use the following command: 24 | 25 | ```bash 26 | docker run --rm -it --name voxelwise_tutorials --publish 8888:8888 voxelwise_tutorials jupyter-lab --ip 0.0.0.0 27 | ``` 28 | 29 | This command will start a Docker container from the `voxelwise_tutorials` image, mapping port 8888 on your host machine to port 8888 in the container. The `--rm` flag ensures that the container is removed after it is stopped, and the `-it` flag allows you to interact with the container. Note that all data that will be downloaded during the tutorial will be stored in the container, and it will be removed when you stop the container. 30 | 31 | You should see output similar to the following: 32 | 33 | ``` 34 | To access the server, open this file in a browser: 35 | file:///home/voxelwise/.local/share/jupyter/runtime/jpserver-7-open.html 36 | Or copy and paste one of these URLs: 37 | http://f4cb3fce5844:8888/lab?token=73d9628b0e8839023e3409945f06b9ddbdedde95fe630e00 38 | http://127.0.0.1:8888/lab?token=73d9628b0e8839023e3409945f06b9ddbdedde95fe630e00 39 | ``` 40 | The URL will contain a token that you can use to access the Jupyter Lab interface. Open your web browser and navigate to `http://127.0.0.1:8888/lab?token=`. This will open the Jupyter Lab interface, where you can start working with the tutorials. 41 | 42 | ## GPU Version 43 | The GPU version of the Dockerfile is designed to take advantage of Nvidia GPUs for faster computation. This version is recommended if you have a compatible Nvidia GPU and the necessary drivers installed. The GPU version will significantly speed up the fitting of voxelwise encoding models. 44 | 45 | ## Prerequisites 46 | - Install Docker on your machine. You can find instructions for your operating system [here](https://docs.docker.com/get-docker/). 47 | - Install the Nvidia Container Toolkit. You can find instructions for your operating system [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html). 48 | 49 | 50 | ## Create a Dockerfile for your version of CUDA 51 | 52 | You will need to create a Dockerfile for your version of CUDA. We provide a bash script that will do this for you. The bash script uses [neurodocker](https://www.repronim.org/neurodocker/) to quickly create a Dockerfile. The bash script is called `generate_dockerfile_gpu.sh` and can be found in this directory. To use the script, run the following command: 53 | 54 | ```bash 55 | bash generate_dockerfile_gpu.sh CUDA_VERSION [] 56 | ``` 57 | 58 | Replace `CUDA_VERSION` with the version of CUDA you have installed (e.g., `10.2`). The optional `` argument allows you to specify the version of Ubuntu you want to use (e.g., `22.04`). If you do not specify an OS version, the script will default to `18.04`. 59 | 60 | For example, to create a Dockerfile with CUDA 10.2 and Ubuntu 18.04, run the following command: 61 | 62 | ```bash 63 | bash generate_dockerfile_gpu.sh 10.2 18.04 64 | ``` 65 | This will create a Dockerfile named `gpu-cu102.Dockerfile` in the current directory. You can then use this Dockerfile to build the GPU version of the tutorials. 66 | 67 | > [!IMPORTANT] 68 | > Make sure to use the correct version of CUDA that matches your Nvidia driver. Also note that not all CUDA versions are available in the latest Ubuntu images, so make sure to select both the appropriate CUDA and Ubuntu versions. 69 | 70 | ## Build the Docker image 71 | To build the GPU version, run the following command, replacing `gpu-cu102.Dockerfile` with the name of the Dockerfile you created in the previous step: 72 | 73 | ```bash 74 | docker build --tag voxelwise_tutorials --file gpu-cu102.Dockerfile . 75 | ``` 76 | 77 | This will create a Docker image named `voxelwise_tutorials` based on the your GPU Dockerfile. 78 | 79 | ## Run the Docker container 80 | To run the GPU version, use the following command: 81 | 82 | ```bash 83 | docker run --rm -it --gpus all --name voxelwise_tutorials --publish 8888:8888 voxelwise_tutorials jupyter-lab --ip 0.0.0.0 84 | ``` 85 | 86 | Then you can follow the same steps as in the CPU version to access the Jupyter Lab interface. -------------------------------------------------------------------------------- /docker/cpu.Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by Neurodocker and Reproenv. 2 | 3 | FROM ubuntu:20.04 4 | ENV LANG="en_US.UTF-8" \ 5 | LC_ALL="en_US.UTF-8" \ 6 | ND_ENTRYPOINT="/neurodocker/startup.sh" 7 | RUN export ND_ENTRYPOINT="/neurodocker/startup.sh" \ 8 | && apt-get update -qq \ 9 | && apt-get install -y -q --no-install-recommends \ 10 | apt-utils \ 11 | bzip2 \ 12 | ca-certificates \ 13 | curl \ 14 | locales \ 15 | unzip \ 16 | && rm -rf /var/lib/apt/lists/* \ 17 | && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ 18 | && dpkg-reconfigure --frontend=noninteractive locales \ 19 | && update-locale LANG="en_US.UTF-8" \ 20 | && chmod 777 /opt && chmod a+s /opt \ 21 | && mkdir -p /neurodocker \ 22 | && if [ ! -f "$ND_ENTRYPOINT" ]; then \ 23 | echo '#!/usr/bin/env bash' >> "$ND_ENTRYPOINT" \ 24 | && echo 'set -e' >> "$ND_ENTRYPOINT" \ 25 | && echo 'export USER="${USER:=`whoami`}"' >> "$ND_ENTRYPOINT" \ 26 | && echo 'if [ -n "$1" ]; then "$@"; else /usr/bin/env bash; fi' >> "$ND_ENTRYPOINT"; \ 27 | fi \ 28 | && chmod -R 777 /neurodocker && chmod a+s /neurodocker 29 | ENV DEBIAN_FRONTEND="noninteractive" 30 | RUN chmod 777 /tmp 31 | RUN apt-get update -qq \ 32 | && apt-get install -y -q --no-install-recommends \ 33 | build-essential \ 34 | ca-certificates \ 35 | git \ 36 | netbase \ 37 | && rm -rf /var/lib/apt/lists/* 38 | ENV CONDA_DIR="/opt/miniconda-py311_24.4.0-0" \ 39 | PATH="/opt/miniconda-py311_24.4.0-0/bin:$PATH" 40 | RUN apt-get update -qq \ 41 | && apt-get install -y -q --no-install-recommends \ 42 | bzip2 \ 43 | ca-certificates \ 44 | curl \ 45 | && rm -rf /var/lib/apt/lists/* \ 46 | # Install dependencies. 47 | && export PATH="/opt/miniconda-py311_24.4.0-0/bin:$PATH" \ 48 | && echo "Downloading Miniconda installer ..." \ 49 | && conda_installer="/tmp/miniconda.sh" \ 50 | && curl -fsSL -o "$conda_installer" https://repo.continuum.io/miniconda/Miniconda3-py311_24.4.0-0-Linux-x86_64.sh \ 51 | && bash "$conda_installer" -b -p /opt/miniconda-py311_24.4.0-0 \ 52 | && rm -f "$conda_installer" \ 53 | # Prefer packages in conda-forge 54 | && conda config --system --prepend channels conda-forge \ 55 | # Packages in lower-priority channels not considered if a package with the same 56 | # name exists in a higher priority channel. Can dramatically speed up installations. 57 | # Conda recommends this as a default 58 | # https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html 59 | && conda config --set channel_priority strict \ 60 | && conda config --system --set auto_update_conda false \ 61 | && conda config --system --set show_channel_urls true \ 62 | # Enable `conda activate` 63 | && conda init bash \ 64 | && conda install -y --name base \ 65 | "gxx_linux-64" \ 66 | "notebook" \ 67 | "jupyterlab" \ 68 | "numpy" \ 69 | "git-annex" \ 70 | "ipywidgets" \ 71 | # Clean up 72 | && sync && conda clean --all --yes && sync \ 73 | && rm -rf ~/.cache/pip/* 74 | RUN chmod 777 /opt/miniconda-py311_24.4.0-0/share 75 | RUN python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu 76 | RUN test "$(getent passwd voxelwise)" \ 77 | || useradd --no-user-group --create-home --shell /bin/bash voxelwise 78 | USER voxelwise 79 | WORKDIR /home/voxelwise 80 | RUN git clone https://github.com/gallantlab/voxelwise_tutorials.git --depth 1 81 | RUN python -m pip install voxelwise_tutorials 82 | RUN git config --global user.email 'you@example.com' 83 | RUN git config --global user.name 'Your Name' 84 | WORKDIR /home/voxelwise/voxelwise_tutorials/tutorials/notebooks/shortclips 85 | ENTRYPOINT ["/neurodocker/startup.sh"] 86 | 87 | # Save specification to JSON. 88 | USER root 89 | RUN printf '{ \ 90 | "pkg_manager": "apt", \ 91 | "existing_users": [ \ 92 | "root" \ 93 | ], \ 94 | "instructions": [ \ 95 | { \ 96 | "name": "from_", \ 97 | "kwds": { \ 98 | "base_image": "ubuntu:20.04" \ 99 | } \ 100 | }, \ 101 | { \ 102 | "name": "env", \ 103 | "kwds": { \ 104 | "LANG": "en_US.UTF-8", \ 105 | "LC_ALL": "en_US.UTF-8", \ 106 | "ND_ENTRYPOINT": "/neurodocker/startup.sh" \ 107 | } \ 108 | }, \ 109 | { \ 110 | "name": "run", \ 111 | "kwds": { \ 112 | "command": "export ND_ENTRYPOINT=\\"/neurodocker/startup.sh\\"\\napt-get update -qq\\napt-get install -y -q --no-install-recommends \\\\\\n apt-utils \\\\\\n bzip2 \\\\\\n ca-certificates \\\\\\n curl \\\\\\n locales \\\\\\n unzip\\nrm -rf /var/lib/apt/lists/*\\nsed -i -e '"'"'s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/'"'"' /etc/locale.gen\\ndpkg-reconfigure --frontend=noninteractive locales\\nupdate-locale LANG=\\"en_US.UTF-8\\"\\nchmod 777 /opt && chmod a+s /opt\\nmkdir -p /neurodocker\\nif [ ! -f \\"$ND_ENTRYPOINT\\" ]; then\\n echo '"'"'#!/usr/bin/env bash'"'"' >> \\"$ND_ENTRYPOINT\\"\\n echo '"'"'set -e'"'"' >> \\"$ND_ENTRYPOINT\\"\\n echo '"'"'export USER=\\"${USER:=`whoami`}\\"'"'"' >> \\"$ND_ENTRYPOINT\\"\\n echo '"'"'if [ -n \\"$1\\" ]; then \\"$@\\"; else /usr/bin/env bash; fi'"'"' >> \\"$ND_ENTRYPOINT\\";\\nfi\\nchmod -R 777 /neurodocker && chmod a+s /neurodocker" \ 113 | } \ 114 | }, \ 115 | { \ 116 | "name": "env", \ 117 | "kwds": { \ 118 | "DEBIAN_FRONTEND": "noninteractive" \ 119 | } \ 120 | }, \ 121 | { \ 122 | "name": "run", \ 123 | "kwds": { \ 124 | "command": "chmod 777 /tmp" \ 125 | } \ 126 | }, \ 127 | { \ 128 | "name": "install", \ 129 | "kwds": { \ 130 | "pkgs": [ \ 131 | "build-essential", \ 132 | "git", \ 133 | "ca-certificates", \ 134 | "netbase" \ 135 | ], \ 136 | "opts": null \ 137 | } \ 138 | }, \ 139 | { \ 140 | "name": "run", \ 141 | "kwds": { \ 142 | "command": "apt-get update -qq \\\\\\n && apt-get install -y -q --no-install-recommends \\\\\\n build-essential \\\\\\n ca-certificates \\\\\\n git \\\\\\n netbase \\\\\\n && rm -rf /var/lib/apt/lists/*" \ 143 | } \ 144 | }, \ 145 | { \ 146 | "name": "env", \ 147 | "kwds": { \ 148 | "CONDA_DIR": "/opt/miniconda-py311_24.4.0-0", \ 149 | "PATH": "/opt/miniconda-py311_24.4.0-0/bin:$PATH" \ 150 | } \ 151 | }, \ 152 | { \ 153 | "name": "run", \ 154 | "kwds": { \ 155 | "command": "apt-get update -qq\\napt-get install -y -q --no-install-recommends \\\\\\n bzip2 \\\\\\n ca-certificates \\\\\\n curl\\nrm -rf /var/lib/apt/lists/*\\n# Install dependencies.\\nexport PATH=\\"/opt/miniconda-py311_24.4.0-0/bin:$PATH\\"\\necho \\"Downloading Miniconda installer ...\\"\\nconda_installer=\\"/tmp/miniconda.sh\\"\\ncurl -fsSL -o \\"$conda_installer\\" https://repo.continuum.io/miniconda/Miniconda3-py311_24.4.0-0-Linux-x86_64.sh\\nbash \\"$conda_installer\\" -b -p /opt/miniconda-py311_24.4.0-0\\nrm -f \\"$conda_installer\\"\\n# Prefer packages in conda-forge\\nconda config --system --prepend channels conda-forge\\n# Packages in lower-priority channels not considered if a package with the same\\n# name exists in a higher priority channel. Can dramatically speed up installations.\\n# Conda recommends this as a default\\n# https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html\\nconda config --set channel_priority strict\\nconda config --system --set auto_update_conda false\\nconda config --system --set show_channel_urls true\\n# Enable `conda activate`\\nconda init bash\\nconda install -y --name base \\\\\\n \\"gxx_linux-64\\" \\\\\\n \\"notebook\\" \\\\\\n \\"jupyterlab\\" \\\\\\n \\"numpy\\" \\\\\\n \\"git-annex\\" \\\\\\n \\"ipywidgets\\"\\n# Clean up\\nsync && conda clean --all --yes && sync\\nrm -rf ~/.cache/pip/*" \ 156 | } \ 157 | }, \ 158 | { \ 159 | "name": "run", \ 160 | "kwds": { \ 161 | "command": "chmod 777 /opt/miniconda-py311_24.4.0-0/share" \ 162 | } \ 163 | }, \ 164 | { \ 165 | "name": "run", \ 166 | "kwds": { \ 167 | "command": "python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu" \ 168 | } \ 169 | }, \ 170 | { \ 171 | "name": "user", \ 172 | "kwds": { \ 173 | "user": "voxelwise" \ 174 | } \ 175 | }, \ 176 | { \ 177 | "name": "workdir", \ 178 | "kwds": { \ 179 | "path": "/home/voxelwise" \ 180 | } \ 181 | }, \ 182 | { \ 183 | "name": "run", \ 184 | "kwds": { \ 185 | "command": "git clone https://github.com/gallantlab/voxelwise_tutorials.git --depth 1" \ 186 | } \ 187 | }, \ 188 | { \ 189 | "name": "run", \ 190 | "kwds": { \ 191 | "command": "python -m pip install voxelwise_tutorials" \ 192 | } \ 193 | }, \ 194 | { \ 195 | "name": "run", \ 196 | "kwds": { \ 197 | "command": "git config --global user.email '"'"'you@example.com'"'"'" \ 198 | } \ 199 | }, \ 200 | { \ 201 | "name": "run", \ 202 | "kwds": { \ 203 | "command": "git config --global user.name '"'"'Your Name'"'"'" \ 204 | } \ 205 | }, \ 206 | { \ 207 | "name": "workdir", \ 208 | "kwds": { \ 209 | "path": "/home/voxelwise/voxelwise_tutorials/tutorials/notebooks/shortclips" \ 210 | } \ 211 | }, \ 212 | { \ 213 | "name": "entrypoint", \ 214 | "kwds": { \ 215 | "args": [ \ 216 | "/neurodocker/startup.sh" \ 217 | ] \ 218 | } \ 219 | } \ 220 | ] \ 221 | }' > /.reproenv.json 222 | USER voxelwise 223 | # End saving to specification to JSON. 224 | -------------------------------------------------------------------------------- /docker/generate_dockerfile_cpu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run --rm repronim/neurodocker:latest generate docker \ 3 | --pkg-manager apt \ 4 | --base-image ubuntu:20.04 \ 5 | --env "DEBIAN_FRONTEND=noninteractive" \ 6 | --run "chmod 777 /tmp" \ 7 | --install build-essential git ca-certificates netbase\ 8 | --miniconda \ 9 | version="py311_24.4.0-0" \ 10 | conda_install="gxx_linux-64 notebook jupyterlab numpy git-annex ipywidgets" \ 11 | --run "chmod 777 /opt/miniconda-py311_24.4.0-0/share" \ 12 | --run "python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu" \ 13 | --user voxelwise \ 14 | --workdir /home/voxelwise \ 15 | --run "git clone https://github.com/gallantlab/voxelwise_tutorials.git --depth 1" \ 16 | --run "python -m pip install voxelwise_tutorials" \ 17 | --run "git config --global user.email 'you@example.com'" \ 18 | --run "git config --global user.name 'Your Name'" \ 19 | --workdir /home/voxelwise/voxelwise_tutorials/tutorials/notebooks/shortclips \ 20 | > cpu.Dockerfile -------------------------------------------------------------------------------- /docker/generate_dockerfile_gpu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Check if an argument is passed. If not, explain how to use it. 3 | # First argument is the CUDA version. 4 | # Second optional argument is the ubuntu version. 5 | if [ $# -eq 0 ]; then 6 | echo "Usage: $0 []" 7 | echo "Example: $0 10.2 18.04" 8 | echo "If OS_VERSION is not provided, it defaults to 18.04." 9 | exit 1 10 | fi 11 | CUDA_VERSION="$1" 12 | # Check if the second argument is provided, if not set it to 18.04 13 | if [ $# -eq 2 ]; then 14 | OS_VERSION="$2" 15 | else 16 | OS_VERSION="18.04" 17 | fi 18 | # Check if the CUDA version is valid 19 | if [[ ! "$CUDA_VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then 20 | echo "Invalid CUDA version format. Please use the format X.Y" 21 | exit 1 22 | fi 23 | 24 | BASE_IMAGE="nvcr.io/nvidia/cuda:${CUDA_VERSION}-base-ubuntu${OS_VERSION}" 25 | echo "Using CUDA version: $CUDA_VERSION" 26 | echo "Using OS version: ubuntu-$OS_VERSION" 27 | echo "Using base image: $BASE_IMAGE" 28 | 29 | CUDA_VERSION_SHORT=$(echo "${CUDA_VERSION}" | cut -d'.' -f1-2 | tr -d '.') 30 | echo "Using CUDA version short: $CUDA_VERSION_SHORT" 31 | 32 | CONDA_VERSION="py310_25.1.1-2" 33 | echo "Using conda version: $CONDA_VERSION" 34 | 35 | # Generate the Dockerfile using Neurodocker 36 | docker run --rm repronim/neurodocker:latest generate docker \ 37 | --pkg-manager apt \ 38 | --base-image $BASE_IMAGE \ 39 | --env "DEBIAN_FRONTEND=noninteractive" \ 40 | --run "chmod 777 /tmp" \ 41 | --install build-essential git ca-certificates netbase\ 42 | --miniconda \ 43 | version="$CONDA_VERSION" \ 44 | conda_install="gxx_linux-64 notebook jupyterlab numpy<2 git-annex ipywidgets" \ 45 | --run "chmod 777 /opt/miniconda-$CONDA_VERSION/share" \ 46 | --run "python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu"$CUDA_VERSION_SHORT \ 47 | --user voxelwise \ 48 | --workdir /home/voxelwise \ 49 | --run "git clone https://github.com/gallantlab/voxelwise_tutorials.git --depth 1" \ 50 | --run "python -m pip install voxelwise_tutorials" \ 51 | --run "git config --global user.email 'you@example.com'" \ 52 | --run "git config --global user.name 'Your Name'" \ 53 | --workdir /home/voxelwise/voxelwise_tutorials/tutorials/notebooks/shortclips \ 54 | > gpu-cu"$CUDA_VERSION_SHORT".Dockerfile -------------------------------------------------------------------------------- /docker/gpu-cu102.Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by Neurodocker and Reproenv. 2 | 3 | FROM nvcr.io/nvidia/cuda:10.2-base-ubuntu18.04 4 | ENV LANG="en_US.UTF-8" \ 5 | LC_ALL="en_US.UTF-8" \ 6 | ND_ENTRYPOINT="/neurodocker/startup.sh" 7 | RUN export ND_ENTRYPOINT="/neurodocker/startup.sh" \ 8 | && apt-get update -qq \ 9 | && apt-get install -y -q --no-install-recommends \ 10 | apt-utils \ 11 | bzip2 \ 12 | ca-certificates \ 13 | curl \ 14 | locales \ 15 | unzip \ 16 | && rm -rf /var/lib/apt/lists/* \ 17 | && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ 18 | && dpkg-reconfigure --frontend=noninteractive locales \ 19 | && update-locale LANG="en_US.UTF-8" \ 20 | && chmod 777 /opt && chmod a+s /opt \ 21 | && mkdir -p /neurodocker \ 22 | && if [ ! -f "$ND_ENTRYPOINT" ]; then \ 23 | echo '#!/usr/bin/env bash' >> "$ND_ENTRYPOINT" \ 24 | && echo 'set -e' >> "$ND_ENTRYPOINT" \ 25 | && echo 'export USER="${USER:=`whoami`}"' >> "$ND_ENTRYPOINT" \ 26 | && echo 'if [ -n "$1" ]; then "$@"; else /usr/bin/env bash; fi' >> "$ND_ENTRYPOINT"; \ 27 | fi \ 28 | && chmod -R 777 /neurodocker && chmod a+s /neurodocker 29 | ENV DEBIAN_FRONTEND="noninteractive" 30 | RUN chmod 777 /tmp 31 | RUN apt-get update -qq \ 32 | && apt-get install -y -q --no-install-recommends \ 33 | build-essential \ 34 | ca-certificates \ 35 | git \ 36 | netbase \ 37 | && rm -rf /var/lib/apt/lists/* 38 | ENV CONDA_DIR="/opt/miniconda-py310_25.1.1-2" \ 39 | PATH="/opt/miniconda-py310_25.1.1-2/bin:$PATH" 40 | RUN apt-get update -qq \ 41 | && apt-get install -y -q --no-install-recommends \ 42 | bzip2 \ 43 | ca-certificates \ 44 | curl \ 45 | && rm -rf /var/lib/apt/lists/* \ 46 | # Install dependencies. 47 | && export PATH="/opt/miniconda-py310_25.1.1-2/bin:$PATH" \ 48 | && echo "Downloading Miniconda installer ..." \ 49 | && conda_installer="/tmp/miniconda.sh" \ 50 | && curl -fsSL -o "$conda_installer" https://repo.continuum.io/miniconda/Miniconda3-py310_25.1.1-2-Linux-x86_64.sh \ 51 | && bash "$conda_installer" -b -p /opt/miniconda-py310_25.1.1-2 \ 52 | && rm -f "$conda_installer" \ 53 | # Prefer packages in conda-forge 54 | && conda config --system --prepend channels conda-forge \ 55 | # Packages in lower-priority channels not considered if a package with the same 56 | # name exists in a higher priority channel. Can dramatically speed up installations. 57 | # Conda recommends this as a default 58 | # https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html 59 | && conda config --set channel_priority strict \ 60 | && conda config --system --set auto_update_conda false \ 61 | && conda config --system --set show_channel_urls true \ 62 | # Enable `conda activate` 63 | && conda init bash \ 64 | && conda install -y --name base \ 65 | "gxx_linux-64" \ 66 | "notebook" \ 67 | "jupyterlab" \ 68 | "numpy<2" \ 69 | "git-annex" \ 70 | "ipywidgets" \ 71 | # Clean up 72 | && sync && conda clean --all --yes && sync \ 73 | && rm -rf ~/.cache/pip/* 74 | RUN chmod 777 /opt/miniconda-py310_25.1.1-2/share 75 | RUN python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu102 76 | RUN test "$(getent passwd voxelwise)" \ 77 | || useradd --no-user-group --create-home --shell /bin/bash voxelwise 78 | USER voxelwise 79 | WORKDIR /home/voxelwise 80 | RUN git clone https://github.com/gallantlab/voxelwise_tutorials.git --depth 1 81 | RUN python -m pip install voxelwise_tutorials 82 | RUN git config --global user.email 'you@example.com' 83 | RUN git config --global user.name 'Your Name' 84 | WORKDIR /home/voxelwise/voxelwise_tutorials/tutorials/notebooks/shortclips 85 | ENTRYPOINT ["/neurodocker/startup.sh"] 86 | 87 | # Save specification to JSON. 88 | USER root 89 | RUN printf '{ \ 90 | "pkg_manager": "apt", \ 91 | "existing_users": [ \ 92 | "root" \ 93 | ], \ 94 | "instructions": [ \ 95 | { \ 96 | "name": "from_", \ 97 | "kwds": { \ 98 | "base_image": "nvcr.io/nvidia/cuda:10.2-base-ubuntu18.04" \ 99 | } \ 100 | }, \ 101 | { \ 102 | "name": "env", \ 103 | "kwds": { \ 104 | "LANG": "en_US.UTF-8", \ 105 | "LC_ALL": "en_US.UTF-8", \ 106 | "ND_ENTRYPOINT": "/neurodocker/startup.sh" \ 107 | } \ 108 | }, \ 109 | { \ 110 | "name": "run", \ 111 | "kwds": { \ 112 | "command": "export ND_ENTRYPOINT=\\"/neurodocker/startup.sh\\"\\napt-get update -qq\\napt-get install -y -q --no-install-recommends \\\\\\n apt-utils \\\\\\n bzip2 \\\\\\n ca-certificates \\\\\\n curl \\\\\\n locales \\\\\\n unzip\\nrm -rf /var/lib/apt/lists/*\\nsed -i -e '"'"'s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/'"'"' /etc/locale.gen\\ndpkg-reconfigure --frontend=noninteractive locales\\nupdate-locale LANG=\\"en_US.UTF-8\\"\\nchmod 777 /opt && chmod a+s /opt\\nmkdir -p /neurodocker\\nif [ ! -f \\"$ND_ENTRYPOINT\\" ]; then\\n echo '"'"'#!/usr/bin/env bash'"'"' >> \\"$ND_ENTRYPOINT\\"\\n echo '"'"'set -e'"'"' >> \\"$ND_ENTRYPOINT\\"\\n echo '"'"'export USER=\\"${USER:=`whoami`}\\"'"'"' >> \\"$ND_ENTRYPOINT\\"\\n echo '"'"'if [ -n \\"$1\\" ]; then \\"$@\\"; else /usr/bin/env bash; fi'"'"' >> \\"$ND_ENTRYPOINT\\";\\nfi\\nchmod -R 777 /neurodocker && chmod a+s /neurodocker" \ 113 | } \ 114 | }, \ 115 | { \ 116 | "name": "env", \ 117 | "kwds": { \ 118 | "DEBIAN_FRONTEND": "noninteractive" \ 119 | } \ 120 | }, \ 121 | { \ 122 | "name": "run", \ 123 | "kwds": { \ 124 | "command": "chmod 777 /tmp" \ 125 | } \ 126 | }, \ 127 | { \ 128 | "name": "install", \ 129 | "kwds": { \ 130 | "pkgs": [ \ 131 | "build-essential", \ 132 | "git", \ 133 | "ca-certificates", \ 134 | "netbase" \ 135 | ], \ 136 | "opts": null \ 137 | } \ 138 | }, \ 139 | { \ 140 | "name": "run", \ 141 | "kwds": { \ 142 | "command": "apt-get update -qq \\\\\\n && apt-get install -y -q --no-install-recommends \\\\\\n build-essential \\\\\\n ca-certificates \\\\\\n git \\\\\\n netbase \\\\\\n && rm -rf /var/lib/apt/lists/*" \ 143 | } \ 144 | }, \ 145 | { \ 146 | "name": "env", \ 147 | "kwds": { \ 148 | "CONDA_DIR": "/opt/miniconda-py310_25.1.1-2", \ 149 | "PATH": "/opt/miniconda-py310_25.1.1-2/bin:$PATH" \ 150 | } \ 151 | }, \ 152 | { \ 153 | "name": "run", \ 154 | "kwds": { \ 155 | "command": "apt-get update -qq\\napt-get install -y -q --no-install-recommends \\\\\\n bzip2 \\\\\\n ca-certificates \\\\\\n curl\\nrm -rf /var/lib/apt/lists/*\\n# Install dependencies.\\nexport PATH=\\"/opt/miniconda-py310_25.1.1-2/bin:$PATH\\"\\necho \\"Downloading Miniconda installer ...\\"\\nconda_installer=\\"/tmp/miniconda.sh\\"\\ncurl -fsSL -o \\"$conda_installer\\" https://repo.continuum.io/miniconda/Miniconda3-py310_25.1.1-2-Linux-x86_64.sh\\nbash \\"$conda_installer\\" -b -p /opt/miniconda-py310_25.1.1-2\\nrm -f \\"$conda_installer\\"\\n# Prefer packages in conda-forge\\nconda config --system --prepend channels conda-forge\\n# Packages in lower-priority channels not considered if a package with the same\\n# name exists in a higher priority channel. Can dramatically speed up installations.\\n# Conda recommends this as a default\\n# https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html\\nconda config --set channel_priority strict\\nconda config --system --set auto_update_conda false\\nconda config --system --set show_channel_urls true\\n# Enable `conda activate`\\nconda init bash\\nconda install -y --name base \\\\\\n \\"gxx_linux-64\\" \\\\\\n \\"notebook\\" \\\\\\n \\"jupyterlab\\" \\\\\\n \\"numpy<2\\" \\\\\\n \\"git-annex\\" \\\\\\n \\"ipywidgets\\"\\n# Clean up\\nsync && conda clean --all --yes && sync\\nrm -rf ~/.cache/pip/*" \ 156 | } \ 157 | }, \ 158 | { \ 159 | "name": "run", \ 160 | "kwds": { \ 161 | "command": "chmod 777 /opt/miniconda-py310_25.1.1-2/share" \ 162 | } \ 163 | }, \ 164 | { \ 165 | "name": "run", \ 166 | "kwds": { \ 167 | "command": "python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu102" \ 168 | } \ 169 | }, \ 170 | { \ 171 | "name": "user", \ 172 | "kwds": { \ 173 | "user": "voxelwise" \ 174 | } \ 175 | }, \ 176 | { \ 177 | "name": "workdir", \ 178 | "kwds": { \ 179 | "path": "/home/voxelwise" \ 180 | } \ 181 | }, \ 182 | { \ 183 | "name": "run", \ 184 | "kwds": { \ 185 | "command": "git clone https://github.com/gallantlab/voxelwise_tutorials.git --depth 1" \ 186 | } \ 187 | }, \ 188 | { \ 189 | "name": "run", \ 190 | "kwds": { \ 191 | "command": "python -m pip install voxelwise_tutorials" \ 192 | } \ 193 | }, \ 194 | { \ 195 | "name": "run", \ 196 | "kwds": { \ 197 | "command": "git config --global user.email '"'"'you@example.com'"'"'" \ 198 | } \ 199 | }, \ 200 | { \ 201 | "name": "run", \ 202 | "kwds": { \ 203 | "command": "git config --global user.name '"'"'Your Name'"'"'" \ 204 | } \ 205 | }, \ 206 | { \ 207 | "name": "workdir", \ 208 | "kwds": { \ 209 | "path": "/home/voxelwise/voxelwise_tutorials/tutorials/notebooks/shortclips" \ 210 | } \ 211 | }, \ 212 | { \ 213 | "name": "entrypoint", \ 214 | "kwds": { \ 215 | "args": [ \ 216 | "/neurodocker/startup.sh" \ 217 | ] \ 218 | } \ 219 | } \ 220 | ] \ 221 | }' > /.reproenv.json 222 | USER voxelwise 223 | # End saving to specification to JSON. 224 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | # get version from voxelwise_tutorials/__init__.py 7 | with open('voxelwise_tutorials/__init__.py') as f: 8 | infos = f.readlines() 9 | __version__ = '' 10 | for line in infos: 11 | if "__version__" in line: 12 | match = re.search(r"__version__ = '([^']*)'", line) 13 | __version__ = match.groups()[0] 14 | 15 | # read the contents of the README file 16 | this_directory = Path(__file__).parent 17 | long_description = (this_directory / "README.rst").read_text() 18 | 19 | requirements = [ 20 | "numpy", 21 | "scipy", 22 | "h5py", 23 | "scikit-learn>=1.6", 24 | "matplotlib", 25 | "networkx", 26 | "pydot", 27 | "nltk", 28 | "pycortex>=1.2.4", 29 | "himalaya", 30 | "pymoten", 31 | "datalad", 32 | ] 33 | 34 | extras_require = { 35 | "docs": ["jupyter-book", "ipywidgets"], 36 | "github": ["pytest"], 37 | } 38 | 39 | 40 | if __name__ == "__main__": 41 | setup( 42 | name='voxelwise_tutorials', 43 | maintainer="Tom Dupre la Tour", 44 | maintainer_email="tom.dupre-la-tour@m4x.org", 45 | description="Tools and tutorials for voxelwise modeling", 46 | license='BSD (3-clause)', 47 | version=__version__, 48 | packages=find_packages(), 49 | install_requires=requirements, 50 | extras_require=extras_require, 51 | long_description=long_description, 52 | long_description_content_type='text/x-rst', 53 | ) 54 | -------------------------------------------------------------------------------- /tutorials/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile to use with jupyter-book 2 | 3 | # You can set these variables from the command line. 4 | SOURCEDIR = . 5 | BUILDDIR = _build 6 | 7 | # Put it first so that "make" without argument is like "make help". 8 | help: 9 | @echo "Available commands:" 10 | @echo " help Show this help message" 11 | @echo " clean Remove build directory" 12 | @echo " merge-notebooks Merge notebook files into single files for Colab" 13 | @echo " build Build the jupyter book" 14 | @echo " push-pages Push built book to GitHub Pages" 15 | @echo " preview Preview the book locally" 16 | 17 | .PHONY: help Makefile clean merge-notebooks build push-pages preview 18 | 19 | clean: 20 | rm -rf $(BUILDDIR)/ 21 | 22 | NBDIR = notebooks/shortclips 23 | 24 | merge-notebooks: 25 | python merge_notebooks.py \ 26 | $(NBDIR)/01_setup_colab.ipynb \ 27 | $(NBDIR)/02_download_shortclips.ipynb \ 28 | $(NBDIR)/03_compute_explainable_variance.ipynb \ 29 | $(NBDIR)/04_understand_ridge_regression.ipynb \ 30 | $(NBDIR)/05_fit_wordnet_model.ipynb \ 31 | $(NBDIR)/06_visualize_hemodynamic_response.ipynb \ 32 | $(NBDIR)/08_fit_motion_energy_model.ipynb \ 33 | $(NBDIR)/09_fit_banded_ridge_model.ipynb \ 34 | > $(NBDIR)/vem_tutorials_merged_for_colab.ipynb 35 | echo "Saved in $(NBDIR)/vem_tutorials_merged_for_colab.ipynb" 36 | 37 | python merge_notebooks.py \ 38 | $(NBDIR)/01_setup_colab.ipynb \ 39 | $(NBDIR)/02_download_shortclips.ipynb \ 40 | $(NBDIR)/03_compute_explainable_variance.ipynb \ 41 | $(NBDIR)/05_fit_wordnet_model.ipynb \ 42 | $(NBDIR)/08_fit_motion_energy_model.ipynb \ 43 | $(NBDIR)/09_fit_banded_ridge_model.ipynb \ 44 | > $(NBDIR)/vem_tutorials_merged_for_colab_model_fitting.ipynb 45 | echo "Saved in $(NBDIR)/vem_tutorials_merged_for_colab_model_fitting.ipynb" 46 | 47 | build: 48 | jupyter book build --all $(SOURCEDIR) 49 | 50 | preview: 51 | python -m http.server --directory _build/html 8000 52 | @echo "Preview the book at http://localhost:8000" 53 | @echo "Press Ctrl+C to stop the server" 54 | 55 | # -b gh_pages --single-branch (to clone only one branch) 56 | # --no-checkout (just fetches the root folder without content) 57 | # --depth 1 (since we don't need the history prior to the last commit) 58 | push-pages: 59 | rm -rf _build/gh_pages 60 | git clone -b gh-pages --single-branch --no-checkout --depth 1 \ 61 | https://github.com/gallantlab/voxelwise_tutorials _build/gh_pages 62 | 63 | cd _build/ && \ 64 | cp -r html/* gh_pages && \ 65 | cd gh_pages && \ 66 | touch .nojekyll && \ 67 | echo "*.ipynb -diff" > .gitattributes && \ 68 | git add * && \ 69 | git add .nojekyll .gitattributes && \ 70 | git commit -a -m 'Make push-pages' && \ 71 | git push 72 | -------------------------------------------------------------------------------- /tutorials/README.md: -------------------------------------------------------------------------------- 1 | # How to build the tutorials website 2 | 3 | Everything can be managed through the Makefile. 4 | The website is built using `jupyter-book`. 5 | Install all the required dependencies with: 6 | 7 | ```bash 8 | git clone https://github.com/gallantlab/voxelwise_tutorials.git 9 | cd voxelwise_tutorials 10 | python -m pip install ".[github,docs]" 11 | ``` 12 | 13 | ## Build the website 14 | 15 | ```bash 16 | make clean # Remove all files generated by the website build 17 | make build # Build the website 18 | ``` 19 | 20 | If your machine has a GPU available, it takes around 10 minutes to run the examples 21 | and build the entire website. I don't recommend building it on a CPU, as it will take 22 | a lot, lot longer. Successive builds will be faster, as the rendered notebooks are 23 | cached and re-run only if they are modified. 24 | 25 | ## Preview the website 26 | 27 | ```bash 28 | make preview # Build and serve the website locally 29 | ``` 30 | 31 | Then open your browser at `http://localhost:8000`. 32 | 33 | ## Publish the website 34 | 35 | ```bash 36 | make publish # Publish the website to GitHub Pages 37 | ``` 38 | 39 | ## Merge notebooks 40 | 41 | ```bash 42 | make merge-notebooks # Create a new version of the merged notebooks 43 | ``` -------------------------------------------------------------------------------- /tutorials/_config.yml: -------------------------------------------------------------------------------- 1 | # Book settings 2 | # Learn more at https://jupyterbook.org/customize/config.html 3 | 4 | title: Voxelwise Encoding Model tutorials 5 | author: Tom Dupré la Tour, Matteo Visconti di Oleggio Castello, Jack L. Gallant 6 | logo: static/flatmap.png 7 | 8 | # Force re-execution of notebooks on each build. 9 | # See https://jupyterbook.org/content/execute.html 10 | execute: 11 | execute_notebooks: cache 12 | timeout: -1 # do not timeout 13 | exclude_patterns: 14 | - notebooks/shortclips/00_setup_colab.ipynb 15 | - notebooks/shortclips/07_extract_motion_energy.ipynb 16 | - notebooks/shortclips/vem_tutorials_merged_for_colab_model_fitting.ipynb 17 | - notebooks/shortclips/vem_tutorials_merged_for_colab.ipynb 18 | - notebooks/vim2/00_download_vim2.ipynb 19 | - notebooks/vim2/01_extract_motion_energy.ipynb 20 | - notebooks/vim2/02_plot_ridge_model.ipynb 21 | 22 | # Define the name of the latex output file for PDF builds 23 | latex: 24 | latex_documents: 25 | targetname: book.tex 26 | 27 | # Add a bibtex file so that we can create citations 28 | bibtex_bibfiles: 29 | - static/references.bib 30 | 31 | # Information about where the book exists on the web 32 | repository: 33 | url: https://github.com/gallantlab/voxelwise_tutorials # Online location of your book 34 | path_to_book: docs # Optional path to your book, relative to the repository root 35 | branch: main # Which branch of the repository should be used when creating links (optional) 36 | 37 | # Add GitHub buttons to your book 38 | # See https://jupyterbook.org/customize/config.html#add-a-link-to-your-repository 39 | html: 40 | use_issues_button: true 41 | use_repository_button: true 42 | 43 | parse: 44 | myst_enable_extensions: 45 | - amsmath 46 | - dollarmath 47 | - colon_fence 48 | - linkify 49 | 50 | sphinx: 51 | config: 52 | bibtex_reference_style: author_year 53 | # bibtex_bibfiles: 'references.bib' 54 | 55 | -------------------------------------------------------------------------------- /tutorials/_toc.yml: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | # Learn more at https://jupyterbook.org/customize/toc.html 3 | 4 | format: jb-article 5 | root: pages/index 6 | sections: 7 | - file: pages/voxelwise_modeling 8 | title: Overview of the VEM framework 9 | - file: notebooks/shortclips/README 10 | title: Shortclips tutorial 11 | sections: 12 | - file: notebooks/shortclips/02_download_shortclips 13 | - file: notebooks/shortclips/03_compute_explainable_variance 14 | - file: notebooks/shortclips/04_understand_ridge_regression 15 | - file: notebooks/shortclips/05_fit_wordnet_model 16 | - file: notebooks/shortclips/06_visualize_hemodynamic_response 17 | - file: notebooks/shortclips/07_extract_motion_energy 18 | - file: notebooks/shortclips/08_fit_motion_energy_model 19 | - file: notebooks/shortclips/09_fit_banded_ridge_model 20 | - file: notebooks/vim2/README 21 | title: Vim-2 tutorial (optional) 22 | sections: 23 | - file: notebooks/vim2/00_download_vim2 24 | - file: notebooks/vim2/01_extract_motion_energy 25 | - file: notebooks/vim2/02_plot_ridge_model 26 | - file: pages/references 27 | - file: pages/voxelwise_package -------------------------------------------------------------------------------- /tutorials/merge_notebooks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Inspired from https://gist.github.com/fperez/e2bbc0a208e82e450f69 3 | """ 4 | usage: 5 | python merge_notebooks.py A.ipynb B.ipynb C.ipynb > merged.ipynb 6 | """ 7 | import io 8 | import os.path 9 | import sys 10 | 11 | import nbformat 12 | 13 | 14 | def merge_notebooks(filenames): 15 | merged = None 16 | for fname in filenames: 17 | with io.open(fname, 'r', encoding='utf-8') as f: 18 | nb = nbformat.read(f, as_version=4) 19 | if merged is None: 20 | merged = nb 21 | # add a Markdown cell with the file name, then all cells 22 | # merged.cells = [cell_with_title(title=os.path.basename(fname)) 23 | # ] + nb.cells 24 | # Do not add the title cell, use the title in each notebook 25 | merged.cells = nb.cells 26 | else: 27 | # add a code cell resetting all variables 28 | merged.cells.append(cell_with_reset()) 29 | # add a Markdown cell with the file name 30 | # merged.cells.append(cell_with_title(title=os.path.basename(fname))) 31 | # add all cells from current notebook 32 | merged.cells.extend(nb.cells) 33 | 34 | if not hasattr(merged.metadata, 'name'): 35 | merged.metadata.name = '' 36 | merged.metadata.name += "_merged" 37 | print(nbformat.writes(merged)) 38 | 39 | 40 | def cell_with_title(title): 41 | """Returns a Markdown cell with a title.""" 42 | return nbformat.from_dict({ 43 | 'cell_type': 'markdown', 44 | 'metadata': {}, 45 | 'source': f'\n# {title}\n', 46 | }) 47 | 48 | 49 | def cell_with_reset(): 50 | """Returns a code cell with magic command to reset all variables.""" 51 | return nbformat.from_dict({ 52 | 'cell_type': 'code', 53 | 'execution_count': None, 54 | 'metadata': {'collapsed': False}, 55 | 'outputs': [], 56 | 'source': '%reset -f', 57 | }) 58 | 59 | 60 | if __name__ == '__main__': 61 | notebooks = sys.argv[1:] 62 | if not notebooks: 63 | print(__doc__, file=sys.stderr) 64 | sys.exit(1) 65 | 66 | merge_notebooks(notebooks) 67 | -------------------------------------------------------------------------------- /tutorials/notebooks/README.md: -------------------------------------------------------------------------------- 1 | # Jupyter notebooks 2 | 3 | This directory contains the jupyter notebooks of the VEM tutorials. 4 | To see rendered versions of the tutorials, go to the 5 | [tutorials website](https://gallantlab.github.io/voxelwise_tutorials). 6 | 7 | ## Run the notebooks 8 | To run the notebooks yourself, you first need to download this repository and 9 | install the `voxelwise_tutorials` package: 10 | 11 | ```bash 12 | git clone https://github.com/gallantlab/voxelwise_tutorials.git 13 | cd voxelwise_tutorials 14 | pip install . 15 | ``` 16 | 17 | For more info about how to run jupyter notebooks, see 18 | [the official documentation](https://jupyter-notebook.readthedocs.io/en/stable/). 19 | -------------------------------------------------------------------------------- /tutorials/notebooks/shortclips/01_setup_colab.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\n", 8 | "# Setup Google Colab\n", 9 | "\n", 10 | "In this script, we setup a Google Colab environment. This script will only work\n", 11 | "when run from [Google Colab](https://colab.research.google.com/). You can\n", 12 | "skip it if you run the tutorials on your machine.\n", 13 | "\n", 14 | "> **Note:** This script will install all the required dependencies and download the data.\n", 15 | "> It will take around 10 minutes to run, but you need to run it only once in your Colab session.\n", 16 | "> If your Colab session is disconnected, you will need to run this script again." 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Change runtime to use a GPU\n", 24 | "\n", 25 | "This tutorial is much faster when a GPU is available to run the computations.\n", 26 | "In Google Colab you can request access to a GPU by changing the runtime type.\n", 27 | "To do so, click the following menu options in Google Colab:\n", 28 | "\n", 29 | "> (Menu) \"Runtime\" -> \"Change runtime type\" -> \"Hardware accelerator\" -> \"GPU\".\n", 30 | "\n" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "## Install all required dependencies\n", 38 | "\n", 39 | "Uncomment and run the following cell to download the required packages." 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": { 46 | "collapsed": false 47 | }, 48 | "outputs": [], 49 | "source": [ 50 | "#!git config --global user.email \"you@example.com\" && git config --global user.name \"Your Name\"\n", 51 | "#!wget -O- http://neuro.debian.net/lists/jammy.us-ca.libre | sudo tee /etc/apt/sources.list.d/neurodebian.sources.list\n", 52 | "#!apt-key adv --recv-keys --keyserver hkps://keyserver.ubuntu.com 0xA5D32F012649A5A9 > /dev/null\n", 53 | "#!apt-get -qq update > /dev/null\n", 54 | "#!apt-get install -qq inkscape git-annex-standalone > /dev/null\n", 55 | "#!pip install -q voxelwise_tutorials" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "For the record, here is what each command does:\n", 63 | "\n" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": { 70 | "collapsed": false 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "# - Set up an email and username to use git, git-annex, and datalad (required to download the data)\n", 75 | "# - Add NeuroDebian to the package sources\n", 76 | "# - Update the gpg keys to use NeuroDebian\n", 77 | "# - Update the list of available packages\n", 78 | "# - Install Inkscape to use more features from Pycortex, and install git-annex to download the data\n", 79 | "# - Install the tutorial helper package, and all the required dependencies" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": { 86 | "collapsed": false 87 | }, 88 | "outputs": [], 89 | "source": [ 90 | "try:\n", 91 | " import google.colab # noqa\n", 92 | " in_colab = True\n", 93 | "except ImportError:\n", 94 | " in_colab = False\n", 95 | "if not in_colab:\n", 96 | " raise RuntimeError(\"This script is only meant to be run from Google \"\n", 97 | " \"Colab. You can skip it if you run the tutorials \"\n", 98 | " \"on your machine.\")" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "Now run the following cell to set up the environment variables for the\n", 106 | "tutorials and pycortex.\n", 107 | "\n" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": null, 113 | "metadata": { 114 | "collapsed": false 115 | }, 116 | "outputs": [], 117 | "source": [ 118 | "import os\n", 119 | "os.environ['VOXELWISE_TUTORIALS_DATA'] = \"/content\"\n", 120 | "\n", 121 | "import sklearn\n", 122 | "sklearn.set_config(assume_finite=True)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": {}, 128 | "source": [ 129 | "Your Google Colab environment is now set up for the voxelwise tutorials.\n", 130 | "\n" 131 | ] 132 | } 133 | ], 134 | "metadata": { 135 | "kernelspec": { 136 | "display_name": "Python 3", 137 | "language": "python", 138 | "name": "python3" 139 | }, 140 | "language_info": { 141 | "codemirror_mode": { 142 | "name": "ipython", 143 | "version": 3 144 | }, 145 | "file_extension": ".py", 146 | "mimetype": "text/x-python", 147 | "name": "python", 148 | "nbconvert_exporter": "python", 149 | "pygments_lexer": "ipython3", 150 | "version": "3.7.12" 151 | } 152 | }, 153 | "nbformat": 4, 154 | "nbformat_minor": 0 155 | } 156 | -------------------------------------------------------------------------------- /tutorials/notebooks/shortclips/02_download_shortclips.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\n", 8 | "# Download the data set\n", 9 | "\n", 10 | "In this script, we download the data set from Wasabi or GIN. No account is required.\n", 11 | "\n", 12 | ":::{note}\n", 13 | "This script will download approximately 2GB of data.\n", 14 | ":::\n", 15 | "\n", 16 | "\n", 17 | "## Cite this data set\n", 18 | "\n", 19 | "This tutorial is based on publicly available data [published on GIN](https://gin.g-node.org/gallantlab/shortclips). If you publish any work using\n", 20 | "this data set, please cite the original publication {cite}`huth2012`, and the data set {cite}`huth2022data`." 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "## Download\n", 28 | "\n" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": { 35 | "collapsed": false 36 | }, 37 | "outputs": [], 38 | "source": [ 39 | "# path of the data directory\n", 40 | "from voxelwise_tutorials.io import get_data_home\n", 41 | "directory = get_data_home(dataset=\"shortclips\")\n", 42 | "print(directory)" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "We will only use the first subject in this tutorial, but you can run the same\n", 50 | "analysis on the four other subjects. Uncomment the lines in ``DATAFILES`` to\n", 51 | "download more subjects.\n", 52 | "\n", 53 | "We also skip the stimuli files, since the dataset provides two preprocessed\n", 54 | "feature spaces to fit voxelwise encoding models without requiring the original\n", 55 | "stimuli." 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "metadata": { 62 | "collapsed": false 63 | }, 64 | "outputs": [], 65 | "source": [ 66 | "from voxelwise_tutorials.io import download_datalad\n", 67 | "\n", 68 | "DATAFILES = [\n", 69 | " \"features/motion_energy.hdf\",\n", 70 | " \"features/wordnet.hdf\",\n", 71 | " \"mappers/S01_mappers.hdf\",\n", 72 | " # \"mappers/S02_mappers.hdf\",\n", 73 | " # \"mappers/S03_mappers.hdf\",\n", 74 | " # \"mappers/S04_mappers.hdf\",\n", 75 | " # \"mappers/S05_mappers.hdf\",\n", 76 | " \"responses/S01_responses.hdf\",\n", 77 | " # \"responses/S02_responses.hdf\",\n", 78 | " # \"responses/S03_responses.hdf\",\n", 79 | " # \"responses/S04_responses.hdf\",\n", 80 | " # \"responses/S05_responses.hdf\",\n", 81 | " # \"stimuli/test.hdf\",\n", 82 | " # \"stimuli/train_00.hdf\",\n", 83 | " # \"stimuli/train_01.hdf\",\n", 84 | " # \"stimuli/train_02.hdf\",\n", 85 | " # \"stimuli/train_03.hdf\",\n", 86 | " # \"stimuli/train_04.hdf\",\n", 87 | " # \"stimuli/train_05.hdf\",\n", 88 | " # \"stimuli/train_06.hdf\",\n", 89 | " # \"stimuli/train_07.hdf\",\n", 90 | " # \"stimuli/train_08.hdf\",\n", 91 | " # \"stimuli/train_09.hdf\",\n", 92 | " # \"stimuli/train_10.hdf\",\n", 93 | " # \"stimuli/train_11.hdf\",\n", 94 | "]\n", 95 | "\n", 96 | "source = \"https://gin.g-node.org/gallantlab/shortclips\"\n", 97 | "\n", 98 | "for datafile in DATAFILES:\n", 99 | " local_filename = download_datalad(datafile, destination=directory,\n", 100 | " source=source)" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "## References\n", 108 | "\n", 109 | "```{bibliography}\n", 110 | ":filter: docname in docnames\n", 111 | "```" 112 | ] 113 | } 114 | ], 115 | "metadata": { 116 | "kernelspec": { 117 | "display_name": "Python 3", 118 | "language": "python", 119 | "name": "python3" 120 | }, 121 | "language_info": { 122 | "codemirror_mode": { 123 | "name": "ipython", 124 | "version": 3 125 | }, 126 | "file_extension": ".py", 127 | "mimetype": "text/x-python", 128 | "name": "python", 129 | "nbconvert_exporter": "python", 130 | "pygments_lexer": "ipython3", 131 | "version": "3.7.12" 132 | } 133 | }, 134 | "nbformat": 4, 135 | "nbformat_minor": 0 136 | } 137 | -------------------------------------------------------------------------------- /tutorials/notebooks/shortclips/03_compute_explainable_variance.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\n", 8 | "# Compute the explainable variance\n", 9 | "\n", 10 | "Before fitting any voxelwise model to fMRI responses, it is good practice to\n", 11 | "quantify the amount of signal in the test set that can be predicted by an\n", 12 | "encoding model. This quantity is called the *explainable variance*.\n", 13 | "\n", 14 | "The measured signal can be decomposed into a sum of two components: the\n", 15 | "stimulus-dependent signal and noise. If we present the same stimulus multiple\n", 16 | "times and we record brain activity for each repetition, the stimulus-dependent\n", 17 | "signal will be the same across repetitions while the noise will vary across\n", 18 | "repetitions. In the Voxelwise Encoding Model framework, \n", 19 | "the features used to model brain activity are the same for each repetition of the \n", 20 | "stimulus. Thus, encoding models will predict only the repeatable stimulus-dependent \n", 21 | "signal.\n", 22 | "\n", 23 | "The stimulus-dependent signal can be estimated by taking the mean of brain\n", 24 | "responses over repeats of the same stimulus or experiment. The variance of the\n", 25 | "estimated stimulus-dependent signal, which we call the explainable variance, is\n", 26 | "proportional to the maximum prediction accuracy that can be obtained by a\n", 27 | "voxelwise encoding model in the test set.\n", 28 | "\n", 29 | "Mathematically, let $y_i, i = 1 \\dots N$ be the measured signal in a\n", 30 | "voxel for each of the $N$ repetitions of the same stimulus and\n", 31 | "$\\bar{y} = \\frac{1}{N}\\sum_{i=1}^Ny_i$ the average brain response\n", 32 | "across repetitions. For each repeat, we define the residual timeseries between\n", 33 | "brain response and average brain response as $r_i = y_i - \\bar{y}$. The\n", 34 | "explainable variance (EV) is estimated as\n", 35 | "\n", 36 | "\\begin{align}\\text{EV} = \\frac{1}{N}\\sum_{i=1}^N\\text{Var}(y_i) - \\frac{N}{N-1}\\sum_{i=1}^N\\text{Var}(r_i)\\end{align}\n", 37 | "\n", 38 | "\n", 39 | "In the literature, the explainable variance is also known as the *signal\n", 40 | "power*. \n", 41 | "\n", 42 | "For more information, see {cite}`Sahani2002,Hsu2004,Schoppe2016`." 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "## Path of the data directory\n", 50 | "\n" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": { 57 | "collapsed": false 58 | }, 59 | "outputs": [], 60 | "source": [ 61 | "from voxelwise_tutorials.io import get_data_home\n", 62 | "\n", 63 | "directory = get_data_home(dataset=\"shortclips\")\n", 64 | "print(directory)" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": { 71 | "collapsed": false 72 | }, 73 | "outputs": [], 74 | "source": [ 75 | "# modify to use another subject\n", 76 | "subject = \"S01\"" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "## Compute the explainable variance\n", 84 | "\n" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "metadata": { 91 | "collapsed": false 92 | }, 93 | "outputs": [], 94 | "source": [ 95 | "import numpy as np\n", 96 | "from voxelwise_tutorials.io import load_hdf5_array" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "First, we load the fMRI responses on the test set, which contains brain\n", 104 | "responses to ten (10) repeats of the same stimulus.\n", 105 | "\n" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "metadata": { 112 | "collapsed": false 113 | }, 114 | "outputs": [], 115 | "source": [ 116 | "import os\n", 117 | "\n", 118 | "file_name = os.path.join(directory, 'responses', f'{subject}_responses.hdf')\n", 119 | "Y_test = load_hdf5_array(file_name, key=\"Y_test\")\n", 120 | "print(\"(n_repeats, n_samples_test, n_voxels) =\", Y_test.shape)" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "Then, we compute the explainable variance for each voxel.\n", 128 | "\n" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": { 135 | "collapsed": false 136 | }, 137 | "outputs": [], 138 | "source": [ 139 | "from voxelwise_tutorials.utils import explainable_variance\n", 140 | "\n", 141 | "ev = explainable_variance(Y_test)\n", 142 | "print(\"(n_voxels,) =\", ev.shape)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "To better understand the concept of explainable variance, we can plot the\n", 150 | "measured signal in a voxel with high explainable variance...\n", 151 | "\n" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "metadata": { 158 | "collapsed": false 159 | }, 160 | "outputs": [], 161 | "source": [ 162 | "import matplotlib.pyplot as plt\n", 163 | "\n", 164 | "voxel_1 = np.argmax(ev)\n", 165 | "time = np.arange(Y_test.shape[1]) * 2 # one time point every 2 seconds\n", 166 | "plt.figure(figsize=(10, 3))\n", 167 | "plt.plot(time, Y_test[:, :, voxel_1].T, color='C0', alpha=0.5)\n", 168 | "plt.plot(time, Y_test[:, :, voxel_1].mean(0), color='C1', label='average')\n", 169 | "plt.xlabel(\"Time (sec)\")\n", 170 | "plt.title(\"Voxel with large explainable variance (%.2f)\" % ev[voxel_1])\n", 171 | "plt.yticks([])\n", 172 | "plt.legend()\n", 173 | "plt.tight_layout()\n", 174 | "plt.show()" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "... and in a voxel with low explainable variance.\n", 182 | "\n" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "metadata": { 189 | "collapsed": false 190 | }, 191 | "outputs": [], 192 | "source": [ 193 | "voxel_2 = np.argmin(ev)\n", 194 | "plt.figure(figsize=(10, 3))\n", 195 | "plt.plot(time, Y_test[:, :, voxel_2].T, color='C0', alpha=0.5)\n", 196 | "plt.plot(time, Y_test[:, :, voxel_2].mean(0), color='C1', label='average')\n", 197 | "plt.xlabel(\"Time (sec)\")\n", 198 | "plt.title(\"Voxel with low explainable variance (%.2f)\" % ev[voxel_2])\n", 199 | "plt.yticks([])\n", 200 | "plt.legend()\n", 201 | "plt.tight_layout()\n", 202 | "plt.show()" 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "metadata": {}, 208 | "source": [ 209 | "We can also plot the distribution of explainable variance over voxels.\n", 210 | "\n" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": null, 216 | "metadata": { 217 | "collapsed": false 218 | }, 219 | "outputs": [], 220 | "source": [ 221 | "plt.hist(ev, bins=np.linspace(0, 1, 100), log=True, histtype='step')\n", 222 | "plt.xlabel(\"Explainable variance\")\n", 223 | "plt.ylabel(\"Number of voxels\")\n", 224 | "plt.title('Histogram of explainable variance')\n", 225 | "plt.grid('on')\n", 226 | "plt.show()" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": {}, 232 | "source": [ 233 | "We see that many voxels have low explainable variance. This is\n", 234 | "expected, since many voxels are not driven by a visual stimulus, and their\n", 235 | "response changes over repeats of the same stimulus.\n", 236 | "We also see that some voxels have high explainable variance (around 0.7). The\n", 237 | "responses in these voxels are highly consistent across repetitions of the\n", 238 | "same stimulus. Thus, they are good targets for encoding models.\n", 239 | "\n" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "## Map to subject flatmap\n", 247 | "\n", 248 | "To better understand the distribution of explainable variance, we map the\n", 249 | "values to the subject brain. This can be done with [pycortex](https://gallantlab.github.io/pycortex/), which can create interactive 3D\n", 250 | "viewers to be displayed in any modern browser. ``pycortex`` can also display\n", 251 | "flattened maps of the cortical surface to visualize the entire cortical\n", 252 | "surface at once.\n", 253 | "\n", 254 | "Here, we do not share the anatomical information of the subjects for privacy\n", 255 | "concerns. Instead, we provide two mappers:\n", 256 | "\n", 257 | "- to map the voxels to a (subject-specific) flatmap\n", 258 | "- to map the voxels to the Freesurfer average cortical surface (\"fsaverage\")\n", 259 | "\n", 260 | "The first mapper is 2D matrix of shape (n_pixels, n_voxels) that maps each\n", 261 | "voxel to a set of pixel in a flatmap. The matrix is efficiently stored in a\n", 262 | "``scipy`` sparse CSR matrix. The function ``plot_flatmap_from_mapper``\n", 263 | "provides an example of how to use the mapper and visualize the flatmap.\n", 264 | "\n" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "metadata": { 271 | "collapsed": false 272 | }, 273 | "outputs": [], 274 | "source": [ 275 | "from voxelwise_tutorials.viz import plot_flatmap_from_mapper\n", 276 | "\n", 277 | "mapper_file = os.path.join(directory, 'mappers', f'{subject}_mappers.hdf')\n", 278 | "plot_flatmap_from_mapper(ev, mapper_file, vmin=0, vmax=0.7)\n", 279 | "plt.show()" 280 | ] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "metadata": {}, 285 | "source": [ 286 | "This figure is a flattened map of the cortical surface. A number of regions\n", 287 | "of interest (ROIs) have been labeled to ease interpretation. If you have\n", 288 | "never seen such a flatmap, we recommend taking a look at a [pycortex brain\n", 289 | "viewer](https://www.gallantlab.org/brainviewer/Deniz2019), which displays\n", 290 | "the brain in 3D. In this viewer, press \"I\" to inflate the brain, \"F\" to\n", 291 | "flatten the surface, and \"R\" to reset the view (or use the ``surface/unfold``\n", 292 | "cursor on the right menu). Press \"H\" for a list of all keyboard shortcuts.\n", 293 | "This viewer should help you understand the correspondence between the flatten\n", 294 | "and the folded cortical surface of the brain.\n", 295 | "\n" 296 | ] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "metadata": {}, 301 | "source": [ 302 | "On this flatmap, we can see that the explainable variance is mainly located\n", 303 | "in the visual cortex, in early visual regions like V1, V2, V3, or in\n", 304 | "higher-level regions like EBA, FFA or IPS. This is expected since this\n", 305 | "dataset contains responses to a visual stimulus.\n", 306 | "\n" 307 | ] 308 | }, 309 | { 310 | "cell_type": "markdown", 311 | "metadata": {}, 312 | "source": [ 313 | "## Map to \"fsaverage\"\n", 314 | "\n", 315 | "The second mapper we provide maps the voxel data to a Freesurfer\n", 316 | "average surface (\"fsaverage\"), that can be used in ``pycortex``.\n", 317 | "\n", 318 | "First, let's download the \"fsaverage\" surface.\n", 319 | "\n" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": null, 325 | "metadata": { 326 | "collapsed": false 327 | }, 328 | "outputs": [], 329 | "source": [ 330 | "import cortex\n", 331 | "\n", 332 | "surface = \"fsaverage\"\n", 333 | "\n", 334 | "if not hasattr(cortex.db, surface):\n", 335 | " cortex.utils.download_subject(subject_id=surface,\n", 336 | " pycortex_store=cortex.db.filestore)\n", 337 | " cortex.db.reload_subjects() # force filestore reload\n", 338 | " assert hasattr(cortex.db, surface)" 339 | ] 340 | }, 341 | { 342 | "cell_type": "markdown", 343 | "metadata": {}, 344 | "source": [ 345 | "Then, we load the \"fsaverage\" mapper. The mapper is a matrix of shape\n", 346 | "(n_vertices, n_voxels), which maps each voxel to some vertices in the\n", 347 | "fsaverage surface. It is stored as a sparse CSR matrix. The mapper is applied\n", 348 | "with a dot product ``@`` (equivalent to ``np.dot``).\n", 349 | "\n" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": null, 355 | "metadata": { 356 | "collapsed": false 357 | }, 358 | "outputs": [], 359 | "source": [ 360 | "from voxelwise_tutorials.io import load_hdf5_sparse_array\n", 361 | "\n", 362 | "voxel_to_fsaverage = load_hdf5_sparse_array(mapper_file,\n", 363 | " key='voxel_to_fsaverage')\n", 364 | "ev_projected = voxel_to_fsaverage @ ev\n", 365 | "print(\"(n_vertices,) =\", ev_projected.shape)" 366 | ] 367 | }, 368 | { 369 | "cell_type": "markdown", 370 | "metadata": {}, 371 | "source": [ 372 | "We can then create a ``Vertex`` object in ``pycortex``, containing the\n", 373 | "projected data. This object can be used either in a ``pycortex`` interactive\n", 374 | "3D viewer, or in a ``matplotlib`` figure showing only the flatmap.\n", 375 | "\n" 376 | ] 377 | }, 378 | { 379 | "cell_type": "code", 380 | "execution_count": null, 381 | "metadata": { 382 | "collapsed": false 383 | }, 384 | "outputs": [], 385 | "source": [ 386 | "vertex = cortex.Vertex(ev_projected, surface, vmin=0, vmax=0.7, cmap='viridis')" 387 | ] 388 | }, 389 | { 390 | "cell_type": "markdown", 391 | "metadata": {}, 392 | "source": [ 393 | "To start an interactive 3D viewer in the browser, we can use the ``webshow``\n", 394 | "function in pycortex. (Note that this method works only if you are running the\n", 395 | "notebooks locally.) You can start an interactive 3D viewer by changing\n", 396 | "``run_webshow`` to ``True`` and running the following cell.\n", 397 | "\n" 398 | ] 399 | }, 400 | { 401 | "cell_type": "code", 402 | "execution_count": null, 403 | "metadata": { 404 | "collapsed": false 405 | }, 406 | "outputs": [], 407 | "source": [ 408 | "run_webshow = False\n", 409 | "if run_webshow:\n", 410 | " cortex.webshow(vertex, open_browser=False, port=8050)" 411 | ] 412 | }, 413 | { 414 | "cell_type": "markdown", 415 | "metadata": {}, 416 | "source": [ 417 | "Alternatively, to plot a flatmap in a ``matplotlib`` figure, use the\n", 418 | "`quickshow` function.\n", 419 | "\n", 420 | "(This function requires Inkscape to be installed. The rest of the tutorial\n", 421 | "does not use this function, so feel free to ignore.)\n", 422 | "\n" 423 | ] 424 | }, 425 | { 426 | "cell_type": "code", 427 | "execution_count": null, 428 | "metadata": { 429 | "collapsed": false 430 | }, 431 | "outputs": [], 432 | "source": [ 433 | "from cortex.testing_utils import has_installed\n", 434 | "\n", 435 | "fig = cortex.quickshow(vertex, colorbar_location='right',\n", 436 | " with_rois=has_installed(\"inkscape\"))\n", 437 | "plt.show()" 438 | ] 439 | }, 440 | { 441 | "cell_type": "markdown", 442 | "metadata": {}, 443 | "source": [ 444 | "## References\n", 445 | "```{bibliography}\n", 446 | ":filter: docname in docnames\n", 447 | "```" 448 | ] 449 | } 450 | ], 451 | "metadata": { 452 | "kernelspec": { 453 | "display_name": "Python 3", 454 | "language": "python", 455 | "name": "python3" 456 | }, 457 | "language_info": { 458 | "codemirror_mode": { 459 | "name": "ipython", 460 | "version": 3 461 | }, 462 | "file_extension": ".py", 463 | "mimetype": "text/x-python", 464 | "name": "python", 465 | "nbconvert_exporter": "python", 466 | "pygments_lexer": "ipython3", 467 | "version": "3.7.12" 468 | } 469 | }, 470 | "nbformat": 4, 471 | "nbformat_minor": 0 472 | } 473 | -------------------------------------------------------------------------------- /tutorials/notebooks/shortclips/07_extract_motion_energy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Extract motion-energy features from the stimuli\n", 8 | "\n", 9 | "This example shows how to extract motion-energy features from the stimuli. Note that the public dataset\n", 10 | "already contains precomputed motion-energy features. Therefore, you do not need to run this script to fit motion-energy models in other parts of this tutorial.\n", 11 | "\n", 12 | ":::{admonition} Long running time!\n", 13 | ":class: warning\n", 14 | "Running this example takes a couple of hours.\n", 15 | ":::\n", 16 | "\n", 17 | "Motion-energy features result from filtering a video\n", 18 | "stimulus with spatio-temporal Gabor filters. A pyramid of filters is used to\n", 19 | "compute the motion-energy features at multiple spatial and temporal scales.\n", 20 | "Motion-energy features were introduced in {cite:t}`nishimoto2011`.\n", 21 | "\n", 22 | "The motion-energy extraction is performed by the package \n", 23 | "[pymoten](https://github.com/gallantlab/pymoten) {cite}`nunez2021software`.\n", 24 | "\n", 25 | "Check the pymoten [gallery of examples](https://gallantlab.github.io/pymoten/auto_examples/index.html) for\n", 26 | "visualizing motion-energy filters, and for pymoten API usage examples." 27 | ] 28 | }, 29 | { 30 | "cell_type": "markdown", 31 | "metadata": {}, 32 | "source": [ 33 | "## Path of the data directory" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": { 40 | "collapsed": false 41 | }, 42 | "outputs": [], 43 | "source": [ 44 | "from voxelwise_tutorials.io import get_data_home\n", 45 | "directory = get_data_home(dataset=\"shortclips\")\n", 46 | "print(directory)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "## Load the stimuli images\n", 54 | "\n", 55 | "Here the data is not loaded in memory, we only take a peek at the data shape.\n", 56 | "\n" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "metadata": { 63 | "collapsed": false 64 | }, 65 | "outputs": [], 66 | "source": [ 67 | "import os\n", 68 | "import h5py\n", 69 | "\n", 70 | "first_file_name = os.path.join(directory, 'stimuli', 'train_00.hdf')\n", 71 | "print(f\"Content of {first_file_name}:\")\n", 72 | "with h5py.File(first_file_name, 'r') as f:\n", 73 | " for key in f.keys():\n", 74 | " print(f[key])" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "## Compute the luminance\n", 82 | "\n", 83 | "The motion energy is typically not computed on RGB (color) images,\n", 84 | "but on the luminance channel of the LAB color space.\n", 85 | "To avoid loading the entire simulus array in memory, we use batches of data.\n", 86 | "These batches can be arbitrary, since the luminance is computed independently\n", 87 | "on each image.\n", 88 | "\n" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": { 95 | "collapsed": false 96 | }, 97 | "outputs": [], 98 | "source": [ 99 | "import numpy as np\n", 100 | "from moten.io import imagearray2luminance\n", 101 | "\n", 102 | "from himalaya.progress_bar import bar\n", 103 | "from voxelwise_tutorials.io import load_hdf5_array\n", 104 | "\n", 105 | "\n", 106 | "def compute_luminance(run_name, size=(96, 96), batch_size=100):\n", 107 | "\n", 108 | " stimuli_file = os.path.join(directory, 'stimuli', run_name)\n", 109 | "\n", 110 | " # get the number of images in the stimuli file\n", 111 | " with h5py.File(stimuli_file, 'r') as f:\n", 112 | " n_images = f['stimuli'].shape[0]\n", 113 | "\n", 114 | " # compute the luminance on each batch\n", 115 | " luminance = np.zeros((n_images, *size))\n", 116 | " for start in bar(range(0, n_images, batch_size),\n", 117 | " title=f'compute_luminance({run_name})'):\n", 118 | " # load the batch of images\n", 119 | " batch = slice(start, start + batch_size)\n", 120 | " images = load_hdf5_array(stimuli_file, key='stimuli', slice=batch)\n", 121 | "\n", 122 | " # ``imagearray2luminance`` uses uint8 arrays\n", 123 | " if images.dtype != 'uint8':\n", 124 | " images = np.int_(np.clip(images, 0, 1) * 255).astype(np.uint8)\n", 125 | "\n", 126 | " # convert RGB images to a single luminance channel\n", 127 | " luminance[batch] = imagearray2luminance(images, size=size)\n", 128 | "\n", 129 | " return luminance\n", 130 | "\n", 131 | "\n", 132 | "luminance_train = np.concatenate(\n", 133 | " [compute_luminance(f\"train_{ii:02d}.hdf\") for ii in range(12)])\n", 134 | "luminance_test = compute_luminance(\"test.hdf\")" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "## Compute the motion energy\n", 142 | "\n", 143 | "This is done with a ``MotionEnergyPyramid`` object of the ``pymoten``\n", 144 | "package. The parameters used are the one described in {cite:t}`nishimoto2011`.\n", 145 | "\n", 146 | "Here we use batches corresponding to run lengths. Indeed, motion energy is\n", 147 | "computed over multiple images, since the filters have a temporal component.\n", 148 | "Therefore, motion-energy is not independent of other images, and we cannot\n", 149 | "arbitrarily split the images.\n", 150 | "\n" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": { 157 | "collapsed": false 158 | }, 159 | "outputs": [], 160 | "source": [ 161 | "from scipy.signal import decimate\n", 162 | "from moten.pyramids import MotionEnergyPyramid\n", 163 | "\n", 164 | "# fixed experiment settings\n", 165 | "N_FRAMES_PER_SEC = 15\n", 166 | "N_FRAMES_PER_TR = 30\n", 167 | "N_TRS_PER_RUN = 300\n", 168 | "\n", 169 | "\n", 170 | "def compute_motion_energy(luminance,\n", 171 | " batch_size=N_TRS_PER_RUN * N_FRAMES_PER_TR,\n", 172 | " noise=0.1):\n", 173 | "\n", 174 | " n_frames, height, width = luminance.shape\n", 175 | "\n", 176 | " # We create a pyramid instance, with the main motion-energy parameters.\n", 177 | " pyramid = MotionEnergyPyramid(stimulus_vhsize=(height, width),\n", 178 | " stimulus_fps=N_FRAMES_PER_SEC,\n", 179 | " spatial_frequencies=[0, 2, 4, 8, 16, 32])\n", 180 | "\n", 181 | " # We batch images run by run.\n", 182 | " motion_energy = np.zeros((n_frames, pyramid.nfilters))\n", 183 | " for ii, start in enumerate(range(0, n_frames, batch_size)):\n", 184 | " batch = slice(start, start + batch_size)\n", 185 | " print(\"run %d\" % ii)\n", 186 | "\n", 187 | " # add some noise to deal with constant black areas\n", 188 | " luminance_batch = luminance[batch].copy()\n", 189 | " luminance_batch += np.random.randn(*luminance_batch.shape) * noise\n", 190 | " luminance_batch = np.clip(luminance_batch, 0, 100)\n", 191 | "\n", 192 | " motion_energy[batch] = pyramid.project_stimulus(luminance_batch)\n", 193 | "\n", 194 | " # decimate to the sampling frequency of fMRI responses\n", 195 | " motion_energy_decimated = decimate(motion_energy, N_FRAMES_PER_TR,\n", 196 | " ftype='fir', axis=0)\n", 197 | " return motion_energy_decimated\n", 198 | "\n", 199 | "\n", 200 | "motion_energy_train = compute_motion_energy(luminance_train)\n", 201 | "motion_energy_test = compute_motion_energy(luminance_test)" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "We end this script with saving the features. These features should be\n", 209 | "approximately equal to the \"motion-energy\" features already precomputed in\n", 210 | "the public data set.\n", 211 | "\n" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": null, 217 | "metadata": { 218 | "collapsed": false 219 | }, 220 | "outputs": [], 221 | "source": [ 222 | "from voxelwise_tutorials.io import save_hdf5_dataset\n", 223 | "\n", 224 | "features_directory = os.path.join(directory, \"features\")\n", 225 | "if not os.path.exists(features_directory):\n", 226 | " os.makedirs(features_directory)\n", 227 | "\n", 228 | "save_hdf5_dataset(\n", 229 | " os.path.join(features_directory, \"motion_energy_recomputed.hdf\"),\n", 230 | " dataset=dict(X_train=motion_energy_train, X_test=motion_energy_test,\n", 231 | " run_onsets=np.arange(0, 3600, 300)))" 232 | ] 233 | }, 234 | { 235 | "cell_type": "markdown", 236 | "metadata": {}, 237 | "source": [ 238 | "## References\n", 239 | "\n", 240 | "```{bibliography}\n", 241 | ":filter: docname in docnames\n", 242 | "```" 243 | ] 244 | } 245 | ], 246 | "metadata": { 247 | "kernelspec": { 248 | "display_name": "Python 3", 249 | "language": "python", 250 | "name": "python3" 251 | }, 252 | "language_info": { 253 | "codemirror_mode": { 254 | "name": "ipython", 255 | "version": 3 256 | }, 257 | "file_extension": ".py", 258 | "mimetype": "text/x-python", 259 | "name": "python", 260 | "nbconvert_exporter": "python", 261 | "pygments_lexer": "ipython3", 262 | "version": "3.7.12" 263 | } 264 | }, 265 | "nbformat": 4, 266 | "nbformat_minor": 0 267 | } 268 | -------------------------------------------------------------------------------- /tutorials/notebooks/shortclips/README.md: -------------------------------------------------------------------------------- 1 | # Shortclips tutorial 2 | 3 | This tutorial describes how to use the Voxelwise Encoding Model framework in a visual 4 | imaging experiment. 5 | 6 | ## Dataset 7 | 8 | This tutorial is based on publicly available data [published on 9 | GIN](https://gin.g-node.org/gallantlab/shortclips) {cite}`huth2022data`. 10 | This data set contains BOLD fMRI responses in human subjects viewing a set of 11 | natural short movie clips. The functional data were collected in five subjects, 12 | in three sessions over three separate days for each subject. Details of the 13 | experiment are described in the original publication {cite}`huth2012`. If 14 | you publish work using this data set, please cite the original publications 15 | {cite}`huth2012`, and the GIN data set {cite}`huth2022data`. 16 | 17 | ## Models 18 | This tutorial shows and compares analyses based on three different voxelwise encoding models: 19 | 20 | 1. A ridge model with [wordnet semantic features](03_plot_wordnet_model.ipynb) as described in {cite}`huth2012`. 21 | 2. A ridge model with [motion-energy features](05_plot_motion_energy_model.ipynb) as described in {cite}`nishimoto2011`. 22 | 3. A banded-ridge model with [both feature spaces](06_plot_banded_ridge_model.ipynb) as described in {cite}`nunez2019`. 23 | 24 | ## Scikit-learn API 25 | These tutorials use [scikit-learn](https://github.com/scikit-learn/scikit-learn) to define the preprocessing steps, the modeling pipeline, and the cross-validation scheme. If you are not 26 | familiar with the scikit-learn API, we recommend the [getting started guide](https://scikit-learn.org/stable/getting_started.html). We also use a lot of 27 | the scikit-learn terminology, which is explained in great details in the 28 | [glossary of common terms and API elements](https://scikit-learn.org/stable/glossary.html#glossary). 29 | 30 | ## Running time 31 | Most of these tutorials can be run in a reasonable time with a GPU (under 1 minute for most examples, ~7 minutes for the banded ridge example). Running these examples on a CPU is much slower (typically 10 times slower). 32 | 33 | ## Requirements 34 | This tutorial requires the following Python packages: 35 | 36 | - `voxelwise_tutorials` and its dependencies (see [this page](../../voxelwise_package.rst) for installation instructions) 37 | - `cupy` or `pytorch` (optional, required to use a GPU backend in himalaya) 38 | 39 | ## References 40 | ```{bibliography} 41 | :filter: docname in docnames 42 | ``` -------------------------------------------------------------------------------- /tutorials/notebooks/vim2/00_download_vim2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\n", 8 | "# Download the data set from CRCNS\n", 9 | "\n", 10 | "In this script, we download the data set from CRCNS.\n", 11 | "A (free) account is required.\n", 12 | "\n", 13 | ":::{warning}\n", 14 | "This script will download approximately 14GB of data, so it may take a while.\n", 15 | ":::\n", 16 | "\n", 17 | "## Cite this data set\n", 18 | "\n", 19 | "This tutorial is based on publicly available data\n", 20 | "[published on CRCNS](https://crcns.org/data-sets/vc/vim-2/about-vim-2).\n", 21 | "If you publish any work using this data set, please cite the original\n", 22 | "publication {cite}`nishimoto2011`, and the data set {cite}`nishimoto2014data`.\n" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "## Download\n", 30 | "\n" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": { 37 | "collapsed": false 38 | }, 39 | "outputs": [], 40 | "source": [ 41 | "# path of the data directory\n", 42 | "from voxelwise_tutorials.io import get_data_home\n", 43 | "directory = get_data_home(dataset=\"vim-2\")\n", 44 | "print(directory)" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "We will only use the first subject in this tutorial, but you can run the same\n", 52 | "analysis on the two other subjects. Uncomment the lines in ``DATAFILES`` to\n", 53 | "download more subjects, or to download the anatomy files.\n", 54 | "\n" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": { 61 | "collapsed": false 62 | }, 63 | "outputs": [], 64 | "source": [ 65 | "import getpass\n", 66 | "\n", 67 | "from voxelwise_tutorials.io import download_crcns\n", 68 | "\n", 69 | "DATAFILES = [\n", 70 | " 'vim-2/Stimuli.tar.gz',\n", 71 | " 'vim-2/VoxelResponses_subject1.tar.gz',\n", 72 | " # 'vim-2/VoxelResponses_subject2.tar.gz',\n", 73 | " # 'vim-2/VoxelResponses_subject3.tar.gz',\n", 74 | " # 'vim-2/anatomy.zip',\n", 75 | " # 'vim-2/checksums.md5',\n", 76 | " # 'vim-2/filelist.txt',\n", 77 | " # 'vim-2/docs/crcns-vim-2-data-description.pdf',\n", 78 | "]" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": { 85 | "collapsed": false 86 | }, 87 | "outputs": [], 88 | "source": [ 89 | "username = input(\"CRCNS username: \")\n", 90 | "password = getpass.getpass(\"CRCNS password: \")\n", 91 | "\n", 92 | "for datafile in DATAFILES:\n", 93 | " local_filename = download_crcns(datafile, username, password,\n", 94 | " destination=directory, unpack=True)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": {}, 100 | "source": [ 101 | "## References\n", 102 | "```{bibliography}\n", 103 | ":filter: docname in docnames\n", 104 | "```" 105 | ] 106 | } 107 | ], 108 | "metadata": { 109 | "kernelspec": { 110 | "display_name": "Python 3", 111 | "language": "python", 112 | "name": "python3" 113 | }, 114 | "language_info": { 115 | "codemirror_mode": { 116 | "name": "ipython", 117 | "version": 3 118 | }, 119 | "file_extension": ".py", 120 | "mimetype": "text/x-python", 121 | "name": "python", 122 | "nbconvert_exporter": "python", 123 | "pygments_lexer": "ipython3", 124 | "version": "3.7.12" 125 | } 126 | }, 127 | "nbformat": 4, 128 | "nbformat_minor": 0 129 | } 130 | -------------------------------------------------------------------------------- /tutorials/notebooks/vim2/01_extract_motion_energy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Extract motion-energy features from the stimuli\n", 8 | "\n", 9 | "This example shows how to extract motion-energy features from the stimuli.\n", 10 | "\n", 11 | ":::{admonition} Long running time!\n", 12 | ":class: warning\n", 13 | "Running this example takes a couple of hours.\n", 14 | ":::\n", 15 | "\n", 16 | "Motion-energy features result from filtering a video\n", 17 | "stimulus with spatio-temporal Gabor filters. A pyramid of filters is used to\n", 18 | "compute the motion-energy features at multiple spatial and temporal scales.\n", 19 | "Motion-energy features were introduced in {cite:t}`nishimoto2011`.\n", 20 | "\n", 21 | "The motion-energy extraction is performed by the package \n", 22 | "[pymoten](https://github.com/gallantlab/pymoten) {cite}`nunez2021software`.\n", 23 | "\n", 24 | "Check the pymoten [gallery of examples](https://gallantlab.github.io/pymoten/auto_examples/index.html) for\n", 25 | "visualizing motion-energy filters, and for pymoten API usage examples." 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## Load the stimuli images\n", 33 | "(We downloaded the files in the previous script.)\n", 34 | "\n" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": { 41 | "collapsed": false 42 | }, 43 | "outputs": [], 44 | "source": [ 45 | "# path of the data directory\n", 46 | "from voxelwise_tutorials.io import get_data_home\n", 47 | "directory = get_data_home(dataset=\"vim-2\")\n", 48 | "print(directory)" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "Here the data is not loaded in memory, we only take a peak at the data shape.\n", 56 | "\n" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "metadata": { 63 | "collapsed": false 64 | }, 65 | "outputs": [], 66 | "source": [ 67 | "import os\n", 68 | "import h5py\n", 69 | "\n", 70 | "with h5py.File(os.path.join(directory, 'Stimuli.mat'), 'r') as f:\n", 71 | " print(f.keys()) # Show all variables\n", 72 | "\n", 73 | " for key in f.keys():\n", 74 | " print(f[key])" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "## Compute the luminance\n", 82 | "\n", 83 | "The motion energy is typically not computed on RGB (color) images,\n", 84 | "but on the luminance channel of the LAB color space.\n", 85 | "To avoid loading the entire simulus array in memory, we use batches of data.\n", 86 | "These batches can be arbitrary, since the luminance is computed independently\n", 87 | "on each image.\n", 88 | "\n" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": { 95 | "collapsed": false 96 | }, 97 | "outputs": [], 98 | "source": [ 99 | "import numpy as np\n", 100 | "from moten.io import imagearray2luminance\n", 101 | "from himalaya.progress_bar import bar\n", 102 | "\n", 103 | "\n", 104 | "def compute_luminance(train_or_test, batch_size=1024):\n", 105 | "\n", 106 | " with h5py.File(os.path.join(directory, 'Stimuli.mat'), 'r') as f:\n", 107 | "\n", 108 | " if train_or_test == 'train':\n", 109 | " data = f['st']\n", 110 | " elif train_or_test == 'test':\n", 111 | " data = f['sv']\n", 112 | " else:\n", 113 | " raise ValueError('Unknown parameter train_or_test=%r.' %\n", 114 | " train_or_test)\n", 115 | "\n", 116 | " title = \"compute_luminance(%s)\" % train_or_test\n", 117 | " luminance = np.zeros((data.shape[0], data.shape[2], data.shape[3]))\n", 118 | " for start in bar(range(0, data.shape[0], batch_size), title):\n", 119 | " batch = slice(start, start + batch_size)\n", 120 | "\n", 121 | " # transpose to corresponds to rgb2lab inputs\n", 122 | " rgb_batch = np.transpose(data[batch], [0, 2, 3, 1])\n", 123 | "\n", 124 | " # make sure we use uint8\n", 125 | " if rgb_batch.dtype != 'uint8':\n", 126 | " rgb_batch = np.int_(np.clip(rgb_batch, 0, 1) * 255).astype(\n", 127 | " np.uint8)\n", 128 | "\n", 129 | " # convert RGB images to a single luminance channel\n", 130 | " luminance[batch] = imagearray2luminance(rgb_batch)\n", 131 | "\n", 132 | " return luminance\n", 133 | "\n", 134 | "\n", 135 | "luminance_train = compute_luminance(\"train\")\n", 136 | "luminance_test = compute_luminance(\"test\")" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "## Compute the motion energy\n", 144 | "\n", 145 | "This is done with a ``MotionEnergyPyramid`` object of the ``pymoten``\n", 146 | "package. The parameters used are the one described in {cite:t}`nishimoto2011`.\n", 147 | "\n", 148 | "Here we use batches corresponding to run lengths. Indeed, motion energy is\n", 149 | "computed over multiple images, since the filters have a temporal component.\n", 150 | "Therefore, motion-energy is not independent of other images, and we cannot\n", 151 | "arbitrarily split the images.\n", 152 | "\n" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "metadata": { 159 | "collapsed": false 160 | }, 161 | "outputs": [], 162 | "source": [ 163 | "from scipy.signal import decimate\n", 164 | "from moten.pyramids import MotionEnergyPyramid\n", 165 | "\n", 166 | "# fixed experiment settings\n", 167 | "N_FRAMES_PER_SEC = 15\n", 168 | "N_FRAMES_PER_TR = 15\n", 169 | "N_TRS_PER_RUN = 600\n", 170 | "\n", 171 | "\n", 172 | "def compute_motion_energy(luminance,\n", 173 | " batch_size=N_TRS_PER_RUN * N_FRAMES_PER_TR,\n", 174 | " noise=0.1):\n", 175 | "\n", 176 | " n_frames, height, width = luminance.shape\n", 177 | "\n", 178 | " # We create a pyramid instance, with the main motion-energy parameters.\n", 179 | " pyramid = MotionEnergyPyramid(stimulus_vhsize=(height, width),\n", 180 | " stimulus_fps=N_FRAMES_PER_SEC,\n", 181 | " spatial_frequencies=[0, 2, 4, 8, 16, 32])\n", 182 | "\n", 183 | " # We batch images run by run.\n", 184 | " motion_energy = np.zeros((n_frames, pyramid.nfilters))\n", 185 | " for ii, start in enumerate(range(0, n_frames, batch_size)):\n", 186 | " batch = slice(start, start + batch_size)\n", 187 | " print(\"run %d\" % ii)\n", 188 | "\n", 189 | " # add some noise to deal with constant black areas\n", 190 | " luminance_batch = luminance[batch].copy()\n", 191 | " luminance_batch += np.random.randn(*luminance_batch.shape) * noise\n", 192 | " luminance_batch = np.clip(luminance_batch, 0, 100)\n", 193 | "\n", 194 | " motion_energy[batch] = pyramid.project_stimulus(luminance_batch)\n", 195 | "\n", 196 | " # decimate to the sampling frequency of fMRI responses\n", 197 | " motion_energy_decimated = decimate(motion_energy, N_FRAMES_PER_TR,\n", 198 | " ftype='fir', axis=0)\n", 199 | " return motion_energy_decimated\n", 200 | "\n", 201 | "\n", 202 | "motion_energy_train = compute_motion_energy(luminance_train)\n", 203 | "motion_energy_test = compute_motion_energy(luminance_test)" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "We end this script with saving the features, to use them in voxelwise\n", 211 | "modeling in the following example.\n", 212 | "\n" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "metadata": { 219 | "collapsed": false 220 | }, 221 | "outputs": [], 222 | "source": [ 223 | "from voxelwise_tutorials.io import save_hdf5_dataset\n", 224 | "\n", 225 | "features_directory = os.path.join(directory, \"features\")\n", 226 | "if not os.path.exists(features_directory):\n", 227 | " os.makedirs(features_directory)\n", 228 | "\n", 229 | "save_hdf5_dataset(\n", 230 | " os.path.join(features_directory, \"motion_energy.hdf\"),\n", 231 | " dataset=dict(X_train=motion_energy_train, X_test=motion_energy_test))" 232 | ] 233 | }, 234 | { 235 | "cell_type": "markdown", 236 | "metadata": {}, 237 | "source": [ 238 | "## References\n", 239 | "\n", 240 | "```{bibliography}\n", 241 | ":filter: docname in docnames\n", 242 | "```" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": {}, 248 | "source": [] 249 | } 250 | ], 251 | "metadata": { 252 | "kernelspec": { 253 | "display_name": "Python 3", 254 | "language": "python", 255 | "name": "python3" 256 | }, 257 | "language_info": { 258 | "codemirror_mode": { 259 | "name": "ipython", 260 | "version": 3 261 | }, 262 | "file_extension": ".py", 263 | "mimetype": "text/x-python", 264 | "name": "python", 265 | "nbconvert_exporter": "python", 266 | "pygments_lexer": "ipython3", 267 | "version": "3.7.12" 268 | } 269 | }, 270 | "nbformat": 4, 271 | "nbformat_minor": 0 272 | } 273 | -------------------------------------------------------------------------------- /tutorials/notebooks/vim2/02_plot_ridge_model.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\n", 8 | "# Fit a ridge model with motion energy features\n", 9 | "\n", 10 | "In this example, we model the fMRI responses with motion-energy features\n", 11 | "extracted from the movie stimulus. The model is a regularized linear regression\n", 12 | "model.\n", 13 | "\n", 14 | "This tutorial reproduces part of the analysis described in {cite:t}`nishimoto2011`. See this publication for more details about the experiment, the\n", 15 | "motion-energy features, along with more results and more discussions.\n", 16 | "\n", 17 | "*Motion-energy features:* Motion-energy features result from filtering a video\n", 18 | "stimulus with spatio-temporal Gabor filters. A pyramid of filters is used to\n", 19 | "compute the motion-energy features at multiple spatial and temporal scales.\n", 20 | "Motion-energy features were introduced in {cite:t}`nishimoto2011`.\n", 21 | "\n", 22 | "*Summary:* We first concatenate the features with multiple delays, to account\n", 23 | "for the slow hemodynamic response. A linear regression model then weights each\n", 24 | "delayed feature with a different weight, to build a predictive model of BOLD\n", 25 | "activity. Again, the linear regression is regularized to improve robustness to\n", 26 | "correlated features and to improve generalization. The optimal regularization\n", 27 | "hyperparameter is selected independently on each voxel over a grid-search with\n", 28 | "cross-validation. Finally, the model generalization performance is evaluated on\n", 29 | "a held-out test set, comparing the model predictions with the ground-truth fMRI\n", 30 | "responses.\n" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "## Load the data\n", 38 | "\n" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": { 45 | "collapsed": false 46 | }, 47 | "outputs": [], 48 | "source": [ 49 | "# path of the data directory\n", 50 | "from voxelwise_tutorials.io import get_data_home\n", 51 | "directory = get_data_home(dataset=\"vim-2\")\n", 52 | "print(directory)\n", 53 | "\n", 54 | "# modify to use another subject\n", 55 | "subject = \"subject1\"" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "Here the data is not loaded in memory, we only take a peek at the data shape.\n", 63 | "\n" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": { 70 | "collapsed": false 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "import os\n", 75 | "import h5py\n", 76 | "\n", 77 | "file_name = os.path.join(directory, f'VoxelResponses_{subject}.mat')\n", 78 | "with h5py.File(file_name, 'r') as f:\n", 79 | " print(f.keys()) # Show all variables\n", 80 | " for key in f.keys():\n", 81 | " print(f[key])" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "Then we load the fMRI responses.\n", 89 | "\n" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": { 96 | "collapsed": false 97 | }, 98 | "outputs": [], 99 | "source": [ 100 | "import numpy as np\n", 101 | "\n", 102 | "from voxelwise_tutorials.io import load_hdf5_array\n", 103 | "\n", 104 | "file_name = os.path.join(directory, f'VoxelResponses_{subject}.mat')\n", 105 | "Y_train = load_hdf5_array(file_name, key='rt')\n", 106 | "Y_test_repeats = load_hdf5_array(file_name, key='rva')\n", 107 | "\n", 108 | "# transpose to fit in scikit-learn's API\n", 109 | "Y_train = Y_train.T\n", 110 | "Y_test_repeats = np.transpose(Y_test_repeats, [1, 2, 0])\n", 111 | "\n", 112 | "# Change to True to select only voxels from (e.g.) left V1 (\"v1lh\");\n", 113 | "# Otherwise, all voxels will be modeled.\n", 114 | "if False:\n", 115 | " roi = load_hdf5_array(file_name, key='/roi/v1lh').ravel()\n", 116 | " mask = (roi == 1)\n", 117 | " Y_train = Y_train[:, mask]\n", 118 | " Y_test_repeats = Y_test_repeats[:, :, mask]\n", 119 | "\n", 120 | "# Z-score test runs, since the mean and scale of fMRI responses changes for\n", 121 | "# each run. The train runs are already zscored.\n", 122 | "Y_test_repeats -= np.mean(Y_test_repeats, axis=1, keepdims=True)\n", 123 | "Y_test_repeats /= np.std(Y_test_repeats, axis=1, keepdims=True)\n", 124 | "\n", 125 | "# Average test repeats, since we cannot model the non-repeatable part of\n", 126 | "# fMRI responses.\n", 127 | "Y_test = Y_test_repeats.mean(0)\n", 128 | "\n", 129 | "# remove nans, mainly present on non-cortical voxels\n", 130 | "Y_train = np.nan_to_num(Y_train)\n", 131 | "Y_test = np.nan_to_num(Y_test)" 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "metadata": {}, 137 | "source": [ 138 | "Here we load the motion-energy features, that are going to be used for the\n", 139 | "linear regression model.\n", 140 | "\n" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": { 147 | "collapsed": false 148 | }, 149 | "outputs": [], 150 | "source": [ 151 | "file_name = os.path.join(directory, \"features\", \"motion_energy.hdf\")\n", 152 | "X_train = load_hdf5_array(file_name, key='X_train')\n", 153 | "X_test = load_hdf5_array(file_name, key='X_test')\n", 154 | "\n", 155 | "# We use single precision float to speed up model fitting on GPU.\n", 156 | "X_train = X_train.astype(\"float32\")\n", 157 | "X_test = X_test.astype(\"float32\")" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "## Define the cross-validation scheme\n", 165 | "\n", 166 | "To select the best hyperparameter through cross-validation, we must define a\n", 167 | "train-validation splitting scheme. Since fMRI time-series are autocorrelated\n", 168 | "in time, we should preserve as much as possible the time blocks.\n", 169 | "In other words, since consecutive time samples are correlated, we should not\n", 170 | "put one time sample in the training set and the immediately following time\n", 171 | "sample in the validation set. Thus, we define here a leave-one-run-out\n", 172 | "cross-validation split, which preserves each recording run.\n", 173 | "\n" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": { 180 | "collapsed": false 181 | }, 182 | "outputs": [], 183 | "source": [ 184 | "from sklearn.model_selection import check_cv\n", 185 | "from voxelwise_tutorials.utils import generate_leave_one_run_out\n", 186 | "\n", 187 | "n_samples_train = X_train.shape[0]\n", 188 | "\n", 189 | "# indice of first sample of each run, each run having 600 samples\n", 190 | "run_onsets = np.arange(0, n_samples_train, 600)\n", 191 | "\n", 192 | "# define a cross-validation splitter, compatible with scikit-learn\n", 193 | "cv = generate_leave_one_run_out(n_samples_train, run_onsets)\n", 194 | "cv = check_cv(cv) # copy the splitter into a reusable list" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "## Define the model\n", 202 | "\n", 203 | "Now, let's define the model pipeline.\n", 204 | "\n" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": { 211 | "collapsed": false 212 | }, 213 | "outputs": [], 214 | "source": [ 215 | "from sklearn.pipeline import make_pipeline\n", 216 | "from sklearn.preprocessing import StandardScaler\n", 217 | "\n", 218 | "# Display the scikit-learn pipeline with an HTML diagram.\n", 219 | "from sklearn import set_config\n", 220 | "set_config(display='diagram') # requires scikit-learn 0.23" 221 | ] 222 | }, 223 | { 224 | "cell_type": "markdown", 225 | "metadata": {}, 226 | "source": [ 227 | "With one target, we could directly use the pipeline in scikit-learn's\n", 228 | "GridSearchCV, to select the optimal hyperparameters over cross-validation.\n", 229 | "However, GridSearchCV can only optimize one score. Thus, in the multiple\n", 230 | "target case, GridSearchCV can only optimize e.g. the mean score over targets.\n", 231 | "Here, we want to find a different optimal hyperparameter per target/voxel, so\n", 232 | "we use himalaya's KernelRidgeCV instead.\n", 233 | "\n" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "metadata": { 240 | "collapsed": false 241 | }, 242 | "outputs": [], 243 | "source": [ 244 | "from himalaya.kernel_ridge import KernelRidgeCV" 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "metadata": {}, 250 | "source": [ 251 | "We first concatenate the features with multiple delays, to account for the\n", 252 | "hemodynamic response. The linear regression model will then weight each\n", 253 | "delayed feature with a different weight, to build a predictive model.\n", 254 | "\n", 255 | "With a sample every 1 second, we use 8 delays [1, 2, 3, 4, 5, 6, 7, 8] to\n", 256 | "cover the most part of the hemodynamic response peak.\n", 257 | "\n" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": null, 263 | "metadata": { 264 | "collapsed": false 265 | }, 266 | "outputs": [], 267 | "source": [ 268 | "from voxelwise_tutorials.delayer import Delayer" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": {}, 274 | "source": [ 275 | "The package``himalaya`` implements different computational backends,\n", 276 | "including GPU backends. The available GPU backends are \"torch_cuda\" and\n", 277 | "\"cupy\". (These backends are only available if you installed the corresponding\n", 278 | "package with CUDA enabled. Check the pytorch/cupy documentation for install\n", 279 | "instructions.)\n", 280 | "\n", 281 | "Here we use the \"torch_cuda\" backend, but if the import fails we continue\n", 282 | "with the default \"numpy\" backend. The \"numpy\" backend is expected to be\n", 283 | "slower since it only uses the CPU.\n", 284 | "\n" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "metadata": { 291 | "collapsed": false 292 | }, 293 | "outputs": [], 294 | "source": [ 295 | "from himalaya.backend import set_backend\n", 296 | "backend = set_backend(\"torch_cuda\", on_error=\"warn\")" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "metadata": {}, 302 | "source": [ 303 | "The scale of the regularization hyperparameter alpha is unknown, so we use\n", 304 | "a large logarithmic range, and we will check after the fit that best\n", 305 | "hyperparameters are not all on one range edge.\n", 306 | "\n" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": null, 312 | "metadata": { 313 | "collapsed": false 314 | }, 315 | "outputs": [], 316 | "source": [ 317 | "alphas = np.logspace(1, 20, 20)" 318 | ] 319 | }, 320 | { 321 | "cell_type": "markdown", 322 | "metadata": {}, 323 | "source": [ 324 | "The scikit-learn Pipeline can be used as a regular estimator, calling\n", 325 | "pipeline.fit, pipeline.predict, etc.\n", 326 | "Using a pipeline can be useful to clarify the different steps, avoid\n", 327 | "cross-validation mistakes, or automatically cache intermediate results.\n", 328 | "\n" 329 | ] 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": null, 334 | "metadata": { 335 | "collapsed": false 336 | }, 337 | "outputs": [], 338 | "source": [ 339 | "pipeline = make_pipeline(\n", 340 | " StandardScaler(with_mean=True, with_std=False),\n", 341 | " Delayer(delays=[1, 2, 3, 4, 5, 6, 7, 8]),\n", 342 | " KernelRidgeCV(\n", 343 | " alphas=alphas, cv=cv,\n", 344 | " solver_params=dict(n_targets_batch=100, n_alphas_batch=2,\n", 345 | " n_targets_batch_refit=50),\n", 346 | " Y_in_cpu=True),\n", 347 | ")\n", 348 | "pipeline" 349 | ] 350 | }, 351 | { 352 | "cell_type": "markdown", 353 | "metadata": {}, 354 | "source": [ 355 | "## Fit the model\n", 356 | "\n", 357 | "We fit on the train set, and score on the test set.\n", 358 | "\n" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": null, 364 | "metadata": { 365 | "collapsed": false 366 | }, 367 | "outputs": [], 368 | "source": [ 369 | "pipeline.fit(X_train, Y_train)\n", 370 | "\n", 371 | "scores = pipeline.score(X_test, Y_test)\n", 372 | "# Since we performed the KernelRidgeCV on GPU, scores are returned as\n", 373 | "# torch.Tensor on GPU. Thus, we need to move them into numpy arrays on CPU, to\n", 374 | "# be able to use them e.g. in a matplotlib figure.\n", 375 | "scores = backend.to_numpy(scores)" 376 | ] 377 | }, 378 | { 379 | "cell_type": "markdown", 380 | "metadata": {}, 381 | "source": [ 382 | "Since the scale of alphas is unknown, we plot the optimal alphas selected by\n", 383 | "the solver over cross-validation. This plot is helpful to refine the alpha\n", 384 | "grid if the range is too small or too large.\n", 385 | "\n", 386 | "Note that some voxels are at the maximum regularization of the grid. These\n", 387 | "are voxels where the model has no predictive power, and where the optimal\n", 388 | "regularization is large to lead to a prediction equal to zero.\n", 389 | "\n" 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": null, 395 | "metadata": { 396 | "collapsed": false 397 | }, 398 | "outputs": [], 399 | "source": [ 400 | "import matplotlib.pyplot as plt\n", 401 | "from himalaya.viz import plot_alphas_diagnostic\n", 402 | "\n", 403 | "plot_alphas_diagnostic(best_alphas=backend.to_numpy(pipeline[-1].best_alphas_),\n", 404 | " alphas=alphas)\n", 405 | "plt.show()" 406 | ] 407 | }, 408 | { 409 | "cell_type": "markdown", 410 | "metadata": {}, 411 | "source": [ 412 | "## Compare with a model without delays\n", 413 | "\n", 414 | "To present an example of model comparison, we define here another model,\n", 415 | "without feature delays (i.e. no Delayer). This model is unlikely to perform\n", 416 | "well, since fMRI responses are delayed in time with respect to the stimulus.\n", 417 | "\n" 418 | ] 419 | }, 420 | { 421 | "cell_type": "code", 422 | "execution_count": null, 423 | "metadata": { 424 | "collapsed": false 425 | }, 426 | "outputs": [], 427 | "source": [ 428 | "pipeline = make_pipeline(\n", 429 | " StandardScaler(with_mean=True, with_std=False),\n", 430 | " KernelRidgeCV(\n", 431 | " alphas=alphas, cv=cv,\n", 432 | " solver_params=dict(n_targets_batch=100, n_alphas_batch=2,\n", 433 | " n_targets_batch_refit=50),\n", 434 | " Y_in_cpu=True),\n", 435 | ")\n", 436 | "pipeline" 437 | ] 438 | }, 439 | { 440 | "cell_type": "code", 441 | "execution_count": null, 442 | "metadata": { 443 | "collapsed": false 444 | }, 445 | "outputs": [], 446 | "source": [ 447 | "pipeline.fit(X_train, Y_train)\n", 448 | "scores_nodelay = pipeline.score(X_test, Y_test)\n", 449 | "scores_nodelay = backend.to_numpy(scores_nodelay)" 450 | ] 451 | }, 452 | { 453 | "cell_type": "markdown", 454 | "metadata": {}, 455 | "source": [ 456 | "Here we plot the comparison of model performances with a 2D histogram. All\n", 457 | "~70k voxels are represented in this histogram, where the diagonal corresponds\n", 458 | "to identical performance for both models. A distribution deviating from the\n", 459 | "diagonal means that one model has better predictive performances than the\n", 460 | "other.\n", 461 | "\n" 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": null, 467 | "metadata": { 468 | "collapsed": false 469 | }, 470 | "outputs": [], 471 | "source": [ 472 | "from voxelwise_tutorials.viz import plot_hist2d\n", 473 | "\n", 474 | "ax = plot_hist2d(scores_nodelay, scores)\n", 475 | "ax.set(title='Generalization R2 scores', xlabel='model without delays',\n", 476 | " ylabel='model with delays')\n", 477 | "plt.show()" 478 | ] 479 | }, 480 | { 481 | "cell_type": "markdown", 482 | "metadata": {}, 483 | "source": [ 484 | "## References\n", 485 | "\n", 486 | "```{bibliography}\n", 487 | ":filter: docname in docnames\n", 488 | "```" 489 | ] 490 | } 491 | ], 492 | "metadata": { 493 | "kernelspec": { 494 | "display_name": "Python 3", 495 | "language": "python", 496 | "name": "python3" 497 | }, 498 | "language_info": { 499 | "codemirror_mode": { 500 | "name": "ipython", 501 | "version": 3 502 | }, 503 | "file_extension": ".py", 504 | "mimetype": "text/x-python", 505 | "name": "python", 506 | "nbconvert_exporter": "python", 507 | "pygments_lexer": "ipython3", 508 | "version": "3.7.12" 509 | } 510 | }, 511 | "nbformat": 4, 512 | "nbformat_minor": 0 513 | } 514 | -------------------------------------------------------------------------------- /tutorials/notebooks/vim2/README.md: -------------------------------------------------------------------------------- 1 | # Vim-2 tutorial 2 | 3 | :::{note} 4 | This tutorial is redundant with the [Shortclips tutorial](../shortclips/README.md). 5 | It uses the "vim-2" data set, a data set with brain responses limited to the occipital 6 | lobe, and with no mappers to plot the data on flatmaps. 7 | Using the "Shortclips tutorial" with full brain responses is recommended. 8 | ::: 9 | 10 | This tutorial describes how to use the Voxelwise Encoding Model framework in a visual 11 | imaging experiment. 12 | 13 | ## Data set 14 | 15 | This tutorial is based on publicly available data published on 16 | [CRCNS](https://crcns.org/data-sets/vc/vim-2/about-vim-2) {cite}`nishimoto2014data`. 17 | The data is briefly described in the dataset description 18 | [PDF](https://crcns.org/files/data/vim-2/crcns-vim-2-data-description.pdf), 19 | and in more details in the original publication {cite}`nishimoto2011`. 20 | If you publish work using this data set, please cite the original 21 | publication {cite}`nishimoto2011`, and the CRCNS data set {cite}`nishimoto2014data`. 22 | 23 | ## Requirements 24 | This tutorial requires the following Python packages: 25 | 26 | - `voxelwise_tutorials` and its dependencies (see [this page](../../voxelwise_package.rst) for installation instructions) 27 | - `cupy` or `pytorch` (optional, required to use a GPU backend in himalaya) 28 | 29 | ## References 30 | ```{bibliography} 31 | :filter: docname in docnames 32 | ``` -------------------------------------------------------------------------------- /tutorials/pages/index.md: -------------------------------------------------------------------------------- 1 | # Voxelwise Encoding Model (VEM) tutorials 2 | 3 | Welcome to the tutorials on the Voxelwise Encoding Model framework from the 4 | [Gallant Lab](https://gallantlab.org). 5 | 6 | If you use these tutorials for your work, consider citing the corresponding paper: 7 | 8 | > Dupré la Tour, T., Visconti di Oleggio Castello, M., & Gallant, J. L. (2025). 9 | > The Voxelwise Encoding Model framework: A tutorial introduction to fitting encoding models to fMRI data. 10 | > *Imaging Neuroscience*. [https://doi.org/10.1162/imag_a_00575](https://doi.org/10.1162/imag_a_00575) 11 | 12 | ## How to use the tutorials 13 | 14 | To explore the VEM tutorials, one can: 15 | 16 | 1. Read the tutorials on this website (recommended) 17 | 2. Run the notebooks in Google Colab (clicking on the following links opens Colab): 18 | [all notebooks](https://colab.research.google.com/github/gallantlab/voxelwise_tutorials/blob/main/tutorials/notebooks/shortclips/vem_tutorials_merged_for_colab.ipynb) or [only the notebooks about model fitting](https://colab.research.google.com/github/gallantlab/voxelwise_tutorials/blob/main/tutorials/notebooks/shortclips/vem_tutorials_merged_for_colab_model_fitting.ipynb) 19 | 3. Use the provided [Dockerfiles](https://github.com/gallantlab/voxelwise_tutorials/tree/main/docker) to run the notebooks locally (recommended for Windows users, as some of the packages used do not support Windows) 20 | 21 | The code of this project is available on GitHub at [gallantlab/voxelwise_tutorials](https://github.com/gallantlab/voxelwise_tutorials). 22 | 23 | The GitHub repository also contains a Python package called 24 | `voxelwise_tutorials`, which contains useful functions to download the data 25 | sets, load the files, process the data, and visualize the results. Install 26 | instructions are available [here](voxelwise_package.rst) 27 | 28 | ## Cite as 29 | 30 | Please cite the corresponding publications if you use the code or data in your work: 31 | 32 | - `voxelwise_tutorials` {cite}`dupre2025` 33 | - `himalaya` {cite}`dupre2022` 34 | - `pycortex` {cite}`gao2015` 35 | - `pymoten` {cite}`nunez2021software` 36 | - `shortclips` dataset {cite}`huth2022data` 37 | - `vim-2` dataset {cite}`nishimoto2014data` 38 | 39 | ## References 40 | 41 | ```{bibliography} 42 | :filter: docname in docnames 43 | ``` 44 | -------------------------------------------------------------------------------- /tutorials/pages/references.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | The Voxelwise Encoding Model (VEM) framework is a powerful approach for functional magnetic resonance 4 | imaging (fMRI) data analysis. Over the years, VEM has led to many high profile 5 | publications {cite}`kay2008,naselaris2009,nishimoto2011,huth2012,cukur2013,cukur2013b,stansbury2013,huth2016,deheer2017,lescroart2019,deniz2019,nunez2019,popham2021,lebel2021,dupre2022`. 6 | 7 | ## Datasets 8 | 9 | - Shortclips dataset {cite}`huth2022data` 10 | - Vim-2 dataset {cite}`nishimoto2014data` 11 | 12 | ## Packages 13 | 14 | - `voxelwise_tutorials` {cite}`dupre2025` 15 | - `himalaya` {cite}`dupre2022` 16 | - `pycortex` {cite}`gao2015` 17 | - `pymoten` {cite}`nunez2021software` 18 | 19 | ## Bibliography 20 | 21 | ```{bibliography} 22 | :filter: docname in docnames 23 | ``` 24 | -------------------------------------------------------------------------------- /tutorials/pages/voxelwise_modeling.md: -------------------------------------------------------------------------------- 1 | # Overview of the Voxelwise Encoding Model (VEM) framework 2 | 3 | A fundamental problem in neuroscience is to identify the information 4 | represented in different brain areas. In the VEM framework, this problem is 5 | solved using encoding models. An encoding model describes how various features 6 | of the stimulus (or task) predict the activity in some part of the brain. Using 7 | VEM to fit an encoding model to blood oxygen level-dependent signals (BOLD) 8 | recorded by fMRI involves several steps. First, brain activity is recorded 9 | while subjects perceive a stimulus or perform a task. Then, a set of features 10 | (that together constitute one or more *feature spaces*) is extracted from the 11 | stimulus or task at each point in time. For example, a video might be 12 | represented in terms of amount of motion in each part of the screen 13 | {cite}`nishimoto2011`, or in terms of semantic categories of the 14 | objects present in the scene {cite}`huth2012`. Each feature space 15 | corresponds to a different representation of the stimulus- or task-related 16 | information. The VEM framework aims to identify if each feature space is encoded 17 | in brain activity. Each feature space thus corresponds to a hypothesis about 18 | the stimulus- or task-related information that might be represented in some 19 | part of the brain. To test this hypothesis for some specific feature space, a 20 | regression model is trained to predict brain activity from that feature space. 21 | The resulting regression model is called an *encoding model*. If the encoding 22 | model predicts brain activity significantly in some part of the brain, then one 23 | may conclude that some information represented in the feature space is also 24 | represented in brain activity. To maximize spatial resolution, in VEM a separate 25 | encoding model is fit on each spatial sample in fMRI recordings (that is on 26 | each voxel), leading to *voxelwise encoding models*. 27 | 28 | Before fitting a voxelwise encoding model, it is sometimes possible to estimate 29 | an upper bound of the model prediction accuracy in each voxel. In VEM, this 30 | upper bound is called the noise ceiling, and it is related to a quantity called 31 | the explainable variance. The explainable variance quantifies the fraction of 32 | the variance in the data that is consistent across repetitions of the same 33 | stimulus. Because an encoding model makes the same predictions across 34 | repetitions of the same stimulus, the explainable variance is the fraction of 35 | the variance in the data that can be explained by the model. 36 | 37 | To estimate the prediction accuracy of an encoding model, the model prediction 38 | is compared with the recorded brain response. However, higher-dimensional 39 | encoding models are more likely to overfit to the training data. Overfitting 40 | causes inflated prediction accuracy on the training set and poor prediction 41 | accuracy on new data. To minimize the chances of overfitting and to obtain a 42 | fair estimate of prediction accuracy, the comparison between model predictions 43 | and brain responses must be performed on a separate test data set that was not 44 | used during model training. The ability to evaluate a model on a separate test 45 | data set is a major strength of the VEM framework. It provides a principled way 46 | to build complex models while limiting the amount of overfitting. To further 47 | reduce overfitting, the encoding model is regularized. In VEM, regularization is 48 | obtained by ridge regression, a common and powerful regularized regression 49 | method. 50 | 51 | To take into account the temporal delay between the stimulus and the 52 | corresponding BOLD response (i.e. the hemodynamic response), the features are 53 | duplicated multiple times using different temporal delays. The regression then 54 | estimates a separate weight for each feature and for each delay. In this way, 55 | the regression builds for each feature the best combination of temporal delays 56 | to predict brain activity. This combination of temporal delays is sometimes 57 | called a finite impulse response (FIR) filter. By estimating a separate FIR 58 | filter per feature and per voxel, VEM does not assume a unique hemodynamic 59 | response function. 60 | 61 | After fitting the regression model, the model prediction accuracy is projected 62 | on the cortical surface for visualization. Our lab created the pycortex 63 | {cite}`gao2015` visualization software specifically for this purpose. 64 | These prediction-accuracy maps reveal how information present in the feature 65 | space is represented across the entire cortical sheet. (Note that VEM can also 66 | be applied to other brain structures, such as the cerebellum 67 | {cite}`lebel2021` and the hippocampus. However, those structures are more 68 | difficult to visualize computationally.) In an encoding model, all features are 69 | not equally useful to predict brain activity. To interpret which features are 70 | most useful to the model, VEM uses the fit regression weights as a measure of 71 | relative importance of each feature. A feature with a large absolute regression 72 | weight has a large impact on the predictions, whereas a feature with a 73 | regression weight close to zero has a small impact on the predictions. Overall, 74 | the regression weight vector describes the *feature tuning* of a voxel, that is 75 | the feature combination that would maximally drive the voxel's activity. To 76 | visualize these high-dimensional feature tunings over all voxels, feature 77 | tunings are projected on fewer dimensions with principal component analysis, 78 | and the first few principal components are visualized over the cortical surface 79 | {cite}`huth2012` {cite}`huth2016`. These feature-tuning maps reflect 80 | the selectivity of each voxel to thousands of stimulus and task features. 81 | 82 | In VEM, comparing the prediction accuracy of different feature spaces within a 83 | single data set amounts to comparing competing hypotheses about brain 84 | representations. In each brain voxel, the best-predicting feature space 85 | corresponds to the best hypothesis about the information represented in that 86 | voxel. However, many voxels represent multiple feature spaces simultaneously. 87 | To take this possibility into account, in VEM a joint encoding model is fit on 88 | multiple feature spaces simultaneously. The joint model automatically combines 89 | the information from all feature spaces to maximize the joint prediction 90 | accuracy. 91 | 92 | Because different feature spaces used in a joint model might require different 93 | regularization levels, VEM uses an extended form of ridge regression that 94 | provides a separate regularization parameter for each feature space. This 95 | extension is called banded ridge regression {cite}`nunez2019`. Banded ridge 96 | regression also contains an implicit feature-space selection mechanism that 97 | tends to ignore feature spaces that are non-predictive or redundant 98 | {cite}`dupre2022`. This feature-space selection mechanism helps to 99 | disentangle correlated feature spaces and it improves generalization to new 100 | data. 101 | 102 | To interpret the joint model, VEM implements a variance decomposition method 103 | that quantifies the separate contributions of each feature space. Variance 104 | decomposition methods include variance partitioning, the split-correlation 105 | measure, or the product measure {cite}`dupre2022`. The obtained variance 106 | decomposition describes the contribution of each feature space to the joint 107 | encoding model predictions. 108 | 109 | ## References 110 | ```{bibliography} 111 | :filter: docname in docnames 112 | ``` -------------------------------------------------------------------------------- /tutorials/pages/voxelwise_package.rst: -------------------------------------------------------------------------------- 1 | ``voxelwise_tutorials`` helper package 2 | ====================================== 3 | 4 | |Github| |Python| |License| 5 | 6 | To run the tutorials, the `gallantlab/voxelwise_tutorials 7 | `_ repository contains a 8 | Python package called ``voxelwise_tutorials``, with useful functions to 9 | download the data sets, load the files, process the data, and visualize the 10 | results. 11 | 12 | Installation 13 | ------------ 14 | 15 | To install the ``voxelwise_tutorials`` package, run: 16 | 17 | .. code-block:: bash 18 | 19 | pip install voxelwise_tutorials 20 | 21 | 22 | To also download the tutorial scripts and notebooks, clone the repository via: 23 | 24 | .. code-block:: bash 25 | 26 | git clone https://github.com/gallantlab/voxelwise_tutorials.git 27 | cd voxelwise_tutorials 28 | pip install . 29 | 30 | 31 | Developers can also install the package in editable mode via: 32 | 33 | .. code-block:: bash 34 | 35 | pip install --editable . 36 | 37 | 38 | Requirements 39 | ------------ 40 | 41 | The package ``voxelwise_tutorials`` has the following dependencies: 42 | `numpy `_, 43 | `scipy `_, 44 | `h5py `_, 45 | `scikit-learn `_, 46 | `matplotlib `_, 47 | `networkx `_, 48 | `nltk `_, 49 | `pycortex `_, 50 | `himalaya `_, 51 | `pymoten `_. 52 | 53 | 54 | .. |Github| image:: https://img.shields.io/badge/github-voxelwise_tutorials-blue 55 | :target: https://github.com/gallantlab/voxelwise_tutorials 56 | 57 | .. |Python| image:: https://img.shields.io/badge/python-3.9%2B-blue 58 | :target: https://www.python.org/downloads/release/python-370 59 | 60 | .. |License| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg 61 | :target: https://opensource.org/licenses/BSD-3-Clause 62 | -------------------------------------------------------------------------------- /tutorials/static/colab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gallantlab/voxelwise_tutorials/fc649ba74febd3644a17c403ac900151a79ddc70/tutorials/static/colab.png -------------------------------------------------------------------------------- /tutorials/static/custom.css: -------------------------------------------------------------------------------- 1 | .sphx-glr-thumbcontainer { 2 | min-height: 230px !important; /*default = 230 */ 3 | margin: 5px !important; /*default = 0 ? */ 4 | } 5 | .sphx-glr-thumbcontainer .figure { 6 | width: 210px !important; /*default = 160 */ 7 | } 8 | .sphx-glr-thumbcontainer img { 9 | max-height: 112px !important; /*default = 112 */ 10 | max-width: 210px !important; /*default = 160 */ 11 | } 12 | .sphx-glr-thumbcontainer a.internal { 13 | padding: 150px 10px 0 !important; /*default = 150px 10px 0 */ 14 | } 15 | div.sphinxsidebar { 16 | max-height: 100%; 17 | overflow-y: auto; 18 | } 19 | div.sphx-glr-download a{ 20 | background-image: none; 21 | background-color: rgb(238, 238, 238); 22 | border-color: rgb(204, 204, 204); 23 | } 24 | -------------------------------------------------------------------------------- /tutorials/static/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gallantlab/voxelwise_tutorials/fc649ba74febd3644a17c403ac900151a79ddc70/tutorials/static/download.png -------------------------------------------------------------------------------- /tutorials/static/flatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gallantlab/voxelwise_tutorials/fc649ba74febd3644a17c403ac900151a79ddc70/tutorials/static/flatmap.png -------------------------------------------------------------------------------- /tutorials/static/moten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gallantlab/voxelwise_tutorials/fc649ba74febd3644a17c403ac900151a79ddc70/tutorials/static/moten.png -------------------------------------------------------------------------------- /tutorials/static/references.bib: -------------------------------------------------------------------------------- 1 | @article{kay2008, 2 | author = {Kay, K. N. and Naselaris, T. and Prenger, R. J. and Gallant, J. L.}, 3 | title = {Identifying natural images from human brain activity}, 4 | journal = {Nature}, 5 | volume = {452}, 6 | number = {7185}, 7 | pages = {352--355}, 8 | year = {2008} 9 | } 10 | 11 | @article{naselaris2009, 12 | author = {Naselaris, T. and Prenger, R. J. and Kay, K. N. and Oliver, M. and Gallant, J. L.}, 13 | title = {Bayesian reconstruction of natural images from human brain activity}, 14 | journal = {Neuron}, 15 | volume = {63}, 16 | number = {6}, 17 | pages = {902--915}, 18 | year = {2009} 19 | } 20 | 21 | @article{nishimoto2011, 22 | author = {Nishimoto, S. and Vu, A. T. and Naselaris, T. and Benjamini, Y. and Yu, B. and Gallant, J. L.}, 23 | title = {Reconstructing visual experiences from brain activity evoked by natural movies}, 24 | journal = {Current Biology}, 25 | volume = {21}, 26 | number = {19}, 27 | pages = {1641--1646}, 28 | year = {2011} 29 | } 30 | 31 | @article{huth2012, 32 | author = {Huth, A. G. and Nishimoto, S. and Vu, A. T. and Gallant, J. L.}, 33 | title = {A continuous semantic space describes the representation of thousands of object and action categories across the human brain}, 34 | journal = {Neuron}, 35 | volume = {76}, 36 | number = {6}, 37 | pages = {1210--1224}, 38 | year = {2012} 39 | } 40 | 41 | @article{cukur2013, 42 | author = {Çukur, T. and Nishimoto, S. and Huth, A. G. and Gallant, J. L.}, 43 | title = {Attention during natural vision warps semantic representation across the human brain}, 44 | journal = {Nature neuroscience}, 45 | volume = {16}, 46 | number = {6}, 47 | pages = {763--770}, 48 | year = {2013} 49 | } 50 | 51 | @article{cukur2013b, 52 | author = {Çukur, T. and Huth, A. G. and Nishimoto, S. and Gallant, J. L.}, 53 | title = {Functional subdomains within human FFA}, 54 | journal = {Journal of Neuroscience}, 55 | volume = {33}, 56 | number = {42}, 57 | pages = {16748--16766}, 58 | year = {2013} 59 | } 60 | 61 | @article{stansbury2013, 62 | author = {Stansbury, D. E. and Naselaris, T. and Gallant, J. L.}, 63 | title = {Natural scene statistics account for the representation of scene categories in human visual cortex}, 64 | journal = {Neuron}, 65 | volume = {79}, 66 | number = {5}, 67 | pages = {1025--1034}, 68 | year = {2013} 69 | } 70 | 71 | @article{huth2016, 72 | author = {Huth, A. G. and De Heer, W. A. and Griffiths, T. L. and Theunissen, F. E. and Gallant, J. L.}, 73 | title = {Natural speech reveals the semantic maps that tile human cerebral cortex}, 74 | journal = {Nature}, 75 | volume = {532}, 76 | number = {7600}, 77 | pages = {453--458}, 78 | year = {2016} 79 | } 80 | 81 | @article{deheer2017, 82 | author = {de Heer, W. A. and Huth, A. G. and Griffiths, T. L. and Gallant, J. L. and Theunissen, F. E.}, 83 | title = {The hierarchical cortical organization of human speech processing}, 84 | journal = {Journal of Neuroscience}, 85 | volume = {37}, 86 | number = {27}, 87 | pages = {6539--6557}, 88 | year = {2017} 89 | } 90 | 91 | @article{lescroart2019, 92 | author = {Lescroart, M. D. and Gallant, J. L.}, 93 | title = {Human scene-selective areas represent 3D configurations of surfaces}, 94 | journal = {Neuron}, 95 | volume = {101}, 96 | number = {1}, 97 | pages = {178--192}, 98 | year = {2019} 99 | } 100 | 101 | @article{deniz2019, 102 | author = {Deniz, F. and Nunez-Elizalde, A. O. and Huth, A. G. and Gallant, J. L.}, 103 | title = {The representation of semantic information across human cerebral cortex during listening versus reading is invariant to stimulus modality}, 104 | journal = {Journal of Neuroscience}, 105 | volume = {39}, 106 | number = {39}, 107 | pages = {7722--7736}, 108 | year = {2019} 109 | } 110 | 111 | @article{nunez2019, 112 | author = {Nunez-Elizalde, A. O. and Huth, A. G. and Gallant, J. L.}, 113 | title = {Voxelwise encoding models with non-spherical multivariate normal priors}, 114 | journal = {Neuroimage}, 115 | volume = {197}, 116 | pages = {482--492}, 117 | year = {2019} 118 | } 119 | 120 | @article{popham2021, 121 | author = {Popham, S. F. and Huth, A. G. and Bilenko, N. Y. and Deniz, F. and Gao, J. S. and Nunez-Elizalde, A. O. and Gallant, J. L.}, 122 | title = {Visual and linguistic semantic representations are aligned at the border of human visual cortex}, 123 | journal = {Nature Neuroscience}, 124 | volume = {24}, 125 | number = {11}, 126 | pages = {1628--1636}, 127 | year = {2021} 128 | } 129 | 130 | @article{lebel2021, 131 | author = {LeBel, A. and Jain, S. and Huth, A. G.}, 132 | title = {Voxelwise encoding models show that cerebellar language representations are highly conceptual}, 133 | journal = {Journal of Neuroscience}, 134 | volume = {41}, 135 | number = {50}, 136 | pages = {10341--10355}, 137 | year = {2021} 138 | } 139 | 140 | @article{dupre2022, 141 | author = {Dupré la Tour, T. and Eickenberg, M. and Nunez-Elizalde, A.O. and Gallant, J. L.}, 142 | title = {Feature-space selection with banded ridge regression}, 143 | journal = {NeuroImage}, 144 | volume = {267}, 145 | pages = {119728}, 146 | year = {2022}, 147 | doi = {10.1016/j.neuroimage.2022.119728} 148 | } 149 | 150 | @misc{nishimoto2014data, 151 | author = {Nishimoto, S. and Vu, A. T. and Naselaris, T. and Benjamini, Y. and Yu, B. and Gallant, J. L.}, 152 | title = {Gallant Lab Natural Movie 4T {fMRI} Data}, 153 | publisher = {CRCNS.org}, 154 | year = {2014}, 155 | doi = {10.6080/K00Z715X} 156 | } 157 | 158 | @misc{huth2022data, 159 | author = {Huth, A. G. and Nishimoto, S. and Vu, A. T. and Dupré la Tour, T. and Gallant, J. L.}, 160 | title = {Gallant Lab Natural Short Clips 3T {fMRI} Data}, 161 | publisher = {GIN}, 162 | year = {2022}, 163 | doi = {10.12751/g-node.vy1zjd} 164 | } 165 | 166 | @article{dupre2025, 167 | author = {Dupré la Tour, T. and Visconti di Oleggio Castello, M. and Gallant, J. L.}, 168 | title = {The {Voxelwise Encoding Model} framework: a tutorial introduction to fitting encoding models to {fMRI} data}, 169 | journal = {Imaging Neuroscience}, 170 | year = {2025}, 171 | doi = {10.1162/imag_a_00575}, 172 | } 173 | 174 | @article{gao2015, 175 | author = {Gao, J. S. and Huth, A. G. and Lescroart, M. D. and Gallant, J. L.}, 176 | title = {Pycortex: an interactive surface visualizer for {fMRI}}, 177 | journal = {Frontiers in Neuroinformatics}, 178 | volume = {23}, 179 | year = {2015}, 180 | doi = {10.3389/fninf.2015.00023} 181 | } 182 | 183 | @misc{nunez2021software, 184 | author = {Nunez-Elizalde, A.O. and Deniz, F. and Dupré la Tour, T. and Visconti di Oleggio Castello, M. and Gallant, J.L.}, 185 | title = {pymoten: scientific python package for computing motion energy features from video}, 186 | year = {2021}, 187 | publisher = {Zenodo}, 188 | doi = {10.5281/zenodo.6349625} 189 | } 190 | 191 | @article{Sahani2002, 192 | title = {How linear are auditory cortical responses?}, 193 | author = {Sahani, M. and Linden, J.}, 194 | journal = {Adv. Neural Inf. Process. Syst.}, 195 | year = {2002} 196 | } 197 | 198 | @article{Hsu2004, 199 | title = {Quantifying variability in neural responses and its 200 | application for the validation of model predictions}, 201 | author = {Hsu, A. and Borst, A. and Theunissen, F. E.}, 202 | journal = {Network}, 203 | year = {2004}, 204 | } 205 | 206 | @article{Schoppe2016, 207 | title = {Measuring the Performance of Neural Models}, 208 | author = {Schoppe, O. and Harper, N. S. and Willmore, B. and 209 | King, A. and Schnupp, J.}, 210 | journal = {Front. Comput. Neurosci.}, 211 | year = {2016}, 212 | } 213 | 214 | @misc{saunders1998, 215 | title={Ridge regression learning algorithm in dual variables}, 216 | author={Saunders, C. and Gammerman, A. and Vovk, V.}, 217 | year={1998} 218 | } 219 | 220 | @book{Hastie2009, 221 | title = {The Elements of Statistical Learning}, 222 | author = {Hastie, Trevor and Tibshirani, Robert and Friedman, Jerome}, 223 | publisher = {Springer New York}, 224 | year = {2009}, 225 | } 226 | -------------------------------------------------------------------------------- /voxelwise_tutorials/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.3' 2 | -------------------------------------------------------------------------------- /voxelwise_tutorials/delayer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.base import BaseEstimator, TransformerMixin 3 | from sklearn.utils.validation import check_is_fitted, validate_data 4 | 5 | 6 | class Delayer(TransformerMixin, BaseEstimator): 7 | """Scikit-learn Transformer to add delays to features. 8 | 9 | This assumes that the samples are ordered in time. 10 | Adding a delay of 0 corresponds to leaving the features unchanged. 11 | Adding a delay of 1 corresponds to using features from the previous sample. 12 | 13 | Adding multiple delays can be used to take into account the slow 14 | hemodynamic response, with for example `delays=[1, 2, 3, 4]`. 15 | 16 | Parameters 17 | ---------- 18 | delays : array-like or None 19 | Indices of the delays applied to each feature. If multiple values are 20 | given, each feature is duplicated for each delay. 21 | 22 | Attributes 23 | ---------- 24 | n_features_in_ : int 25 | Number of features seen during the fit. 26 | 27 | Example 28 | ------- 29 | >>> from sklearn.pipeline import make_pipeline 30 | >>> from voxelwise_tutorials.delayer import Delayer 31 | >>> from himalaya.kernel_ridge import KernelRidgeCV 32 | >>> pipeline = make_pipeline(Delayer(delays=[1, 2, 3, 4]), KernelRidgeCV()) 33 | """ 34 | 35 | def __init__(self, delays=None): 36 | self.delays = delays 37 | 38 | def fit(self, X, y=None): 39 | """Fit the delayer. 40 | 41 | Parameters 42 | ---------- 43 | X : array of shape (n_samples, n_features) 44 | Training data. 45 | 46 | y : array of shape (n_samples,) or (n_samples, n_targets) 47 | Target values. Ignored. 48 | 49 | Returns 50 | ------- 51 | self : returns an instance of self. 52 | """ 53 | X = validate_data(self, X, dtype='numeric') 54 | self.n_features_in_ = X.shape[1] 55 | return self 56 | 57 | def transform(self, X): 58 | """Transform the input data X, copying features with different delays. 59 | 60 | Parameters 61 | ---------- 62 | X : array of shape (n_samples, n_features) 63 | Input data. 64 | 65 | Returns 66 | ------- 67 | Xt : array of shape (n_samples, n_features * n_delays) 68 | Transformed data. 69 | """ 70 | check_is_fitted(self) 71 | X = validate_data(self, X, reset=False, copy=True) 72 | 73 | n_samples, n_features = X.shape 74 | 75 | if self.delays is None: 76 | return X 77 | 78 | X_delayed = np.zeros((n_samples, n_features * len(self.delays)), 79 | dtype=X.dtype) 80 | for idx, delay in enumerate(self.delays): 81 | beg, end = idx * n_features, (idx + 1) * n_features 82 | if delay == 0: 83 | X_delayed[:, beg:end] = X 84 | elif delay > 0: 85 | X_delayed[delay:, beg:end] = X[:-delay] 86 | elif delay < 0: 87 | X_delayed[:-abs(delay), beg:end] = X[abs(delay):] 88 | 89 | return X_delayed 90 | 91 | def reshape_by_delays(self, Xt, axis=1): 92 | """Reshape an array, splitting and stacking across delays. 93 | 94 | Parameters 95 | ---------- 96 | Xt : array of shape (n_samples, n_features * n_delays) 97 | Transformed array. 98 | axis : int, default=1 99 | Axis to split. 100 | 101 | Returns 102 | ------- 103 | Xt_split :array of shape (n_delays, n_samples, n_features) 104 | Reshaped array, splitting across delays. 105 | """ 106 | delays = self.delays or [0] # deals with None 107 | return np.stack(np.split(Xt, len(delays), axis=axis)) 108 | -------------------------------------------------------------------------------- /voxelwise_tutorials/delays_toy.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from scipy.stats import gamma 4 | 5 | 6 | def simulate_bold(stimulus_array, TR=2.0, duration=32.0): 7 | """Simulate BOLD signal by convolving a stimulus time series with a canonical HRF. 8 | 9 | Parameters: 10 | ----------- 11 | stimulus_array : array-like 12 | Binary array representing stimulus onset (1) and absence (0) 13 | TR : float, optional 14 | Repetition time of the scanner in seconds (default: 2.0s) 15 | duration : float, optional 16 | Total duration to model the HRF in seconds (default: 32.0s) 17 | 18 | Returns: 19 | -------- 20 | bold_signal : ndarray 21 | The simulated BOLD signal after convolution 22 | """ 23 | # Create time array based on TR 24 | t = np.arange(0, duration, TR) 25 | 26 | # Define canonical double-gamma HRF 27 | # Parameters for the canonical HRF (based on SPM defaults) 28 | peak_delay = 6.0 # delay of peak in seconds 29 | undershoot_delay = 16.0 # delay of undershoot 30 | peak_disp = 1.0 # dispersion of peak 31 | undershoot_disp = 1.0 # dispersion of undershoot 32 | peak_amp = 1.0 # amplitude of peak 33 | undershoot_amp = 0.1 # amplitude of undershoot 34 | 35 | # Compute positive and negative gamma functions 36 | pos_gamma = peak_amp * gamma.pdf(t, peak_delay / peak_disp, scale=peak_disp) 37 | neg_gamma = undershoot_amp * gamma.pdf( 38 | t, undershoot_delay / undershoot_disp, scale=undershoot_disp 39 | ) 40 | 41 | # Canonical HRF = positive gamma - negative gamma 42 | hrf = pos_gamma - neg_gamma 43 | 44 | # Normalize HRF to have sum = 1 45 | hrf = hrf / np.sum(hrf) 46 | 47 | # Perform convolution 48 | bold_signal = np.convolve(stimulus_array, hrf, mode="full") 49 | 50 | # Return only the part of the signal that matches the length of the input 51 | return bold_signal[: len(stimulus_array)] 52 | 53 | 54 | def create_voxel_data(n_trs=50, TR=2.0, onset=30, duration=10, random_seed=42): 55 | """Create a toy dataset with a single voxel and a single feature. 56 | 57 | Parameters 58 | ---------- 59 | n_trs : int 60 | Number of time points (TRs). 61 | TR : float 62 | Repetition time in seconds. 63 | activation_t : float 64 | Time point of activation onset in seconds. 65 | activation_duration : float 66 | Duration of activation in seconds. 67 | random_seed : int 68 | Seed for random number generation. 69 | 70 | Returns 71 | ------- 72 | X : array of shape (n_trs,) 73 | The generated feature. 74 | Y : array of shape (n_trs,) 75 | The generated voxel data. 76 | times : array of shape (n_trs,) 77 | The time points corresponding to the voxel data. 78 | """ 79 | if onset > n_trs * TR: 80 | raise ValueError("onset must be less than n_trs * TR") 81 | if duration > n_trs * TR: 82 | raise ValueError("duration must be less than n_trs * TR") 83 | if duration < 0: 84 | raise ValueError("duration must be greater than 0") 85 | if onset < 0: 86 | raise ValueError("onset must be greater than 0") 87 | rng = np.random.RandomState(random_seed) 88 | # figure out slices of activation 89 | activation = slice(int(onset / TR), int((onset + duration) / TR)) 90 | X = np.zeros(n_trs) 91 | # add some arbitrary value to our feature 92 | X[activation] = 1 93 | Y = simulate_bold(X, TR=TR) 94 | # add some noise 95 | Y += rng.randn(n_trs) * 0.1 96 | times = np.arange(n_trs) * TR 97 | return X, Y, times 98 | 99 | 100 | def plot_delays_toy(X_delayed, Y, times, highlight=None): 101 | """Creates a figure showing a BOLD response and delayed versions of a stimulus. 102 | 103 | Parameters 104 | ---------- 105 | X_delayed : ndarray, shape (n_timepoints, n_delays) 106 | The delayed stimulus, where each column corresponds to a different delay. 107 | Y : ndarray, shape (n_timepoints,) 108 | The BOLD response time series. 109 | times : ndarray, shape (n_timepoints,) 110 | Time points in seconds. 111 | highlight : float or None, optional 112 | Time point to highlight in the plot (default: 30 seconds). 113 | 114 | Returns 115 | ------- 116 | axs : ndarray 117 | Array of matplotlib axes objects containing the plots. 118 | """ 119 | if X_delayed.ndim == 1: 120 | X_delayed = X_delayed[:, np.newaxis] 121 | n_delays = X_delayed.shape[1] 122 | n_rows = n_delays + 1 123 | TR = times[1] - times[0] 124 | 125 | fig, axs = plt.subplots( 126 | n_rows, 127 | 1, 128 | figsize=(6, n_rows), 129 | constrained_layout=True, 130 | sharex=True, 131 | sharey=True, 132 | ) 133 | axs[0].plot(times, Y, color="r") 134 | axs[0].set_title("BOLD response") 135 | if len(axs) == 2: 136 | axs[1].set_title("Feature") 137 | axs[1].plot(times, X_delayed, color="k") 138 | else: 139 | for i, (ax, xx) in enumerate(zip(axs.flat[1:], X_delayed.T)): 140 | ax.plot(times, xx, color="k") 141 | ax.set_title( 142 | "$x(t - {0:.0f})$ (feature delayed by {1} sample{2})".format( 143 | i * TR, i, "" if i == 1 else "s" 144 | ) 145 | ) 146 | if highlight is not None: 147 | for ax in axs.flat: 148 | ax.axvline(highlight, color="gray") 149 | ax.set_yticks([]) 150 | _ = axs[-1].set_xlabel("Time [s]") 151 | # add more margin at the top of the y axis 152 | ylim = axs[0].get_ylim() 153 | axs[0].set_ylim(ylim[0], ylim[1] * 1.2) 154 | return axs 155 | -------------------------------------------------------------------------------- /voxelwise_tutorials/io.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import shutil 4 | 5 | import h5py 6 | import scipy.sparse 7 | from himalaya.progress_bar import ProgressBar 8 | 9 | 10 | URL_CRCNS = 'https://portal.nersc.gov/project/crcns/download/index.php' 11 | 12 | 13 | def download_crcns(datafile, username, password, destination, 14 | chunk_size=2 ** 20, unpack=True): 15 | """Download a file from CRCNS, with a progress bar. 16 | 17 | Parameters 18 | ---------- 19 | datafile : str 20 | Name of the file on CRCNS. 21 | username : str 22 | Username on CRCNS. 23 | password : str 24 | Password on CRCNS. 25 | destination: str 26 | Directory where the data will be saved. 27 | chunk_size : int 28 | Size of the data downloaded at each iteration. 29 | unpack : bool 30 | If True, archives will be uncompress locally after the download. 31 | 32 | Returns 33 | ------- 34 | local_filename : str 35 | Local name of the downloaded file. 36 | """ 37 | 38 | login_data = dict(username=username, password=password, fn=datafile, 39 | submit='Login') 40 | 41 | with requests.Session() as s: 42 | response = s.post(URL_CRCNS, data=login_data, stream=True) 43 | 44 | # get content length for error checking and progress bar 45 | content_length = int(response.headers['Content-Length']) 46 | 47 | # check errors if the content is small 48 | if content_length < 1000: 49 | if "Error" in response.text: 50 | raise RuntimeError(response.text) 51 | 52 | # remove the dataset name 53 | filename = os.path.join(*login_data['fn'].split('/')[1:]) 54 | local_filename = os.path.join(destination, filename) 55 | 56 | # create subdirectory if necessary 57 | local_directory = os.path.dirname(local_filename) 58 | if not os.path.exists(local_directory) or not os.path.isdir( 59 | local_directory): 60 | os.makedirs(local_directory) 61 | 62 | # download the file if it does not already exist 63 | if os.path.exists(local_filename): 64 | print("%s already exists." % local_filename) 65 | else: 66 | bar = ProgressBar(title=filename, max_value=content_length) 67 | with open(local_filename, 'wb') as f: 68 | for chunk in response.iter_content(chunk_size=chunk_size): 69 | bar.update_with_increment_value(chunk_size) 70 | if chunk: 71 | f.write(chunk) 72 | print('%s downloaded.' % local_filename) 73 | 74 | # uncompress archives 75 | if unpack and os.path.splitext(local_filename)[1] in [".zip", ".gz"]: 76 | unpack_archive(local_filename) 77 | 78 | return local_filename 79 | 80 | 81 | def unpack_archive(archive_name): 82 | """Unpack an archive, saving files on the same directory. 83 | 84 | Parameters 85 | ---------- 86 | archive_name : str 87 | Local name of the archive. 88 | """ 89 | print('\tUnpacking') 90 | extract_dir = os.path.dirname(archive_name) 91 | shutil.unpack_archive(archive_name, extract_dir=extract_dir) 92 | 93 | 94 | def download_datalad(datafile, destination, source, 95 | siblings=["wasabi", "origin"]): 96 | """ 97 | Parameters 98 | ---------- 99 | datafile : str 100 | Name of the file in the dataset. 101 | destination: str 102 | Directory where the data will be saved. 103 | source : str 104 | URL of the dataset. 105 | siblings : list of str 106 | List of sibling/remote on which the download will be attempted. 107 | 108 | Returns 109 | ------- 110 | local_filename : str 111 | Local name of the downloaded file. 112 | 113 | Examples 114 | -------- 115 | >>> from voxelwise_tutorials.io import get_data_home 116 | >>> from voxelwise_tutorials.io import download_datalad 117 | >>> destination = get_data_home(dataset="shortclips") 118 | >>> download_datalad("features/wordnet.hdf", destination=destination, 119 | source="https://gin.g-node.org/gallantlab/shortclips") 120 | """ 121 | import datalad.api 122 | dataset = datalad.api.Dataset(path=destination) 123 | if not dataset.is_installed(): 124 | dataset = datalad.api.install(path=destination, source=source) 125 | 126 | def has_content(): 127 | """Double check that the file is actually present.""" 128 | repo = datalad.support.annexrepo.AnnexRepo(destination) 129 | return repo.file_has_content(datafile) 130 | 131 | for sibling in siblings: # Try the download successively on each sibling 132 | result = dataset.get(datafile, source=sibling) 133 | if has_content(): 134 | return result[0]["path"] 135 | 136 | raise RuntimeError("Failed to download %s." % datafile) 137 | 138 | 139 | def load_hdf5_array(file_name, key=None, slice=slice(0, None)): 140 | """Function to load data from an hdf file. 141 | 142 | Parameters 143 | ---------- 144 | file_name: string 145 | hdf5 file name. 146 | key: string 147 | Key name to load. If not provided, all keys will be loaded. 148 | slice: slice, or tuple of slices 149 | Load only a slice of the hdf5 array. It will load `array[slice]`. 150 | Use a tuple of slices to get a slice in multiple dimensions. 151 | 152 | Returns 153 | ------- 154 | result : array or dictionary 155 | Array, or dictionary of arrays (if `key` is None). 156 | """ 157 | with h5py.File(file_name, mode='r') as hf: 158 | if key is None: 159 | data = dict() 160 | for k in hf.keys(): 161 | data[k] = hf[k][slice] 162 | return data 163 | else: 164 | # Some keys have been renamed. Use old key on KeyError. 165 | if key not in hf.keys() and key in OLD_KEYS: 166 | key = OLD_KEYS[key] 167 | 168 | return hf[key][slice] 169 | 170 | 171 | def load_hdf5_sparse_array(file_name, key): 172 | """Load a scipy sparse array from an hdf file 173 | 174 | Parameters 175 | ---------- 176 | file_name : string 177 | File name containing array to be loaded. 178 | key : string 179 | Name of variable to be loaded. 180 | 181 | Notes 182 | ----- 183 | This function relies on variables being stored with specific naming 184 | conventions, so cannot be used to load arbitrary sparse arrays. 185 | """ 186 | with h5py.File(file_name, mode='r') as hf: 187 | 188 | # Some keys have been renamed. Use old key on KeyError. 189 | if '%s_data' % key not in hf.keys() and key in OLD_KEYS: 190 | key = OLD_KEYS[key] 191 | 192 | # The voxel_to_fsaverage mapper is sometimes split between left/right. 193 | if (key == "voxel_to_fsaverage" and '%s_data' % key not in hf.keys() 194 | and "vox_to_fsavg_left_data" in hf.keys()): 195 | left = load_hdf5_sparse_array(file_name, "vox_to_fsavg_left") 196 | right = load_hdf5_sparse_array(file_name, "vox_to_fsavg_right") 197 | return scipy.sparse.vstack([left, right]) 198 | 199 | data = (hf['%s_data' % key], hf['%s_indices' % key], 200 | hf['%s_indptr' % key]) 201 | sparsemat = scipy.sparse.csr_matrix(data, shape=hf['%s_shape' % key]) 202 | return sparsemat 203 | 204 | 205 | # Correspondence between old keys and new keys. {new_key: old_key} 206 | OLD_KEYS = { 207 | "flatmap_mask": "pixmask", 208 | "voxel_to_flatmap": "pixmap", 209 | } 210 | 211 | 212 | def save_hdf5_dataset(file_name, dataset, mode='w'): 213 | """Save a dataset of arrays and sparse arrays. 214 | 215 | Parameters 216 | ---------- 217 | file_name : str 218 | Full name of the file. 219 | dataset : dict of arrays 220 | Mappers to save. 221 | mode : str 222 | File opening model. 223 | Use 'w' to write from scratch, 'a' to add to existing file. 224 | """ 225 | print("Saving... ", end="", flush=True) 226 | 227 | with h5py.File(file_name, mode=mode) as hf: 228 | for name, array in dataset.items(): 229 | 230 | if scipy.sparse.issparse(array): # sparse array 231 | array = array.tocsr() 232 | hf.create_dataset(name + '_indices', data=array.indices, 233 | compression='gzip') 234 | hf.create_dataset(name + '_data', data=array.data, 235 | compression='gzip') 236 | hf.create_dataset(name + '_indptr', data=array.indptr, 237 | compression='gzip') 238 | hf.create_dataset(name + '_shape', data=array.shape, 239 | compression='gzip') 240 | else: # dense array 241 | hf.create_dataset(name, data=array, compression='gzip') 242 | 243 | print("Saved %s" % file_name) 244 | 245 | 246 | def get_data_home(dataset=None, data_home=None) -> str: 247 | """Return the path of the voxelwise tutorials data directory. 248 | 249 | This folder is used by some large dataset loaders to avoid downloading the 250 | data several times. By default the data dir is set to a folder named 251 | 'voxelwise_tutorials' in the user home folder. Alternatively, it can be set 252 | by the 'VOXELWISE_TUTORIALS_DATA' environment variable or programmatically 253 | by giving an explicit folder path. The '~' symbol is expanded to the user 254 | home folder. If the folder does not already exist, it is automatically 255 | created. 256 | 257 | Parameters 258 | ---------- 259 | dataset : str | None 260 | Optional name of a particular dataset subdirectory, to append to the 261 | data_home path. 262 | data_home : str | None 263 | Optional path to voxelwise tutorials data dir, to use instead of the 264 | default one. 265 | 266 | Returns 267 | ------- 268 | data_home : str 269 | The path to voxelwise tutorials data directory. 270 | """ 271 | if data_home is None: 272 | data_home = os.environ.get( 273 | 'VOXELWISE_TUTORIALS_DATA', 274 | os.path.join('~', 'voxelwise_tutorials_data')) 275 | 276 | data_home = os.path.expanduser(data_home) 277 | if not os.path.exists(data_home): 278 | os.makedirs(data_home) 279 | 280 | if dataset is not None: 281 | data_home = os.path.join(data_home, dataset) 282 | 283 | return data_home 284 | 285 | 286 | def clear_data_home(dataset=None, data_home=None): 287 | """Delete all the content of the data home cache. 288 | 289 | Parameters 290 | ---------- 291 | dataset : str | None 292 | Optional name of a particular dataset subdirectory, to append to the 293 | data_home path. 294 | data_home : str | None 295 | Optional path to voxelwise tutorials data dir, to use instead of the 296 | default one. 297 | """ 298 | data_home = get_data_home(dataset=dataset, data_home=data_home) 299 | shutil.rmtree(data_home) 300 | -------------------------------------------------------------------------------- /voxelwise_tutorials/progress_bar.py: -------------------------------------------------------------------------------- 1 | # Simply use himalaya 2 | from himalaya import bar, ProgressBar 3 | 4 | __all__ = ["bar", "ProgressBar"] 5 | -------------------------------------------------------------------------------- /voxelwise_tutorials/regression_toy.py: -------------------------------------------------------------------------------- 1 | """These helper functions are used in the tutorial on ridge regression 2 | (tutorials/shortclips/02_plot_ridge_regression.py). 3 | """ 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | 7 | COEFS = np.array([0.4, 0.3]) 8 | 9 | 10 | def create_regression_toy(n_samples=50, n_features=2, noise=0.3, correlation=0, 11 | random_state=0): 12 | """Create a regression toy dataset.""" 13 | if n_features > 2: 14 | raise ValueError("n_features must be <= 2.") 15 | 16 | # create features 17 | rng = np.random.RandomState(random_state) 18 | X = rng.randn(n_samples, n_features) 19 | X -= X.mean(0) 20 | X /= X.std(0) 21 | 22 | # makes correlation(X[:, 1], X[:, 0]) = correlation 23 | if n_features == 2: 24 | X[:, 1] -= (X[:, 0] @ X[:, 1]) * X[:, 0] / (X[:, 0] @ X[:, 0]) 25 | X /= X.std(0) 26 | if correlation != 0: 27 | X[:, 1] *= np.sqrt(correlation ** (-2) - 1) 28 | X[:, 1] += X[:, 0] * np.sign(correlation) 29 | X /= X.std(0) 30 | 31 | # create linear coefficients 32 | w = COEFS[:n_features] 33 | 34 | # create target 35 | y = X @ w 36 | y += rng.randn(*y.shape) * noise 37 | 38 | return X, y 39 | 40 | 41 | def l2_loss(X, y, w): 42 | if w.ndim == 1: 43 | w = w[:, None] 44 | return np.sum((X @ w - y[:, None]) ** 2, axis=0) 45 | 46 | 47 | def ridge(X, y, alpha): 48 | n_features = X.shape[1] 49 | return np.linalg.solve(X.T @ X + np.eye(n_features) * alpha, X.T @ y) 50 | 51 | 52 | def plot_1d(X, y, w): 53 | w = np.atleast_1d(w) 54 | 55 | fig, axes = plt.subplots(1, 2, figsize=(6.7, 2.5)) 56 | 57 | # left plot: y = f(x) 58 | ax = axes[0] 59 | ax.scatter(X, y, alpha=0.5, color="C0") 60 | ylim = ax.get_ylim() 61 | ax.plot([X.min(), X.max()], [X.min() * w[0], X.max() * w[0]], color="C1") 62 | ax.set(xlabel="X[:, 0]", ylabel="y", ylim=ylim) 63 | ax.grid() 64 | for xx, yy in zip(X[:, 0], y): 65 | ax.plot([xx, xx], [yy, xx * w[0]], c='gray', alpha=0.5) 66 | 67 | # right plot: loss = f(w) 68 | ax = axes[1] 69 | w_range = np.linspace(-0.1, 0.8, 100) 70 | ax.plot(w_range, l2_loss(X, y, w_range[None]), color="C2") 71 | ax.scatter([w[0]], l2_loss(X, y, w), color="C1") 72 | ax.set(xlabel="w[0]", ylabel="Squared loss") 73 | ax.grid() 74 | 75 | fig.tight_layout() 76 | plt.show() 77 | 78 | 79 | def plot_2d(X, y, w, flat=True, alpha=None, show_noiseless=True): 80 | from mpl_toolkits import mplot3d # noqa 81 | w = np.array(w) 82 | 83 | fig = plt.figure(figsize=(6.7, 2.5)) 84 | 85 | ##################### 86 | # left plot: y = f(x) 87 | 88 | try: # computed_zorder is only available in matplotlib >= 3.4 89 | ax = fig.add_subplot(121, projection='3d', computed_zorder=False) 90 | except AttributeError: 91 | ax = fig.add_subplot(121, projection='3d') 92 | 93 | # to help matplotlib displays scatter points behind any surface, we 94 | # first plot the point below, then the surface, then the points above, 95 | # and use computed_zorder=False. 96 | above = y > X @ w 97 | ax.scatter3D(X[~above, 0], X[~above, 1], y[~above], alpha=0.5, color="C0") 98 | 99 | xmin, xmax = X.min(), X.max() 100 | xx, yy = np.meshgrid(np.linspace(xmin, xmax, 10), 101 | np.linspace(xmin, xmax, 10)) 102 | ax.plot_surface(xx, yy, xx * w[0] + yy * w[1], color=[0, 0, 0, 0], 103 | edgecolor=[1., 0.50, 0.05, 0.50]) 104 | 105 | # plot the point above *after* the surface 106 | ax.scatter3D(X[above, 0], X[above, 1], y[above], alpha=0.5, color="C0") 107 | 108 | ax.set(xlabel="X[:, 0]", ylabel="X[:, 1]", zlabel="y", 109 | zlim=[yy.min(), yy.max()]) 110 | 111 | ######################### 112 | # right plot: loss = f(w) 113 | if flat: 114 | ax = fig.add_subplot(122) 115 | w0, w1 = np.meshgrid(np.linspace(-0.1, 0.9, 100), 116 | np.linspace(-0.1, 0.9, 100)) 117 | w_range = np.stack([w0.ravel(), w1.ravel()]) 118 | zz = l2_loss(X, y, w_range).reshape(w0.shape) 119 | # zz_reg = (w_range ** 2).sum(0).reshape(w0.shape) * alpha 120 | ax.imshow(zz, extent=(w0.min(), w0.max(), w1.min(), w1.max()), 121 | origin="lower") 122 | im = ax.contourf(w0, w1, zz, levels=20, vmax=zz.max(), 123 | extent=(w0.min(), w0.max(), w1.min(), w1.max()), 124 | origin="lower") 125 | 126 | ax.scatter([w[0]], [w[1]], color="C1", s=[20], label="w") 127 | if show_noiseless: 128 | ax.scatter([COEFS[0]], [COEFS[1]], color="k", s=[20], marker="x", 129 | label="w_noiseless") 130 | if alpha is not None: 131 | w_ols = np.linalg.solve(X.T @ X, X.T @ y) 132 | ax.scatter([w_ols[0]], [w_ols[1]], color="C3", s=[20], 133 | marker="o", label="w_OLS") 134 | ax.legend(framealpha=0.2) 135 | 136 | if alpha is not None: 137 | xlim, ylim = ax.get_xlim(), ax.get_ylim() 138 | angle = np.linspace(-np.pi, np.pi, 100) 139 | radius = np.sqrt(np.sum(w ** 2)) 140 | ax.plot(np.cos(angle) * radius, np.sin(angle) * radius, c='k') 141 | ax.set_xlim(xlim), ax.set_ylim(ylim) 142 | 143 | ax.set(xlabel="w[0]", ylabel="w[1]") 144 | cbar = plt.colorbar(im, ax=ax) 145 | cbar.ax.set(ylabel="Squared loss") 146 | 147 | else: # 3D version of the right plot 148 | ax = fig.add_subplot(122, projection='3d') 149 | w0, w1 = np.meshgrid(np.linspace(0, 1, 10), np.linspace(0, 1, 10)) 150 | w_range = np.stack([w0.ravel(), w1.ravel()]) 151 | zz = l2_loss(X, y, w_range).reshape(w0.shape) 152 | ax.plot_surface(w0, w1, zz, color="C2", alpha=0.4, edgecolor='gray') 153 | ax.scatter3D([w[0]], [w[1]], [l2_loss(X, y, w)], color="C1") 154 | ax.set(xlabel="w[0]", ylabel="w[1]", zlabel="Squared loss") 155 | 156 | fig.tight_layout() 157 | plt.show() 158 | 159 | 160 | def plot_kfold2(X, y, alpha=0, fit=True, flip=False): 161 | half = X.shape[0] // 2 162 | 163 | if not fit: 164 | fig, ax = plt.subplots(1, 1, figsize=(3.4, 2.5)) 165 | ax.scatter(X[:half], y[:half], alpha=0.5, color="C0") 166 | ax.scatter(X[half:], y[half:], alpha=0.5, color="C1") 167 | ax.set(xlabel="x1", ylabel="y") 168 | ax.grid() 169 | fig.tight_layout() 170 | plt.show() 171 | return None 172 | 173 | fig, axes = plt.subplots(1, 2, figsize=(6.7, 2.5), sharex=True, 174 | sharey=True) 175 | 176 | w_ridge1 = ridge(X[:half], y[:half], alpha) 177 | w_ridge2 = ridge(X[half:], y[half:], alpha) 178 | 179 | ax = axes[0] 180 | if not flip: 181 | ax.scatter(X[:half], y[:half], alpha=0.5, color="C0") 182 | else: 183 | ax.scatter(X[half:], y[half:], alpha=0.5, color="C1") 184 | ax.plot([X.min(), X.max()], 185 | [X.min() * w_ridge1, X.max() * w_ridge1], color="C0") 186 | ax.set(xlabel="X[:, 0]", ylabel="y", title='model 1') 187 | ax.grid() 188 | 189 | ax = axes[1] 190 | if flip: 191 | ax.scatter(X[:half], y[:half], alpha=0.5, color="C0") 192 | else: 193 | ax.scatter(X[half:], y[half:], alpha=0.5, color="C1") 194 | ax.plot([X.min(), X.max()], 195 | [X.min() * w_ridge2, X.max() * w_ridge2], color="C1") 196 | ax.set(xlabel="X[:, 0]", ylabel="y", title='model 2') 197 | ax.grid() 198 | 199 | fig.tight_layout() 200 | plt.show() 201 | 202 | 203 | def plot_cv_path(X, y): 204 | losses = [] 205 | alphas = np.logspace(-2, 4, 12) 206 | 207 | half = X.shape[0] // 2 208 | for alpha in alphas: 209 | 210 | w_ridge1 = ridge(X[:half], y[:half], alpha) 211 | w_ridge2 = ridge(X[half:], y[half:], alpha) 212 | 213 | losses.append( 214 | l2_loss(X[half:], y[half:], w_ridge1) + 215 | l2_loss(X[:half], y[:half], w_ridge2)) 216 | 217 | best = np.argmin(losses) 218 | 219 | # final cv plot 220 | fig, ax = plt.subplots(1, 1, figsize=(4, 3)) 221 | ax.semilogx(alphas, losses, '-o', label="candidates") 222 | ax.set(xlabel="alpha", ylabel="cross-validation error") 223 | ax.plot([alphas[best]], [losses[best]], "o", c="C3", label="best") 224 | ax.legend() 225 | fig.tight_layout() 226 | plt.show() 227 | -------------------------------------------------------------------------------- /voxelwise_tutorials/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gallantlab/voxelwise_tutorials/fc649ba74febd3644a17c403ac900151a79ddc70/voxelwise_tutorials/tests/__init__.py -------------------------------------------------------------------------------- /voxelwise_tutorials/tests/test_delayer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | 4 | import sklearn.kernel_ridge 5 | import sklearn.utils.estimator_checks 6 | 7 | from voxelwise_tutorials.delayer import Delayer 8 | 9 | 10 | @sklearn.utils.estimator_checks.parametrize_with_checks([Delayer()]) 11 | def test_check_estimator(estimator, check): 12 | check(estimator) 13 | 14 | 15 | @pytest.mark.parametrize('delays', [None, [0]]) 16 | def test_no_delays(delays): 17 | X = np.random.randn(10, 3) 18 | Xt = Delayer(delays=delays).fit_transform(X) 19 | np.testing.assert_array_equal(Xt, X) 20 | 21 | 22 | @pytest.mark.parametrize('delays', [[0], [0, 1], [0, -1, 2]]) 23 | def test_zero_delay_identity(delays): 24 | X = np.random.randn(10, 3) 25 | Xt = Delayer(delays=delays).fit_transform(X) 26 | np.testing.assert_array_equal(Xt[:, :X.shape[1]], X) 27 | 28 | 29 | @pytest.mark.parametrize('delays', [[1], [1, 2], [-1, 0, 2]]) 30 | def test_nonzero_delay(delays): 31 | X = np.random.randn(10, 3) 32 | Xt = Delayer(delays=delays).fit_transform(X) 33 | with pytest.raises(AssertionError): 34 | np.testing.assert_array_equal(Xt[:, :X.shape[1]], X) 35 | 36 | 37 | @pytest.mark.parametrize('delays', [[1], [1, 2], [-1, 0, 2]]) 38 | def test_reshape_by_delays(delays): 39 | X = np.random.randn(10, 3) 40 | trans = Delayer(delays=delays) 41 | Xt = trans.fit_transform(X) 42 | Xtt = trans.reshape_by_delays(Xt) 43 | 44 | assert Xtt.shape == (len(delays), X.shape[0], X.shape[1]) 45 | -------------------------------------------------------------------------------- /voxelwise_tutorials/tests/test_delays_toy.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | import pytest 4 | 5 | from voxelwise_tutorials import delays_toy 6 | 7 | 8 | def test_simulate_bold(): 9 | """Test that simulate_bold function works correctly""" 10 | # Create a simple stimulus array with an onset 11 | stimulus = np.zeros(50) 12 | stimulus[10:15] = 1 # activation for 5 time points 13 | 14 | # Test with default parameters 15 | bold = delays_toy.simulate_bold(stimulus) 16 | 17 | # Check that output has the same length as input 18 | assert len(bold) == len(stimulus) 19 | 20 | # Check that there is activation (values > 0) after stimulus onset 21 | assert np.any(bold[10:] > 0) 22 | 23 | # Test with different TR 24 | bold_tr1 = delays_toy.simulate_bold(stimulus, TR=1.0) 25 | assert len(bold_tr1) == len(stimulus) 26 | 27 | 28 | def test_create_voxel_data(): 29 | """Test that create_voxel_data function works correctly""" 30 | # Test with default parameters 31 | X, Y, times = delays_toy.create_voxel_data() 32 | 33 | # Check shapes 34 | assert X.shape == (50,) 35 | assert Y.shape == (50,) 36 | assert times.shape == (50,) 37 | 38 | # Check that activation happens at expected time 39 | onset_idx = int(30 / 2.0) # 30 seconds at TR=2.0 40 | assert np.all( 41 | X[onset_idx : onset_idx + 5] == 1 42 | ) # Should be active for 5 TRs (10s duration) 43 | 44 | # Test with different parameters 45 | X2, Y2, times2 = delays_toy.create_voxel_data( 46 | n_trs=100, TR=1.0, onset=20, duration=5 47 | ) 48 | assert X2.shape == (100,) 49 | assert Y2.shape == (100,) 50 | assert times2.shape == (100,) 51 | 52 | # Test error cases 53 | with pytest.raises(ValueError): 54 | delays_toy.create_voxel_data(n_trs=50, onset=120) # onset > n_trs * TR 55 | 56 | with pytest.raises(ValueError): 57 | delays_toy.create_voxel_data(duration=-5) # negative duration 58 | 59 | 60 | def test_plot_delays_toy(): 61 | """Test that plot_delays_toy function works correctly""" 62 | # Create sample data 63 | X, Y, times = delays_toy.create_voxel_data() 64 | 65 | # Test with 1D array (single delay) 66 | axs = delays_toy.plot_delays_toy(X, Y, times) 67 | assert len(axs) == 2 # Should have 2 subplots 68 | plt.close() 69 | 70 | # Test with 2D array (multiple delays) 71 | X_delayed = np.column_stack([X, np.roll(X, 1), np.roll(X, 2)]) 72 | axs = delays_toy.plot_delays_toy(X_delayed, Y, times) 73 | assert len(axs) == 4 # Should have 4 subplots (1 for Y, 3 for X) 74 | plt.close() 75 | 76 | # Test with highlight 77 | axs = delays_toy.plot_delays_toy(X, Y, times, highlight=30) 78 | plt.close() 79 | -------------------------------------------------------------------------------- /voxelwise_tutorials/tests/test_io.py: -------------------------------------------------------------------------------- 1 | from tempfile import NamedTemporaryFile 2 | 3 | import numpy as np 4 | import scipy.sparse as sp 5 | 6 | from voxelwise_tutorials.io import save_hdf5_dataset 7 | from voxelwise_tutorials.io import load_hdf5_array 8 | from voxelwise_tutorials.io import load_hdf5_sparse_array 9 | 10 | 11 | def test_save_dataset_to_hdf5(): 12 | tmp_file = NamedTemporaryFile(suffix='.hdf5') 13 | file_name = tmp_file.name 14 | 15 | dataset = { 16 | 'pixmap': sp.csr_matrix(np.random.rand(10, 3)), 17 | 'array': np.random.rand(10, 3), 18 | } 19 | save_hdf5_dataset(file_name, dataset) 20 | 21 | # test loading sparse arrays 22 | pixmap = load_hdf5_sparse_array(file_name, 'pixmap') 23 | np.testing.assert_array_equal(pixmap.toarray(), dataset['pixmap'].toarray()) 24 | # test new name 25 | pixmap = load_hdf5_sparse_array(file_name, 'voxel_to_flatmap') 26 | np.testing.assert_array_equal(pixmap.toarray(), dataset['pixmap'].toarray()) 27 | # test loading dense arrays 28 | array = load_hdf5_array(file_name, 'array') 29 | np.testing.assert_array_equal(array, dataset['array']) 30 | -------------------------------------------------------------------------------- /voxelwise_tutorials/tests/test_mappers.py: -------------------------------------------------------------------------------- 1 | """Unit tests on the mappers. 2 | 3 | Requires the shortclips dataset locally. 4 | """ 5 | 6 | import os 7 | 8 | import numpy as np 9 | import cortex 10 | from cortex.testing_utils import has_installed 11 | import matplotlib.pyplot as plt 12 | 13 | from voxelwise_tutorials.io import load_hdf5_array 14 | from voxelwise_tutorials.io import load_hdf5_sparse_array 15 | from voxelwise_tutorials.viz import plot_flatmap_from_mapper 16 | from voxelwise_tutorials.viz import plot_2d_flatmap_from_mapper 17 | 18 | from voxelwise_tutorials.io import get_data_home 19 | from voxelwise_tutorials.io import download_datalad 20 | 21 | subject = "S01" 22 | directory = get_data_home(dataset="shortclips") 23 | file_name = os.path.join("mappers", f'{subject}_mappers.hdf') 24 | mapper_file = os.path.join(directory, file_name) 25 | 26 | # download mapper if not already present 27 | download_datalad(file_name, destination=directory, 28 | source="https://gin.g-node.org/gallantlab/shortclips") 29 | 30 | # Change to save = True to save the figures locally and check the results 31 | save_fig = False 32 | 33 | 34 | def test_flatmap_mappers(): 35 | 36 | ################## 37 | # create fake data 38 | voxel_to_flatmap = load_hdf5_sparse_array(mapper_file, 'voxel_to_flatmap') 39 | voxels = np.linspace(0, 1, voxel_to_flatmap.shape[1]) 40 | 41 | ###################### 42 | # plot with the mapper 43 | ax = plot_flatmap_from_mapper(voxels=voxels, mapper_file=mapper_file, 44 | ax=None) 45 | fig = ax.figure 46 | if save_fig: 47 | fig.savefig('test.png') 48 | plt.close(fig) 49 | 50 | 51 | def test_plot_2d_flatmap_from_mapper(): 52 | 53 | # Change to save = True to save the figures locally and check the results 54 | save_fig = False 55 | 56 | ################## 57 | # create fake data 58 | voxel_to_flatmap = load_hdf5_sparse_array(mapper_file, 'voxel_to_flatmap') 59 | phase = np.linspace(0, 2 * np.pi, voxel_to_flatmap.shape[1]) 60 | sin = np.sin(phase) 61 | cos = np.cos(phase) 62 | 63 | ###################### 64 | # plot with the mapper 65 | ax = plot_2d_flatmap_from_mapper(sin, cos, mapper_file=mapper_file, 66 | vmin=-1, vmax=1, vmin2=-1, vmax2=1) 67 | fig = ax.figure 68 | if save_fig: 69 | fig.savefig('test_2d.png') 70 | plt.close(fig) 71 | 72 | 73 | def test_roi_masks_shape(): 74 | all_mappers = load_hdf5_array(mapper_file, key=None) 75 | 76 | n_pixels, n_voxels = all_mappers['voxel_to_flatmap_shape'] 77 | n_vertices, n_voxels_ = all_mappers['voxel_to_fsaverage_shape'] 78 | assert n_voxels_ == n_voxels 79 | 80 | for key, val in all_mappers.items(): 81 | if 'roi_mask_' in key: 82 | assert val.shape == (n_voxels, ) 83 | 84 | 85 | def test_fsaverage_mappers(): 86 | 87 | # Change to save = True to save the figures locally and check the results 88 | save_fig = False 89 | 90 | ################## 91 | # create fake data 92 | voxel_to_fsaverage = load_hdf5_sparse_array(mapper_file, 93 | 'voxel_to_fsaverage') 94 | voxels = np.linspace(0, 1, voxel_to_fsaverage.shape[1]) 95 | 96 | ################## 97 | # download fsaverage subject 98 | if not hasattr(cortex.db, "fsaverage"): 99 | cortex.utils.download_subject(subject_id="fsaverage", 100 | pycortex_store=cortex.db.filestore) 101 | cortex.db.reload_subjects() # force filestore reload 102 | 103 | ############################# 104 | # plot with fsaverage mappers 105 | projected = voxel_to_fsaverage @ voxels 106 | vertex = cortex.Vertex(projected, 'fsaverage', vmin=0, vmax=0.3, 107 | cmap='inferno', alpha=0.7, with_curvature=True) 108 | fig = cortex.quickshow(vertex, with_rois=has_installed("inkscape")) 109 | if save_fig: 110 | fig.savefig('test_fsaverage.png') 111 | plt.close(fig) 112 | -------------------------------------------------------------------------------- /voxelwise_tutorials/tests/test_model.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import numpy as np 5 | 6 | from sklearn.model_selection import check_cv 7 | from sklearn.pipeline import make_pipeline 8 | from sklearn.preprocessing import StandardScaler 9 | 10 | from himalaya.backend import set_backend 11 | from himalaya.kernel_ridge import KernelRidgeCV 12 | 13 | from voxelwise_tutorials.delayer import Delayer 14 | from voxelwise_tutorials.io import load_hdf5_array 15 | from voxelwise_tutorials.io import get_data_home 16 | from voxelwise_tutorials.io import download_datalad 17 | from voxelwise_tutorials.utils import explainable_variance 18 | from voxelwise_tutorials.utils import generate_leave_one_run_out 19 | 20 | # use "cupy" or "torch_cuda" for faster computation with GPU 21 | backend = set_backend("numpy", on_error="warn") 22 | 23 | # Download the dataset 24 | subject = "S01" 25 | feature_spaces = ["motion_energy", "wordnet"] 26 | directory = get_data_home(dataset="shortclips") 27 | for file_name in [ 28 | "features/motion_energy.hdf", 29 | "features/wordnet.hdf", 30 | "mappers/S01_mappers.hdf", 31 | "responses/S01_responses.hdf", 32 | ]: 33 | download_datalad(file_name, destination=directory, 34 | source="https://gin.g-node.org/gallantlab/shortclips") 35 | 36 | 37 | def run_model(X_train, X_test, Y_train, Y_test, run_onsets): 38 | ############## 39 | # define model 40 | n_samples_train = Y_train.shape[0] 41 | cv = generate_leave_one_run_out(n_samples_train, run_onsets, 42 | random_state=0, n_runs_out=1) 43 | cv = check_cv(cv) 44 | 45 | alphas = np.logspace(-4, 15, 20) 46 | 47 | model = make_pipeline( 48 | StandardScaler(with_mean=True, with_std=False), 49 | Delayer(delays=[1, 2, 3, 4]), 50 | KernelRidgeCV( 51 | kernel="linear", alphas=alphas, cv=cv, 52 | solver_params=dict(n_targets_batch=1000, n_alphas_batch=10)), 53 | ) 54 | 55 | ########### 56 | # run model 57 | model.fit(X_train, Y_train) 58 | test_scores = model.score(X_test, Y_test) 59 | 60 | test_scores = backend.to_numpy(test_scores) 61 | # cv_scores = backend.to_numpy(model[-1].cv_scores_) 62 | 63 | return test_scores 64 | 65 | 66 | @pytest.mark.parametrize('feature_space', feature_spaces) 67 | def test_model_fitting(feature_space): 68 | ########################################### 69 | # load the data 70 | 71 | # load X 72 | features_file = os.path.join(directory, 'features', 73 | feature_space + ".hdf") 74 | features = load_hdf5_array(features_file) 75 | X_train = features['X_train'] 76 | X_test = features['X_test'] 77 | 78 | # load Y 79 | responses_file = os.path.join(directory, 'responses', 80 | subject + "_responses.hdf") 81 | responses = load_hdf5_array(responses_file) 82 | Y_train = responses['Y_train'] 83 | Y_test_repeats = responses['Y_test'] 84 | run_onsets = responses['run_onsets'] 85 | 86 | ############################################# 87 | # select voxels based on explainable variance 88 | ev = explainable_variance(Y_test_repeats) 89 | mask = ev > 0.4 90 | assert mask.sum() > 0 91 | Y_train = Y_train[:, mask] 92 | Y_test = Y_test_repeats[:, :, mask].mean(0) 93 | 94 | ########################################### 95 | # fit a ridge model and compute test scores 96 | test_scores = run_model(X_train, X_test, Y_train, Y_test, run_onsets) 97 | assert np.percentile(test_scores, 95) > 0.05 98 | assert np.percentile(test_scores, 99) > 0.15 99 | assert np.percentile(test_scores, 100) > 0.35 100 | -------------------------------------------------------------------------------- /voxelwise_tutorials/tests/test_regression_toy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | from voxelwise_tutorials.regression_toy import create_regression_toy 5 | from voxelwise_tutorials.regression_toy import plot_1d 6 | from voxelwise_tutorials.regression_toy import plot_2d 7 | from voxelwise_tutorials.regression_toy import plot_kfold2 8 | from voxelwise_tutorials.regression_toy import plot_cv_path 9 | 10 | 11 | def test_smoke_regression_toy(): 12 | """Follow tutorials/shortclips/02_plot_ridge_regression.py.""" 13 | X, y = create_regression_toy(n_samples=50, n_features=1) 14 | plot_1d(X, y, w=[0]) 15 | plt.close('all') 16 | w_ols = np.linalg.solve(X.T @ X, X.T @ y) 17 | plot_1d(X, y, w=w_ols) 18 | plt.close('all') 19 | 20 | X, y = create_regression_toy(n_features=2) 21 | plot_2d(X, y, w=[0, 0], show_noiseless=False) 22 | plt.close('all') 23 | plot_2d(X, y, w=[0.4, 0], show_noiseless=False) 24 | plt.close('all') 25 | plot_2d(X, y, w=[0, 0.3], show_noiseless=False) 26 | plt.close('all') 27 | 28 | w_ols = np.linalg.solve(X.T @ X, X.T @ y) 29 | plot_2d(X, y, w=w_ols) 30 | plt.close('all') 31 | 32 | X, y = create_regression_toy(n_features=2, correlation=0.9) 33 | w_ols = np.linalg.solve(X.T @ X, X.T @ y) 34 | plot_2d(X, y, w=w_ols) 35 | plt.close('all') 36 | 37 | X, y = create_regression_toy(n_features=2, correlation=0.9) 38 | alpha = 23 39 | w_ridge = np.linalg.solve(X.T @ X + np.eye(X.shape[1]) * alpha, X.T @ y) 40 | plot_2d(X, y, w_ridge, alpha=alpha) 41 | plt.close('all') 42 | 43 | X, y = create_regression_toy(n_features=1) 44 | plot_kfold2(X, y, fit=False) 45 | plt.close('all') 46 | alpha = 0.1 47 | plot_kfold2(X, y, alpha, fit=True) 48 | plt.close('all') 49 | plot_kfold2(X, y, alpha, fit=True, flip=True) 50 | plt.close('all') 51 | 52 | noise = 0.1 53 | X, y = create_regression_toy(n_features=2, noise=noise) 54 | plot_cv_path(X, y) 55 | plt.close('all') 56 | -------------------------------------------------------------------------------- /voxelwise_tutorials/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from voxelwise_tutorials.utils import generate_leave_one_run_out, zscore_runs 5 | 6 | 7 | def test_generate_leave_one_run_out_disjoint(): 8 | n_samples = 40 9 | run_onsets = [0, 10, 20, 30] 10 | 11 | for train, val in generate_leave_one_run_out(n_samples, run_onsets): 12 | assert len(train) > 0 13 | assert len(val) > 0 14 | assert not np.any(np.isin(train, val)) 15 | assert not np.any(np.isin(val, train)) 16 | 17 | 18 | @pytest.mark.parametrize("run_onsets", 19 | [[0, 10, 20, 30, 40], [0, 10, 20, 20, 30]]) 20 | def test_generate_leave_one_run_out_empty_runs(run_onsets): 21 | n_samples = 40 22 | with pytest.raises(ValueError): 23 | list(generate_leave_one_run_out(n_samples, run_onsets)) 24 | 25 | 26 | def test_zscore_runs(): 27 | # Create sample data 28 | data = np.array([ 29 | [1, 2, 3], 30 | [2, 3, 4], 31 | [3, 4, 5], 32 | [4, 5, 6], 33 | [10, 11, 12], 34 | [11, 12, 13], 35 | [12, 13, 14], 36 | [13, 14, 15] 37 | ], dtype=np.float64) 38 | 39 | run_onsets = [0, 4] # Two runs: first 4 samples and last 4 samples 40 | 41 | # Apply zscore_runs 42 | zscored_data = zscore_runs(data, run_onsets) 43 | 44 | # Check shape preserved 45 | assert zscored_data.shape == data.shape 46 | 47 | # Check that each run has mean 0 and std 1 48 | run1 = zscored_data[:4] 49 | run2 = zscored_data[4:] 50 | 51 | # For each run and each feature column, mean should be close to 0 and std close to 1 52 | for run in [run1, run2]: 53 | assert np.allclose(run.mean(axis=0), 0, atol=1e-10) 54 | assert np.allclose(run.std(axis=0), 1, atol=1e-10) 55 | 56 | # Test with integer data to ensure dtype handling works correctly 57 | int_data = np.array([ 58 | [1, 2, 3], 59 | [2, 3, 4], 60 | [3, 4, 5], 61 | [4, 5, 6] 62 | ], dtype=np.int32) 63 | 64 | int_result = zscore_runs(int_data, [0]) 65 | assert int_result.dtype == np.int32 -------------------------------------------------------------------------------- /voxelwise_tutorials/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.stats 3 | from sklearn.utils.validation import check_random_state 4 | 5 | 6 | def generate_leave_one_run_out(n_samples, run_onsets, random_state=None, 7 | n_runs_out=1): 8 | """Generate a leave-one-run-out split for cross-validation. 9 | 10 | Generates as many splits as there are runs. 11 | 12 | Parameters 13 | ---------- 14 | n_samples : int 15 | Total number of samples in the training set. 16 | run_onsets : array of int of shape (n_runs, ) 17 | Indices of the run onsets. 18 | random_state : None | int | instance of RandomState 19 | Random state for the shuffling operation. 20 | n_runs_out : int 21 | Number of runs to leave out in the validation set. Default to one. 22 | 23 | Yields 24 | ------ 25 | train : array of int of shape (n_samples_train, ) 26 | Training set indices. 27 | val : array of int of shape (n_samples_val, ) 28 | Validation set indices. 29 | """ 30 | random_state = check_random_state(random_state) 31 | 32 | n_runs = len(run_onsets) 33 | # With permutations, we are sure that all runs are used as validation runs. 34 | # However here for n_runs_out > 1, a run can be chosen twice as validation 35 | # in the same split. 36 | all_val_runs = np.array( 37 | [random_state.permutation(n_runs) for _ in range(n_runs_out)]) 38 | 39 | all_samples = np.arange(n_samples) 40 | runs = np.split(all_samples, run_onsets[1:]) 41 | if any(len(run) == 0 for run in runs): 42 | raise ValueError("Some runs have no samples. Check that run_onsets " 43 | "does not include any repeated index, nor the last " 44 | "index.") 45 | 46 | for val_runs in all_val_runs.T: 47 | train = np.hstack( 48 | [runs[jj] for jj in range(n_runs) if jj not in val_runs]) 49 | val = np.hstack([runs[jj] for jj in range(n_runs) if jj in val_runs]) 50 | yield train, val 51 | 52 | 53 | def explainable_variance(data, bias_correction=True, do_zscore=True): 54 | """Compute explainable variance for a set of voxels. 55 | 56 | Parameters 57 | ---------- 58 | data : array of shape (n_repeats, n_times, n_voxels) 59 | fMRI responses of the repeated test set. 60 | bias_correction: bool 61 | Perform bias correction based on the number of repetitions. 62 | do_zscore: bool 63 | z-score the data in time. Only set to False if your data time courses 64 | are already z-scored. 65 | 66 | Returns 67 | ------- 68 | ev : array of shape (n_voxels, ) 69 | Explainable variance per voxel. 70 | """ 71 | if do_zscore: 72 | data = scipy.stats.zscore(data, axis=1) 73 | 74 | mean_var = data.var(axis=1, dtype=np.float64, ddof=1).mean(axis=0) 75 | var_mean = data.mean(axis=0).var(axis=0, dtype=np.float64, ddof=1) 76 | ev = var_mean / mean_var 77 | 78 | if bias_correction: 79 | n_repeats = data.shape[0] 80 | ev = ev - (1 - ev) / (n_repeats - 1) 81 | return ev 82 | 83 | 84 | def zscore_runs(data, run_onsets): 85 | """Perform z-scoring of fMRI data within each run. 86 | 87 | Parameters 88 | ---------- 89 | data : array of shape (n_samples, n_features) 90 | fMRI responses in the training set. 91 | run_onsets : array of int of shape (n_runs, ) 92 | Indices of the run onsets. 93 | The first run is assumed to start at index 0. 94 | 95 | Returns 96 | ------- 97 | zscored_data : array of shape (n_samples, n_features) 98 | fMRI responses z-scored within each run. 99 | """ 100 | # zscore each training run separately 101 | orig_dtype = data.dtype 102 | data = np.split(data.astype(np.float64), run_onsets[1:]) 103 | data = np.concatenate([scipy.stats.zscore(run, axis=0) for run in data], axis=0) 104 | return data.astype(orig_dtype) 105 | -------------------------------------------------------------------------------- /voxelwise_tutorials/wordnet.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from matplotlib.collections import LineCollection 4 | 5 | cache = dict() 6 | 7 | 8 | def load_wordnet(directory=None, recache=False): 9 | """Load the wordnet graph and wordnet categories used in [Huth et al 2012]. 10 | 11 | Parameters 12 | ---------- 13 | directory : str or None 14 | Directory where the dataset has been downloaded. If None, use 15 | "shortclips" in ``voxelwise_tutorials.io.get_data_home()``. 16 | 17 | Returns 18 | ------- 19 | wordnet_graph : networkx MultiDiGraph 20 | Graph of the wordnet categories (1583 nodes). 21 | wordnet_categories : list of str 22 | Names of the wordnet categories (1705 str). 23 | 24 | References 25 | ---------- 26 | Huth, A. G., Nishimoto, S., Vu, A. T., & Gallant, J. L. (2012). A 27 | continuous semantic space describes the representation of thousands of 28 | object and action categories across the human brain. Neuron, 76(6), 29 | 1210-1224. 30 | """ 31 | if "wordnet_graph" in cache and "wordnet_categories" in cache and not recache: # noqa 32 | return cache["wordnet_graph"], cache["wordnet_categories"] 33 | 34 | import os 35 | 36 | import networkx 37 | 38 | if directory is None: 39 | from voxelwise_tutorials.io import get_data_home 40 | 41 | directory = get_data_home("shortclips") 42 | 43 | dot_file = os.path.join(directory, "utils", "wordnet_graph.dot") 44 | txt_file = os.path.join(directory, "utils", "wordnet_categories.txt") 45 | 46 | wordnet_graph = networkx.drawing.nx_pydot.read_dot(dot_file) 47 | with open(txt_file) as fff: 48 | wordnet_categories = fff.read().splitlines() 49 | 50 | # Remove nodes in the graph that aren't in the categories list 51 | for name in list(wordnet_graph.nodes().keys()): 52 | if name not in wordnet_categories: 53 | wordnet_graph.remove_node(name) 54 | 55 | cache["wordnet_graph"] = wordnet_graph 56 | cache["wordnet_categories"] = wordnet_categories 57 | 58 | return wordnet_graph, wordnet_categories 59 | 60 | 61 | def correct_coefficients(primal_coef, feature_names, norm_by_depth=True): 62 | """Corrects coefficients across wordnet features as in [Huth et al 2012]. 63 | 64 | Parameters 65 | ---------- 66 | primal_coef : array of shape (n_features, ...) 67 | Regression coefficient on all wordnet features. 68 | feature_names : list of str, of length (n_features, ) 69 | Names of the wordnet features. 70 | norm_by_depth : bool 71 | If True, normalize the correction by the number of ancestors. 72 | 73 | Returns 74 | ------- 75 | corrected_coef : array of shape (n_features, ...) 76 | Corrected coefficient. 77 | 78 | References 79 | ---------- 80 | Huth, A. G., Nishimoto, S., Vu, A. T., & Gallant, J. L. (2012). A 81 | continuous semantic space describes the representation of thousands of 82 | object and action categories across the human brain. Neuron, 76(6), 83 | 1210-1224. 84 | """ 85 | import itertools 86 | 87 | import nltk 88 | 89 | nltk.download("wordnet", quiet=True) 90 | nltk.download("omw-1.4", quiet=True) 91 | from nltk.corpus import wordnet 92 | 93 | def _get_hypernyms(name): 94 | hypernyms = set() 95 | for path in wordnet.synset(name).hypernym_paths(): 96 | hypernyms = hypernyms.union(path) 97 | return list(hypernyms) 98 | 99 | assert primal_coef.shape[0] == len(feature_names) 100 | 101 | feature_names = list(feature_names) 102 | corrected_coef = np.zeros_like(primal_coef) 103 | 104 | for ii, name in enumerate(feature_names): 105 | for hypernym in _get_hypernyms(name): 106 | if hypernym.name() in feature_names: 107 | idx = feature_names.index(hypernym.name()) 108 | update = primal_coef[idx] 109 | 110 | if norm_by_depth: 111 | ancestors = [ 112 | hh.name() 113 | for hh in itertools.chain(*hypernym.hypernym_paths()) 114 | if hh.name() in feature_names 115 | ] 116 | update = update / len(ancestors) 117 | 118 | corrected_coef[ii] += update 119 | 120 | return corrected_coef 121 | 122 | 123 | DEFAULT_HIGHLIGHTED_NODES = [ 124 | "move.v.03", 125 | "turn.v.01", 126 | "jump.v.01", 127 | "change.v.02", 128 | "lean.v.01", 129 | "bloom.v.01", 130 | "travel.v.01", 131 | "gallop.v.01", 132 | "walk.v.01", 133 | "rappel.v.01", 134 | "touch.v.01", 135 | "hit.v.03", 136 | "move.v.02", 137 | "drag.v.01", 138 | "consume.v.02", 139 | "fasten.v.01", 140 | "breathe.v.01", 141 | "organism.n.01", 142 | "animal.n.01", 143 | "plant.n.02", 144 | "person.n.01", 145 | "athlete.n.01", 146 | "arthropod.n.01", 147 | "fish.n.01", 148 | "reptile.n.01", 149 | "bird.n.01", 150 | "placental.n.01", 151 | "rodent.n.01", 152 | "ungulate.n.01", 153 | "carnivore.n.01", 154 | "plant_organ.n.01", 155 | "geological_formation.n.01", 156 | "hill.n.01", 157 | "location.n.01", 158 | "city.n.01", 159 | "grassland.n.01", 160 | "body_part.n.01", 161 | "leg.n.01", 162 | "eye.n.01", 163 | "matter.n.03", 164 | "food.n.01", 165 | "sky.n.01", 166 | "water.n.01", 167 | "material.n.01", 168 | "bamboo.n.01", 169 | "atmospheric_phenomenon.n.01", 170 | "mist.n.01", 171 | "artifact.n.01", 172 | "way.n.06", 173 | "road.n.01", 174 | "clothing.n.01", 175 | "structure.n.01", 176 | "building.n.01", 177 | "room.n.01", 178 | "shop.n.01", 179 | "door.n.01", 180 | "implement.n.01", 181 | "kettle.n.01", 182 | "equipment.n.01", 183 | "ball.n.01", 184 | "vehicle.n.01", 185 | "boat.n.01", 186 | "wheeled_vehicle.n.01", 187 | "car.n.01", 188 | "furniture.n.01", 189 | "device.n.01", 190 | "weapon.n.01", 191 | "gas_pump.n.01", 192 | "container.n.01", 193 | "bottle.n.01", 194 | "laptop.n.01", 195 | "group.n.01", 196 | "herd.n.01", 197 | "measure.n.02", 198 | "communication.n.02", 199 | "text.n.01", 200 | "attribute.n.02", 201 | "dirt.n.02", 202 | "event.n.01", 203 | "rodeo.n.01", 204 | "wave.n.01", 205 | "communicate.v.02", 206 | "talk.v.02", 207 | "rub.v.03", 208 | ] 209 | 210 | 211 | def plot_wordnet_graph( 212 | node_colors, 213 | node_sizes, 214 | zorder=None, 215 | node_scale=200, 216 | alpha=1.0, 217 | ax=None, 218 | extra_edges=None, 219 | highlighted_nodes="default", 220 | directory=None, 221 | font_size=12, 222 | ): 223 | """Plot a wordnet graph, as in [Huth et al 2012]. 224 | 225 | Note: Only plot categories that are in the wordnet graph loaded in the 226 | function ``load_wordnet``. 227 | 228 | Parameters 229 | ---------- 230 | node_colors : array of shape (1705, 3) 231 | RGB colors of each feature. If you want to plot an array of shape 232 | (1705, ) use ``apply_cmap`` to map it to RGB. 233 | node_sizes : array of shape (1705, ) 234 | Size of each feature. Values are scaled by the maximum. 235 | zorder : array of shape (1705, ) or None 236 | Order of node, larger values are plotted on top. 237 | node_scale : float 238 | Scaling factor for the node sizes. 239 | alpha : float 240 | Transparency of the nodes. 241 | ax : Axes or None 242 | Matplotlib Axes where the graph will be plotted. If None, the current 243 | figure is used. 244 | extra_edges : list of (str, str) 245 | Add extra edges between named nodes. See the function ``load_wordnet`` 246 | to have the list of names. 247 | highlighted_nodes : list of str, or in {"default", "random_42"} 248 | List of nodes to be highlighted (with name). If "default", use a fixed 249 | list of 84 nodes. If "random_42", choose 42 random nodes. See the 250 | function ``load_wordnet`` to have the list of names. 251 | directory : str or None 252 | Directory where the dataset has been downloaded. If None, use 253 | "shortclips" in ``voxelwise_tutorials.io.get_data_home()``. 254 | font_size : int 255 | Font size for the labels of the highlighted nodes. 256 | 257 | Returns 258 | ------- 259 | ax : Axes 260 | Matplotlib Axes where the histogram was plotted. 261 | 262 | Examples 263 | -------- 264 | >>> import numpy as np 265 | >>> import matplotlib.pyplot as plt 266 | >>> from voxelwise_tutorials.wordnet import plot_wordnet_graph 267 | >>> node_colors = np.random.rand(1705, 3) 268 | >>> node_sizes = np.random.rand(1705) + 0.5 269 | >>> plot_wordnet_graph(node_colors=node_colors, node_sizes=node_sizes) 270 | >>> plt.show() 271 | 272 | References 273 | ---------- 274 | Huth, A. G., Nishimoto, S., Vu, A. T., & Gallant, J. L. (2012). A 275 | continuous semantic space describes the representation of thousands of 276 | object and action categories across the human brain. Neuron, 76(6), 277 | 1210-1224. 278 | """ 279 | import networkx 280 | 281 | ################ 282 | # initialization 283 | 284 | if ax is None: 285 | fig = plt.figure(figsize=(8.5, 8.5)) 286 | ax = fig.add_axes([0, 0, 1, 1]) 287 | ax.set_facecolor("black") 288 | ax.set_xticks([]) 289 | ax.set_yticks([]) 290 | 291 | wordnet_graph, wordnet_categories = load_wordnet(directory=directory) 292 | 293 | ################ 294 | # remove features not in the nodes (1705 -> 1583) 295 | node_names = list(wordnet_graph.nodes().keys()) 296 | indices = [ii for ii, name in enumerate(wordnet_categories) if name in node_names] 297 | order = [ 298 | list(np.array(wordnet_categories)[indices]).index(name) for name in node_names 299 | ] 300 | np.testing.assert_array_equal( 301 | node_names, np.array(wordnet_categories)[indices][order] 302 | ) 303 | node_colors = node_colors[indices][order] 304 | node_sizes = node_sizes[indices][order] 305 | if zorder is not None: 306 | zorder = zorder[indices][order] 307 | 308 | assert len(node_sizes) == len(node_names) 309 | 310 | ################ 311 | # Various checks 312 | if node_colors.min() < 0: 313 | raise ValueError( 314 | "Negative value in node_colors, all values should be in [0, 1]." 315 | ) 316 | if node_colors.max() > 1: 317 | raise ValueError( 318 | "Negative value in node_colors, all values should be in [0, 1]." 319 | ) 320 | if node_sizes.min() < 0: 321 | raise ValueError( 322 | "Negative value in node_sizes, all values should be non-negative." 323 | ) 324 | node_sizes /= node_sizes.max() 325 | 326 | ################ 327 | # highlighted nodes 328 | if highlighted_nodes == "default": 329 | highlighted_nodes = DEFAULT_HIGHLIGHTED_NODES 330 | elif highlighted_nodes == "random_42": 331 | highlighted_nodes = [ 332 | node_names[ii] for ii in np.random.choice(len(node_names), 42) 333 | ] 334 | elif highlighted_nodes is None: 335 | highlighted_nodes = [] 336 | 337 | ################ 338 | # create node positions and size dictionaries 339 | node_positions = dict( 340 | [ 341 | (key.strip('"'), list(map(float, val["pos"].strip('"').split(",")))) 342 | for key, val in wordnet_graph.nodes().items() 343 | ] 344 | ) 345 | node_sizes = node_sizes * node_scale 346 | node_sizes_dict = dict(zip(node_names, node_sizes)) 347 | 348 | ################ 349 | # plot edges using LineCollection 350 | edges = wordnet_graph.edges() 351 | linestyles = [ 352 | ":" if np.isnan([node_sizes_dict[e[0]], node_sizes_dict[e[1]]]).any() else "-" 353 | for e in edges 354 | ] 355 | edge_positions = np.asarray( 356 | [(node_positions[e[0]], node_positions[e[1]]) for e in edges] 357 | ) 358 | edge_collection = LineCollection( 359 | edge_positions, 360 | colors=(0.7, 0.7, 0.7, 1.0), 361 | linewidths=0.7, 362 | antialiaseds=(1,), 363 | linestyles=linestyles, 364 | ) 365 | edge_collection.set_zorder(1) # edges go behind nodes 366 | ax.add_collection(edge_collection) 367 | 368 | if extra_edges: 369 | # include these edges that aren't part of the layout 370 | extra_edge_positions = np.asarray( 371 | [(node_positions[e[0]], node_positions[e[1]]) for e in extra_edges] 372 | ) 373 | extra_edge_collection = LineCollection( 374 | extra_edge_positions, 375 | colors=(0.7, 0.7, 0.7, 1.0), 376 | linewidths=0.5, 377 | antialiaseds=(1,), 378 | linestyle="-", 379 | ) 380 | 381 | extra_edge_collection.set_zorder(1) 382 | ax.add_collection(extra_edge_collection) 383 | 384 | ################ 385 | # plot nodes with scatter 386 | xy = np.asarray([node_positions[v] for v in node_names]) 387 | highlighted_node_indices = [node_names.index(hn) for hn in highlighted_nodes] 388 | 389 | kind_dict = dict(n="o", v="s") 390 | node_kinds = np.array([n.split(".")[1] for n in node_names]) 391 | edgecolors = ["none"] * xy.shape[0] 392 | for hni in highlighted_node_indices: 393 | edgecolors[hni] = "white" 394 | 395 | for node_kind, marker in kind_dict.items(): 396 | indices = np.nonzero(node_kinds == node_kind)[0] 397 | 398 | # reorder to have highlighted nodes on top 399 | if highlighted_node_indices: 400 | hnvec = np.zeros((indices.shape[0],)) 401 | intersect = np.intersect1d(highlighted_node_indices, indices) 402 | if intersect.size > 0: 403 | hnvec[np.array([list(indices).index(hn) for hn in intersect])] = 1 404 | indices = indices[np.argsort(hnvec)] 405 | 406 | if zorder is not None: 407 | orders = zorder[indices] 408 | indices = indices[np.argsort(orders)] 409 | 410 | # normalize area of squares to be same as circles 411 | if marker == "s": 412 | norm_sizes = np.nan_to_num(node_sizes[indices]) * (np.pi / 4.0) 413 | else: 414 | norm_sizes = np.nan_to_num(node_sizes[indices]) 415 | 416 | ax.scatter( 417 | xy[indices, 0], 418 | xy[indices, 1], 419 | s=norm_sizes, 420 | c=node_colors[indices], 421 | marker=marker, 422 | edgecolors=list(np.array(edgecolors)[indices]), 423 | alpha=alpha, 424 | ) 425 | 426 | ################ 427 | # add labels for the highlighted nodes 428 | labels = dict([(name, name.split(".")[0]) for name in highlighted_nodes]) 429 | pos = dict([(n, (x, y - 60)) for (n, (x, y)) in node_positions.items()]) 430 | networkx.draw_networkx_labels( 431 | wordnet_graph, 432 | font_color="white", 433 | labels=labels, 434 | font_weight="bold", 435 | pos=pos, 436 | font_size=font_size, 437 | ax=ax, 438 | ) 439 | 440 | return ax 441 | 442 | 443 | def scale_to_rgb_cube(node_colors, clip=2.0): 444 | """Scale array to RGB cube, as in [Huth et al 2012]. 445 | 446 | Parameters 447 | ---------- 448 | node_colors : array of shape (n_nodes, 3) 449 | RGB colors of each node, raw values. 450 | clip : float 451 | After z-scoring, values outside [-clip, clip] will be clipped. 452 | 453 | Returns 454 | ------- 455 | node_colors : array of shape (n_nodes, 3) 456 | Transformed RGB colors of each node, in [0, 1] 457 | 458 | Examples 459 | -------- 460 | >>> import numpy as np 461 | >>> import matplotlib.pyplot as plt 462 | >>> from voxelwise_tutorials.wordnet import plot_wordnet_graph 463 | >>> from voxelwise_tutorials.wordnet import scale_to_rgb_cube 464 | >>> node_colors = np.random.randn(1705, 3) 465 | >>> node_sizes = np.random.rand(1705) + 0.5 466 | >>> plot_wordnet_graph(node_colors=scale_to_rgb_cube(node_colors), 467 | node_sizes=node_sizes) 468 | >>> plt.show() 469 | 470 | References 471 | ---------- 472 | Huth, A. G., Nishimoto, S., Vu, A. T., & Gallant, J. L. (2012). A 473 | continuous semantic space describes the representation of thousands of 474 | object and action categories across the human brain. Neuron, 76(6), 475 | 1210-1224. 476 | """ 477 | # z-score and clip 478 | node_colors = node_colors.copy() 479 | node_colors -= node_colors.mean(0) 480 | node_colors /= node_colors.std(0) 481 | node_colors[node_colors > clip] = clip 482 | node_colors[node_colors < -clip] = -clip 483 | 484 | # normalize each node by the L_inf norm and the L_2 norm 485 | l2_norm = np.linalg.norm(node_colors, axis=1) 486 | linf_norm = np.max(np.abs(node_colors), axis=1) 487 | node_colors *= (l2_norm / linf_norm)[:, None] 488 | 489 | # clip again 490 | node_colors[node_colors > clip] = clip 491 | node_colors[node_colors < -clip] = -clip 492 | 493 | # from [-clip, clip] to [0, 1] 494 | node_colors = node_colors / clip / 2.0 + 0.5 495 | 496 | return node_colors 497 | 498 | 499 | def apply_cmap(data, cmap=None, vmin=None, vmax=None, n_colors=None, norm=None): 500 | """Apply a colormap to a 1D array, to get RGB colors. 501 | 502 | Parameters 503 | ---------- 504 | data : array of shape (n_features, ) 505 | Input array. 506 | cmap : str or None 507 | Matplotlib colormap. 508 | vmin : float or None 509 | Minimum value of the color mapping. If None, use ``data.min()``. 510 | Only used if ``norm`` is None. 511 | vmax : float or None 512 | Maximum value of the color mapping. If None, use ``data.max()``. 513 | Only used if ``norm`` is None. 514 | n_colors : int or None 515 | If not None, use a discretized version of the colormap. 516 | norm : matplotlib.colors.Normalize instance, or None 517 | The normalizing object which scales data, typically into the 518 | interval ``[0, 1]``. If None, it defaults to a 519 | ``matplotlib.colors.Normalize`` object using ``vmin`` and ``vmax``. 520 | 521 | Returns 522 | ------- 523 | data_rgb : array of shape (n_features, 3) 524 | Input array mapped to RGB. 525 | 526 | Examples 527 | -------- 528 | >>> import numpy as np 529 | >>> import matplotlib.pyplot as plt 530 | >>> from voxelwise_tutorials.wordnet import plot_wordnet_graph, apply_cmap 531 | >>> node_colors = np.random.rand(1705,) 532 | >>> node_sizes = np.random.rand(1705) + 0.5 533 | >>> plot_wordnet_graph(node_colors=apply_cmap(node_colors), 534 | node_sizes=node_sizes) 535 | >>> plt.show() 536 | """ 537 | from matplotlib import cm 538 | 539 | cmap = plt.get_cmap(cmap, lut=n_colors) 540 | if norm is None: 541 | from matplotlib import colors 542 | 543 | norm = colors.Normalize(vmin, vmax) 544 | cmapper = cm.ScalarMappable(norm=norm, cmap=cmap) 545 | data_rgb = cmapper.to_rgba(data) 546 | return data_rgb 547 | --------------------------------------------------------------------------------