Custom montage with MNE-BIDS-Pipeline

Hello,

I am trying to create a custom montage for use with the MNE-BIDS-Pipeline by combining the biosemi64 standard montage with some estimated locations for face and mastoid electrodes.

import mne
from mne.channels import make_dig_montage

# Load BioSemi64 montage
biosemi_montage = mne.channels.make_standard_montage('biosemi64')

# Get positions from the BioSemi montage
biosemi_ch_pos = biosemi_montage.get_positions()['ch_pos']

# Define custom electrode positions
custom_ch_pos = {
    'LO1': [-0.06,  0.00,  0.02],  # left outer eye
    'LO2': [ 0.06,  0.00,  0.02],  # right outer eye
    'UVE': [ 0.03,  0.06,  0.05],  # above right eye
    'LVE': [ 0.03,  0.06, -0.02],  # below right eye
    'M1':  [-0.08, -0.06,  0.00],  # left mastoid
    'M2':  [ 0.08, -0.06,  0.00],  # right mastoid
}

# Combine standard and custom positions
combined_ch_pos = {**biosemi_ch_pos, **custom_ch_pos}

# Make new combined montage
combined_montage = make_dig_montage(ch_pos=combined_ch_pos, coord_frame='head')

# Plot montage
combined_montage.plot()

The external electrodes were placed above and below the right eye, to the side of each eye, and on each mastoid, which I gather is a pretty standard setup. I want these electrodes included in the montage so they can be considered by the MNE-BIDS-Pipeline. Does anyone know if my estimated positions are valid? Also, how can I specify this montage in the MNE-BIDS-Pipeline?

Thank you!

Hello and welcome to the forum!

You can pass a custom montage as a DigMontage object to the pipeline via the eeg_template_montage configuration setting.

The documentation is currently not clear on this and should be improved.

Best wishes,
Richard

1 Like

Hello @richard ,

Thanks for this, I managed to get it working! Are you able to comment on whether it is appropriate to label the external electrodes as EEG and assign estimated locations in the montage? Is there a better approach that will also allow these channels to be analyzed in the MNE-BIDS-Pipeline?

Thank you!

1 Like

Maybe @sappelhoff can comment on this :slight_smile:

you are mentioning that you placed sensors (electrodes) next to the eyes. I would not add these sensor positions to the montage. Instead I would use these β€œEOG” (!) sensors exclusively for data cleaning (e.g., ICA for eye component removal). The EOG channels will of course also contain some EEG … but you have enough sensors to not worry about salvaging that.

However, you also placed electrodes on the Mastoids. For these, adding sensor positions may be a good idea, and I would include them in downstream EEG analyses, too. :slight_smile:

If I were you, I would double check the β€œestimated” positions by plotting them on a realistic surface first. You can take some inspiration from here: Plot sensors on realistic surfaces β€” eeg_positions 2.1.2 documentation or here: Plotting EEG sensors on the scalp β€” MNE 1.10.0 documentation

Hi @sappelhoff ,

Thank you! This makes good sense to me. My EOG electrodes are labelled as EXG1, EXG2, etc., and marked in my *channels.tsv type MISC. In the MNE-BIDS-Pipeline config.py, is it simply a question of passing:

eog_channels = ['EXG1','EXG2','EXG3','EXG4']

And then it will include these channels in the ICA for cleaning?

Best

Yes, that is correct. Ideally, though, you’d label them correctly in the BIDS dataset: BIDS does support the β€œEOG” channel type. MNE-BIDS-Pipeline would immediately handle them correctly automatically in that case.

Thanks, @richard ! I’ve modified my script to rename the channels when creating the bids data structure and they are showing as being correctly names in the *channels.tsv. But when I run the pipeline I get the following error message:

(mne) PS C:\> mne_bids_pipeline 'C:\Users\jmarti2\OneDrive - University of Edinburgh\wellcome\scripts\02_mne_bids_config_new.py'
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬ Welcome aboard MNE-BIDS-Pipeline! πŸ‘‹ ────────────────────────────────────────────────────────────────────────
Β¦09:11:46Β¦ πŸ“ Using configuration: C:\Users\jmarti2\OneDrive - University of Edinburgh\wellcome\scripts\02_mne_bids_config_new.py
└────────┴
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬ init/_01_init_derivatives_dir ───────────────────────────────────────────────────────────────────────────────
¦09:11:46¦ ⏳️ Initializing output directories.
└────────┴ done (1s)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬ init/_02_find_empty_room ────────────────────────────────────────────────────────────────────────────────────
Β¦09:11:46Β¦ ⏩ Skipping, empty-room data only relevant for MEG …
└────────┴ done (1s)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬ preprocessing/_01_data_quality ──────────────────────────────────────────────────────────────────────────────
¦09:11:50¦ ⏳️ sub-1014 Reading experimental recording: sub-1014_task-vep
¦09:11:54¦ ⏳️ sub-1014 Setting EEG channel locations to template montage: biosemi64.
¦09:11:54¦ ❌ sub-1014 A critical error occurred. The error message was: DigMontage is only a subset of info. There are 8 channel positions not present in the DigMontage. The channels missing from the montage are:

['EXG1', 'EXG2', 'EXG3', 'EXG4', 'EXG5', 'EXG6', 'EXG7', 'EXG8'].

Consider using inst.rename_channels to match the montage nomenclature, or inst.set_channel_types if these are not EEG channels, or use the on_missing parameter if the channel positions are allowed to be unknown in your analyses.

Aborting pipeline run. The traceback is:

  File "C:\Users\jmarti2\AppData\Local\anaconda3\envs\mne\Lib\site-packages\mne_bids_pipeline\steps\preprocessing\_01_data_quality.py", line 98, in assess_data_quality
    raw = import_experimental_data(
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jmarti2\AppData\Local\anaconda3\envs\mne\Lib\site-packages\mne_bids_pipeline\_import_data.py", line 384, in import_experimental_data
    _set_eeg_montage(cfg=cfg, raw=raw, subject=subject, session=session, run=run)
  File "C:\Users\jmarti2\AppData\Local\anaconda3\envs\mne\Lib\site-packages\mne_bids_pipeline\_import_data.py", line 332, in _set_eeg_montage
    raw.set_montage(montage, match_case=False, match_alias=True)
  File "<decorator-gen-22>", line 12, in set_montage
  File "C:\Users\jmarti2\AppData\Local\anaconda3\envs\mne\Lib\site-packages\mne\_fiff\meas_info.py", line 428, in set_montage
    _set_montage(info, montage, match_case, match_alias, on_missing)
  File "C:\Users\jmarti2\AppData\Local\anaconda3\envs\mne\Lib\site-packages\mne\channels\montage.py", line 1273, in _set_montage
    _on_missing(on_missing, missing_coord_msg)
  File "C:\Users\jmarti2\AppData\Local\anaconda3\envs\mne\Lib\site-packages\mne\utils\check.py", line 1218, in _on_missing
    raise error_klass(msg)

Here is my config.py:

# -*- coding: utf-8 -*-
"""
Created on Tue Oct 22 15:01:44 2024

@author: jmarti2

MNE-BIDS-Pipeline configuration file for analysing VEP and SSVEP data for the
HELIOS-BD project. Check the documentation online for further options and more
detailed explanations of the configuration parameters.

"""
import mne

##############################################################
# Set these values appropriately before running the pipeline 
subjects = ['1014']                                          
task = 'vep'                                               
##############################################################

# Sets the appropriate output directories
bids_root = fr"C:\helios_bids_{task}"
deriv_root = fr"C:\helios_bids_{task}\derivatives\mne-bids-pipeline-{task}"
subjects_dir = None

# Use to exclude subjects
exclude_subjects = []


ch_types = ["eeg"]
data_type = "eeg"
eeg_reference = "average"  # EEG reference to use
#montage = mne.channels.read_dig_fif(r"C:\Users\jmarti2\OneDrive - University of Edinburgh\wellcome\scripts\combined_montage.fif")
eeg_template_montage = (
    "biosemi64"  # Apply 64-channel Biosemi 10/20 template montage:
)
eog_channels = ['HEOG-left','HEOG-right','VEOG-upper','VEOG-lower']
analyze_channels = "ch_types"
plot_psd_for_runs = "all"  # For which runs to add a power spectral density (PSD) plot to the generated report.
random_state = (
    42  # Passed to ICA and decoding algos to ensure reproduicibility
)
# Break detection
find_breaks = (
    True  # Automatically find break periods, and annotate them as BAD_break.
)
min_break_duration = 15.0
t_break_annot_start_after_previous_event = 5.0
t_break_annot_stop_before_next_event = 5.0

# Filtering
l_freq = 0.1  # The low-frequency cut-off in the highpass filtering step.
h_freq = 40.0  # The high-frequency cut-off in the highpass filtering step.
notch_freq = (50)  # Notch filter frequency. More than one frequency can be supplied
epochs_decim = 4  # Decimate epochs to 256 Hz
conditions = [
    "Lum",
    "LM",
    "S",
    "Lum/1",
    "Lum/2",
    "Lum/3",
    "Lum/4",
    "LM/1",
    "LM/2",
    "LM/3",
    "LM/4",
    "S/1",
    "S/2",
    "S/3",
    "S/4",
]

# Set the task specific parameters
if task == 'vep':
    epochs_tmin = -0.2  # The beginning of an epoch, relative to the respective event, in seconds.
    epochs_tmax = 0.8  # The end of an epoch, relative to the respective event, in seconds.
elif task == 'ssvep':
    epochs_tmin = -0.2
    epochs_tmax = 2.5
else:
    raise RuntimeError(f"Task {task} not currently supported")
    
baseline = (-0.1, 0)  # Beginning of epoch until time point zero

# Artifact removal
spatial_filter = "ica"  # Use ica
ica_reject = "autoreject_local"  # Find local (per channel) thresholds and repair epochs before fitting ICA
ica_algorithm = "picard-extended_infomax"
ica_l_freq = 1.0
ica_max_iterations = 500
ica_n_components = 64 - 1
ica_decim = None
reject = "autoreject_local"  # Before and after ICA recommended

# Sensor level analysis
contrasts = [("Lum", "S"), ("Lum", "LM"), ("LM", "S")]

if task == 'vep':
    decode = True
    decoding_time_generalization = True  # ?
    decoding_time_generalization_decim = 1
elif task =='ssvep':
    decode = False
else:
    raise RuntimeError(f"Task {task} not currently supported")

# No source estimation
run_source_estimation = False

# Execution
n_jobs = 4

Any ideas what is causing this?

1 Like

Thanks, could you kindly share the matching _channels.tsv, @lightwind?

I figured out the problem. In the script I was using to generate the BIDS data structure, I wasn’t preloading the .bdf files, so the changes I made to channel types and names were not being saved when I called write_raw_bids (but they were showing up as being renamed in *channels.tsv). I now preload the .bdf and use format=β€˜EDF’ when I call write_raw_bids.

Thanks for your help!

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.