Topoplot/map for preprocessed data with custom montage

Hi all,

I’m absolutely new to MNE but was hoping to get some topoplots illustrating the value of an exponent from a prior analysis per electrode. I have 17 electrodes I am satisfied with, and for which I have used mne.channels.read_custom_montage from a .elp file supplied by a colleague to generate the appropriate montage, as in the code below. Given I do not have a fif-formatted eeg dataset with epochs and such, I am struggling to find how to get this to work as the examples online seem to be entirely from fif format data. I have tried supplying the topomap ‘pos’ argument with both the neonatal montage I created and it’s .get_positions argument, and read on stack overflow some users had used .get_positions2d() on a montage ot get the appropriate x and y inputs for this function. I have had no luck in this regard as the documentation and subsequent errors suggest this method is not part of the montage function. I am sure there is a minor mis-specification someone can correct in my code, and I welcome any suggestions in this light.

I had tried using a .set file from EEGLAB which had its values set as the vectors I wanted rather than a timeseries per se, just to enable me to read into MNE and try and get a fif structure, but this quickly identified the file was not epoched or in a typical manner. Ultimately, I am flexible on the approach to get the plot, but I just wanted an MNE version in python rather than having my code locked off in MATLAB and EEGLAB, which I am trying to transition away from.

Any help is very welcome.

Ryan


print(f'{channels}')

import mne
!which mne

#The .set files are imported ok
fname = '/Users/ryanstanyard/Desktop/Research/EEGfMRI/EEG_final/PSDs_extracted/1170_avgPSD_17only.set'
neonatal_montage = '/Users/ryanstanyard/Desktop/Research/EEGfMRI/EEG_final/standard-10-5-cap385.elp'
# mydata = mne.io.read_epochs_eeglab(fname)
mydata = mne.io.read_raw_eeglab(fname)
neonatal_montage = mne.channels.read_custom_montage(neonatal_montage)
#The data look ok, and channel labels are correctly displayed
mydata
# mydata.plot()
mydata.ch_names

#Create a montage based on the standard 1005, unsure if this is appropriate
montage = mne.channels.make_standard_montage('standard_1005')

mydata.set_montage(neonatal_montage)

#But the channel locations are not found
mydata.plot_sensors()   
neonatal_montage.get_positions

# fig, ax = plt.subplots(ncols=2, figsize=(8, 4), gridspec_kw=dict(top=0.9),
#                        sharex=True, sharey=True)
                                             
# mne.viz.plot_topomap(averagedElectrodes_aperiodic['Channel_exponent_averages_Corrected'], neonatal_montage.get_positions, vmin=None, vmax=None, cmap=None, 
#                      sensors=True, res=64, axes=ax[0], names=channels, show_names=True, mask=None, mask_params=None, outlines='head', 
#                      show=True)

# mne.viz.plot_topomap(averagedElectrodes_aperiodic['Channel_exponent_averages_Corrected_SD'], montage, vmin=None, vmax=None, cmap=None, 
#                      sensors=True, res=64, axes=ax[1], names=channels, show_names=True, mask=None, mask_params=None, outlines='head', 
#                      show=True)

# ax[0].set_title('Aperiodic Exponent', fontweight='bold')
# ax[1].set_title('Aperiodic Exponent SD Map', fontweight='bold')

Hello and welcome,

As the structure in your files and the underlying data array is not very clear, I’ll give you some information on the raw object and on topographic plots.
MNE can read data from a variety of formats, not only .fif, and in any case, raw data will be read into a raw instance.

A raw instance has an info attribute that you can access with raw.info that stores everything MNE knows about your data. If a montage is set with raw.set_montage(), that’s where the additional information is located.

A raw instance can be created directly from a data array with a RawArray. For instance, let’s say you have a 3 channel system and 10 data points:

from mne import create_info
from mne.io import RawArray
import numpy as np

data = np.random.randn(3, 10)  # the data array
# create info instance
info = create_info(ch_names=['ch1', 'ch2', 'ch3'], sfreq=1000, ch_types='eeg')
raw = RawArray(data, info)

Note that you have to create an info for this method to work. Have a look at the documentation for create_info

On this raw, you can then set a montage with raw.set_montage() to specify the location of your channels.

But you can also directly set the montage on the info if you want! Let’s take an example with topographic plots using plot_topomap. It takes in input a data vector (n_chans, ) and the position of those channels. The easiest is to provide the position as an MNE info instance.

from mne import create_info
from mne.viz import plot_topomap
import numpy as np

# create array with 4 points for our 4 channels
# in the same order as provided in ch_names
data = np.random.randn(4)  
info = create_info(ch_names=['CPz', 'Oz', 'POz', 'Fz'], sfreq=1000, ch_types='eeg')
# channel names I provided are part of a standard montage
info.set_montage('standard_1020')

plot_topomap(data, info)

With the randomization, I got this array array([ 1.10767276, 1.0705067 , 1.00477807, -2.51317749]) which results in this topographic plot:

image

Hi Mathieu,

Thanks for your reply - I responded at the time but can see my reply does not appear, apologies. I have since extended this script and wish to have a colorbar alongside the topomap, but am having issues in its visibility, I don’t know if this is related to the backend (when I first ran it, it worked, but since does not, and I’ve tried a few different plt backends).

The minimal working case is this:

fname = '1170_avgPSD_17only.set'
infant_example = mne.io.read_raw_eeglab(fname)
averageElectrodes_aperiodic = pd.read_csv('avg_aperiodic_adjusted_power.csv')
fig, axes = plt.subplots(nrows=2, ncols=1)
names = infant_example.ch_names  
im,_ = mne.viz.plot_topomap(averageElectrodes_aperiodic['Channel_exponent_averages'], infant_example.info, 
                    names=names, show_names=True,  axes=axes[0])
mn_lim = min(averageElectrodes_aperiodic['Channel_exponent_averages'])
mx_lim = max(averageElectrodes_aperiodic['Channel_exponent_averages'])
mid_lim = mn_lim + ((mx_lim-mn_lim)/2)
clim = dict(kind='value', lims=[mn_lim,mid_lim,mx_lim])
cbar = mne.viz.plot_brain_colorbar(axes[1], clim, label='Aperiodic Exponent')

A longer form version where I am trying to show a structural image of the brain (placeholder) beneath which is the topomap with colourbar is below. I note two major issues - first, the colorbar, including a plt version, do not show. I can get a plt colorbar without MNE, but obviously this doesn’t relate to the topo. The second is the size of the channel labels, I’m unclear how to make these actually visible, as it seems I can get a bigger brain, but smaller labels, or vice versa. Any help very much appreciated.

from mpl_toolkits.axes_grid1 import make_axes_locatable
# figsize unit is inches
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(27, 18),
                         gridspec_kw=dict(height_ratios=[1, 4]))

# image plot in the upper axes, brain below
something_idx = 0
placeholder = matplotlib.image.imread('Screenshot 2022-04-07 at 17.15.27.png')
axes[something_idx].imshow(placeholder)
axes[something_idx].axis('off')

brain_idx = 1
names = infant_example.ch_names
im,_ = plot_topomap(averagedElectrodes_aperiodic['Channel_exponent_averages'], infant_example.info, 
                    names=names, show_names=True, cmap='RdBu_r',contours=0, axes=axes[brain_idx])
axes[brain_idx].axis('off')
mn_lim = min(averagedElectrodes_aperiodic['Channel_exponent_averages'])
mx_lim = max(averagedElectrodes_aperiodic['Channel_exponent_averages'])
mid_lim = mn_lim + ((mx_lim-mn_lim)/2)
clim = dict(kind='value', lims=[mn_lim,mid_lim,mx_lim])
divider = make_axes_locatable(axes[brain_idx])
cax = divider.append_axes('right', size='5%', pad=0.2)
cbar = mne.viz.plot_brain_colorbar(cax, clim, label='Aperiodic Exponent')
# im,_ = plot_topomap(data[i],layout.pos,axes=axes[np.unravel_index(i,axes.shape)],
#                                 vmin=min_value,vmax=max_value,show=False)

# divider = make_axes_locatable(axes[brain_idx])
# ax_cb = divider.new_horizontal(size="5%", pad=0.05)
# fig = ax.get_figure()
# fig.add_axes(ax_cb)

# Z, extent = get_demo_image()
# im = ax.imshow(Z, extent=extent)

# plt.colorbar(im, cax=ax_cb)
# ax_cb.yaxis.tick_right()
# ax_cb.yaxis.set_tick_params(labelright=False)

# tweak margins and spacing
fig.subplots_adjust(
    left=0.15, right=0.9, bottom=0.01, top=0.9, wspace=0.1, hspace=0.5)

# add subplot labels
for ax, label in zip(axes, 'AB'):
    ax.text(0.03, ax.get_position().ymax, label, transform=fig.transFigure,fontsize=14, fontweight='bold', va='top', ha='left')

Please start by formatting your post, and especially your code with backticks, e.g. code (between single backticks for in-line code) or:

code

(between triple backticks for code-blocks)

Don’t forget the indentation, and could you use a dummy dataset (as I did in my reply) to create a reproductible example that can be copy/paste in an interpreter and run?

Your question is very long; and difficult to read and make sense of as it is.

I appreciate your point - half the request for help was because I was struggling to unpick this provide a reproducable example. I have now built one below using a randomised vector of comparable scale, and shown the colorbar issue as best as I can whilst being concise.

from mne import create_info
from mne.io import RawArray
from mne.viz import plot_topomap
import numpy as np
import random
from mpl_toolkits.axes_grid1 import make_axes_locatable
# figsize unit is inches
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(20, 10),
                         gridspec_kw=dict(height_ratios=[4, 3]))

data = [3.55535486, 3.60046432, 3.35458864, 3.38464982,
 3.42284116, 3.5401117, 4.115964, 4.15131806,
 4.11246116, 4.04290848, 4.14782134, 4.21891112,
 4.09735244,  3.84983566, 3.79780728,  3.90177292,
 4.10791276]  # the data array

channels = ['C3','C4','F3','F4','F7','F8','O1','O2',
            'P3','P4','P7', 'P8','Pz','T7','T8','TP10',
            'TP9']

# create info instance
info = create_info(ch_names=channels, sfreq=512, ch_types='eeg')
info.set_montage('standard_1005')


something_idx = 0
placeholder = matplotlib.image.imread("brain.png")
axes[something_idx].imshow(placeholder)
axes[something_idx].axis('off')

brain_idx = 1
im,_ = plot_topomap(data, info)
axes[brain_idx].axis('off')
mn_lim = min(data)
mx_lim = max(data)
mid_lim = mn_lim + ((mx_lim-mn_lim)/2)
clim = dict(kind='value', lims=[mn_lim,mid_lim,mx_lim])
divider = make_axes_locatable(axes[brain_idx])
cax = divider.append_axes('right', size='5%', pad=0.2)
cbar = mne.viz.plot_brain_colorbar(cax, clim, label='Aperiodic Exponent')
plt.colorbar(im,cax=axes[brain_idx]) # if no ax= or cax=, plots solo beneath
# please also demo how to have this colour bar shared across several different subplots with differing vector values

You’ll need this image for the top image -

I am in part trying to emulate this, but with my panel ‘A’ as a (yet to be added) plt/sns plot and panel ‘B’ being quite a complex series of subplots (essentially 3 rows, row 1 with 2 large topoplots, and rows 2 and 3 with varying numbers of plots, smaller, with an accompanying colorbar on the side, shared across all plots ) but using several topoplots. If this sounds like chaos, I’ll arrange it in other software, but would like much prefer to have a scripted solution.

Hi, @rstanyard, I fixed the link in your last post. BTW, from reading the post I don’t find it clear what exactly you have problem with, I assume it is just the colorbar from your previous posts.

The root of the colorbar issue is that you use mne.viz.plot_brain_colorbar, which does not create colorbar for a topomap, but for a source space activation - that’s why the function refers to the “brain”, and not “topomap” (but the docstring of the function could be slightly modified to make this clear).

You can just use a normal matplotlib colorbar, here is a slightly modified version of your code:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable

from mne import create_info
from mne.viz import plot_topomap


data = [3.55535486, 3.60046432, 3.35458864, 3.38464982, 3.42284116,
        3.5401117, 4.115964, 4.15131806, 4.11246116, 4.04290848,
        4.14782134, 4.21891112, 4.09735244,  3.84983566, 3.79780728,
        3.90177292, 4.10791276]  # the data array

channels = ['C3','C4','F3','F4','F7','F8','O1','O2',
            'P3','P4','P7', 'P8','Pz','T7','T8','TP10',
            'TP9']

placeholder = np.random.random((25, 25))

# create info instance
info = create_info(ch_names=channels, sfreq=512, ch_types='eeg')
info.set_montage('standard_1005')


# figsize unit is inches
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(20, 10),
                         gridspec_kw=dict(height_ratios=[4, 3]))

axes[0].imshow(placeholder)
axes[0].axis('off')

im,_ = plot_topomap(data, info, axes=axes[1], show=False)

divider = make_axes_locatable(axes[1])
cax = divider.append_axes('right', size='5%', pad=0.2)
cbar = plt.colorbar(im, cax=cax)

which produces the image below:

1 Like

Thanks Mikolaj, apologies for the lack of clarity. Yes - the main issue was the colorbar, now resolved. I did try something similar to this using plt directly a few times but it didn’t work in the subplot. Your solution does work however, so I can build from this - I’ve since extended things quite a bit. Thank you also for noting the documentation update, that would be handy.

2 Likes