├── .gitignore ├── LICENSE ├── README.md ├── analysis ├── estimate_trfs.py ├── estimate_trfs_basis.py ├── estimate_trfs_reference_strategy.py ├── estimate_word_acoustics.py └── make_erps.py ├── download_alice.py ├── environment.yml ├── figures ├── Auditory-TRFs.py ├── Auditory-scale.py ├── Collinearity.py ├── Comparison-ERP-TRF.py ├── Convolution.py ├── Reference-strategy.py ├── TRF-Basis.py ├── TRF.py ├── Time-series.py └── Word-class-acoustics.py ├── import_dataset ├── check-triggers.py ├── convert-all.py └── easycapM10-acti61_elec.sfp ├── pipeline ├── Auditory-TRFs.py ├── README.md ├── alice.py ├── environment.yml └── jobs.py └── predictors ├── explore_word_predictors.py ├── make_gammatone.py ├── make_gammatone_predictors.py └── make_word_predictors.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | *.ipynb 131 | 132 | # Datagrab tracker 133 | .temppath.pickled 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christian Brodbeck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alice dataset for Eelbrain 2 | 3 | This repository contains scripts and instructions to reproduce the results from [*Eelbrain, a toolkit for time-continuous analysis with temporal response functions*](https://doi.org/10.7554/eLife.85012). 4 | 5 | 6 | # Setup 7 | 8 | ## Download this repository 9 | 10 | If you're familiar with git, clone this repository. If not, simply download it as a [zip file](https://github.com/Eelbrain/Alice/archive/refs/heads/main.zip). 11 | 12 | ## Create the Python environment 13 | 14 | The easiest way to install all the required libraries is using the environment file provided in this repository (`environment.yml`) as described in the [Instructions for installing Eelbrain: Full Setup](https://eelbrain.readthedocs.io/en/latest/installing.html#full-setup). 15 | 16 | ## Download the Alice dataset 17 | 18 | Download the Alice EEG dataset. This repository comes with a script that can automatically download the required data from [UMD DRUM](https://drum.lib.umd.edu/handle/1903/27591) by running: 19 | 20 | ```bash 21 | $ python download_alice.py 22 | ``` 23 | 24 | The default download location is ``~/Data/Alice``. The scripts in the Alice repository expect to find the dataset at this location. If you want to store the dataset at a different location, provide the location as argument for the download: 25 | 26 | ```bash 27 | $ python download_alice.py download/directory 28 | ``` 29 | 30 | then either create a link to the dataset at ``~/Data/Alice``, or change the root path where it occurs in scripts (always near the beginning). 31 | 32 | This data has been derived from the [original dataset](https://deepblue.lib.umich.edu/data/concern/data_sets/bg257f92t) using the script at `import_dataset/convert-all.py`. 33 | 34 | ## Create (or download) predictors and TRFs 35 | 36 | In order to create predictors used in the analysis (and for some plots in the figures), execute the scripts in the `predictors` directory (see [Subdirectories](#subdirectories) below). 37 | 38 | All TRFs used in the different figures can be computed and saved using the scripts in the `analysis` directory. However, this may require substantial computing time. To get started faster, the TRFs can also be downloaded from the data repository ([TRFs.zip](https://drum.lib.umd.edu/bitstreams/c46d0bfe-3ca9-496d-b248-8f39d6772b61/download)). Just move the downloaded `TRFs` folder into the `~/Data/Alice` directory, i.e., as `~/Data/Alice/TRFs`. 39 | 40 | > [!NOTE] 41 | > Replicability: Due to numerical issues, results can differ slightly between different operating systems and hardware used. 42 | > Similarly, implementation changes (e.g., optimization) can affect results, even if the underlying algorithms are mathematically equivalent. 43 | > Changes in the boosting implementation are noted in the Eelbrain [Version History](https://eelbrain.readthedocs.io/en/stable/changes.html#major-changes). 44 | 45 | 46 | ## Notebooks 47 | 48 | Many Python scripts in this repository are actually [Jupyter](https://jupyter.org/documentation) notebooks. They can be recognized as such because of their header that starts with: 49 | 50 | ```python 51 | # --- 52 | # jupyter: 53 | # jupytext: 54 | # formats: ipynb,py:light 55 | ``` 56 | 57 | These scripts were converted to Python scripts with [Jupytext](http://jupytext.readthedocs.io) for efficient management with git. 58 | To turn such a script back into a notebook, run this command (assuming the script is called `my_trf_analysis.py`): 59 | 60 | ```bash 61 | $ jupytext --to notebook my_trf_analysis.py 62 | ``` 63 | 64 | # Subdirectories 65 | 66 | ## Predictors 67 | 68 | The `predictors` directory contains scripts for generating predictor variables. These should be created first, as they are used in many of the other scripts: 69 | 70 | - `make_gammatone.py`: Generate high resolution gammatone spectrograms which are used by `make_gammatone_predictors.py` 71 | - `make_gammatone_predictors.py`: Generate continuous acoustic predictor variables 72 | - `make_word_predictors.py`: Generate word-level predictor variables consisting of impulses at word onsets 73 | 74 | 75 | ## Analysis 76 | 77 | The `analysis` directory contains scripts used to estimate and save various mTRF models for the EEG dataset. These mTRF models are used in some of the figure scripts. 78 | 79 | 80 | ## Figures 81 | 82 | The `figures` directory contains the code used to generate all the figures in the paper. 83 | 84 | 85 | ## Import_dataset 86 | 87 | This directory contains the scripts that were used to convert the data from the original Alice EEG dataset to the format used here. 88 | 89 | 90 | # Experimental pipeline 91 | 92 | The `pipeline` directory contains instructions for using an experimental pipeline that simplifies and streamlines TRF analysis. For more information, see the [Pipeline](pipeline) Readme file. 93 | 94 | 95 | # Further resources 96 | 97 | This tutorial and dataset: 98 | - [Ask questions](https://github.com/Eelbrain/Alice/discussions) 99 | - [Report issues](https://github.com/Eelbrain/Alice/issues) 100 | 101 | Eelbrain: 102 | - [Command reference](https://eelbrain.readthedocs.io/en/stable/reference.html) 103 | - [Examples](https://eelbrain.readthedocs.io/en/stable/auto_examples/index.html) 104 | - [Ask questions](https://github.com/christianbrodbeck/Eelbrain/discussions) 105 | - [Report issues](https://github.com/christianbrodbeck/Eelbrain/issues) 106 | 107 | Other libraries: 108 | - [Matplotlib](https://matplotlib.org) 109 | - [MNE-Python](https://mne.tools/) 110 | 111 | -------------------------------------------------------------------------------- /analysis/estimate_trfs.py: -------------------------------------------------------------------------------- 1 | """This script estimates TRFs for several models and saves them""" 2 | from pathlib import Path 3 | import re 4 | 5 | import eelbrain 6 | import mne 7 | 8 | 9 | STIMULI = [str(i) for i in range(1, 13)] 10 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 11 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 12 | EEG_DIR = DATA_ROOT / 'eeg' 13 | SUBJECTS = [path.name for path in EEG_DIR.iterdir() if re.match(r'S\d*', path.name)] 14 | # Define a target directory for TRF estimates and make sure the directory is created 15 | TRF_DIR = DATA_ROOT / 'TRFs' 16 | TRF_DIR.mkdir(exist_ok=True) 17 | 18 | # Load stimuli 19 | # ------------ 20 | # Make sure to name the stimuli so that the TRFs can later be distinguished 21 | # Load the gammatone-spectrograms; use the time axis of these as reference 22 | gammatone = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-8.pickle') for stimulus in STIMULI] 23 | # Resample the spectrograms to 100 Hz (time-step = 0.01 s), which we will use for TRFs 24 | gammatone = [x.bin(0.01, dim='time', label='start') for x in gammatone] 25 | # Pad onset with 100 ms and offset with 1 second; make sure to give the predictor a unique name as that will make it easier to identify the TRF later 26 | gammatone = [eelbrain.pad(x, tstart=-0.100, tstop=x.time.tstop + 1, name='gammatone') for x in gammatone] 27 | # Filter the predictor with the same parameters as we will filter the EEG data 28 | gammatone = [eelbrain.filter_data(x, 0.5, 20) for x in gammatone] 29 | 30 | # Load the broad-band envelope and process it in the same way 31 | envelope = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-1.pickle') for stimulus in STIMULI] 32 | envelope = [x.bin(0.01, dim='time', label='start') for x in envelope] 33 | envelope = [eelbrain.pad(x, tstart=-0.100, tstop=x.time.tstop + 1, name='envelope') for x in envelope] 34 | envelope = [eelbrain.filter_data(x, 0.5, 20) for x in envelope] 35 | onset_envelope = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-on-1.pickle') for stimulus in STIMULI] 36 | onset_envelope = [x.bin(0.01, dim='time', label='start') for x in onset_envelope] 37 | onset_envelope = [eelbrain.pad(x, tstart=-0.100, tstop=x.time.tstop + 1, name='onset') for x in onset_envelope] 38 | onset_envelope = [eelbrain.filter_data(x, 0.5, 20) for x in onset_envelope] 39 | # Load onset spectrograms and make sure the time dimension is equal to the gammatone spectrograms 40 | gammatone_onsets = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-on-8.pickle') for stimulus in STIMULI] 41 | gammatone_onsets = [x.bin(0.01, dim='time', label='start') for x in gammatone_onsets] 42 | gammatone_onsets = [eelbrain.set_time(x, gt.time, name='gammatone_on') for x, gt in zip(gammatone_onsets, gammatone)] 43 | gammatone_onsets = [eelbrain.filter_data(x, 0.5, 20) for x in gammatone_onsets] 44 | # Load linear and powerlaw scaled spectrograms 45 | gammatone_lin = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-lin-8.pickle') for stimulus in STIMULI] 46 | gammatone_lin = [x.bin(0.01, dim='time', label='start') for x in gammatone_lin] 47 | gammatone_lin = [eelbrain.set_time(x, gt.time, name='gammatone_on') for x, gt in zip(gammatone_lin, gammatone)] 48 | gammatone_lin = [eelbrain.filter_data(x, 0.5, 20) for x in gammatone_lin] 49 | gammatone_pow = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-pow-8.pickle') for stimulus in STIMULI] 50 | gammatone_pow = [x.bin(0.01, dim='time', label='start') for x in gammatone_pow] 51 | gammatone_pow = [eelbrain.set_time(x, gt.time, name='gammatone_on') for x, gt in zip(gammatone_pow, gammatone)] 52 | gammatone_pow = [eelbrain.filter_data(x, 0.5, 20) for x in gammatone_pow] 53 | # Load word tables and convert tables into continuous time-series with matching time dimension 54 | word_tables = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~word.pickle') for stimulus in STIMULI] 55 | word_onsets = [eelbrain.event_impulse_predictor(gt.time, data=data, name='word') for gt, data in zip(gammatone, word_tables)] 56 | # Function and content word impulses based on the boolean variables in the word-tables 57 | word_lexical = [eelbrain.event_impulse_predictor(gt.time, value='lexical', data=data, name='lexical') for gt, data in zip(gammatone, word_tables)] 58 | word_nlexical = [eelbrain.event_impulse_predictor(gt.time, value='nlexical', data=data, name='non_lexical') for gt, data in zip(gammatone, word_tables)] 59 | 60 | # Extract the duration of the stimuli, so we can later match the EEG to the stimuli 61 | durations = [gt.time.tmax for stimulus, gt in zip(STIMULI, gammatone)] 62 | 63 | # Models 64 | # ------ 65 | # Pre-define models here to have easier access during estimation. In the future, additional models could be added here and the script re-run to generate additional TRFs. 66 | models = { 67 | 'envelope': [envelope], 68 | # Compare different scales for the acoustic response 69 | 'gammatone': [gammatone], 70 | 'gammatone-lin': [gammatone_lin], 71 | 'gammatone-pow': [gammatone_pow], 72 | 'gammatone-lin+log': [gammatone_lin, gammatone], 73 | # The acoustic edge detection model 74 | 'envelope+onset': [envelope, onset_envelope], 75 | 'acoustic': [gammatone, gammatone_onsets], 76 | # Models with word-onsets and word-class 77 | 'words': [word_onsets], 78 | 'words+lexical': [word_onsets, word_lexical, word_nlexical], 79 | 'acoustic+words': [gammatone, gammatone_onsets, word_onsets], 80 | 'acoustic+words+lexical': [gammatone, gammatone_onsets, word_onsets, word_lexical, word_nlexical], 81 | } 82 | 83 | # Estimate TRFs 84 | # ------------- 85 | # Loop through subjects to estimate TRFs 86 | for subject in SUBJECTS: 87 | subject_trf_dir = TRF_DIR / subject 88 | subject_trf_dir.mkdir(exist_ok=True) 89 | # Generate all TRF paths so we can check whether any new TRFs need to be estimated 90 | trf_paths = {model: subject_trf_dir / f'{subject} {model}.pickle' for model in models} 91 | # Skip this subject if all files already exist 92 | if all(path.exists() for path in trf_paths.values()): 93 | continue 94 | # Load the EEG data 95 | raw = mne.io.read_raw(EEG_DIR / subject / f'{subject}_alice-raw.fif', preload=True) 96 | # Band-pass filter the raw data between 0.5 and 20 Hz 97 | raw.filter(0.5, 20, n_jobs=1) 98 | # Interpolate bad channels 99 | raw.interpolate_bads() 100 | # Extract the events marking the stimulus presentation from the EEG file 101 | events = eelbrain.load.fiff.events(raw) 102 | # Not all subjects have all trials; determine which stimuli are present 103 | trial_indexes = [STIMULI.index(stimulus) for stimulus in events['event']] 104 | # Extract the EEG data segments corresponding to the stimuli 105 | trial_durations = [durations[i] for i in trial_indexes] 106 | eeg = eelbrain.load.fiff.variable_length_epochs(events, -0.100, trial_durations, decim=5, connectivity='auto') 107 | # Since trials are of unequal length, we will concatenate them for the TRF estimation. 108 | eeg_concatenated = eelbrain.concatenate(eeg) 109 | for model, predictors in models.items(): 110 | path = trf_paths[model] 111 | # Skip if this file already exists 112 | if path.exists(): 113 | continue 114 | print(f"Estimating: {subject} ~ {model}") 115 | # Select and concetenate the predictors corresponding to the EEG trials 116 | predictors_concatenated = [] 117 | for predictor in predictors: 118 | predictors_concatenated.append(eelbrain.concatenate([predictor[i] for i in trial_indexes])) 119 | # Fit the mTRF 120 | trf = eelbrain.boosting(eeg_concatenated, predictors_concatenated, -0.100, 1.000, error='l1', basis=0.050, partitions=5, test=1, selective_stopping=True) 121 | # Save the TRF for later analysis 122 | eelbrain.save.pickle(trf, path) 123 | -------------------------------------------------------------------------------- /analysis/estimate_trfs_basis.py: -------------------------------------------------------------------------------- 1 | """This script estimates TRFs for comparing different basis windows""" 2 | from pathlib import Path 3 | import re 4 | 5 | import eelbrain 6 | import mne 7 | 8 | 9 | STIMULI = [str(i) for i in range(1, 13)] 10 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 11 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 12 | EEG_DIR = DATA_ROOT / 'eeg' 13 | SUBJECTS = [path.name for path in EEG_DIR.iterdir() if re.match(r'S\d*', path.name)] 14 | # Define a target directory for TRF estimates and make sure the directory is created 15 | TRF_DIR = DATA_ROOT / 'TRFs' 16 | TRF_DIR.mkdir(exist_ok=True) 17 | 18 | # Load stimuli 19 | # ------------ 20 | # Make sure to name the stimuli so that the TRFs can later be distinguished 21 | # Load the gammatone-spectrograms; use the time axis of these as reference 22 | gammatone = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-8.pickle') for stimulus in STIMULI] 23 | # Resample the spectrograms to 100 Hz (time-step = 0.01 s), which we will use for TRFs 24 | gammatone = [x.bin(0.01, dim='time', label='start') for x in gammatone] 25 | # Pad onset with 100 ms and offset with 1 second; make sure to give the predictor a unique name as that will make it easier to identify the TRF later 26 | gammatone = [eelbrain.pad(x, tstart=-0.100, tstop=x.time.tstop + 1, name='gammatone') for x in gammatone] 27 | # Filter the predictor with the same parameters as we will filter the EEG data 28 | gammatone = [eelbrain.filter_data(x, 0.5, 20) for x in gammatone] 29 | 30 | # Extract the duration of the stimuli, so we can later match the EEG to the stimuli 31 | durations = [gt.time.tmax for stimulus, gt in zip(STIMULI, gammatone)] 32 | 33 | # Models 34 | # ------ 35 | # Pre-define models here to have easier access during estimation. In the future, additional models could be added here and the script re-run to generate additional TRFs. 36 | model, predictors = 'gammatone', [gammatone] 37 | basis_values = [0, 0.050, 0.100] 38 | 39 | # Estimate TRFs 40 | # ------------- 41 | # Loop through subjects to estimate TRFs 42 | for subject in SUBJECTS: 43 | subject_trf_dir = TRF_DIR / subject 44 | subject_trf_dir.mkdir(exist_ok=True) 45 | # Generate all TRF paths so we can check whether any new TRFs need to be estimated 46 | trf_paths = {basis: subject_trf_dir / f'{subject} {model} basis-{basis*1000:.0f}.pickle' for basis in basis_values} 47 | # Skip this subject if all files already exist 48 | if all(path.exists() for path in trf_paths.values()): 49 | continue 50 | # Load the EEG data 51 | raw = mne.io.read_raw(EEG_DIR / subject / f'{subject}_alice-raw.fif', preload=True) 52 | # Band-pass filter the raw data between 0.5 and 20 Hz 53 | raw.filter(0.5, 20, n_jobs=1) 54 | # Interpolate bad channels 55 | raw.interpolate_bads() 56 | # Extract the events marking the stimulus presentation from the EEG file 57 | events = eelbrain.load.fiff.events(raw) 58 | # Not all subjects have all trials; determine which stimuli are present 59 | trial_indexes = [STIMULI.index(stimulus) for stimulus in events['event']] 60 | # Extract the EEG data segments corresponding to the stimuli 61 | trial_durations = [durations[i] for i in trial_indexes] 62 | eeg = eelbrain.load.fiff.variable_length_epochs(events, -0.100, trial_durations, decim=5, connectivity='auto') 63 | # Since trials are of unequal length, we will concatenate them for the TRF estimation. 64 | eeg_concatenated = eelbrain.concatenate(eeg) 65 | # Select and concetenate the predictors corresponding to the EEG trials 66 | predictors_concatenated = [] 67 | for predictor in predictors: 68 | predictors_concatenated.append(eelbrain.concatenate([predictor[i] for i in trial_indexes])) 69 | 70 | for basis, path in trf_paths.items(): 71 | # Skip if this file already exists 72 | if path.exists(): 73 | continue 74 | print(f"Estimating: {subject} ~ {basis}") 75 | # Fit the mTRF 76 | trf = eelbrain.boosting(eeg_concatenated, predictors_concatenated, -0.100, 1.000, error='l1', basis=basis, partitions=5, test=1, selective_stopping=True) 77 | eelbrain.save.pickle(trf, path) 78 | -------------------------------------------------------------------------------- /analysis/estimate_trfs_reference_strategy.py: -------------------------------------------------------------------------------- 1 | """This script estimates TRFs for several models and saves them""" 2 | from pathlib import Path 3 | import re 4 | 5 | import eelbrain 6 | import mne 7 | import numpy 8 | 9 | 10 | STIMULI = [str(i) for i in range(1, 13)] 11 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 12 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 13 | EEG_DIR = DATA_ROOT / 'eeg' 14 | SUBJECTS = [path.name for path in EEG_DIR.iterdir() if re.match(r'S\d*', path.name)] 15 | # Define a target directory for TRF estimates and make sure the directory is created 16 | TRF_DIR = DATA_ROOT / 'TRFs' 17 | TRF_DIR.mkdir(exist_ok=True) 18 | 19 | # Load stimuli 20 | # ------------ 21 | # Load the broad-band envelope 22 | envelope = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-1.pickle') for stimulus in STIMULI] 23 | envelope = [x.bin(0.01, dim='time', label='start') for x in envelope] 24 | envelope = [eelbrain.pad(x, tstart=-0.100, tstop=x.time.tstop + 1, name='envelope') for x in envelope] 25 | 26 | # Extract the duration of the stimuli, so we can later match the EEG to the stimuli 27 | durations = [stimulus.time.tmax for stimulus in envelope] 28 | 29 | # Models 30 | # ------ 31 | # Pre-define models here to have easier access during estimation. In the future, additional models could be added here and the script re-run to generate additional TRFs. 32 | models = { 33 | # Acoustic models 34 | 'envelope': [envelope], 35 | } 36 | 37 | # Estimate TRFs 38 | # ------------- 39 | # Loop through subjects to estimate TRFs 40 | for subject in SUBJECTS: 41 | subject_trf_dir = TRF_DIR / subject 42 | subject_trf_dir.mkdir(exist_ok=True) 43 | 44 | for reference in ['cz', 'average']: 45 | # Generate all TRF paths so we can check whether any new TRFs need to be estimated 46 | trf_paths = {model: subject_trf_dir / f'{subject} {model}_{reference}.pickle' for model in models} 47 | # Skip this subject if all files already exist 48 | if all(path.exists() for path in trf_paths.values()): 49 | continue 50 | # Load the EEG data 51 | raw = mne.io.read_raw(EEG_DIR / subject / f'{subject}_alice-raw.fif', preload=True) 52 | # Band-pass filter the raw data between 0.5 and 20 Hz 53 | raw.filter(0.5, 20) 54 | # Interpolate bad channels 55 | raw.interpolate_bads() 56 | # Do referencing 57 | if reference == 'cz': 58 | raw.set_eeg_reference(['33']) 59 | else: 60 | raw.set_eeg_reference('average') 61 | # Extract the events marking the stimulus presentation from the EEG file 62 | events = eelbrain.load.fiff.events(raw) 63 | # Not all subjects have all trials; determine which stimuli are present 64 | trial_indexes = [STIMULI.index(stimulus) for stimulus in events['event']] 65 | # Extract the EEG data segments corresponding to the stimuli 66 | trial_durations = [durations[i] for i in trial_indexes] 67 | eeg = eelbrain.load.fiff.variable_length_epochs(events, -0.100, trial_durations, decim=5, connectivity='auto') 68 | # Since trials are of unequal length, we will concatenate them for the TRF estimation. 69 | eeg_concatenated = eelbrain.concatenate(eeg) 70 | 71 | if reference == 'cz': 72 | # As the Cz-channel was used for reference, the channel contains zeros (which cannot be used for TRF estimation) 73 | # Therefore, this channel is replaced with random noise to preserve the 64-sensor dimension. 74 | n_times = len(eeg_concatenated.time) 75 | rng = numpy.random.default_rng() 76 | eeg_concatenated['33'] = rng.standard_normal(n_times) * eeg_concatenated.std() 77 | 78 | for model, predictors in models.items(): 79 | path = trf_paths[model] 80 | # Skip if this file already exists 81 | if path.exists(): 82 | continue 83 | print(f"Estimating: {subject} ~ {model} ({reference})") 84 | # Select and concetenate the predictors corresponding to the EEG trials 85 | predictors_concatenated = [] 86 | for predictor in predictors: 87 | predictors_concatenated.append(eelbrain.concatenate([predictor[i] for i in trial_indexes])) 88 | # Fit the mTRF 89 | trf = eelbrain.boosting(eeg_concatenated, predictors_concatenated, -0.100, 1.000, error='l1', basis=0.050, partitions=5, test=1, selective_stopping=True) 90 | # Save the TRF for later analysis 91 | eelbrain.save.pickle(trf, path) 92 | -------------------------------------------------------------------------------- /analysis/estimate_word_acoustics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Estimate deconvolution of the spectrogram depending on function and content words. Keep results for each test-partition, so as to be able to statistically assess the models. 3 | """ 4 | from pathlib import Path 5 | 6 | import eelbrain 7 | 8 | 9 | # Data locations 10 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 11 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 12 | TRF_DIR = DATA_ROOT / 'TRFs' 13 | 14 | # Collect stimulus data from all trials 15 | gammatone_trials = [] 16 | word_trials = [] 17 | lexical_trials = [] 18 | non_lexical_trials = [] 19 | # loop through trials to load all stimuli 20 | for trial in range(1, 13): 21 | gammatone = eelbrain.load.unpickle(PREDICTOR_DIR / f'{trial}~gammatone-8.pickle') 22 | gammatone = gammatone.bin(0.01) 23 | events = eelbrain.load.unpickle(PREDICTOR_DIR / f'{trial}~word.pickle') 24 | # turn categorial predictors into time-series matching the spectrogram 25 | word = eelbrain.event_impulse_predictor(gammatone, time='time', value=1, data=events, name='word') 26 | lexical = eelbrain.event_impulse_predictor(gammatone, time='time', value='lexical', data=events, name='lexical') 27 | non_lexical = eelbrain.event_impulse_predictor(gammatone, time='time', value='nlexical', data=events, name='non_lexical') 28 | # store ndvars in lists 29 | gammatone_trials.append(gammatone) 30 | word_trials.append(word) 31 | lexical_trials.append(lexical) 32 | non_lexical_trials.append(non_lexical) 33 | # concatenate trials 34 | gammatone = eelbrain.concatenate(gammatone_trials) 35 | word = eelbrain.concatenate(word_trials) 36 | lexical = eelbrain.concatenate(lexical_trials) 37 | non_lexical = eelbrain.concatenate(non_lexical_trials) 38 | 39 | trf_word = eelbrain.boosting(gammatone, word, -0.100, 1.001, partitions=15, partition_results=True, test=True) 40 | eelbrain.save.pickle(trf_word, TRF_DIR / 'gammatone~word.pickle') 41 | 42 | trf_lexical = eelbrain.boosting(gammatone, [word, lexical, non_lexical], -0.100, 1.001, partitions=15, partition_results=True, test=True) 43 | eelbrain.save.pickle(trf_lexical, TRF_DIR / 'gammatone~word+lexical.pickle') 44 | -------------------------------------------------------------------------------- /analysis/make_erps.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # text_representation: 5 | # extension: .py 6 | # format_name: light 7 | # format_version: '1.5' 8 | # jupytext_version: 1.11.4 9 | # kernelspec: 10 | # display_name: tutorialAlice 11 | # language: python 12 | # name: tutorialalice 13 | # --- 14 | 15 | # + 16 | from pathlib import Path 17 | import re 18 | 19 | import eelbrain 20 | import mne 21 | # - 22 | 23 | STIMULI = [str(i) for i in range(1, 13)] 24 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 25 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 26 | EEG_DIR = DATA_ROOT / 'eeg' 27 | SUBJECTS = [path.name for path in EEG_DIR.iterdir() if re.match(r'S\d*', path.name)] 28 | # Define a target directory for epoched data and make sure the directory is created 29 | EPOCH_DIR = DATA_ROOT / 'ERPs' 30 | EPOCH_DIR.mkdir(exist_ok=True) 31 | 32 | TSTART = -0.1 33 | TSTOP = 1 34 | 35 | # + 36 | # Load stimuli 37 | # ------------ 38 | # load word information (including the onset times) 39 | word_tables = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~word.pickle') for stimulus in STIMULI] 40 | durations = [word_table['time'][-1] + TSTOP for word_table in word_tables] 41 | word_onsets = [word_table['time'] for word_table in word_tables] 42 | # - 43 | 44 | # Loop through subjects to get the epoched data 45 | for subject in SUBJECTS: 46 | subject_epoch_dir = EPOCH_DIR / subject 47 | subject_epoch_dir.mkdir(exist_ok=True) 48 | # Generate epoch path so we can check whether it already exists 49 | erp_path = subject_epoch_dir / f'{subject}_erp_word.pickle' 50 | # Skip this subject if the file already exists 51 | if erp_path.exists(): 52 | continue 53 | 54 | # Load the EEG data 55 | raw = mne.io.read_raw(EEG_DIR / subject / f'{subject}_alice-raw.fif', preload=True) 56 | # Band-pass filter the raw data between 0.5 and 20 Hz 57 | raw.filter(0.5, 20) 58 | # Interpolate bad channels 59 | raw.interpolate_bads() 60 | # Extract the events marking the stimulus presentation from the EEG file 61 | events = eelbrain.load.fiff.events(raw) 62 | # Not all subjects have all trials; determine which stimuli are present 63 | trial_indexes = [STIMULI.index(stimulus) for stimulus in events['event']] 64 | # Extract the EEG data segments corresponding to the stimuli 65 | trial_durations = [durations[i] for i in trial_indexes] 66 | eeg = eelbrain.load.fiff.variable_length_epochs(events, -0.100, trial_durations, decim=5, connectivity='auto') 67 | current_word_onsets = [word_onsets[i] for i in trial_indexes] 68 | 69 | # Make epoched data 70 | epochs = [] 71 | for eeg_segment, matched_word_onsets in zip(eeg, current_word_onsets): 72 | for onset_time in matched_word_onsets: 73 | # Note: tstart is negative! 74 | if onset_time + TSTART < 0: 75 | continue 76 | elif onset_time + TSTOP > eeg_segment.time.tstop: 77 | continue 78 | 79 | epoch = eeg_segment.sub(time=(onset_time + TSTART, onset_time + TSTOP)) 80 | # Update the epoch's time to be relative to word onset 81 | epoch = eelbrain.set_tmin(epoch, tmin=TSTART) 82 | epochs.append(epoch) 83 | 84 | # Take average 85 | epochs = eelbrain.combine(epochs) 86 | erp = epochs.mean('case') 87 | # Baseline correction: subtract from each sensor its average over time 88 | baseline_corrected_ERP = erp - erp.mean('time') 89 | eelbrain.save.pickle(baseline_corrected_ERP, erp_path) 90 | -------------------------------------------------------------------------------- /download_alice.py: -------------------------------------------------------------------------------- 1 | # Author: Proloy Das 2 | # License: MIT 3 | """ 4 | Usage: run the script without any arguments (downloaded at default location):: 5 | 6 | $ python download_alice.py 7 | 8 | Or with a valid pathname argument (downloaded at pathname):: 9 | 10 | $ python download_alice.py /home/xyz/alice_data 11 | 12 | Disclaimer: The following functions are heavily inspired by the namesake 13 | functions in mne/datasets/utils.py and mne/utils/check.py which are 14 | distributed under following license terms: 15 | 16 | Copyright © 2011-2019, authors of MNE-Python 17 | All rights reserved. 18 | 19 | Redistribution and use in source and binary forms, with or without 20 | modification, are permitted provided that the following conditions are met: 21 | * Redistributions of source code must retain the above copyright 22 | notice, this list of conditions and the following disclaimer. 23 | * Redistributions in binary form must reproduce the above copyright 24 | notice, this list of conditions and the following disclaimer in the 25 | documentation and/or other materials provided with the distribution. 26 | * Neither the name of the copyright holder nor the names of its 27 | contributors may be used to endorse or promote products derived from 28 | this software without specific prior written permission. 29 | 30 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 31 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 32 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 33 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY 34 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 35 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 36 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 37 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 39 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | """ 41 | import os 42 | import os.path as op 43 | import sys 44 | import shutil 45 | import tarfile 46 | import stat 47 | import zipfile 48 | from urllib.request import urlretrieve 49 | 50 | from mne.utils import logger 51 | from mne.utils.numerics import hashfunc 52 | 53 | 54 | _alice_license_text = """ 55 | This dataset accompanies "Eelbrain: A Python toolkit for time-continuous 56 | analysis with temporal response functions" (Brodbeck et al., 2021) and is a 57 | derivative of the Alice EEG datasets collected at the University of Michigan 58 | Computational Neurolinguistics Lab (Bhattasali et al., 2020). The files were 59 | converted from the original matlab format to fif format in order to be 60 | compatible with Eelbrain. 61 | 62 | 63 | The original dataset is licensed under CC BY 64 | (https://creativecommons.org/licenses/by/4.0/) and by downloading the dataset 65 | you pledge to comply with all relevant rules and regulations imposed by the 66 | above-mentioned license terms. 67 | 68 | The original work can be found at DOI: 10.7302/Z29C6VNH. 69 | """ 70 | 71 | 72 | def _get_path(path, name): 73 | """Get a dataset path.""" 74 | # 1. Input 75 | if path is not None: 76 | if not isinstance(path, str): 77 | raise ValueError('path must be a string or None') 78 | return path 79 | logger.info('Using default location ~/Data/Alice for %s...' % name) 80 | path = op.join(os.getenv('_ALICE_FAKE_HOME_DIR', op.expanduser("~")), 'Data', 'Alice') 81 | if not op.exists(path): 82 | logger.info(f'Creating {path}') 83 | try: 84 | os.makedirs(path) 85 | except OSError: 86 | raise OSError("User does not have write permissions " 87 | "at '%s', try giving the path as an " 88 | "argument to data_path() where user has " 89 | "write permissions, for ex:data_path" 90 | "('/home/xyz/me2/')" % (path,)) 91 | return path 92 | 93 | 94 | def _data_path(path=None, force_update=False, update_path=True, download=True, 95 | name=None, check_version=False, return_version=False, 96 | archive_name=None, accept=False): 97 | """Aux function.""" 98 | path = _get_path(path, name) 99 | 100 | # try to match url->archive_name->folder_name 101 | urls = dict( # the URLs to use 102 | alice=[ 103 | 'https://drum.lib.umd.edu/bitstream/handle/1903/27591/stimuli.zip', 104 | 'https://drum.lib.umd.edu/bitstream/handle/1903/27591/eeg.0.zip', 105 | 'https://drum.lib.umd.edu/bitstream/handle/1903/27591/eeg.1.zip', 106 | 'https://drum.lib.umd.edu/bitstream/handle/1903/27591/eeg.2.zip', 107 | ], 108 | mtrfs="unknown", 109 | ) 110 | # filename of the resulting downloaded archive (only needed if the URL 111 | # name does not match resulting filename) 112 | archive_names = dict( 113 | alice=['stimuli.zip', 114 | 'eeg.0.zip', 115 | 'eeg.1.zip', 116 | 'eeg.2.zip'], 117 | mtrfs='whoknows.zip', 118 | ) 119 | # original folder names that get extracted (only needed if the 120 | # archive does not extract the right folder name; e.g., usually GitHub) 121 | folder_origs = dict( # not listed means None (no need to move) 122 | ) 123 | # finally, where we want them to extract to (only needed if the folder name 124 | # is not the same as the last bit of the archive name without the file 125 | # extension) 126 | folder_names = dict( 127 | brainstorm='MNE-brainstorm-data', 128 | ) 129 | md5_hashes = dict( 130 | alice=[ 131 | '4336a47bef7d3e63239c40c0623dc186', 132 | 'd63d96a6e5080578dbf71320ddbec0a0', 133 | 'bdc65f168db4c0f19bb0fed20eae129b', 134 | '3fb33ca1c4640c863a71bddd45006815', 135 | ], 136 | mtrfs='3194e9f7b46039bb050a74f3e1ae9908', 137 | ) 138 | assert set(md5_hashes) == set(urls) 139 | url = urls[name] 140 | hash_ = md5_hashes[name] 141 | folder_orig = folder_origs.get(name, None) 142 | url = [url] if not isinstance(url, list) else url 143 | hash_ = [hash_] if not isinstance(hash_, list) else hash_ 144 | archive_name = archive_names.get(name) 145 | if archive_name is None: 146 | archive_name = [u.split('/')[-1] for u in url] 147 | if not isinstance(archive_name, list): 148 | archive_name = [archive_name] 149 | folder_path = [ 150 | op.join(path, folder_names.get(name, a.split('.')[0])) 151 | for a in archive_name 152 | ] 153 | if not isinstance(folder_orig, list): 154 | folder_orig = [folder_orig] * len(url) 155 | folder_path = [op.abspath(f) for f in folder_path] 156 | assert hash_ is not None 157 | assert all(isinstance(x, list) for x in (url, archive_name, 158 | folder_path)) 159 | assert len(url) == len(archive_name) == len(folder_path) 160 | logger.debug('URL: %s' % (url,)) 161 | logger.debug('archive_name: %s' % (archive_name,)) 162 | logger.debug('hash: %s' % (hash_,)) 163 | logger.debug('folder_path: %s' % (folder_path,)) 164 | 165 | need_download = any(not op.exists(f) for f in folder_path) 166 | if need_download and not download: 167 | return '' 168 | 169 | if need_download or force_update: 170 | logger.debug('Downloading: need_download=%s, force_update=%s' 171 | % (need_download, force_update)) 172 | for f in folder_path: 173 | logger.debug(' Exists: %s: %s' % (f, op.exists(f))) 174 | # # License 175 | if name == 'alice': 176 | if accept or '--accept-alice-license' in sys.argv: 177 | answer = 'y' 178 | else: 179 | # If they don't have stdin, just accept the license 180 | # https://github.com/mne-tools/mne-python/issues/8513#issuecomment-726823724 # noqa: E501 181 | answer = _safe_input( 182 | '%sAgree (y/[n])? ' % _alice_license_text, use='y') 183 | if answer.lower() != 'y': 184 | raise RuntimeError('You must agree to the license to use this ' 185 | 'dataset') 186 | assert len(url) == len(hash_) 187 | assert len(url) == len(archive_name) 188 | assert len(url) == len(folder_orig) 189 | assert len(url) == len(folder_path) 190 | assert len(url) > 0 191 | # 1. Get all the archives 192 | full_name = list() 193 | for u, an, h in zip(url, archive_name, hash_): 194 | remove_archive, full = _download(path, u, an, h) 195 | full_name.append(full) 196 | del archive_name 197 | # 2. Extract all of the files 198 | remove_dir = True 199 | for u, fp, an, h, fo in zip(url, folder_path, full_name, hash_, 200 | folder_orig): 201 | _extract(path, name, fp, an, None, remove_dir) 202 | remove_dir = False # only do on first iteration 203 | # 3. Remove all of the archives 204 | if remove_archive: 205 | for an in full_name: 206 | os.remove(op.join(path, an)) 207 | 208 | logger.info('Successfully extracted to: %s' % path) 209 | 210 | return path 211 | 212 | 213 | def _download(path, url, archive_name, hash_, hash_type='md5'): 214 | """Download and extract an archive, completing the filename.""" 215 | full_name = op.join(path, archive_name) 216 | remove_archive = True 217 | fetch_archive = True 218 | if op.exists(full_name): 219 | logger.info('Archive exists (%s), checking hash %s.' 220 | % (archive_name, hash_,)) 221 | fetch_archive = False 222 | if hashfunc(full_name, hash_type=hash_type) != hash_: 223 | if input('Archive already exists but the hash does not match: ' 224 | '%s\nOverwrite (y/[n])?' 225 | % (archive_name,)).lower() == 'y': 226 | os.remove(full_name) 227 | fetch_archive = True 228 | if fetch_archive: 229 | logger.info('Downloading archive %s to %s' % (archive_name, path)) 230 | try: 231 | temp_file_name, header = urlretrieve(url) 232 | # check hash sum eg md5sum 233 | if hash_ is not None: 234 | logger.info('Verifying hash %s.' % (hash_,)) 235 | hashsum = hashfunc(temp_file_name, hash_type=hash_type) 236 | if hash_ != hashsum: 237 | raise RuntimeError('Hash mismatch for downloaded file %s, ' 238 | 'expected %s but got %s' 239 | % (temp_file_name, hash_, hashsum)) 240 | shutil.move(temp_file_name, full_name) 241 | except Exception: 242 | logger.error('Error while fetching file %s.' 243 | ' Dataset fetching aborted.' % url) 244 | raise 245 | # _fetch_file(url, full_name, print_destination=False, 246 | # hash_=hash_, hash_type=hash_type) 247 | return remove_archive, full_name 248 | 249 | 250 | def _extract(path, name, folder_path, archive_name, folder_orig, remove_dir): 251 | if op.exists(folder_path) and remove_dir: 252 | logger.info('Removing old directory: %s' % (folder_path,)) 253 | 254 | def onerror(func, path, exc_info): 255 | """Deal with access errors (e.g. testing dataset read-only).""" 256 | # Is the error an access error ? 257 | do = False 258 | if not os.access(path, os.W_OK): 259 | perm = os.stat(path).st_mode | stat.S_IWUSR 260 | os.chmod(path, perm) 261 | do = True 262 | if not os.access(op.dirname(path), os.W_OK): 263 | dir_perm = (os.stat(op.dirname(path)).st_mode | 264 | stat.S_IWUSR) 265 | os.chmod(op.dirname(path), dir_perm) 266 | do = True 267 | if do: 268 | func(path) 269 | else: 270 | raise exc_info[1] 271 | shutil.rmtree(folder_path, onerror=onerror) 272 | 273 | logger.info('Decompressing the archive: %s' % archive_name) 274 | logger.info('(please be patient, this can take some time)') 275 | if archive_name.endswith('.zip'): 276 | with zipfile.ZipFile(archive_name, 'r') as ff: 277 | ff.extractall(path) 278 | else: 279 | if archive_name.endswith('.bz2'): 280 | ext = 'bz2' 281 | else: 282 | ext = 'gz' 283 | with tarfile.open(archive_name, 'r:%s' % ext) as tf: 284 | tf.extractall(path=path) 285 | 286 | if folder_orig is not None: 287 | shutil.move(op.join(path, folder_orig), folder_path) 288 | 289 | 290 | def _safe_input(msg, *, alt=None, use=None): 291 | "copied from mne/utils/check.py" 292 | try: 293 | return input(msg) 294 | except EOFError: # MATLAB or other non-stdin 295 | if use is not None: 296 | return use 297 | raise RuntimeError( 298 | f'Could not use input() to get a response to:\n{msg}\n' 299 | f'You can {alt} to avoid this error.') 300 | 301 | 302 | def data_path(path=None, force_update=False, update_path=True, download=True, 303 | accept=False, verbose=None): 304 | return _data_path(path=path, force_update=force_update, 305 | update_path=update_path, name='alice', 306 | download=download, accept=accept) 307 | 308 | 309 | if __name__ == '__main__': 310 | if len(sys.argv) <= 1: 311 | path = data_path() 312 | elif isinstance(sys.argv[1], str): 313 | path = data_path(path=sys.argv[1]) 314 | else: 315 | raise ValueError(f'{sys.argv[1]} is not a valid pathname.' 316 | f'Run script either with a valid path argument or' 317 | f'or without any argument.') 318 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # Environment for Alice TRF analysis 2 | # usage: $ conda env create --file=environment.yml 3 | name: eelbrain 4 | channels: 5 | - conda-forge 6 | dependencies: 7 | - eelbrain >= 0.40 8 | - pip 9 | - ipython 10 | - jupyter 11 | - ipywidgets 12 | - jupytext 13 | - seaborn 14 | - pip: 15 | - gammatone 16 | - https://github.com/Hugo-W/pyEEG/archive/refs/heads/master.zip 17 | -------------------------------------------------------------------------------- /figures/Auditory-TRFs.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # + 17 | from pathlib import Path 18 | 19 | import eelbrain 20 | from matplotlib import pyplot 21 | import re 22 | 23 | 24 | # Data locations 25 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 26 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 27 | EEG_DIR = DATA_ROOT / 'eeg' 28 | TRF_DIR = DATA_ROOT / 'TRFs' 29 | SUBJECTS = [path.name for path in EEG_DIR.iterdir() if re.match(r'S\d*', path.name)] 30 | 31 | # Where to save the figure 32 | DST = DATA_ROOT / 'figures' 33 | DST.mkdir(exist_ok=True) 34 | 35 | # Configure the matplotlib figure style 36 | FONT = 'Arial' 37 | FONT_SIZE = 8 38 | RC = { 39 | 'figure.dpi': 100, 40 | 'savefig.dpi': 300, 41 | 'savefig.transparent': True, 42 | # Font 43 | 'font.family': 'sans-serif', 44 | 'font.sans-serif': FONT, 45 | 'font.size': FONT_SIZE, 46 | 'figure.labelsize': FONT_SIZE, 47 | 'figure.titlesize': FONT_SIZE, 48 | 'axes.labelsize': FONT_SIZE, 49 | 'axes.titlesize': FONT_SIZE, 50 | 'xtick.labelsize': FONT_SIZE, 51 | 'ytick.labelsize': FONT_SIZE, 52 | 'legend.fontsize': FONT_SIZE, 53 | } 54 | pyplot.rcParams.update(RC) 55 | # - 56 | 57 | # # A) Envelope 58 | # Examine results from predicting EEG data from the speech envelope alone. 59 | 60 | # Load predictive power and TRFs of the envelope models 61 | rows = [] 62 | for subject in SUBJECTS: 63 | trf = eelbrain.load.unpickle(TRF_DIR / subject / f'{subject} envelope.pickle') 64 | rows.append([subject, trf.proportion_explained, trf.h[0]]) 65 | data_envelope = eelbrain.Dataset.from_caselist(['subject', 'det', 'trf'], rows) 66 | 67 | # test that model predictive power on held-out data is > 0 68 | test_envelope = eelbrain.testnd.TTestOneSample('det', data=data_envelope, tail=1, pmin=0.05) 69 | p = eelbrain.plot.Topomap(test_envelope, clip='circle', w=2) 70 | cb = p.plot_colorbar(width=0.1, w=2) 71 | 72 | # ## Envelope TRF 73 | # Test the TRF with a one-sample *t*-test against 0. This tests the null-hypothesis that the electrical current direction at each time point was random across subjects. The systematic current directions shown below at anterior electrodes are typical of auditory responses. 74 | 75 | trf_envelope = eelbrain.testnd.TTestOneSample('trf', data=data_envelope, pmin=0.05) 76 | 77 | p = eelbrain.plot.TopoArray(trf_envelope, t=[0.040, 0.090, 0.140, 0.250, 0.400], clip='circle', cmap='xpolar') 78 | cb = p.plot_colorbar(width=0.1) 79 | 80 | # # B Envelope + onset envelope 81 | # Test a second model which adds acoustic onsets (onsets are also represented as one-dimensional time-series, with onsets collapsed across frequency bands). 82 | 83 | # load cross-validated predictive power and TRFs of the spectrogram models 84 | rows = [] 85 | x_names = None 86 | for subject in SUBJECTS: 87 | trf = eelbrain.load.unpickle(TRF_DIR / subject / f'{subject} envelope+onset.pickle') 88 | rows.append([subject, trf.proportion_explained, *trf.h]) 89 | x_names = trf.x 90 | data_onset = eelbrain.Dataset.from_caselist(['subject', 'det', *x_names], rows) 91 | 92 | # Compare predictive power of the two models 93 | test_onset = eelbrain.testnd.TTestOneSample('det', data=data_onset, tail=1, pmin=0.05) 94 | # Paired t-test by specifying two measurement NDVars with matched cases 95 | # Note that this presupposes that subjects are in the same order 96 | test_onset_envelope = eelbrain.testnd.TTestRelated(data_onset['det'], data_envelope['det'], tail=1, pmin=0.05) 97 | p = eelbrain.plot.Topomap( 98 | [test_onset.masked_difference(), test_onset_envelope.masked_difference()], 99 | axtitle=[['Envelope + Onsets\n', test_onset], ['Envelope + Onsets > Envelope\n', test_onset_envelope]], 100 | ncol=2, clip='circle') 101 | cb = p.plot_colorbar(width=0.1) 102 | 103 | trf_eo_envelope = eelbrain.testnd.TTestOneSample('envelope', data=data_onset, pmin=0.05) 104 | trf_eo_onset = eelbrain.testnd.TTestOneSample('onset', data=data_onset, pmin=0.05) 105 | 106 | # # C) Full acoustic model 107 | # Load results form the full which included spectrogram as well as an onset spectrogram, both predictors represented as 2d time-series with 8 frequency bins each. 108 | 109 | # Load cross-validated preditive power of the full acoustic models 110 | rows = [] 111 | x_names = None 112 | for subject in SUBJECTS: 113 | trf = eelbrain.load.unpickle(TRF_DIR / subject / f'{subject} acoustic.pickle') 114 | rows.append([subject, trf.proportion_explained, *trf.h]) 115 | x_names = trf.x 116 | data_acoustic = eelbrain.Dataset.from_caselist(['subject', 'det', *x_names], rows) 117 | print(x_names) 118 | 119 | # Compare predictive power of the two models 120 | test_acoustic = eelbrain.testnd.TTestOneSample('det', data=data_acoustic, tail=1, pmin=0.05) 121 | # Paired t-test by specifying two measurement NDVars with matched cases 122 | # Note that this presupposes that subjects are in the same order 123 | test_acoustic_onset = eelbrain.testnd.TTestRelated(data_acoustic['det'], data_onset['det'], tail=1, pmin=0.05) 124 | p = eelbrain.plot.Topomap( 125 | [test_acoustic.masked_difference(), test_acoustic_onset.masked_difference()], 126 | axtitle=[[['Spectrogram\n', test_acoustic], ], ['Spectrogram > Envelope\n', test_acoustic_onset]], 127 | ncol=2, clip='circle') 128 | cb = p.plot_colorbar(width=0.1) 129 | 130 | # ## TRFs 131 | # Since these spectrogram mTRFs have a frequency dimension in addition to time and sensor we have to slice or aggregate them for visualization on a 2d plot. We take two approaches: 132 | # 133 | # 1) Sum across the frequency, based on the assumtopn that TRFs are similar for different frequency bands 134 | # 2) Average across a group of neighboring sensors, to verify this assumtopn 135 | 136 | trf_spectrogram = eelbrain.testnd.TTestOneSample("gammatone.sum('frequency')", data=data_acoustic, pmin=0.05) 137 | trf_onset_spectrogram = eelbrain.testnd.TTestOneSample("gammatone_on.sum('frequency')", data=data_acoustic, pmin=0.05) 138 | 139 | p = eelbrain.plot.TopoArray([trf_spectrogram, trf_onset_spectrogram], t=[0.050, 0.100, 0.150, 0.450], xlim=(-0.050, 0.950)) 140 | 141 | # Manually define sensors that are sensitive to acoustic responses 142 | auditory_sensors = ['59', '20', '21', '7', '8', '9', '49', '19' ,'44', '45', '34' ,'35' ,'36' ,'10'] 143 | p = eelbrain.plot.SensorMap(data_acoustic['det'], h=2, mark=auditory_sensors) 144 | 145 | strf_spectrogram = data_acoustic['gammatone'].mean(sensor=auditory_sensors).smooth('frequency', window_samples=7, fix_edges=True) 146 | strf_onset_spectrogram = data_acoustic['gammatone_on'].mean(sensor=auditory_sensors) 147 | p = eelbrain.plot.Array([strf_spectrogram, strf_onset_spectrogram], ncol=2, xlim=(-0.050, 0.950)) 148 | 149 | # # Figure 150 | # ## Load data 151 | 152 | # Load stimuli 153 | gammatone = eelbrain.load.unpickle(DATA_ROOT / 'stimuli' / '1-gammatone.pickle').sub(time=(0, 3.001)) 154 | gammatone = (gammatone.clip(0) + 1).log() 155 | gammatone_on = eelbrain.edge_detector(gammatone, c=30) 156 | gammatone /= gammatone.max() 157 | gammatone_on /= gammatone_on.max() 158 | 159 | # Load cross-validated predictive power of all models 160 | models = ['envelope', 'envelope+onset', 'acoustic'] 161 | rows = [] 162 | for model in models: 163 | for subject in SUBJECTS: 164 | trf = eelbrain.load.unpickle(TRF_DIR / subject / f'{subject} {model}.pickle') 165 | rows.append([subject, model, trf.proportion_explained]) 166 | model_data = eelbrain.Dataset.from_caselist(['subject', 'model', 'det'], rows) 167 | 168 | # Max predictive power per model (reported in paper) 169 | table = eelbrain.fmtxt.Table('ll') 170 | table.cells('Model', 'Max % explained') 171 | for model in models: 172 | m_data = model_data.sub(f"model == '{model}'") 173 | det_max = m_data['det'].mean('case').max('sensor') 174 | table.cells(model, f'{det_max:.2%}') 175 | table 176 | 177 | # ## Generate figure 178 | 179 | # + 180 | # Initialize figure 181 | figure = pyplot.figure(figsize=(7.5, 8)) 182 | gridspec = figure.add_gridspec(10, 10, top=0.95, bottom=0.05, left=0.05, right=0.95, hspace=0.5, height_ratios=[2, 2, 2, 2, 2, 2, 2, 2, 1, 2], width_ratios=[2, 2, 2, 2, 2, 1, 2, 2, 2, 2]) 183 | topo_args = dict(clip='circle') 184 | array_args = dict(xlim=(-0.050, 1.0), axtitle=False) 185 | topo_array_args = dict(topo_labels='below', **array_args, **topo_args) 186 | det_args = dict(**topo_args, vmax=0.01, cmap='lux-gray') 187 | det_delta_args = dict(**topo_args, vmax=0.001, cmap='lux-gray') 188 | cbar_args = dict(h=.5) 189 | t_envelope = [0.050, 0.100, 0.150, 0.400] 190 | t_onset = [0.060, 0.110, 0.180] 191 | 192 | # A) Predictors 193 | # ------------- 194 | axes = [ 195 | figure.add_subplot(gridspec[0, 0:3]), 196 | figure.add_subplot(gridspec[1, 0:3]), 197 | ] 198 | eelbrain.plot.Array([gammatone, gammatone_on], axes=axes, axtitle=False, xticklabels=-1, yticklabels=False) 199 | axes[0].set_title('Spectrogram (& envelope)', size=FONT_SIZE, loc='left', y=0.91) 200 | axes[1].set_title('Onset spectrogram (& onsets)', size=FONT_SIZE, loc='left', y=0.91) 201 | for ax, y in zip(axes, (gammatone, gammatone_on)): 202 | y = y.sub(time=(1, None)).sum('frequency') 203 | y -= y.min() 204 | y *= 90 / y.max() 205 | y += 20 206 | ax.plot(y.time.times, y.x) 207 | ax.set_yticks(()) 208 | 209 | 210 | # B) Envelope 211 | # ----------- 212 | # Predictive power tests 213 | axes = figure.add_subplot(gridspec[1,4]) 214 | p = eelbrain.plot.Topomap(test_envelope.masked_difference(), axes=axes, **det_args) 215 | axes.set_title("Envelope\npredictive power", loc='left') 216 | p.plot_colorbar(below=axes, offset=0.1, **cbar_args, clipmin=0, ticks=5, label='% variability\nexplained', unit=1e-2) 217 | # TRF 218 | axes = [ 219 | figure.add_subplot(gridspec[0, 7:10]), 220 | figure.add_subplot(gridspec[1, 6]), 221 | figure.add_subplot(gridspec[1, 7]), 222 | figure.add_subplot(gridspec[1, 8]), 223 | figure.add_subplot(gridspec[1, 9]), 224 | ] 225 | p = eelbrain.plot.TopoArray(trf_envelope, t=t_envelope, axes=axes, **topo_array_args) 226 | vmin, vmax = p.get_vlim() 227 | axes[0].set_title('Envelope TRF', loc='left') 228 | axes[0].set_yticks(range(0, 61, 15)) 229 | p.plot_colorbar(below=axes[1], offset=-0.1, **cbar_args, ticks=0, label='TRF (a.u.)') 230 | 231 | # C) Envelope + onsets 232 | # -------------------- 233 | # Predictive power tests 234 | axes = figure.add_subplot(gridspec[4,0]) 235 | p = eelbrain.plot.Topomap(test_onset_envelope.masked_difference(), axes=axes, **det_delta_args) 236 | axes.set_title("Predictive Power\n> Envelope", loc='left') 237 | p.plot_colorbar(right_of=axes, offset=0., **cbar_args, ticks=3, label='∆ % variability\nexplained', unit=1e-2) 238 | 239 | # TRFs 240 | axes = [ 241 | figure.add_subplot(gridspec[3, 3:6]), 242 | figure.add_subplot(gridspec[4, 2]), 243 | figure.add_subplot(gridspec[4, 3]), 244 | figure.add_subplot(gridspec[4, 4]), 245 | ] 246 | p = eelbrain.plot.TopoArray(trf_eo_onset, t=t_onset, axes=axes, vmin=vmin, vmax=vmax, **topo_array_args) 247 | axes[0].set_title('Onset TRF', loc='left') 248 | axes[0].set_yticks(range(0, 61, 15)) 249 | axes = [ 250 | figure.add_subplot(gridspec[3, 7:10]), 251 | figure.add_subplot(gridspec[4, 6]), 252 | figure.add_subplot(gridspec[4, 7]), 253 | figure.add_subplot(gridspec[4, 8]), 254 | figure.add_subplot(gridspec[4, 9]), 255 | ] 256 | p = eelbrain.plot.TopoArray(trf_eo_envelope, t=t_envelope, axes=axes, **topo_array_args, yticklabels=False, ylabel=False) 257 | axes[0].set_title('Envelope TRF', loc='left') 258 | axes[0].set_yticks(range(0, 61, 15)) 259 | y_b = axes[0].get_position().y1 260 | 261 | # D) Spectrograms 262 | # --------------- 263 | # Predictive power tests 264 | axes = figure.add_subplot(gridspec[7, 0]) 265 | p = eelbrain.plot.Topomap(test_acoustic_onset.masked_difference(), axes=axes, **det_delta_args) 266 | axes.set_title("Predictive Power\n> Envelope + Onsets", loc='left') 267 | p.plot_colorbar(right_of=axes, offset=0., **cbar_args, ticks=3, label='∆ % variability\nexplained', unit=1e-2) 268 | 269 | # TRFs 270 | axes = [ 271 | figure.add_subplot(gridspec[6, 3:6]), 272 | figure.add_subplot(gridspec[7, 2]), 273 | figure.add_subplot(gridspec[7, 3]), 274 | figure.add_subplot(gridspec[7, 4]), 275 | ] 276 | p = eelbrain.plot.TopoArray(trf_onset_spectrogram, t=t_onset, axes=axes, **topo_array_args) 277 | axes[0].set_title('Onset STRF (sum across frequency)', loc='left') 278 | axes[0].set_yticks(range(0, 61, 15)) 279 | axes = [ 280 | figure.add_subplot(gridspec[6, 7:10]), 281 | figure.add_subplot(gridspec[7, 6]), 282 | figure.add_subplot(gridspec[7, 7]), 283 | figure.add_subplot(gridspec[7, 8]), 284 | figure.add_subplot(gridspec[7, 9]), 285 | ] 286 | p = eelbrain.plot.TopoArray(trf_spectrogram, t=t_envelope, axes=axes, **topo_array_args, yticklabels=False, ylabel=False) 287 | axes[0].set_title('Envelope STRF (sum across frequency)', loc='left') 288 | axes[0].set_yticks(range(0, 61, 15)) 289 | y_c = axes[0].get_position().y1 290 | 291 | # E) STRFs 292 | # -------- 293 | # Channel selection 294 | axes = figure.add_subplot(gridspec[9,0]) 295 | p = eelbrain.plot.Topomap(test_acoustic.difference, axes=axes, **det_args) 296 | p.mark_sensors(auditory_sensors, s=2, c='green') 297 | axes.set_title("Channels for\nSTRF", loc='left') 298 | p.plot_colorbar(right_of=axes, offset=0., **cbar_args, clipmin=0, ticks=5, label='% variability\nexplained', unit=1e-2) 299 | # STRFs 300 | axes = [ 301 | figure.add_subplot(gridspec[9, 3:6]), 302 | figure.add_subplot(gridspec[9, 7:10]), 303 | ] 304 | eelbrain.plot.Array([strf_onset_spectrogram, strf_spectrogram], axes=axes, **array_args) 305 | for ax in axes: 306 | ax.set_yticks(range(0, 8, 2)) 307 | axes[0].set_title("Onset STRF", loc='left') 308 | axes[1].set_title("Spectrogram STRF", loc='left') 309 | y_d = axes[0].get_position().y1 310 | 311 | figure.text(0.01, 0.98, 'A) Predictors', size=10) 312 | figure.text(0.40, 0.98, 'B) Envelope', size=10) 313 | figure.text(0.01, y_b + 0.04, 'C) Envelope + onsets', size=10) 314 | figure.text(0.01, y_c + 0.04, 'D) Spectrogram + onset spectrogram', size=10) 315 | figure.text(0.01, y_d + 0.04, 'E) Spectrogram + onset spectrogram: spectro-temporal response functions (STRFs)', size=10) 316 | 317 | figure.savefig(DST / 'Auditory-TRFs.pdf') 318 | eelbrain.plot.figure_outline() 319 | -------------------------------------------------------------------------------- /figures/Auditory-scale.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # + 17 | from pathlib import Path 18 | 19 | import eelbrain 20 | from matplotlib import pyplot 21 | import numpy 22 | import re 23 | 24 | 25 | # Data locations 26 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 27 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 28 | EEG_DIR = DATA_ROOT / 'eeg' 29 | TRF_DIR = DATA_ROOT / 'TRFs' 30 | SUBJECTS = [path.name for path in EEG_DIR.iterdir() if re.match(r'S\d*', path.name)] 31 | 32 | # Where to save the figure 33 | DST = DATA_ROOT / 'figures' 34 | DST.mkdir(exist_ok=True) 35 | 36 | # Configure the matplotlib figure style 37 | FONT = 'Arial' 38 | FONT_SIZE = 8 39 | RC = { 40 | 'figure.dpi': 100, 41 | 'savefig.dpi': 300, 42 | 'savefig.transparent': True, 43 | # Font 44 | 'font.family': 'sans-serif', 45 | 'font.sans-serif': FONT, 46 | 'font.size': FONT_SIZE, 47 | 'figure.labelsize': FONT_SIZE, 48 | 'figure.titlesize': FONT_SIZE, 49 | 'axes.labelsize': FONT_SIZE, 50 | 'axes.titlesize': FONT_SIZE, 51 | 'xtick.labelsize': FONT_SIZE, 52 | 'ytick.labelsize': FONT_SIZE, 53 | 'legend.fontsize': FONT_SIZE, 54 | } 55 | pyplot.rcParams.update(RC) 56 | pyplot.rcParams['hatch.linewidth'] = 4 57 | # - 58 | 59 | # # Load the data 60 | 61 | # + 62 | # load cross-validated predictive power and TRFs of the different spectrogram models 63 | SCALES = ['linear', 'power-law', 'log'] 64 | MODELS = { 65 | 'linear': 'gammatone-lin', 66 | 'power-law': 'gammatone-pow', 67 | 'log': 'gammatone', 68 | 'linear+log': 'gammatone-lin+log', 69 | } 70 | COLORS = { 71 | 'linear': '#1f77b4', 72 | 'power-law': '#ff7f0e', 73 | 'log': '#d62728', 74 | } 75 | COLORS['linear+log'] = COLORS['linear'] 76 | datasets = {} 77 | for scale, model in MODELS.items(): 78 | rows = [] 79 | for subject in SUBJECTS: 80 | trf = eelbrain.load.unpickle(TRF_DIR / subject / f'{subject} {model}.pickle') 81 | rows.append([subject, scale, trf.proportion_explained, *trf.h]) 82 | trf_names = trf.x 83 | data = eelbrain.Dataset.from_caselist(['subject', 'scale', 'det', *trf_names], rows, info={'trfs': trf_names}) 84 | # Average predictive power across sensors for easier comparison 85 | data['det_mean'] = data['det'].mean('sensor') 86 | datasets[scale] = data 87 | 88 | # Create an aggregated dataset for predictive power (TRFs can't be included 89 | # because there is a different number of predictors in the lin+log model) 90 | data_det = eelbrain.combine([data['subject', 'scale', 'det_mean'] for data in datasets.values()]) 91 | 92 | # Verify the Dataset 93 | data_det.head() 94 | # - 95 | 96 | # # Model comparisons 97 | 98 | p = eelbrain.plot.Barplot('det_mean', 'scale', match='subject', cells=SCALES, data=data_det, h=3, w=2, xtick_rotation=-20) 99 | 100 | # ## Pairwise tests 101 | 102 | eelbrain.test.TTestRelated('det_mean', 'scale', 'power-law', 'linear', match='subject', data=data_det) 103 | 104 | eelbrain.test.TTestRelated('det_mean', 'scale', 'log', 'power-law', match='subject', data=data_det) 105 | 106 | eelbrain.test.TTestRelated('det_mean', 'scale', 'linear+log', 'log', match='subject', data=data_det) 107 | 108 | # ## STRFs 109 | 110 | for scale, data in datasets.items(): 111 | p = eelbrain.plot.Array([f"{trf}.mean('sensor')" for trf in data.info['trfs']], ncol=1, data=data, title=scale, axh=2, axw=3) 112 | 113 | # # Figure 114 | 115 | STIMULUS = 1 116 | gammatone_lin = eelbrain.load.unpickle(DATA_ROOT / 'stimuli' / f'{STIMULUS}-gammatone.pickle') 117 | gammatone_pow = gammatone_lin ** 0.6 118 | gammatone_log = (1 + gammatone_lin).log() 119 | 120 | # + 121 | # Figure layout 122 | figure = pyplot.figure(figsize=(7.5, 4)) 123 | hs = [1, 1, 1, 1, 1, 1] 124 | ws = [1, 1, 3, 1] 125 | gridspec = figure.add_gridspec(len(hs), len(ws), top=0.92, bottom=0.15, left=0.09, right=0.99, hspace=2., wspace=0.1, height_ratios=hs, width_ratios=ws) 126 | # Plotting parameters for reusing 127 | topo_args = dict(clip='circle') 128 | array_args = dict(xlim=(-0.050, 1.0), axtitle=False) 129 | topo_array_args = dict(topo_labels='below', **array_args, **topo_args) 130 | det_args = dict(**topo_args, vmax=1, cmap='lux-gray') 131 | cbar_args = dict(h=.5) 132 | t_envelope = [0.050, 0.100, 0.150, 0.400] 133 | t_onset = [0.060, 0.110, 0.180] 134 | 135 | # Log scale graph 136 | figure.text(0.01, 0.96, 'A) Nonlinear scales', size=10) 137 | ax = figure.add_subplot(gridspec[0:2, 0]) 138 | x = numpy.linspace(0, 100) 139 | ax.plot(x, numpy.log(x+1), label='log', color=COLORS['log'], zorder=2.2) 140 | ax.plot(x, x**0.6/3, label='power-law', color=COLORS['power-law'], zorder=2.1) 141 | ax.plot(x, x * 0.05, label='linear', color=COLORS['linear']) 142 | ax.set_ylabel('Brain response') 143 | ax.set_xlabel('Acoustic power') 144 | ax.set_xticks(()) 145 | ax.set_yticks(()) 146 | pyplot.legend(loc=(.8, .05)) 147 | 148 | # Spectrograms 149 | figure.text(0.38, 0.96, 'B', size=10) 150 | sgrams = [gammatone_lin, gammatone_pow, gammatone_log] 151 | for i, sgram, scale in zip(range(3), sgrams, SCALES): 152 | ax = figure.add_subplot(gridspec[i*2:(i+1)*2, 2]) 153 | x = sgram.sub(time=(0, 3)) 154 | eelbrain.plot.Array(x, axes=ax, ylabel=i==2, xticklabels=i==2, xlabel=i==2) 155 | ax.set_yticks(range(0, 129, 32)) 156 | ax.set_title(scale.capitalize(), loc='left') 157 | 158 | # Predictive power topography 159 | for i, scale in enumerate(SCALES): 160 | ax = figure.add_subplot(gridspec[i*2:(i+1)*2, 3]) 161 | data = datasets[scale] 162 | p = eelbrain.plot.Topomap('det * 100', data=data, axes=ax, **det_args) 163 | if i == 2: 164 | p.plot_colorbar(below=ax, clipmin=0, ticks=5, label='% variability explained') 165 | 166 | # Predictive power barplot 167 | figure.text(0.01, 0.55, 'C) Predictive power', size=10) 168 | ax = figure.add_subplot(gridspec[3:, 0]) 169 | p = eelbrain.plot.Barplot('det_mean * 100', 'scale', match='subject', data=data_det, cells=MODELS, axes=ax, test=False, ylabel='% variability explained', xlabel='Scale', frame=False, xtick_rotation=-30, top=.22, colors=COLORS) 170 | res = eelbrain.test.TTestRelated('det_mean', 'scale', 'power-law', 'linear', 'subject', data=data_det) 171 | p.mark_pair('linear', 'power-law', .2, mark=res.p) 172 | res = eelbrain.test.TTestRelated('det_mean', 'scale', 'log', 'power-law', 'subject', data=data_det) 173 | p.mark_pair('power-law', 'log', .23, mark=res.p) 174 | # hatch for linear+log bar 175 | p.bars.patches[-1].set_hatch('//') 176 | p.bars.patches[-1].set_edgecolor(COLORS['log']) 177 | p.bars.patches[-1].set_linewidth(0) 178 | 179 | figure.savefig(DST / 'Auditory-Scale.pdf') 180 | eelbrain.plot.figure_outline() 181 | -------------------------------------------------------------------------------- /figures/Collinearity.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # Simulations comparing boosting with ridge regression in presence collinearity. 17 | 18 | # + 19 | from pathlib import Path 20 | 21 | import numpy as np 22 | import matplotlib.pyplot as pyplot 23 | import eelbrain 24 | 25 | from scipy.signal import windows 26 | from pyeeg.models import TRFEstimator 27 | 28 | 29 | STIMULI = [str(i) for i in range(1, 13)] 30 | # Data locations 31 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 32 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 33 | TRF_DIR = DATA_ROOT / 'TRFs' 34 | TRF_DIR.mkdir(exist_ok=True) 35 | 36 | # Where to cache simulation results 37 | SIMULATION_DIR = DATA_ROOT / 'simulations' 38 | SIMULATION_DIR.mkdir(exist_ok=True) 39 | 40 | # Where to save the figure 41 | DST = DATA_ROOT / 'figures' 42 | DST.mkdir(exist_ok=True) 43 | 44 | # Configure the matplotlib figure style 45 | FONT = 'Arial' 46 | FONT_SIZE = 8 47 | RC = { 48 | 'figure.dpi': 100, 49 | 'savefig.dpi': 300, 50 | 'savefig.transparent': True, 51 | # Font 52 | 'font.family': 'sans-serif', 53 | 'font.sans-serif': FONT, 54 | 'font.size': FONT_SIZE, 55 | 'figure.labelsize': FONT_SIZE, 56 | 'figure.titlesize': FONT_SIZE, 57 | 'axes.labelsize': FONT_SIZE, 58 | 'axes.titlesize': FONT_SIZE, 59 | 'xtick.labelsize': FONT_SIZE, 60 | 'ytick.labelsize': FONT_SIZE, 61 | 'legend.fontsize': FONT_SIZE, 62 | } 63 | pyplot.rcParams.update(RC) 64 | # - 65 | 66 | # # Load stimuli 67 | 68 | # + 69 | # Make sure to name the stimuli so that the TRFs can later be distinguished 70 | # Load the gammatone-spectrograms; use the time axis of these as reference 71 | gammatone = [eelbrain.load.unpickle(PREDICTOR_DIR / f'{stimulus}~gammatone-8.pickle') for stimulus in STIMULI] 72 | # Resample the spectrograms to 100 Hz (time-step = 0.01 s), which we will use for TRFs 73 | gammatone = [x.bin(0.01, dim='time', label='start') for x in gammatone] 74 | # Pad onset with 100 ms and offset with 1 second; make sure to give the predictor a unique name as that will make it easier to identify the TRF later 75 | gammatone = [eelbrain.pad(x, tstart=-0.100, tstop=x.time.tstop + 1, name='gammatone') for x in gammatone] 76 | 77 | # Extract the duration of the stimuli, so we can later match the EEG to the stimuli 78 | durations = [gt.time.tmax for stimulus, gt in zip(STIMULI, gammatone)] 79 | # - 80 | 81 | # # Simulate the EEG 82 | 83 | # + 84 | # two of the adjacent bands in the gammatone (band 3 and 4) are assumed to drive 85 | # the auditory response with a spatiotemporally alternating pattern. 86 | tstep = gammatone[0].time.tstep 87 | frequency = gammatone[0].get_dim(('frequency')) 88 | time = eelbrain.UTS(0, 0.001, 1000) 89 | strf = eelbrain.NDVar.zeros((frequency, time), name='Gammatone TRF') 90 | strf.x[3, :100] += - 0.5 * windows.gaussian(100, 12) 91 | strf.x[3, :200] += 0.65 * windows.gaussian(200, 17) 92 | strf.x[3, 150:450] += - 0.15 * windows.gaussian(300, 50) 93 | strf.x[4, 20:120] += + 0.2 * windows.gaussian(100, 12) 94 | strf.x[4, 20:220] += - 0.25 * windows.gaussian(200, 17) 95 | strf.x[4, 170:470] += + 0.15 * windows.gaussian(300, 50) 96 | strf.x *= 1e-8 97 | strf = eelbrain.resample(strf, 1/tstep) 98 | gammatone_response = [eelbrain.convolve(strf, x) for x in gammatone] 99 | 100 | # Add pink noise to the auditory responses to simulate raw EEG data 101 | eeg = [] 102 | for ii, response in enumerate(gammatone_response): 103 | response -= response.mean('time') 104 | noise = eelbrain.powerlaw_noise(response.dims, 1, seed=134+ii) 105 | # SNR ~ -5dB 106 | eeg.append(response + 1.7783 * noise * response.std() / noise.std()) 107 | 108 | # Since trials are of unequal length, we will concatenate them for the TRF 109 | # estimation 110 | eeg_concatenated = eelbrain.concatenate(eeg) 111 | predictors_concatenated = eelbrain.concatenate(gammatone) 112 | # - 113 | 114 | # # Learn TRFs via boosting 115 | 116 | # selective_stopping controls one facet of regularization 117 | cache_path = SIMULATION_DIR / 'boosting.pickle' 118 | if cache_path.exists(): 119 | boosting_trfs = eelbrain.load.unpickle(cache_path) 120 | else: 121 | boosting_trfs = [eelbrain.boosting(eeg_concatenated, predictors_concatenated, -0.1, 1., basis=0.05, error='l1', partitions=10, selective_stopping=ii, test=1, partition_results=True) for ii in range(1, 15, 1)] 122 | eelbrain.save.pickle(boosting_trfs, cache_path) 123 | # Select selective_stopping when explained variances in test data starts decreasing 124 | explained_variances_in_test = [model.proportion_explained for model in boosting_trfs] 125 | increments = np.diff(explained_variances_in_test, prepend=0) 126 | best_stopping = np.where(increments < 0)[0][0] - 1 127 | boosting_trf = boosting_trfs[best_stopping] 128 | 129 | # # Learn TRFs via Ridge regression using pyEEG 130 | cache_path = SIMULATION_DIR / 'ridge.pickle' 131 | if cache_path.exists(): 132 | ridge_trf = eelbrain.load.unpickle(cache_path) 133 | else: 134 | x = predictors_concatenated.get_data(('time', 'frequency')) 135 | y = eeg_concatenated.get_data('time')[:, None] 136 | reg_param = [1e5, 2e5, 5e5, 1e6, 2e6, 5e6, 1e7] # Ridge parameters 137 | ridge_trf = TRFEstimator(tmin=-0.1, tmax=1., srate=1/eeg[0].time.tstep, alpha=reg_param) 138 | scores, alpha = ridge_trf.xfit(x, y, n_splits=10) 139 | params = ridge_trf.get_params() 140 | tt = eelbrain.UTS.from_range(params['tmin'], params['tmax'], 1 / params['srate']) 141 | ridge_trf.best_alpha = alpha 142 | ridge_trf.h_scaled = eelbrain.NDVar(ridge_trf.coef_[:, :, 0].T, (frequency, tt), name='gammatone') 143 | eelbrain.save.pickle(ridge_trf, cache_path) 144 | 145 | # # Figure 146 | 147 | # + 148 | # Prepare mTRFs for ploting 149 | # hs = eelbrain.combine((strf, boosting_trf.h_scaled, ridge_trf.h_scaled), dim_intersection=True) 150 | hs = [strf, boosting_trf.h_scaled, ridge_trf.h_scaled] 151 | titles = ('Ground truth', 'Boosting', 'Ridge') 152 | vmax = 8e-9 153 | 154 | # Initialize figure 155 | figure = pyplot.figure(figsize=(7.5, 3.5)) 156 | gridspec = figure.add_gridspec(2, 3, left=0.1, right=0.9, hspace=1., bottom=0.1) 157 | 158 | # Plot TRFs as arrays 159 | axes = [figure.add_subplot(gridspec[0, idx]) for idx in range(3)] 160 | p = eelbrain.plot.Array(hs, axes=axes, vmax=vmax, xlim=(-0.100, 1.000), axtitle=False) 161 | p.plot_colorbar(right_of=axes[-1], label="TRF weights [a.u.]", ticks=2, w=2) 162 | for ax, title in zip(axes, titles): 163 | ax.set_title(title, loc='left', size=10) 164 | 165 | # Plot two active, and one of the inactive frequency TRFs 166 | interesting_frequencies = frequency.values[[3, 4, 5]] 167 | colors = {key: eelbrain.plot.unambiguous_color(color) + (0.70,) for key, color in zip(titles, ('black', 'orange', 'sky blue'))} 168 | axes = [figure.add_subplot(gridspec[1, idx]) for idx in range(3)] 169 | # plot.UTS takes a nested list 170 | freq_hs = [[h.sub(frequency=freq, name=title) for h, title in zip(hs, titles)] for freq in interesting_frequencies] 171 | p = eelbrain.plot.UTS(freq_hs, axtitle=False, axes=axes, legend=False, ylabel='TRF weights [a.u.]', colors=colors, xlim=(-0.100, 1.000), bottom=-vmax, top=vmax) 172 | for ax, frequency_ in zip(axes, interesting_frequencies): 173 | ax.set_yticks([-vmax, 0, vmax]) 174 | ax.set_title(f"{frequency_:.0f} Hz", loc='right', size=10) 175 | 176 | figure.text(0.01, 0.96, 'A) Gammatone TRF', size=10) 177 | figure.text(0.01, 0.47, 'B) TRF comparison', size=10) 178 | p.plot_legend((0.53, 0.44), ncols=3) 179 | 180 | figure.savefig(DST / 'Simulation boosting vs ridge.pdf') 181 | eelbrain.plot.figure_outline() 182 | -------------------------------------------------------------------------------- /figures/Comparison-ERP-TRF.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # + 17 | import os 18 | from pathlib import Path 19 | 20 | import eelbrain 21 | from matplotlib import pyplot 22 | 23 | # Data locations 24 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 25 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 26 | STIMULI_DIR = DATA_ROOT / 'stimuli' 27 | TRF_DIR = DATA_ROOT / 'TRFs' 28 | ERP_DIR = DATA_ROOT / 'ERPs' 29 | 30 | # Where to save the figure 31 | DST = DATA_ROOT / 'figures' 32 | DST.mkdir(exist_ok=True) 33 | 34 | # Configure the matplotlib figure style 35 | FONT = 'Arial' 36 | FONT_SIZE = 8 37 | RC = { 38 | 'figure.dpi': 100, 39 | 'savefig.dpi': 300, 40 | 'savefig.transparent': True, 41 | # Font 42 | 'font.family': 'sans-serif', 43 | 'font.sans-serif': FONT, 44 | 'font.size': FONT_SIZE, 45 | 'figure.labelsize': FONT_SIZE, 46 | 'figure.titlesize': FONT_SIZE, 47 | 'axes.labelsize': FONT_SIZE, 48 | 'axes.titlesize': FONT_SIZE, 49 | 'xtick.labelsize': FONT_SIZE, 50 | 'ytick.labelsize': FONT_SIZE, 51 | 'legend.fontsize': FONT_SIZE, 52 | } 53 | pyplot.rcParams.update(RC) 54 | # - 55 | 56 | # get all subjects 57 | subjects = [subject for subject in os.listdir(TRF_DIR) if subject.startswith('S') ] 58 | assert os.path.exists(ERP_DIR), "ERP directory is not found. Please, run script analysis/make_erps.py to create the different ERPs per subject." 59 | 60 | # Get the ERP response to a word onset 61 | cases = [] 62 | for subject in subjects: 63 | erp = eelbrain.load.unpickle(ERP_DIR / subject / f'{subject}_erp_word.pickle') 64 | cases.append([subject, erp]) 65 | # Use the same column names for ERPs and TRFs so that ERP and TRF datasets can be merged 66 | data_erp = eelbrain.Dataset.from_caselist(['subject', 'pattern'], cases) 67 | data_erp[:, 'type'] = 'ERP' 68 | 69 | # Get the TRF to word onsets when controlled for acoustic representations 70 | cases = [] 71 | for subject in subjects: 72 | mtrf = eelbrain.load.unpickle(TRF_DIR/ subject / f'{subject} acoustic+words.pickle') 73 | trf = mtrf.h[-1] 74 | cases.append([subject, trf]) 75 | data_trfs_controlled = eelbrain.Dataset.from_caselist(['subject', 'pattern'], cases) 76 | data_trfs_controlled[:, 'type'] = 'TRF' 77 | 78 | # Merge ERP and TRF data 79 | data = eelbrain.combine([data_erp, data_trfs_controlled], dim_intersection=True) 80 | 81 | 82 | # + 83 | # Normalize responses 84 | pattern_normalized = data['pattern'] - data['pattern'].mean('time') 85 | normalize_by = pattern_normalized.std('time') 86 | normalize_by[normalize_by == 0] = 1 # Avoid division by 0 87 | pattern_normalized /= normalize_by 88 | data['norm_pattern'] = pattern_normalized 89 | data['norm_pattern_Fz'] = data['norm_pattern'].sub(sensor='1') 90 | 91 | # Tests to compare ERP and TRF 92 | res_fz = eelbrain.testnd.TTestRelated('norm_pattern_Fz', 'type', match='subject', data=data, pmin=0.05) 93 | res_topo = eelbrain.testnd.TTestRelated('norm_pattern', 'type', match='subject', data=data, pmin=0.05) 94 | 95 | 96 | # + 97 | # Compose the figure 98 | figure = pyplot.figure(figsize=(7.5, 6.5)) 99 | gridspec = figure.add_gridspec(8,6, height_ratios=[2, 3, 3, 5, 2, 2, 2, 2], left=.10, right=.95, bottom=.02, top=.95, hspace=0.3) 100 | vmax = 2 101 | topo_args = dict(clip='circle', vmax=vmax) 102 | cbar_args = dict(ticks=3, label_rotation=90) 103 | 104 | axes = [ 105 | figure.add_subplot(gridspec[3, 0:2]), 106 | figure.add_subplot(gridspec[3, 2:4]), 107 | figure.add_subplot(gridspec[3, 4:6]), 108 | 109 | figure.add_subplot(gridspec[5, 0]), 110 | figure.add_subplot(gridspec[5, 1]), 111 | figure.add_subplot(gridspec[5, 2]), 112 | figure.add_subplot(gridspec[5, 3]), 113 | figure.add_subplot(gridspec[5, 4]), 114 | figure.add_subplot(gridspec[5, 5]), 115 | 116 | figure.add_subplot(gridspec[6, 0]), 117 | figure.add_subplot(gridspec[6, 1]), 118 | figure.add_subplot(gridspec[6, 2]), 119 | figure.add_subplot(gridspec[6, 3]), 120 | figure.add_subplot(gridspec[6, 4]), 121 | figure.add_subplot(gridspec[6, 5]), 122 | 123 | figure.add_subplot(gridspec[7, 0]), 124 | figure.add_subplot(gridspec[7, 1]), 125 | figure.add_subplot(gridspec[7, 2]), 126 | figure.add_subplot(gridspec[7, 3]), 127 | figure.add_subplot(gridspec[7, 4]), 128 | figure.add_subplot(gridspec[7, 5]), 129 | ] 130 | 131 | # A) Fz ERP/TRF plot 132 | c_axes = figure.add_subplot(gridspec[0:2, 1:5]) 133 | plot = eelbrain.plot.UTSStat('norm_pattern_Fz', 'type', data=data, axes=c_axes, frame='t', ylabel='Normalized pattern [a.u.]', legend=(.58, .88)) 134 | plot.set_clusters(res_fz.clusters, pmax=0.05, ptrend=None, color='.5', y=0, dy=0.1) 135 | # Sensor map 136 | c_axes = figure.add_subplot(gridspec[0, 4]) 137 | sensormap = eelbrain.plot.SensorMap(data['pattern'], labels='none', head_radius=0.45, axes=c_axes) 138 | sensormap.mark_sensors('1', c='r') 139 | 140 | # B) Array plots 141 | plot = eelbrain.plot.Array(res_topo, axes=axes[0:3], axtitle=['ERP', 'TRF','ERP - TRF'], vmax=vmax) 142 | # Times for topomaps 143 | times = [-0.050 ,0.000, 0.100, 0.270, 0.370, 0.800] 144 | # Add vertical lines on the times of the topographies 145 | for time in times: 146 | plot.add_vline(time, color='k', linestyle='--') 147 | 148 | # C) ERP/TRF topographies 149 | for type_idx, c_type in enumerate(['ERP', 'TRF']): 150 | topographies = [data[data['type'] == c_type, 'norm_pattern'].sub(time=time) for time in times] 151 | 152 | if type_idx == 0: 153 | c_axes = axes[3:9] 154 | axtitle = [f'{time*1000:g} ms' for time in times] 155 | else: 156 | c_axes = axes[9:15] 157 | axtitle = None 158 | topomaps = eelbrain.plot.Topomap(topographies, axes=c_axes, axtitle=axtitle, **topo_args) 159 | c_axes[0].text(-0.3, 0.5, c_type, ha='right') 160 | if type_idx == 1: 161 | topomaps.plot_colorbar(right_of=c_axes[-1], label='V (normalized)', **cbar_args) 162 | 163 | # Difference topographies 164 | c_axes = axes[15:21] 165 | topographies = [res_topo.masked_difference().sub(time=time) for time in times] 166 | topomaps = eelbrain.plot.Topomap(topographies, axes=c_axes, axtitle=None, **topo_args) 167 | c_axes[0].text(-0.3, 0.5, 'ERP - TRF', ha='right') 168 | 169 | # Panel labels 170 | figure.text(.01, .98, 'A) Comparison of ERP and TRF at single channel', size=10) 171 | figure.text(.01, .63, 'B) Comparison of ERP and TRF across channels', size=10) 172 | figure.text(.01, .33, 'C) Corresponding topographies', size=10) 173 | 174 | figure.savefig(DST / 'ERP-TRF.pdf') 175 | eelbrain.plot.figure_outline() 176 | -------------------------------------------------------------------------------- /figures/Convolution.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # # Convolution model figure 17 | 18 | # + 19 | from pathlib import Path 20 | 21 | import eelbrain 22 | from matplotlib import pyplot 23 | from matplotlib.patches import ConnectionPatch 24 | 25 | 26 | # Data locations 27 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 28 | STIMULUS_DIR = DATA_ROOT / 'stimuli' 29 | 30 | # Where to save the figure 31 | DST = DATA_ROOT / 'figures' 32 | DST.mkdir(exist_ok=True) 33 | 34 | # Configure the matplotlib figure style 35 | FONT = 'Arial' 36 | FONT_SIZE = 8 37 | RC = { 38 | 'figure.dpi': 100, 39 | 'savefig.dpi': 300, 40 | 'savefig.transparent': True, 41 | # Font 42 | 'font.family': 'sans-serif', 43 | 'font.sans-serif': FONT, 44 | 'font.size': FONT_SIZE, 45 | 'figure.labelsize': FONT_SIZE, 46 | 'figure.titlesize': FONT_SIZE, 47 | 'axes.labelsize': FONT_SIZE, 48 | 'axes.titlesize': FONT_SIZE, 49 | 'xtick.labelsize': FONT_SIZE, 50 | 'ytick.labelsize': FONT_SIZE, 51 | 'legend.fontsize': FONT_SIZE, 52 | } 53 | pyplot.rcParams.update(RC) 54 | 55 | # + 56 | trf_time = eelbrain.UTS(0, 0.010, 80) 57 | trf = eelbrain.gaussian(0.150, 0.040, trf_time) - eelbrain.gaussian(0.350, 0.100, trf_time) * .6 + eelbrain.gaussian(0.400, 0.200, trf_time) * .1 58 | trf_2 = eelbrain.gaussian(0.150, 0.050, trf_time) 59 | 60 | time = eelbrain.UTS(0, 0.010, 5*100) 61 | recording = eelbrain.NDVar.zeros(time) 62 | event_times = [0.2, 1.1, 2.0, 3.2, 4.1] 63 | impulse_times = [0.2, 1.1, 2.0, 3.2, 3.4, 3.7, 4.1, 4.2, 4.3] 64 | impulse_value = [1.0, 0.6, 0.4, 1.2, 0.9, 0.3, 1.1, 0.6, 1.1] 65 | for t in event_times: 66 | recording[t:t+0.800] += trf 67 | 68 | stimulus = eelbrain.NDVar.zeros(time) 69 | for t, v in zip(impulse_times, impulse_value): 70 | stimulus[t] = v 71 | 72 | wav = eelbrain.load.wav(STIMULUS_DIR / '1.wav') 73 | wav_envelope = wav.sub(time=(0, 5)).envelope() 74 | stimulus_envelope = eelbrain.resample(eelbrain.filter_data(wav_envelope, 0, 10), 100) 75 | stimulus_envelope *= 4e-5 76 | 77 | # + 78 | # initialize figure 79 | figure = pyplot.figure(figsize=(7.5, 6.5), facecolor='w') 80 | shape = (11, 8) 81 | ax_args = dict(frame_on=False) 82 | uts_args = dict(xlabel=False, yticklabels='none', ylabel=False, clip=False) 83 | 84 | def decorate(ax): 85 | ax.set_yticks(()) 86 | ax.set_xticks(()) 87 | ax.tick_params(bottom=False) 88 | ax.set_clip_on(False) 89 | 90 | # A) Average-based 91 | ax = pyplot.subplot2grid(shape, (0, 0), colspan=7, **ax_args) 92 | ax.set_title('A) Traditional average model: response at discrete time points', loc='left', size=10) 93 | eelbrain.plot.UTS(recording, axes=ax, **uts_args) 94 | # Boxes and arrows 95 | for t in event_times: 96 | box = pyplot.Rectangle((t, -1), 0.800, 2.2, ec='k', fill=False, alpha=0.5) 97 | ax.add_artist(box) 98 | ax.arrow(t+0.050, -1.8, 0, 1, color='b', head_width=0.05, head_length=0.5, clip_on=False) 99 | decorate(ax) 100 | ax.set_ylim(-1.1, 1.3) 101 | # Average 102 | ax = pyplot.subplot2grid(shape, (0, 7), **ax_args) 103 | eelbrain.plot.UTS(trf, axes=ax, **uts_args) 104 | ax.set_title('Average') 105 | decorate(ax) 106 | 107 | # B) TRF impulse 108 | ax_b1 = ax = pyplot.subplot2grid(shape, (2, 0), colspan=7, **ax_args) 109 | ax.set_title('B) TRF to discrete events: each impulse elicits a response', loc='left', size=10) 110 | eelbrain.plot.UTS(stimulus, axes=ax, colors='b', stem=True, **uts_args) 111 | decorate(ax) 112 | # TRF 113 | ax = pyplot.subplot2grid(shape, (2, 7), **ax_args) 114 | eelbrain.plot.UTS(trf, axes=ax, **uts_args) 115 | ax.set_title('TRF 1') 116 | decorate(ax) 117 | # TRF response 118 | response_impulse = eelbrain.convolve(trf, stimulus) 119 | ax_b2 = ax = pyplot.subplot2grid(shape, (3, 0), colspan=7, **ax_args) 120 | eelbrain.plot.UTS(response_impulse, axes=ax, **uts_args) 121 | decorate(ax) 122 | 123 | # C) TRF continuous 124 | ax_c1 = ax = pyplot.subplot2grid(shape, (5, 0), colspan=7, **ax_args) 125 | ax.set_title('C) TRF with a continuous predictor: each time point elicits a response', loc='left', size=10) 126 | plot = eelbrain.plot.UTS(stimulus_envelope, axes=ax, colors='b', **uts_args) 127 | decorate(ax) 128 | stimulus_handle = plot.plots[0].legend_handles['1.wav'] 129 | # TRF 130 | ax = pyplot.subplot2grid(shape, (5, 7), **ax_args) 131 | eelbrain.plot.UTS(trf, axes=ax, **uts_args) 132 | ax.set_title('TRF 2') 133 | decorate(ax) 134 | # TRF response 135 | response_continuous = eelbrain.convolve(trf, stimulus_envelope, name='response') 136 | ax_c2 = ax = pyplot.subplot2grid(shape, (6, 0), colspan=7, **ax_args) 137 | plot = eelbrain.plot.UTS(response_continuous, axes=ax, **uts_args) 138 | decorate(ax) 139 | response_handle = plot.plots[0].legend_handles['response'] 140 | 141 | # D) mTRF: continuous stimulus 142 | style = eelbrain.plot.Style('C1', linestyle='--') 143 | # Impulse predictor 144 | trf_2_response = eelbrain.convolve(trf_2, stimulus) 145 | ax = pyplot.subplot2grid(shape, (8, 0), colspan=7, **ax_args) 146 | ax.set_title('D) mTRF: simultaneous additive responses to multiple predictors', loc='left', size=10) 147 | eelbrain.plot.UTS(stimulus, axes=ax, colors='b', stem=True, **uts_args) 148 | eelbrain.plot.UTS(trf_2_response, axes=ax, colors=style, **uts_args) 149 | decorate(ax) 150 | # TRF 151 | ax = pyplot.subplot2grid(shape, (8, 7), **ax_args) 152 | ax.set_title('mTRF') 153 | eelbrain.plot.UTS(trf_2, axes=ax, **uts_args) 154 | decorate(ax) 155 | # Continuous predictor 156 | ax = pyplot.subplot2grid(shape, (9, 0), colspan=7, **ax_args) 157 | trf_1_response = eelbrain.convolve(trf, stimulus_envelope, name='response') 158 | eelbrain.plot.UTS(stimulus_envelope, axes=ax, colors='b', **uts_args) 159 | plot = eelbrain.plot.UTS(trf_1_response * .1, axes=ax, colors=style, **uts_args) 160 | decorate(ax) 161 | partial_response_handle = plot.plots[0].legend_handles['response * 0.1'] 162 | # TRF 163 | ax = pyplot.subplot2grid(shape, (9, 7), **ax_args) 164 | eelbrain.plot.UTS(trf, axes=ax, **uts_args) 165 | decorate(ax) 166 | # mTRF response 167 | mtrf_response = trf_1_response + trf_2_response 168 | ax_d3 = ax = pyplot.subplot2grid(shape, (10, 0), colspan=7, **ax_args) 169 | eelbrain.plot.UTS(mtrf_response, axes=ax, **uts_args) 170 | decorate(ax) 171 | 172 | #add legend 173 | handles = [stimulus_handle, response_handle, partial_response_handle] 174 | labels = ['Stimulus', 'Response', 'Partial response'] 175 | pyplot.figlegend(handles, labels, loc='lower right') 176 | 177 | pyplot.tight_layout(h_pad=0.5) 178 | ax_b2.set_ylim(0, 1) 179 | 180 | # Arrows 181 | arrow_args = dict(color='0.5', arrowstyle="->, head_width=0.25, head_length=0.5", linestyle=':') 182 | # B) 183 | for t in impulse_times: 184 | con = ConnectionPatch( 185 | xyA=(t, -0.2), coordsA=ax_b1.transData, 186 | xyB=(t, response_impulse[t] + 0.2), coordsB=ax_b2.transData, 187 | **arrow_args) 188 | ax_b1.add_artist(con) 189 | # C) 190 | t = 0.01 191 | con = ConnectionPatch( 192 | xyA=(t, stimulus_envelope[t] - 0.08), coordsA=ax_c1.transData, 193 | xyB=(t, response_continuous[t] + 0.5), coordsB=ax_c2.transData, 194 | **arrow_args) 195 | ax_c1.add_artist(con) 196 | ax_c1.text(t+0.04, -0.3, '...', ha='left', color='0.5', size=12) 197 | 198 | figure.savefig(DST / 'Convolution.pdf') 199 | eelbrain.plot.figure_outline() 200 | -------------------------------------------------------------------------------- /figures/Reference-strategy.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # # Prerequisites 17 | # 18 | # This scripts requires the files generated by: 19 | # - `analysis/estimate_trfs.pyy` 20 | # - `analysis/estimate_trfs_reference_strategy.py` 21 | # - `analysis/make_erps.py` 22 | 23 | # + 24 | import os 25 | from pathlib import Path 26 | 27 | import eelbrain 28 | from matplotlib import pyplot 29 | 30 | 31 | # Data locations 32 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 33 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 34 | STIMULI_DIR = DATA_ROOT / 'stimuli' 35 | TRF_DIR = DATA_ROOT / 'TRFs' 36 | EPOCH_DIR = DATA_ROOT / 'Epochs' 37 | 38 | # Where to save the figure 39 | DST = DATA_ROOT / 'figures' 40 | DST.mkdir(exist_ok=True) 41 | 42 | # Configure the matplotlib figure style 43 | FONT = 'Arial' 44 | FONT_SIZE = 8 45 | RC = { 46 | 'figure.dpi': 100, 47 | 'savefig.dpi': 300, 48 | 'savefig.transparent': True, 49 | # Font 50 | 'font.family': 'sans-serif', 51 | 'font.sans-serif': FONT, 52 | 'font.size': FONT_SIZE, 53 | 'figure.labelsize': FONT_SIZE, 54 | 'figure.titlesize': FONT_SIZE, 55 | 'axes.labelsize': FONT_SIZE, 56 | 'axes.titlesize': FONT_SIZE, 57 | 'xtick.labelsize': FONT_SIZE, 58 | 'ytick.labelsize': FONT_SIZE, 59 | 'legend.fontsize': FONT_SIZE, 60 | } 61 | pyplot.rcParams.update(RC) 62 | 63 | # Get all subjects 64 | subjects = [subject for subject in os.listdir(TRF_DIR) if subject.startswith('S')] 65 | # - 66 | 67 | # # Load the TRFs for the different reference strategies 68 | 69 | # + 70 | cases = [] 71 | for subject in subjects: 72 | for reference, name in zip(['mastoids', 'cz', 'average'], ['', '_cz','_average']): 73 | trf = eelbrain.load.unpickle(TRF_DIR / subject / f"{subject} envelope{name}.pickle") 74 | prediction_accuracy = trf.proportion_explained * 100 # to % 75 | cases.append([subject, trf.h[0], prediction_accuracy, reference]) 76 | 77 | column_names = ['subject', 'trf', 'prediction_accuracy','reference'] 78 | data_trfs = eelbrain.Dataset.from_caselist(column_names, cases, random='subject') 79 | # - 80 | 81 | # # Figure 82 | 83 | # + 84 | # Initialize figure 85 | figure = pyplot.figure(figsize=(7.5, 5)) 86 | gridspec = figure.add_gridspec(9, 9, left=0.01, right=0.95, hspace=1.5) 87 | topo_args = dict(clip='circle') 88 | det_args = dict(**topo_args, vmax=0.5, cmap='lux-gray') 89 | trf_vmax = 0.007 90 | reference_labels = { 91 | 'mastoids': 'mastoids', 92 | 'cz': 'Cz', 93 | 'average': 'average', 94 | } 95 | 96 | # A) Prediction accuracies 97 | for reference_idx, reference in enumerate(reference_labels): 98 | axes = figure.add_subplot(gridspec[reference_idx*3: reference_idx*3+3, 0:3]) 99 | p = eelbrain.plot.Topomap('prediction_accuracy', data=data_trfs[data_trfs['reference']==reference], axes=axes, **det_args) 100 | label = reference_labels[reference] 101 | axes.set_title(f"Referenced to {label}", loc='left', size=10) 102 | p.plot_colorbar(below=axes, label="% variability explained", clipmin=0, ticks=5, h=2) 103 | 104 | # B) TRFs 105 | times = [0.04, 0.14, 0.24] 106 | time_labels = ['%d ms' % (time*1000) for time in times] 107 | for reference_idx, reference in enumerate(reference_labels): 108 | axes = figure.add_subplot(gridspec[reference_idx*3: reference_idx*3+3, 3:9]) 109 | reference_index = data_trfs['reference'] == reference 110 | trf = data_trfs[reference_index, 'trf'] 111 | 112 | # Plot butterfly TRF 113 | kwargs = dict(vmin=-0.004, vmax=0.007, linewidth=0.5, color='#808080', ylabel='TRF weights [a.u.]', frame='t', yticklabels='none', xlim=(-0.050, 1.000), clip=True) 114 | if reference_idx != 2: 115 | kwargs.update(dict(xticklabels='none', xlabel='')) 116 | plot = eelbrain.plot.Butterfly(trf, axes=axes, **kwargs) 117 | for time in times: 118 | plot.add_vline(time, color='r', linestyle='--') 119 | 120 | # Plot topomaps 121 | axes = [ 122 | figure.add_subplot(gridspec[reference_idx*3: reference_idx*3+2, 6]), 123 | figure.add_subplot(gridspec[reference_idx*3: reference_idx*3+2, 7]), 124 | figure.add_subplot(gridspec[reference_idx*3: reference_idx*3+2, 8]), 125 | ] 126 | plot_topo = eelbrain.plot.Topomap([trf.sub(time=time) for time in times], axes=axes, axtitle=time_labels, ncol=len(times), vmax=trf_vmax, **topo_args) 127 | plot_topo.plot_colorbar(right_of=axes[-1], label='', label_rotation=90, ticks={trf_vmax:'+', -trf_vmax:'-', 0:'0'}) 128 | 129 | figure.text(0.01, 0.96, 'A) Predictive power', size=10) 130 | figure.text(0.30, 0.96, 'B) Envelope TRF', size=10) 131 | 132 | figure.savefig(DST / 'Reference-strategy.pdf') 133 | eelbrain.plot.figure_outline() 134 | -------------------------------------------------------------------------------- /figures/TRF-Basis.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # + 17 | from pathlib import Path 18 | 19 | import eelbrain 20 | import matplotlib 21 | from matplotlib import pyplot 22 | import numpy 23 | import re 24 | 25 | 26 | # Data locations 27 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 28 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 29 | EEG_DIR = DATA_ROOT / 'eeg' 30 | TRF_DIR = DATA_ROOT / 'TRFs' 31 | SUBJECTS = [path.name for path in EEG_DIR.iterdir() if re.match(r'S\d*', path.name)] 32 | 33 | # Where to save the figure 34 | DST = DATA_ROOT / 'figures' 35 | DST.mkdir(exist_ok=True) 36 | 37 | # Configure the matplotlib figure style 38 | FONT = 'Arial' 39 | FONT_SIZE = 8 40 | RC = { 41 | 'figure.dpi': 100, 42 | 'savefig.dpi': 300, 43 | 'savefig.transparent': True, 44 | # Font 45 | 'font.family': 'sans-serif', 46 | 'font.sans-serif': FONT, 47 | 'font.size': FONT_SIZE, 48 | 'figure.labelsize': FONT_SIZE, 49 | 'figure.titlesize': FONT_SIZE, 50 | 'axes.labelsize': FONT_SIZE, 51 | 'axes.titlesize': FONT_SIZE, 52 | 'xtick.labelsize': FONT_SIZE, 53 | 'ytick.labelsize': FONT_SIZE, 54 | 'legend.fontsize': FONT_SIZE, 55 | } 56 | pyplot.rcParams.update(RC) 57 | 58 | # + 59 | # Load cross-validated predictive power and TRFs of the different basis widths 60 | model = 'gammatone' 61 | basis_values = [0, 0.050, 0.100] 62 | 63 | datasets = {} 64 | for basis in basis_values: 65 | rows = [] 66 | for subject in SUBJECTS: 67 | trf_path = TRF_DIR / subject / f'{subject} {model} basis-{basis*1000:.0f}.pickle' 68 | trf = eelbrain.load.unpickle(trf_path) 69 | rows.append([subject, basis, trf.proportion_explained, *trf.h]) 70 | data = eelbrain.Dataset.from_caselist(['subject', 'basis', 'det', 'gammatone'], rows) 71 | data[:, 'basis_ms'] = int(basis * 1000) 72 | datasets[basis] = data 73 | # Combined dataset for explanatory power 74 | data = eelbrain.combine([data['subject', 'basis', 'basis_ms', 'det'] for data in datasets.values()]) 75 | # Average predictive power across sensors for easier comparison 76 | data['det_mean'] = data['det'].mean('sensor') * 100 77 | 78 | # Verify the Dataset 79 | data.head() 80 | # - 81 | 82 | # Plot predictive power by basis window 83 | p = eelbrain.plot.Barplot('det_mean', 'basis', match='subject', data=data, bottom=0.199, corr=False, h=3) 84 | 85 | # Plot the three TRFs 86 | for basis, data_basis in datasets.items(): 87 | p = eelbrain.plot.TopoButterfly("gammatone.sum('frequency')", data=data_basis, t=0.040, clip='circle') 88 | 89 | # Plot the sensor map to determine which sensor to use in the figure 90 | p = eelbrain.plot.SensorMap(datasets[0]['gammatone']) 91 | 92 | # + 93 | # Settings for the figure 94 | SENSOR = '19' 95 | 96 | # Extract TRF for SENSOR 97 | ys = [] 98 | for data_i in datasets.values(): 99 | y = data_i['gammatone'].sub(sensor=SENSOR, time=(-0.050, 0.400)).sum('frequency') 100 | ys.append(y) 101 | data['gammatone_sensor'] = eelbrain.combine(ys) 102 | 103 | # + 104 | # Figure 105 | LABELS = { 106 | '0': '0 (impulse basis)', 107 | '50': '50 ms basis', 108 | '100': '100 ms basis', 109 | } 110 | COLORS = eelbrain.plot.colors_for_oneway(LABELS, unambiguous=True) 111 | 112 | # Figure layout 113 | figure = pyplot.figure(figsize=(7.5, 3.5)) 114 | hs = [1, 1, 1] 115 | ws = [1, 1, 3] 116 | gridspec = figure.add_gridspec(len(hs), len(ws), top=0.92, bottom=0.15, left=0.11, right=0.99, hspace=0.4, wspace=0.2, height_ratios=hs, width_ratios=ws) 117 | # Plotting parameters for reusing 118 | topo_args = dict(clip='circle') 119 | array_args = dict(xlim=(-0.050, 1.0), axtitle=False) 120 | topo_array_args = dict(topo_labels='below', **array_args, **topo_args) 121 | det_args = dict(**topo_args, vmax=0.01, cmap='lux-a') 122 | cbar_args = dict(h=.5) 123 | t_envelope = [0.050, 0.100, 0.150, 0.400] 124 | t_onset = [0.060, 0.110, 0.180] 125 | 126 | # Predictive power comparison 127 | figure.text(0.01, 0.96, 'A) Predictive power', size=10) 128 | ax = figure.add_subplot(gridspec[:2, 0]) 129 | pos = ax.get_position() 130 | pos.y0 -= 0.1 131 | pos.y1 -= 0.1 132 | ax.set_position(pos) 133 | ax.yaxis.set_major_formatter(matplotlib.ticker.PercentFormatter(decimals=3, symbol='')) 134 | ax.yaxis.set_major_locator(matplotlib.ticker.MultipleLocator(0.005)) 135 | p = eelbrain.plot.Barplot('det_mean', 'basis_ms', match='subject', data=data, axes=ax, corr=False, ylabel='% variability explained', xlabel='Basis [ms]', frame=False, bottom=.195, top=0.205, colors=COLORS) 136 | 137 | # Sensor map 138 | figure.text(0.31, 0.96, 'B', size=10) 139 | ax = figure.add_subplot(gridspec[0, 1]) 140 | p = eelbrain.plot.SensorMap(datasets[0]['gammatone'], labels='none', axes=ax, mark=SENSOR, head_radius=0.45) 141 | 142 | # TRFs - individuals 143 | for i, subject in enumerate(['S04', 'S06']): 144 | figure.text(0.45, 0.96, 'C', size=10) 145 | ax = figure.add_subplot(gridspec[i, 2]) 146 | s_data = data.sub(f"subject == '{subject}'") 147 | legend = (0.80, 0.82) if i == 0 else False 148 | eelbrain.plot.UTSStat('gammatone_sensor*1000', 'basis_ms', error=False, data=s_data, axes=ax, frame='t', xlabel=False, xticklabels=False, ylabel=False, labels=LABELS, colors=COLORS, legend=legend) 149 | ax.set_title(f'Subject {subject}', loc='left') 150 | 151 | # Average TRF 152 | ax = figure.add_subplot(gridspec[2, 2], sharey=ax) 153 | eelbrain.plot.UTSStat('gammatone_sensor*1000', 'basis_ms', data=data, axes=ax, frame='t', legend=False, colors=COLORS, ylabel=r"V (normalized $\times 10^4$)") 154 | ax.set_title('All subjects', loc='left') 155 | 156 | # Add windows to TRF plot 157 | figure.text(0.85, 0.35, 'D', size=10) 158 | y0 = 5 159 | window_time = eelbrain.UTS(0.280, 0.010, 12) 160 | window = eelbrain.NDVar.zeros(window_time) 161 | window[0.330] += 10 162 | ax.plot(window.time, y0 + window.x, color=COLORS['0']) 163 | ax.plot(window.time, y0 + window.smooth('time', 0.050).x, color=COLORS['50']) 164 | ax.plot(window.time.times-0.005, y0 + window.smooth('time', 0.100).x, color=COLORS['100']) 165 | 166 | figure.savefig(DST / 'TRF-Basis.pdf') 167 | eelbrain.plot.figure_outline() 168 | -------------------------------------------------------------------------------- /figures/TRF.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # # Deconvolution example figure 17 | 18 | # + 19 | from pathlib import Path 20 | 21 | import eelbrain 22 | from matplotlib import pyplot 23 | from matplotlib.patches import ConnectionPatch 24 | import mne 25 | 26 | 27 | # Data locations 28 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 29 | STIMULUS_DIR = DATA_ROOT / 'stimuli' 30 | SUBJECT, SENSOR = 'S15', '19' 31 | STIMULUS = '1' 32 | 33 | # Where to save the figure 34 | DST = DATA_ROOT / 'figures' 35 | DST.mkdir(exist_ok=True) 36 | 37 | # Configure the matplotlib figure style 38 | FONT = 'Arial' 39 | FONT_SIZE = 8 40 | RC = { 41 | 'figure.dpi': 100, 42 | 'savefig.dpi': 300, 43 | 'savefig.transparent': True, 44 | # Font 45 | 'font.family': 'sans-serif', 46 | 'font.sans-serif': FONT, 47 | 'font.size': FONT_SIZE, 48 | 'figure.labelsize': FONT_SIZE, 49 | 'figure.titlesize': FONT_SIZE, 50 | 'axes.labelsize': FONT_SIZE, 51 | 'axes.titlesize': FONT_SIZE, 52 | 'xtick.labelsize': FONT_SIZE, 53 | 'ytick.labelsize': FONT_SIZE, 54 | 'legend.fontsize': FONT_SIZE, 55 | } 56 | pyplot.rcParams.update(RC) 57 | # - 58 | 59 | # ## Load data and estimate deconvolution 60 | 61 | # Load and pre-process the EEG data 62 | raw = mne.io.read_raw_fif(DATA_ROOT / 'eeg' / SUBJECT / f'{SUBJECT}_alice-raw.fif', preload=True) 63 | raw = raw.filter(1, 8, n_jobs=1) 64 | # Extract the events from the EEG data, and select the trial corresponding to the stimulus 65 | events = eelbrain.load.fiff.events(raw) 66 | # Define one second of silence to pad stimuli 67 | silence = eelbrain.NDVar.zeros(eelbrain.UTS(0, 1/100, 100)) 68 | # Load the stimuli coresponding to the events 69 | stimuli = [] 70 | envelopes = [] 71 | for stimulus in events['event']: 72 | wave = eelbrain.load.wav(STIMULUS_DIR / f'{stimulus}.wav') 73 | # Stimulus for plotting 74 | stimulus_wave = eelbrain.resample(wave, 2000) 75 | stimuli.append(stimulus_wave) 76 | # Envelope predictors 77 | envelope = wave.envelope() 78 | envelope = eelbrain.resample(envelope, 100).clip(0) 79 | envelope = eelbrain.concatenate([envelope, silence]) 80 | # Log transform to approximate auditory system response characteristics 81 | envelope = (envelope + 1).log() 82 | # Apply the same filter as for the EEG data 83 | envelope = eelbrain.filter_data(envelope, 1, 8) 84 | envelopes.append(envelope) 85 | events['stimulus'] = stimuli 86 | events['envelope'] = envelopes 87 | # Find the stimulus duration based on the envelopes 88 | durations = [envelope.time.tstop for envelope in envelopes] 89 | # Load the EEG data corresponding to this event/stimulus 90 | events['eeg'] = eelbrain.load.fiff.variable_length_epochs(events, tmin=0, tstop=durations, decim=5) 91 | 92 | events.summary() 93 | 94 | # Concatenate the first 11 trials for estimating the deconvolution 95 | eeg = eelbrain.concatenate(events[:11, 'eeg']) 96 | envelope = eelbrain.concatenate(events[:11, 'envelope']) 97 | # Estimate the TRFs (one for each EEG sensor) 98 | trf = eelbrain.boosting(eeg, envelope, -0.100, 0.500, basis=0.050, partitions=4) 99 | 100 | # Visualize the TRFs 101 | p = eelbrain.plot.TopoArray(trf.h, t=[0.040, 0.150, 0.380], clip='circle') 102 | 103 | # Predict response to the 12th stimulus 104 | envelope_12 = events[11, 'envelope'] 105 | eeg_12 = events[11, 'eeg'] 106 | eeg_12_predicted = eelbrain.convolve(trf.h_scaled, envelope_12) 107 | 108 | # Evaluate cross-validated predictions 109 | ss_total = eeg_12.abs().sum('time') 110 | ss_residual = (eeg_12 - eeg_12_predicted).abs().sum('time') 111 | proportion_explained_12 = 1 - (ss_residual / ss_total) 112 | # Plot correlation on estimation and testing data 113 | titles = [f'Training data\nMax explained = {trf.proportion_explained.max():.2%}$', f'Testing data\nMax explained = {proportion_explained_12.max():.2%}'] 114 | p = eelbrain.plot.Topomap([trf.proportion_explained, proportion_explained_12], sensorlabels='name', clip='circle', nrow=1, axtitle=titles) 115 | p_cb = p.plot_colorbar(width=.1, w=2) 116 | 117 | # # Figure 118 | 119 | # + 120 | # Initialize figure 121 | figure = pyplot.figure(figsize=(7.5, 4)) 122 | pyplot.subplots_adjust(left=.05, right=.99, hspace=.1, wspace=.1) 123 | ax_args = dict() 124 | uts_args = dict(yticklabels='none', clip=True, frame='none') 125 | continuous_args = dict(**uts_args, xlim=(11, 16)) 126 | 127 | # Define a function to format axes 128 | def decorate(ax): 129 | ax.set_yticks(()) 130 | ax.tick_params(bottom=False) 131 | ax.set_clip_on(False) 132 | 133 | # Function to normalize before plotting 134 | def normalize(y): 135 | return (y - y.mean()) / y.std() 136 | 137 | # Stimulus 138 | args = dict(color='k', **continuous_args) 139 | ax_sti1 = ax = pyplot.subplot2grid((4, 9), (3, 0), colspan=4, **ax_args) 140 | eelbrain.plot.UTS(events[0, 'stimulus'], axes=ax, **args, ylabel='Stimulus') 141 | decorate(ax) 142 | # held-out 143 | ax_sti2 = ax = pyplot.subplot2grid((4, 9), (3, 5), colspan=4, **ax_args) 144 | eelbrain.plot.UTS(events[11, 'stimulus'], axes=ax, **args, ylabel=False) 145 | decorate(ax) 146 | 147 | # Envelope 148 | args = dict(color='b', xlabel=False, xticklabels=False, **continuous_args) 149 | ax_env1 = ax = pyplot.subplot2grid((4, 9), (2, 0), colspan=4, **ax_args) 150 | eelbrain.plot.UTS(envelope, axes=ax, **args, ylabel='Predictor') 151 | decorate(ax) 152 | # held-out 153 | ax_env2 = ax = pyplot.subplot2grid((4, 9), (2, 5), colspan=4, **ax_args) 154 | eelbrain.plot.UTS(envelope_12, axes=ax, **args, ylabel=False) 155 | decorate(ax) 156 | 157 | # EEG 158 | args = dict(color='k', xlabel=False, xticklabels=False, **continuous_args) 159 | ax_eeg1 = ax = pyplot.subplot2grid((4, 9), (0, 0), colspan=4, **ax_args) 160 | ax.set_title("Training data", loc='left') 161 | eelbrain.plot.UTS(normalize(eeg.sub(sensor=SENSOR)), axes=ax, **args, ylabel=f'EEG-{SENSOR}') 162 | decorate(ax) 163 | # held-out 164 | ax_eeg2 = ax = pyplot.subplot2grid((4,9), (0, 5), colspan=4, sharey=ax, **ax_args) 165 | ax.set_title("Held-out testing data", loc='left') 166 | eelbrain.plot.UTS(normalize(eeg_12.sub(sensor=SENSOR)), axes=ax, **args, ylabel=False) 167 | args['color'] = eelbrain.plot.Style('red', linestyle='-') 168 | eelbrain.plot.UTS(normalize(eeg_12_predicted.sub(sensor=SENSOR)), axes=ax, **args, ylabel=False) 169 | decorate(ax) 170 | 171 | # TRF 172 | ax_trf = ax = pyplot.subplot2grid((4, 9), (1, 4), **ax_args) 173 | eelbrain.plot.UTS(trf.h.sub(sensor=SENSOR), axes=ax, **uts_args, color='purple', ylabel='TRF', xlabel='Lag $\\tau$ (ms)') 174 | decorate(ax) 175 | 176 | # Predictive power 177 | ax_power = ax = pyplot.subplot2grid((4, 9), (1, 7)) 178 | eelbrain.plot.Topomap(proportion_explained_12, axes=ax, clip='circle', cmap='lux-gray', mark=[SENSOR], mcolor='#009E73', msize=1) 179 | ax.set_title('Predictive power\n(% variability explained)') 180 | 181 | pyplot.tight_layout() 182 | 183 | # Arrows 184 | args = dict(color='red', arrowstyle="fancy, tail_width=0.2, head_width=0.5, head_length=0.5") 185 | con = ConnectionPatch( 186 | xyA=(0.5, 0), coordsA=ax_eeg1.transAxes, 187 | xyB=(-0.5, 0.5), coordsB=ax_trf.transAxes, 188 | connectionstyle='arc3,rad=.2', **args) 189 | ax_eeg1.add_artist(con) 190 | con = ConnectionPatch( 191 | xyA=(0.5, 1), coordsA=ax_env1.transAxes, 192 | xyB=(-0.6, 0.3), coordsB=ax_trf.transAxes, 193 | connectionstyle='arc3,rad=-.2', **args) 194 | ax_eeg1.add_artist(con) 195 | figure.text(-2, 0.4, 'B', transform=ax_trf.transAxes, size=10) 196 | con = ConnectionPatch( 197 | xyA=(1.2, 0.5), coordsA=ax_trf.transAxes, 198 | xyB=(0.2, 0), coordsB=ax_eeg2.transAxes, 199 | connectionstyle='arc3,rad=.3', **args) 200 | ax_eeg2.add_artist(con) 201 | con = ConnectionPatch( 202 | xyA=(0.22, 1), coordsA=ax_env2.transAxes, 203 | xyB=(0.22, -0.3), coordsB=ax_eeg2.transAxes, 204 | connectionstyle='arc3,rad=.0', **args) 205 | ax_eeg2.add_artist(con) 206 | figure.text(2.4, 1.2, 'C', transform=ax_trf.transAxes, size=10) 207 | # Prediction -> predictive power 208 | con = ConnectionPatch( 209 | xyA=(0.6, 0), coordsA=ax_eeg2.transAxes, 210 | xyB=(0.35, 1.9), coordsB=ax_power.transAxes, 211 | connectionstyle='arc3,rad=.0', **args) 212 | ax_eeg2.add_artist(con) 213 | figure.text(0.55, -0.4, 'D', transform=ax_eeg2.transAxes, size=10) 214 | # Stimulus -> predictor 215 | args['color'] = (.5, .5, .5) 216 | con = ConnectionPatch( 217 | xyA=(0.5, 1), coordsA=ax_sti1.transAxes, 218 | xyB=(0.5, -0.1), coordsB=ax_env1.transAxes, 219 | connectionstyle='arc3,rad=.0', **args) 220 | ax_env1.add_artist(con) 221 | figure.text(0.43, 1.4, 'A', transform=ax_sti1.transAxes, size=10) 222 | con = ConnectionPatch( 223 | xyA=(0.5, 1), coordsA=ax_sti2.transAxes, 224 | xyB=(0.5, -0.1), coordsB=ax_env2.transAxes, 225 | connectionstyle='arc3,rad=.0', **args) 226 | ax_env2.add_artist(con) 227 | figure.text(0.43, 1.4, 'A', transform=ax_sti2.transAxes, size=10) 228 | 229 | figure.savefig(DST / 'Deconvolution.pdf') 230 | eelbrain.plot.figure_outline() 231 | -------------------------------------------------------------------------------- /figures/Time-series.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.11.3 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # + 17 | from pathlib import Path 18 | 19 | import eelbrain 20 | from matplotlib import pyplot 21 | import mne 22 | 23 | 24 | # Data locations 25 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 26 | STIMULUS_DIR = DATA_ROOT / 'stimuli' 27 | SUBJECT = 'S01' 28 | STIMULUS = '1' 29 | # Crop the data for this demonstration 30 | TSTOP = 5.001 # Python indexing is exclusive of the specified stop sample 31 | 32 | # Where to save the figure 33 | DST = DATA_ROOT / 'figures' 34 | DST.mkdir(exist_ok=True) 35 | 36 | # Configure the matplotlib figure style 37 | FONT = 'Arial' 38 | FONT_SIZE = 8 39 | RC = { 40 | 'figure.dpi': 100, 41 | 'savefig.dpi': 300, 42 | 'savefig.transparent': True, 43 | # Font 44 | 'font.family': 'sans-serif', 45 | 'font.sans-serif': FONT, 46 | 'font.size': FONT_SIZE, 47 | } 48 | pyplot.rcParams.update(RC) 49 | # - 50 | 51 | # Create a raw object for the EEG data - does not yet load the data itself 52 | raw = mne.io.read_raw_fif(DATA_ROOT / 'eeg' / SUBJECT / f'{SUBJECT}_alice-raw.fif') 53 | # Extract only the events form the EEG data, and show the events table 54 | events = eelbrain.load.fiff.events(raw) 55 | events.head() 56 | 57 | # For this illustration, pick only the first stimulus 58 | events = events.sub(f"event == {STIMULUS!r}") 59 | # Load the EEG data corresponding to this event; load only the first 5 seconds of data here 60 | eeg = eelbrain.load.fiff.epochs(events, tmin=0, tstop=TSTOP) 61 | # Filter the data and resample it to 100 Hz 62 | eeg = eelbrain.filter_data(eeg, 1, 20) 63 | eeg = eelbrain.resample(eeg, 100) 64 | 65 | # + 66 | # Load the stimulus wave file 67 | wave = eelbrain.load.wav(STIMULUS_DIR / f'{STIMULUS}.wav') 68 | # Load the high-resolution gammatone spectrogram from disk (run predictors/make_gammatone.py to generate this file) 69 | gammatone = eelbrain.load.unpickle(STIMULUS_DIR / f'{STIMULUS}-gammatone.pickle') 70 | # Remove artifacts and apply a log transform to simulate compression in the auditory systems 71 | gammatone = gammatone.clip(0) 72 | gammatone = (gammatone + 1).log() 73 | 74 | # Crop the data 75 | wave = wave.sub(time=(0, TSTOP)) 76 | gammatone = gammatone.sub(time=(0, TSTOP)) 77 | # - 78 | 79 | # For discrete events, load the word table 80 | word_table = eelbrain.load.tsv(STIMULUS_DIR / 'AliceChapterOne-EEG.csv') 81 | # Restrict to the stimulus 82 | word_table = word_table.sub(f"Segment == {STIMULUS}") 83 | # As with the sounds, remove words that occured after TSTOP 84 | word_table = word_table.sub(f"onset < {TSTOP}") 85 | word_table.head() 86 | 87 | # + 88 | # Setup the figure layout 89 | fig, axes = pyplot.subplots(9, figsize=(7.5, 7), sharex=True, subplot_kw=dict(frame_on=False)) 90 | 91 | # Plot the EEG data 92 | eelbrain.plot.Butterfly(eeg, axes=axes[0], ylabel='EEG') 93 | 94 | # plot the sound wave; downsample it first to make plotting faster 95 | wave_rs = eelbrain.resample(wave, 1000) 96 | eelbrain.plot.UTS(wave_rs, colors='k', axes=axes[1], ylabel='Sound\nWave') 97 | 98 | # plot the full gammatone spectrogram 99 | eelbrain.plot.Array(gammatone, axes=axes[2], ylabel='Gammatone\nSpectrogram', vmax=25) 100 | 101 | # Generate an 8-band version of the spectrogram by averaging in frequency bins 102 | gammatone_8 = gammatone.bin(dim='frequency', nbins=8) 103 | # Resample it to match the EEG sampling rate 104 | gammatone_8 = eelbrain.resample(gammatone_8, 100) 105 | eelbrain.plot.Array(gammatone_8, axes=axes[3], ylabel='Gammatone\n8 Bands', interpolation='none', vmax=25) 106 | 107 | # Generate an envelope representation by summing across all frequency bands 108 | gammatone_envelope = gammatone.sum('frequency') 109 | gammatone_envelope = eelbrain.resample(gammatone_envelope, 100) 110 | eelbrain.plot.UTS(gammatone_envelope, colors='red', axes=axes[4], ylabel='Gammatone\nEnvelope') 111 | 112 | # Use an edge detector model to extract acoustic onsets from the gammatone spectrogram 113 | gammatone_onsets = eelbrain.edge_detector(gammatone, 30) 114 | eelbrain.plot.Array(gammatone_onsets, axes=axes[5], ylabel='Acoustic\nOnsets', interpolation='none') 115 | 116 | # Generate an impulse at every word onset. Use a time dimension with the same properties as the EEG data. 117 | eeg_time = eelbrain.UTS(0, 1/100, 501) 118 | words = eelbrain.NDVar.zeros(eeg_time) 119 | for time in word_table['onset']: 120 | words[time] = 1 121 | eelbrain.plot.UTS(words, stem=True, top=1.5, bottom=-0.5, colors='blue', axes=axes[6], ylabel='Words') 122 | 123 | # For illustration, add the words to the plot 124 | for time, word in word_table.zip('onset', 'Word'): 125 | axes[6].text(time, -0.1, word, va='top', fontsize=8) 126 | 127 | # Generate an impulse at every word onset, scaled by a variable 128 | ngram_surprisal = eelbrain.NDVar.zeros(eeg_time) 129 | for time, value in word_table.zip('onset', 'NGRAM'): 130 | ngram_surprisal[time] = value 131 | eelbrain.plot.UTS(ngram_surprisal, stem=True, colors='blue', axes=axes[7], ylabel='N-Gram') 132 | 133 | # Generate an alternative impulse, only at content word onsets, and scaled by a different variable 134 | cfg_surprisal = eelbrain.NDVar.zeros(eeg_time) 135 | for time, value, is_lexical in word_table.zip('onset', 'NGRAM', 'IsLexical'): 136 | if is_lexical: 137 | cfg_surprisal[time] = value 138 | eelbrain.plot.UTS(cfg_surprisal, stem=True, colors='blue', axes=axes[8], ylabel='N-Gram\nLexical') 139 | 140 | # Fine-tune layout 141 | LAST = 8 142 | for i, ax in enumerate(axes): 143 | if i == LAST: 144 | pass 145 | else: 146 | ax.set_xlabel(None) 147 | ax.set_yticks(()) 148 | ax.tick_params(bottom=False) 149 | 150 | fig.tight_layout() 151 | fig.savefig(DST / 'Time series.pdf') 152 | eelbrain.plot.figure_outline() 153 | -------------------------------------------------------------------------------- /figures/Word-class-acoustics.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.14.5 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # + 17 | from pathlib import Path 18 | 19 | import eelbrain 20 | import matplotlib 21 | from matplotlib import pyplot 22 | import re 23 | 24 | 25 | # Data locations 26 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 27 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 28 | EEG_DIR = DATA_ROOT / 'eeg' 29 | TRF_DIR = DATA_ROOT / 'TRFs' 30 | SUBJECTS = [path.name for path in EEG_DIR.iterdir() if re.match(r'S\d*', path.name)] 31 | 32 | # Where to save the figure 33 | DST = DATA_ROOT / 'figures' 34 | DST.mkdir(exist_ok=True) 35 | 36 | # Configure the matplotlib figure style 37 | FONT = 'Arial' 38 | FONT_SIZE = 8 39 | RC = { 40 | 'figure.dpi': 100, 41 | 'savefig.dpi': 300, 42 | 'savefig.transparent': True, 43 | # Font 44 | 'font.family': 'sans-serif', 45 | 'font.sans-serif': FONT, 46 | 'font.size': FONT_SIZE, 47 | 'figure.labelsize': FONT_SIZE, 48 | 'figure.titlesize': FONT_SIZE, 49 | 'axes.labelsize': FONT_SIZE, 50 | 'axes.titlesize': FONT_SIZE, 51 | 'xtick.labelsize': FONT_SIZE, 52 | 'ytick.labelsize': FONT_SIZE, 53 | 'legend.fontsize': FONT_SIZE, 54 | } 55 | matplotlib.rcParams.update(RC) 56 | # - 57 | 58 | # # Do brain responses differ between word class? 59 | # Test whether adding predcitors that distinguish function and content words improves the predictive power of the TRF models. 60 | 61 | # Load predictive power of all models 62 | models = ['words', 'words+lexical', 'acoustic+words', 'acoustic+words+lexical'] 63 | rows = [] 64 | for model in models: 65 | for subject in SUBJECTS: 66 | trf = eelbrain.load.unpickle(TRF_DIR / subject / f'{subject} {model}.pickle') 67 | rows.append([subject, model, trf.proportion_explained]) 68 | model_data = eelbrain.Dataset.from_caselist(['subject', 'model', 'det'], rows) 69 | 70 | lexical_model_test = eelbrain.testnd.TTestRelated('det', 'model', 'words+lexical', 'words', match='subject', data=model_data, tail=1, pmin=0.05) 71 | 72 | p = eelbrain.plot.Topomap(lexical_model_test, ncol=3, title=lexical_model_test, axh=1, clip='circle') 73 | 74 | # ## How do the responses differ? 75 | # Compare the TRFs corresponding to content and function words. 76 | 77 | # Load the TRFs: 78 | # Keep `h_scaled` instead of `h` so that we can compare and add TRFs to different predictors 79 | # Because each predictor gets normalized for estimation, the scale of the TRFs in `h` are all different 80 | # The `h_scaled` attribute reverses that normalization, so that the TRFs are all in a common scale 81 | rows = [] 82 | for subject in SUBJECTS: 83 | trf = eelbrain.load.unpickle(TRF_DIR / subject / f'{subject} words+lexical.pickle') 84 | rows.append([subject, model, *trf.h_scaled]) 85 | trfs = eelbrain.Dataset.from_caselist(['subject', 'model', *trf.x], rows) 86 | 87 | # Each word has an impulse of the general word predictor, as well as one form the word-class specific predictor 88 | # Accordingly, each word's response consists of the general word TRF and the word-class specific TRF 89 | # To reconstruct the responses to the two kinds of words, we thus want to add the general word TRF and the word-class specific TRF: 90 | word_difference = eelbrain.testnd.TTestRelated('non_lexical + word', 'lexical + word', data=trfs, pmin=0.05) 91 | 92 | p = eelbrain.plot.TopoArray(word_difference, t=[0.100, 0.220, 0.400], clip='circle', h=2, topo_labels='below') 93 | 94 | # ## When controlling for auditory responses? 95 | # Do the same test, but include predictors controlling for responses to acoustic features in both models 96 | 97 | lexical_acoustic_model_test = eelbrain.testnd.TTestRelated('det', 'model', 'acoustic+words+lexical', 'acoustic+words', match='subject', data=model_data, tail=1, pmin=0.05) 98 | print(lexical_acoustic_model_test) 99 | 100 | p = eelbrain.plot.Topomap(lexical_acoustic_model_test, ncol=3, title=lexical_acoustic_model_test, clip='circle', h=1.8) 101 | 102 | # ## Acoustic responses? 103 | # Do acoustic predictors have predictive power in the area that's affected? 104 | 105 | acoustic_model_test = eelbrain.testnd.TTestRelated('det', 'model', 'acoustic+words', 'words', match='subject', data=model_data, tail=1, pmin=0.05) 106 | p = eelbrain.plot.Topomap(acoustic_model_test, ncol=3, title=acoustic_model_test, clip='circle', h=1.8) 107 | 108 | # # Analyze spectrogram by word class 109 | # If auditory responses can explain the difference in response to function and content words, then that suggests that acoustic properties differ between function and content words. We can analyze this directly with TRFs. 110 | # 111 | # NOTE: This requires `analysis/estimate_word_acoustics.py` to be run first, otherwise the next cell will cause a `FileNotFoundError`. 112 | 113 | trf_word = eelbrain.load.unpickle(TRF_DIR / 'gammatone~word.pickle') 114 | trf_lexical = eelbrain.load.unpickle(TRF_DIR / 'gammatone~word+lexical.pickle') 115 | 116 | # Test whether information about the lexical status of the words improves prediction of the acoustic signal. 117 | 118 | data_word = trf_word.partition_result_data() 119 | data_lexical = trf_lexical.partition_result_data() 120 | 121 | # Test and plot predictive power difference 122 | res = eelbrain.testnd.TTestRelated(data_lexical['det'], data_word['det'], tail=1) 123 | data_word[:, 'model'] = 'word' 124 | data_lexical[:, 'model'] = 'word+lexical' 125 | data = eelbrain.combine([data_word, data_lexical], incomplete='drop') 126 | p = eelbrain.plot.UTSStat('det', 'model', match='i_test', data=data, title=res, h=2) 127 | 128 | # For a univariate test, average across frequency 129 | eelbrain.test.TTestRelated("det.mean('frequency')", 'model', match='i_test', data=data) 130 | 131 | # Compare TRFs 132 | word_acoustics_difference = eelbrain.testnd.TTestRelated('word + non_lexical', 'word + lexical', data=data_lexical) 133 | p = eelbrain.plot.Array(word_acoustics_difference, ncol=3, h=2) 134 | 135 | # # Generate figure 136 | 137 | # + 138 | # Initialize figure 139 | figure = pyplot.figure(figsize=(7.5, 5)) 140 | gridspec = figure.add_gridspec(4, 9, height_ratios=[2,2,2,2], left=0.05, right=0.95, hspace=0.3, bottom=0.09) 141 | topo_args = dict(clip='circle') 142 | det_args = dict(**topo_args, vmax=0.001, cmap='lux-gray') 143 | cbar_args = dict(label='∆ % variability\nexplained', unit=1e-2, ticks=3, h=.5) 144 | 145 | # Add predictive power tests 146 | axes = figure.add_subplot(gridspec[0,0]) 147 | p = eelbrain.plot.Topomap(lexical_model_test.masked_difference(), axes=axes, **det_args) 148 | axes.set_title("Word class\nwithout acoustics", loc='left') 149 | p.plot_colorbar(right_of=axes, **cbar_args) 150 | 151 | axes = figure.add_subplot(gridspec[1,0]) 152 | p = eelbrain.plot.Topomap(lexical_acoustic_model_test.masked_difference(), axes=axes, **det_args) 153 | axes.set_title("Word class\ncontrolling for acoustics", loc='left') 154 | p.plot_colorbar(right_of=axes, **cbar_args) 155 | 156 | det_args['vmax'] = 0.01 157 | axes = figure.add_subplot(gridspec[2,0]) 158 | p = eelbrain.plot.Topomap(acoustic_model_test.masked_difference(), axes=axes, **det_args) 159 | axes.set_title("Acoustics", loc='left') 160 | p.plot_colorbar(right_of=axes, **cbar_args) 161 | 162 | # Add TRFs 163 | axes = [ 164 | figure.add_subplot(gridspec[0,3:5]), 165 | figure.add_subplot(gridspec[1,3]), 166 | figure.add_subplot(gridspec[1,4]), 167 | figure.add_subplot(gridspec[0,5:7]), 168 | figure.add_subplot(gridspec[1,5]), 169 | figure.add_subplot(gridspec[1,6]), 170 | figure.add_subplot(gridspec[0,7:9]), 171 | figure.add_subplot(gridspec[1,7]), 172 | figure.add_subplot(gridspec[1,8]), 173 | ] 174 | p = eelbrain.plot.TopoArray(word_difference, t=[0.120, 0.220], axes=axes, axtitle=False, **topo_args, xlim=(-0.050, 1.00), topo_labels='below') 175 | axes[0].set_title('Function words', loc='left') 176 | axes[3].set_title('Content words', loc='left') 177 | axes[6].set_title('Function > Content', loc='left') 178 | p.plot_colorbar(left_of=axes[1], ticks=3) 179 | 180 | # Add acoustic patterns 181 | axes = [ 182 | figure.add_subplot(gridspec[3,3:5]), 183 | figure.add_subplot(gridspec[3,5:7]), 184 | figure.add_subplot(gridspec[3,7:9]), 185 | ] 186 | plots = [word_acoustics_difference.c1_mean, word_acoustics_difference.c0_mean, word_acoustics_difference.difference] 187 | p = eelbrain.plot.Array(plots, axes=axes, axtitle=False) 188 | axes[0].set_title('Function words', loc='left') 189 | axes[1].set_title('Content words', loc='left') 190 | axes[2].set_title('Function > Content', loc='left') 191 | # Add a line to highlight difference 192 | for ax in axes: 193 | ax.axvline(0.070, color='k', alpha=0.5, linestyle=':') 194 | 195 | figure.text(0.01, 0.96, 'A) Predictive power', size=10) 196 | figure.text(0.27, 0.96, 'B) Word class TRFs (without acoustics)', size=10) 197 | figure.text(0.27, 0.34, 'C) Spectrogram by word class', size=10) 198 | 199 | figure.savefig(DST / 'Word-class-acoustics.pdf') 200 | eelbrain.plot.figure_outline() 201 | -------------------------------------------------------------------------------- /import_dataset/check-triggers.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.4.2 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # # Check alignments 17 | # Check alignemnts of stimuli with the EEG data. The EEG recording contains a record of the acoustic stimulus, which can be compare with the stimulus itself. This loads the events through the pipeline in `alice.py`, i.e. the trigger correction is already applied and all subjects should have the correct alignment. 18 | 19 | # + 20 | # %matplotlib inline 21 | from eelbrain import * 22 | 23 | from alice import alice 24 | 25 | 26 | # load the acoustic envelope predictor for each stimulus 27 | gt = {f'{i}': alice.load_predictor(f'{i}~gammatone-1', 0.002, 1000, name='WAV') for i in range(1, 13)} 28 | for y in gt.values(): 29 | y /= y.std() 30 | # - 31 | 32 | for subject in alice: 33 | events = alice.load_events(raw='raw', data_raw=True) 34 | raw = events.info['raw'] 35 | raw.load_data() 36 | # S16, S22 have broken AUX channels 37 | if subject in ['S05', 'S38']: 38 | continue # no AUD channel 39 | for name in ['AUD', 'Aux5']: 40 | if name in raw.ch_names: 41 | ch = raw.ch_names.index(name) 42 | break 43 | else: 44 | print(subject, raw.ch_names) 45 | raise 46 | xs = [] 47 | # extract audio from EEG 48 | for segment, i0 in events.zip('event', 'i_start'): 49 | x = NDVar(raw._data[ch, i0:i0+1000], UTS(0, 0.002, 1000), name='EEG') 50 | x -= x.min() 51 | x /= x.std() 52 | xs.append([x, gt[segment]]) 53 | p = plot.UTS(xs, axh=2, w=10, ncol=1, title=subject, axtitle=events['trigger']) 54 | # display and close to avoid having too many open figures 55 | display(p) 56 | p.close() 57 | 58 | -------------------------------------------------------------------------------- /import_dataset/convert-all.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # --- 3 | # jupyter: 4 | # jupytext: 5 | # formats: ipynb,py:light 6 | # text_representation: 7 | # extension: .py 8 | # format_name: light 9 | # format_version: '1.5' 10 | # jupytext_version: 1.4.2 11 | # kernelspec: 12 | # display_name: Python 3 13 | # language: python 14 | # name: python3 15 | # --- 16 | 17 | # # Convert dataset 18 | # This notebook converts all EEG files from the Alice dataset to `*.fif` files. It assumes that the dataset is downloaded at the location `SRC` specified in the first cell. The converted data will be saved at the location specified in the `DST` variable. 19 | 20 | # + 21 | # %matplotlib inline 22 | # Basic imports 23 | from pathlib import Path 24 | import shutil 25 | import warnings 26 | 27 | import eelbrain 28 | import mne 29 | from mne.externals.pymatreader import read_mat 30 | import numpy 31 | from scipy.linalg import pinv 32 | 33 | # Location of the Alice dataset 34 | SRC = Path('/Volumes/GoogleDrive/Shared drives/WS2020/alice-dataset') 35 | DST = Path('~').expanduser() / 'Data' / 'Alice' 36 | # - 37 | 38 | # ## Copy stimuli 39 | 40 | DST_STIMULI = DST / 'stimuli' 41 | DST_STIMULI.mkdir(exist_ok=True) 42 | DST_CSV = DST_STIMULI / 'AliceChapterOne-EEG.csv' 43 | if not DST_CSV.exists(): 44 | shutil.copy(SRC / 'AliceChapterOne-EEG.csv', DST_STIMULI) 45 | for segment in range(1, 13): 46 | src_file = SRC / 'audio' / f'DownTheRabbitHoleFinal_SoundFile{segment}.wav' 47 | dst_file = DST_STIMULI / f'{segment}.wav' 48 | if not dst_file.exists(): 49 | shutil.copy(src_file, dst_file) 50 | 51 | # ## Import sensor map 52 | 53 | # + 54 | ch_default = { 55 | 'scanno': 307, 56 | 'logno': 1, 57 | 'kind': 3, 58 | 'range': 1.0, 59 | 'cal': 1.0, 60 | 'coil_type': 0, 61 | 'loc': numpy.array([0., 0., 0., 1., 0., 0., 0., 1., 0., 0., 0., 1.]), 62 | 'unit': 107, 63 | 'unit_mul': 0, 64 | 'coord_frame': 0, 65 | } 66 | 67 | samplingrate = 500 68 | montage = mne.channels.read_custom_montage('easycapM10-acti61_elec.sfp') 69 | montage.plot() 70 | info = mne.create_info(montage.ch_names, samplingrate, 'eeg') 71 | info.set_montage(montage) 72 | info['highpass'] = 0.1 73 | info['lowpass'] = 200 74 | for ch_name in ['VEOG', 'Aux5', 'AUD']: 75 | info['chs'].append({**ch_default, 'ch_name': ch_name}) 76 | info['ch_names'].append(ch_name) 77 | info['nchan'] += 1 78 | # - 79 | 80 | # ## Find subject data 81 | 82 | # + 83 | # find subjects 84 | datasets = read_mat(SRC / 'datasets.mat') 85 | subjects = [s[:3] for s in datasets['use']] 86 | 87 | # find audio start locations 88 | word_table = eelbrain.load.tsv(SRC / 'AliceChapterOne-EEG.csv') 89 | segment_start = {} # segment start relative to first word 90 | for segment in range(1, 13): 91 | first_word = word_table['Segment'].index(segment)[0] 92 | segment_start[segment] = -word_table[first_word, 'onset'] 93 | # - 94 | 95 | # ## Convert EEG recordings 96 | # For subjects S26, S34, S35 and S36, the first event is missing, and labels are shifted by 1. 97 | 98 | # + 99 | warnings.filterwarnings('ignore', category=RuntimeWarning) 100 | for subject in subjects: 101 | dst_dir = DST / 'eeg' / subject 102 | dst_dir.mkdir(exist_ok=True, parents=True) 103 | dst = dst_dir / f'{subject}_alice-raw.fif' 104 | if dst.exists(): 105 | continue 106 | 107 | proc = read_mat(SRC / 'proc' / f'{subject}.mat')['proc'] 108 | assert proc['subject'] == subject 109 | raw = mne.io.read_raw_fieldtrip(SRC / f'{subject}.mat', info, 'raw') 110 | raw._data *= 1e-6 # FieldTrip data in µV 111 | 112 | # reference 113 | assert proc['implicitref'] == '29' 114 | assert proc['refchannels'] == ['25', '29'] 115 | mne.add_reference_channels(raw, '29', False) 116 | raw.set_montage(montage) 117 | # raw.plot_sensors(show_names=True) 118 | raw.set_eeg_reference(['25', '29']) 119 | 120 | # events 121 | assert proc['varnames'] == ['segment', 'tmin', 'Order'] 122 | data = proc['trl'] 123 | proc_table = eelbrain.Dataset({ 124 | 'istart': data[:, 0], 125 | 'istop': data[:, 1], 126 | 'bl': data[:, 2], 127 | 'segment': data[:, 4 if subject == 'S02' else 3], 128 | 'tstart': data[:, 5 if subject == 'S02' else 4], 129 | }) 130 | # fix events for subjects missing trial 1 131 | if subject in ('S26', 'S34', 'S35', 'S36'): 132 | proc_table['segment'] += 1 133 | # collect stimulus onset times 134 | onsets = [] 135 | segments = [] 136 | for segment in range(1, 13): 137 | if segment not in proc_table['segment']: 138 | continue 139 | segments.append(segment) 140 | first_word = proc_table['segment'].index(segment)[0] 141 | tstart = proc_table[first_word, 'istart'] / samplingrate - proc_table[first_word, 'tstart'] - proc_table[first_word, 'bl'] / samplingrate 142 | onsets.append(tstart) 143 | events = mne.Annotations(onsets, [0.1] * len(onsets), segments) 144 | raw.set_annotations(events) 145 | 146 | # ICA 147 | mixing = pinv(proc['ica']['unmixing']) 148 | index = [raw.ch_names.index(ch) for ch in proc['ica']['topolabel']] 149 | sources = proc['ica']['unmixing'].dot(raw._data[index]) 150 | reject = [int(i) - 1 for i in proc['ica']['rejcomp']] 151 | sources[reject] = 0 152 | raw._data[index] = mixing.dot(sources) 153 | 154 | # Bad channels 155 | raw.info['bads'] = proc['rejections']['badchans'] 156 | # Add a bad channel that was missed 157 | if subject == 'S44': 158 | raw.info['bads'].append('27') 159 | elif subject in ('S11', 'S25'): 160 | raw.info['bads'].append('29') 161 | 162 | # Save 163 | raw.save(dst) 164 | -------------------------------------------------------------------------------- /import_dataset/easycapM10-acti61_elec.sfp: -------------------------------------------------------------------------------- 1 | 1 0.000 3.517 8.285 2 | 2 3.045 1.758 8.285 3 | 3 3.045 -1.758 8.285 4 | 4 0.000 -3.517 8.285 5 | 5 -3.045 -1.758 8.285 6 | 6 -3.045 1.758 8.285 7 | 7 0.000 8.345 3.371 8 | 8 3.394 7.623 3.371 9 | 9 6.201 5.584 3.371 10 | 10 7.936 2.579 3.371 11 | 11 8.299 -0.872 3.371 12 | 12 7.227 -4.172 3.371 13 | 13 4.905 -6.751 3.371 14 | 14 1.735 -8.162 3.371 15 | 15 -1.735 -8.162 3.371 16 | 16 -4.905 -6.751 3.371 17 | 17 -7.227 -4.172 3.371 18 | 18 -8.299 -0.872 3.371 19 | 19 -7.936 2.579 3.371 20 | 20 -6.201 5.584 3.371 21 | 21 -3.394 7.623 3.371 22 | 22 6.786 4.752 -3.517 23 | 23 8.159 1.439 -3.517 24 | 24 8.002 -2.144 -3.517 25 | 25 6.346 -5.325 -3.517 26 | 26 3.501 -7.508 -3.517 27 | 27 0.000 -8.285 -3.517 28 | 28 -3.501 -7.508 -3.517 29 | 29 -6.346 -5.325 -3.517 30 | 30 -8.002 -2.144 -3.517 31 | 31 -8.159 1.439 -3.517 32 | 32 -6.786 4.752 -3.517 33 | 33 0.000 0.000 9.000 34 | 34 0.000 6.364 6.364 35 | 35 2.588 5.814 6.364 36 | 36 5.337 3.466 6.364 37 | 37 6.364 0.000 6.364 38 | 38 5.337 -3.466 6.364 39 | 39 2.588 -5.814 6.364 40 | 40 0.000 -6.364 6.364 41 | 41 -2.588 -5.814 6.364 42 | 42 -5.337 -3.466 6.364 43 | 43 -6.364 0.000 6.364 44 | 44 -5.337 3.466 6.364 45 | 45 -2.588 5.814 6.364 46 | 46 0.000 9.000 0.000 47 | 47 3.371 8.345 0.000 48 | 48 6.364 6.364 0.000 49 | 49 8.345 3.371 0.000 50 | 50 9.000 0.000 0.000 51 | 51 8.345 -3.371 0.000 52 | 52 6.364 -6.364 0.000 53 | 53 3.371 -8.345 0.000 54 | 54 0.000 -9.000 0.000 55 | 55 -3.371 -8.345 0.000 56 | 56 -6.364 -6.364 0.000 57 | 57 -8.345 -3.371 0.000 58 | 58 -9.000 0.000 0.000 59 | 59 -8.345 3.371 0.000 60 | 60 -6.364 6.364 0.000 61 | 61 -3.371 8.345 0.000 -------------------------------------------------------------------------------- /pipeline/Auditory-TRFs.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.16.0 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # # Setup 17 | # Import the pipeline and analysis parameters. On every import, the pipeline checks the TRFExperiment definition in `alice.py` for changes, and deletes cached files that have become outdated. 18 | 19 | # + 20 | from eelbrain import * 21 | from alice import PARAMETERS, alice 22 | 23 | 24 | # Define parameters commonly used for tests 25 | TEST = { 26 | 'pmin': 0.05, # for threshold cluster based permutation tests 27 | 'metric': 'det', # Test model difference in % explained variability 28 | } 29 | # - 30 | 31 | # # Model comparisons 32 | # Model comparisons can be directly visualized using the `TRFExperiment.show_model_test` method. For an explanation of the model syntax see the [TRFExperiment documentation](https://trf-tools.readthedocs.io/doc/pipeline.html#models). 33 | # 34 | # Calling this function assumes that TRFs have already been estimated (see the `jobs.py` file in the same directory). Alternatively, to estimate (and cache) the TRFs on the fly, add another parameter: `make=True` (note that this may take a while). 35 | 36 | alice.show_model_test('gammatone-1 +@ gammatone-on-1', **PARAMETERS, **TEST) 37 | 38 | # Multiple model comparisons can be specified in a dictionary, where keys are names that are used in the table and plot. Use this to simultaneously show all comparisons from the figure: 39 | 40 | alice.show_model_test({ 41 | 'Envelope only': 'gammatone-1 > 0', 42 | 'Add Onsets': 'gammatone-1 +@ gammatone-on-1', 43 | 'STRF > TRF': 'gammatone-8 + gammatone-on-8 > gammatone-1 + gammatone-on-1', 44 | 'STRF onset': 'auditory-gammatone @ gammatone-on-8', 45 | }, **PARAMETERS, **TEST) 46 | 47 | # # TRFs 48 | # Because models usually contain more than one term, the results are returned in a `ResultCollection` dictionary 49 | 50 | trfs = alice.load_trf_test('gammatone-1', **PARAMETERS, pmin=0.05, make=True) 51 | trfs 52 | 53 | p = plot.TopoArray(trfs['gammatone-1'], t=[0.050, 0.100, 0.150, 0.400], clip='circle') 54 | _ = p.plot_colorbar() 55 | 56 | # Access information about the clusters 57 | trfs['gammatone-1'].clusters 58 | 59 | # ## mTRFs 60 | # The same procedure applys for models with multiple TRFs. 61 | 62 | trfs = alice.load_trf_test('gammatone-1 + gammatone-on-1', **PARAMETERS, pmin=0.05, make=True) 63 | 64 | trfs 65 | 66 | # TRFs can be plotted together (although topography times will be the same) ... 67 | 68 | p = plot.TopoArray(trfs, t=[0.050, 0.100, 0.150, 0.400], clip='circle') 69 | vmin, vmax = p.get_vlim() # store colormap values for next plot 70 | _ = p.plot_colorbar() 71 | 72 | # ... or individually 73 | 74 | p = plot.TopoArray(trfs['gammatone-on-1'], t=[0.060, 0.110, 0.180, None], clip='circle', vmax=vmax) 75 | _ = p.plot_colorbar() 76 | 77 | # ## Accessing single subject TRFs 78 | # For more specific TRF analyses, the TRFs can be loaded as a `Dataset`. Note that the TRFs are listed as `NDVar`s at the bottom of the table, and that names have been normalize to valid Python variable names (i.e., `-` has been replaced with `_`): 79 | 80 | data = alice.load_trfs(-1, 'gammatone-1 + gammatone-on-1', **PARAMETERS) 81 | data.head() 82 | 83 | data['gammatone_1'] 84 | -------------------------------------------------------------------------------- /pipeline/README.md: -------------------------------------------------------------------------------- 1 | # TRF-Experiment Pipeline 2 | 3 | This folder demonstrates how to use the experimental TRF-Experiment pipeline with the Alice dataset. The pipeline performs TRF analysis as shown in the main repository, but with considerably less code and more automation. 4 | 5 | 6 | ## Installing 7 | 8 | The pipeline is implemented in the [TRF-Tools](https://trf-tools.readthedocs.io/) library, which can be installed and updated with `pip`: 9 | 10 | ```bash 11 | $ pip install --upgrade https://github.com/christianbrodbeck/TRF-Tools/archive/refs/heads/main.zip 12 | ``` 13 | 14 | A suitable [environment](environment.yml) file can be found in the same folder as this README file (see [Instructions for installing Eelbrain: Full Setup](https://eelbrain.readthedocs.io/en/latest/installing.html#full-setup)). 15 | 16 | 17 | ## Getting started 18 | 19 | The pipeline assumes the same file system organization for EEG data and predictors as the main tutorial. However, all intermediate files and results will be managed by the pipeline. 20 | 21 | If starting from scratch, only the following scripts from the base repository would need to be executed, 22 | to create the files required for the pipeline: 23 | 24 | - `download_alice.py` to download the dataset 25 | - `predictors/make_gammatone.py` to create high resolution gammatone spectrograms 26 | - `predictors/make_gammatone_predictors.py` to create predictors derived from the spectrograms 27 | - `predictors/make_word_predictors.py` to create word-based predictors 28 | 29 | 30 | The core of the pipeline is the TRF-Experiment specification in [`alice.py`](alice.py). 31 | This experiment can then be imported and used from other Python scripts and notebooks to access data and results. 32 | This is demonstrated in the [`Auditory-TRFs.py`](Auditory-TRFs.py) notebook in this folder, which performs an analysis similar to the original [`figures/Auditory-TRFs.py`](https://github.com/Eelbrain/Alice/blob/main/figures/Auditory-TRFs.py), but using the pipeline instead of the individual TRFs created through the script in the base repository (see the [Alice readme](../#notebooks) on how to restore notebooks from `*.py` files). 33 | 34 | The [`TRFExperiment`](https://trf-tools.readthedocs.io/latest/pipeline.html) pipeline is an extension of the Eelbrain [`MneExperiment`](https://eelbrain.readthedocs.io/en/stable/experiment.html) pipeline. It uses `MneExperiment` mechanisms to preprocess data up to the epoch stage. Documentation for the functionality of `MneExperiment` is best found in Eelbrain [documentation](http://eelbrain.readthedocs.io/en/stable/). 35 | 36 | 37 | > [!WARNING] 38 | > The `TRFExperiment` checks its cache for consistency every time the pipeline object is initialized (Like its parent class, [`MneExperiment`](https://eelbrain.readthedocs.io/en/stable/experiment.html)). There is, however, one exception: The pipeline cannot currently detect changes in predictor *files*. Whenever you change a predictor file, you *must* call the `TRFExperiment.invalidate_cache()` method with the given predictor name. This will delete all cached files that depend on a given predictor. 39 | 40 | 41 | ## Batch estimating TRFs 42 | 43 | The pipeline computes and caches TRFs whenever they are requested through one of the methods for accessing results. However, sometimes one might want to estimate a large number of TRFs. This can be done by creating a list of TRF jobs, as in the [`jobs.py`](jobs.py) example file. These TRFs can then be pre-computed by running the following command in a terminal: 44 | 45 | ```bash 46 | $ trf-tools-make-jobs jobs.py 47 | ``` 48 | 49 | When running this command, TRFs that have already been cached will be skipped automatically, so there is no need to remove previous jobs from `jobs.py`. For example, when adding new subjects to a dataset this command can be used to compuate all TRFs for the new subjects. The pipeline also performs a cache check for every TRF, so this is a convenient way to re-create all TRFs after, for example, changing a preprocessing parameter. 50 | -------------------------------------------------------------------------------- /pipeline/alice.py: -------------------------------------------------------------------------------- 1 | # This file contains a pipeline specification. The pipeline is defined as a subclass of TRFExperiment, and adds information about the specific paradigm and data. TRFExperiment is a subclass of eelbrain.MneExperiment, which is documented extensively on the Eelbrain website. 2 | from eelbrain.pipeline import * 3 | from trftools.pipeline import * 4 | 5 | 6 | # This is the root directory where the pipeline expects the experiment's data. The directory used here corresponds to the default download location when using the download_alic.py script in this repository. Generally, the file locations used in the example analysis scripts is consistent with the location and naming convention for this pipeline. 7 | DATA_ROOT = "~/Data/Alice" 8 | # Since each of the audio files used as stimuli had a different length, we define that information here 9 | SEGMENT_DURATION = { 10 | '1': 57.541, 11 | '2': 60.845, 12 | '3': 63.259, 13 | '4': 69.989, 14 | '5': 66.273, 15 | '6': 63.778, 16 | '7': 62.897, 17 | '8': 57.311, 18 | '9': 57.226, 19 | '10': 61.27, 20 | '11': 56.17, 21 | '12': 46.983, 22 | } 23 | 24 | # One may also want to define parameters used for estimating TRFs here, as they are often re-used along with the pipeline in multiple location (see notebooks in this directory and jobs.py for examples) 25 | PARAMETERS = { 26 | 'raw': '0.5-20', 27 | 'samplingrate': 50, 28 | 'data': 'eeg', 29 | 'tstart': -0.100, 30 | 'tstop': 1.00, 31 | 'filter_x': 'continuous', 32 | 'error': 'l1', 33 | 'basis': 0.050, 34 | 'partitions': -5, 35 | 'selective_stopping': 1, 36 | } 37 | 38 | 39 | # Start by defining a subclass of the pipeline 40 | class Alice(TRFExperiment): 41 | 42 | # This is the directory withing the root directory that contains the EEG data 43 | data_dir = 'eeg' 44 | # This is how subject names are identified ("S" followed by two digits). See the documentation of the builtin Python regular expression (re) module for details on how to build patterns 45 | subject_re = r'S\d\d' 46 | 47 | # This is used to identify the *-raw.fif file in the eeg directory (some experiments contain more than one session per participant) 48 | sessions = ['alice'] 49 | 50 | # This defines the preprocessing pipeline. For details see https://eelbrain.readthedocs.io/en/stable/experiment.html 51 | raw = { 52 | 'raw': RawSource(connectivity='auto'), 53 | '0.5-20': RawFilter('raw', 0.5, 20, cache=False), 54 | } 55 | 56 | # This adds the segment duration (plus 1 second) to the events marking stimulus onset in the eeg files. For details see https://eelbrain.readthedocs.io/en/stable/experiment.html 57 | variables = { 58 | 'duration': LabelVar('event', {k: v + 1 for k, v in SEGMENT_DURATION.items()}), 59 | } 60 | 61 | # This defines a data "epoch" to extract the event-related data segments from the raw EEG data during which the story was presented. For details see https://eelbrain.readthedocs.io/en/stable/experiment.html 62 | epochs = { 63 | 'chapter-1': PrimaryEpoch('alice', tmin=0, tmax='duration', samplingrate=50), 64 | } 65 | 66 | # This defines which variable (among the variables assigned to the events in the EEG recordings) designates the stimulus that was presented. This is used to identify the predictor file corresponding to each event. Here we use the segment number for this purpose, as the EEG recordings already contains events labeled 1-12, according to which of the stimulus wave files was presented. This name is also the first part of each predictor filename, followed by ``~``. For example, ``1~gammatone-8.pickle`` is the ``gammatone-8`` predictor for segment ``1``. The value used here, 'event', is a variable that is already contained in the EEG files, but any variable added by the user could also be used (see the *Events* section in https://eelbrain.readthedocs.io/en/stable/experiment.html) 67 | stim_var = 'event' 68 | 69 | # This specifies how the pipeline will find and handle predictors. The key indicates the first part of the filename in the /predictors directory. The FilePredictor object allows specifying some options on how the predictor files are used (see the FilePredictor documentation for details). 70 | predictors = { 71 | 'gammatone': FilePredictor(resample='bin'), 72 | 'word': FilePredictor(columns=True), 73 | } 74 | 75 | # Models are shortcuts for invoking multiple predictors 76 | models = { 77 | 'auditory-gammatone': 'gammatone-8 + gammatone-on-8', 78 | } 79 | 80 | 81 | # This creates an instance of the pipeline. Doing this here will allow other scripts to import the instance directly. 82 | alice = Alice(DATA_ROOT) 83 | -------------------------------------------------------------------------------- /pipeline/environment.yml: -------------------------------------------------------------------------------- 1 | # Environment for Alice TRF analysis 2 | # usage: $ conda env create --file=environment.yml 3 | name: eelbrain 4 | channels: 5 | - conda-forge 6 | dependencies: 7 | - eelbrain >= 0.39.10 8 | - pip 9 | - ipython 10 | - jupyter 11 | - ipywidgets 12 | - jupytext 13 | - seaborn 14 | - pip: 15 | - gammatone 16 | - https://github.com/christianbrodbeck/TRF-Tools/archive/refs/heads/main.zip 17 | -------------------------------------------------------------------------------- /pipeline/jobs.py: -------------------------------------------------------------------------------- 1 | # This file lists batch jobs. Run the batch jobs with:: 2 | # 3 | # $ trf-tools-make-jobs jobs.py 4 | # 5 | from alice import PARAMETERS, alice 6 | 7 | 8 | JOBS = [ 9 | # Batch-compute TRFs for all subjects: 10 | alice.trf_job('gammatone-1', **PARAMETERS), 11 | alice.trf_job('gammatone-1 + gammatone-on-1', **PARAMETERS), 12 | alice.trf_job('gammatone-8 + gammatone-on-8', **PARAMETERS), 13 | # Batch compute TRFs for both models in a model comparison: 14 | alice.trf_job('auditory-gammatone @ gammatone-on-8', **PARAMETERS), 15 | ] 16 | -------------------------------------------------------------------------------- /predictors/explore_word_predictors.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:light 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.11.3 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # # Original Alice word predictors 17 | # Generate word-level predictors from the original analysis. 18 | 19 | # + 20 | from pathlib import Path 21 | 22 | import eelbrain 23 | import seaborn 24 | 25 | 26 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 27 | STIMULUS_DIR = DATA_ROOT / 'stimuli' 28 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 29 | 30 | word_table = eelbrain.load.tsv(STIMULUS_DIR / 'AliceChapterOne-EEG.csv') 31 | # Add word frequency as variable that scales with the expected response: larger response for less frequent words 32 | word_table['InvLogFreq'] = 17 - word_table['LogFreq'] 33 | 34 | # Preview table 35 | word_table.head(10) 36 | # - 37 | 38 | # Variables to process 39 | VARIABLES = ['InvLogFreq', 'NGRAM', 'RNN', 'CFG', 'Position'] 40 | # Colors for plotting 41 | colors = eelbrain.plot.colors_for_oneway(VARIABLES) 42 | 43 | # # Word variable properties 44 | # Explore some properties of the predictors 45 | 46 | # Density plot with Seaborn 47 | word_table_long = eelbrain.table.melt('value', VARIABLES, 'variable', word_table) 48 | data = word_table_long.as_dataframe() 49 | _ = seaborn.displot(data=data, x='value', hue='variable', kind='kde', clip=(0, None), palette=colors) 50 | 51 | # pairwise scatter-plots 52 | eelbrain.report.scatter_table(VARIABLES, data=word_table) 53 | 54 | eelbrain.test.pairwise_correlations(VARIABLES, data=word_table) 55 | 56 | # # Generate predictors 57 | # Generate a table that can serve to easily generate predictor time series. Here only one stimulus is used. For actually generating the predictors, run `make_word_predictors.py`. 58 | 59 | # + 60 | segment_table = word_table.sub(f"Segment == 1") 61 | # Initialize a Dataset to contain the predictors 62 | data = eelbrain.Dataset( 63 | {'time': segment_table['onset']}, # column with time-stamps 64 | info={'tstop': segment_table[-1, 'offset']}, # store stimulus end time for generating time-continuous predictor 65 | ) 66 | # add columns for predictor variables 67 | for key in VARIABLES: 68 | data[key] = segment_table[key] 69 | # add masks for lexical and non-lexical words 70 | data['lexical'] = segment_table['IsLexical'] == True 71 | data['nlexical'] = segment_table['IsLexical'] == False 72 | 73 | # preview the result 74 | data.head(10) 75 | -------------------------------------------------------------------------------- /predictors/make_gammatone.py: -------------------------------------------------------------------------------- 1 | """Generate high-resolution gammatone spectrograms""" 2 | from pathlib import Path 3 | 4 | import eelbrain 5 | 6 | 7 | # Define paths to data 8 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 9 | STIMULUS_DIR = DATA_ROOT / 'stimuli' 10 | 11 | # Loop through the stimuli 12 | for i in range(1, 13): 13 | # Define a filename for the gammatone spectrogram corresponding to this predictor. 14 | dst = STIMULUS_DIR / f'{i}-gammatone.pickle' 15 | # If the file already exists, we can skip it 16 | if dst.exists(): 17 | continue 18 | # Load the sound file corresponding to the predictor 19 | wav = eelbrain.load.wav(STIMULUS_DIR / f'{i}.wav') 20 | # Apply a gammatone filterbank, producing a high resolution spectrogram 21 | gt = eelbrain.gammatone_bank(wav, 80, 15000, 128, location='left', tstep=0.001) 22 | # Save the gammatone spectrogram at the intended destination 23 | eelbrain.save.pickle(gt, dst) 24 | -------------------------------------------------------------------------------- /predictors/make_gammatone_predictors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Predictors based on gammatone spectrograms 3 | 4 | Assumes that ``make_gammatone.py`` has been run to create the high resolution 5 | spectrograms. 6 | """ 7 | from pathlib import Path 8 | 9 | import eelbrain 10 | 11 | 12 | # Define paths to data, and destination for predictors 13 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 14 | STIMULUS_DIR = DATA_ROOT / 'stimuli' 15 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 16 | 17 | # If the directory for predictors does not exist yet, create it 18 | PREDICTOR_DIR.mkdir(exist_ok=True) 19 | # Loop through stimuli 20 | for i in range(1, 13): 21 | # Load the high resolution gammatone spectrogram 22 | gt = eelbrain.load.unpickle(STIMULUS_DIR / f'{i}-gammatone.pickle') 23 | 24 | # Apply a log transform to approximate peripheral auditory processing 25 | gt_log = (gt + 1).log() 26 | # Apply the edge detector model to generate an acoustic onset spectrogram 27 | gt_on = eelbrain.edge_detector(gt_log, c=30) 28 | 29 | # Create and save 1 band versions of the two predictors (i.e., temporal envelope predictors) 30 | eelbrain.save.pickle(gt_log.sum('frequency'), PREDICTOR_DIR / f'{i}~gammatone-1.pickle') 31 | eelbrain.save.pickle(gt_on.sum('frequency'), PREDICTOR_DIR / f'{i}~gammatone-on-1.pickle') 32 | # Create and save 8 band versions of the two predictors (binning the frequency axis into 8 bands) 33 | x = gt_log.bin(nbins=8, func='sum', dim='frequency') 34 | eelbrain.save.pickle(x, PREDICTOR_DIR / f'{i}~gammatone-8.pickle') 35 | x = gt_on.bin(nbins=8, func='sum', dim='frequency') 36 | eelbrain.save.pickle(x, PREDICTOR_DIR / f'{i}~gammatone-on-8.pickle') 37 | 38 | # Create gammatone spectrograms with linear scale, only 8 bin versions 39 | x = gt.bin(nbins=8, func='sum', dim='frequency') 40 | eelbrain.save.pickle(x, PREDICTOR_DIR / f'{i}~gammatone-lin-8.pickle') 41 | # Powerlaw scale 42 | gt_pow = gt ** 0.6 43 | x = gt_pow.bin(nbins=8, func='sum', dim='frequency') 44 | eelbrain.save.pickle(x, PREDICTOR_DIR / f'{i}~gammatone-pow-8.pickle') 45 | -------------------------------------------------------------------------------- /predictors/make_word_predictors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generate predictors for word-level variables 3 | 4 | See the `explore_word_predictors.py` notebook for more background 5 | """ 6 | from pathlib import Path 7 | 8 | import eelbrain 9 | 10 | 11 | # Define paths to source data, and destination for predictors 12 | DATA_ROOT = Path("~").expanduser() / 'Data' / 'Alice' 13 | STIMULUS_DIR = DATA_ROOT / 'stimuli' 14 | PREDICTOR_DIR = DATA_ROOT / 'predictors' 15 | 16 | # Load the text file with word-by-word predictor variables 17 | word_table = eelbrain.load.tsv(STIMULUS_DIR / 'AliceChapterOne-EEG.csv') 18 | # Add word frequency as variable that scales with the expected response (in 19 | # impulse-based continuous predictor variables, impulses quantify the difference 20 | # from the baseline, 0, i.e. larger magnitude impulses always predict larger 21 | # magnitude of responses; however, based on previous research, we expect larger 22 | # responses to less frequent words) 23 | word_table['InvLogFreq'] = 17 - word_table['LogFreq'] 24 | 25 | # Loop through the stimuli 26 | for segment in range(1, 13): 27 | # Take the subset of the table corresponding to the current stimulus 28 | segment_table = word_table.sub(f"Segment == {segment}") 29 | # Initialize a new Dataset with just the time-stamp of the words; add an 30 | # info dictionary with the duration of the stimulus ('tstop') 31 | data = eelbrain.Dataset({'time': segment_table['onset']}, info={'tstop': segment_table[-1, 'offset']}) 32 | # Add predictor variables to the new Dataset 33 | data['LogFreq'] = segment_table['InvLogFreq'] 34 | for key in ['NGRAM', 'RNN', 'CFG', 'Position']: 35 | data[key] = segment_table[key] 36 | # Create and add boolean masks for lexical and non-lexical words 37 | data['lexical'] = segment_table['IsLexical'] == True 38 | data['nlexical'] = segment_table['IsLexical'] == False 39 | # Save the Dataset for this stimulus 40 | eelbrain.save.pickle(data, PREDICTOR_DIR / f'{segment}~word.pickle') 41 | --------------------------------------------------------------------------------