├── .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 |
--------------------------------------------------------------------------------