mne-tools / mne-python

MNE: Magnetoencephalography (MEG) and Electroencephalography (EEG) in Python
https://mne.tools
BSD 3-Clause "New" or "Revised" License
2.7k stars 1.31k forks source link

Relationship between Layout.box and Layout.pos, creating custom layout from 2D X/Y array ignores padding #12349

Open mscheltienne opened 9 months ago

mscheltienne commented 9 months ago

Proposed documentation enhancement

I'm struggling to debug a topographic plot, partially because I'm lacking information as to how the custom layout I provide is used by the viz functions. A layout has a box argument which defines the 'box' dimension (x_min, x_max, y_min, y_max) and a pos argument which defines the unit-normalized position of the channel.. as (x, y, width, height), i.e. it also includes the 'box' size in the form of width and height.

Does someone know why we have both present, I would like to include this explanation in the documentation in someform.


Next, those 'box-size' information are not even in sync. If I generate a 2D layout from an X/Y numpy array with:

layout = generate_2d_layout(pos, w=0.11, h=0.08, ch_names=raw.ch_names, name="custom", pad=0.2)

I get:

In [7]: layout.box
Out[7]: (0, 0, 0.06675489570238238, 0.08784025372548736)

In [8]: layout.pos
Out[8]: 
array([[3.23252215e-01, 9.93798268e-01, 1.10000000e-01, 8.00000000e-02],
       [4.99765412e-01, 1.00000000e+00, 1.10000000e-01, 8.00000000e-02],
...

The padding argument does impact layout.box but not layout.pos. What's more, plotting a topographic plot with mne.viz.plot_evoked_topo and a custom layout using different pad argument has no impact.


Finally, for some reason despite generate_2d_layout supposely normalizing the x/y position, all the axes do not fit in the figure yielded by mne.viz.plot_evoked_topo.

Code snippet if someone wants to play with the vizualization code: (requires MNE-LSL as I was looking for a dataset with 64 channels in a 10/20 montage)

import numpy as np
from matplotlib import pyplot as plt
from mne import make_fixed_length_epochs
from mne.channels import generate_2d_layout
from mne.io import read_raw_fif
from mne.viz import plot_evoked_topo
from mne_lsl.datasets import sample

fname = sample.data_path() / "sample-ant-raw.fif"
raw = read_raw_fif(fname, preload=True)
raw.drop_channels(["TRIGGER", "hEOG", "ECG", "EDA", "vEOG"])
raw.add_reference_channels(["CPz"])
raw.drop_channels(["M1", "M2", "PO5", "PO6"])
# fmt: off
raw.reorder_channels(
    [
        "Fp1", "Fpz", "Fp2", "F7", "F3", "Fz", "F4", "F8", "FC5", "FC1", "FC2",
        "FC6", "T7", "C3", "Cz", "C4", "T8", "CP5", "CP1", "CP2", "CP6", "P7",
        "P3", "Pz", "P4", "P8", "POz", "O1", "O2", "AF7", "AF3", "AF4", "AF8",
        "F5", "F1", "F2", "F6", "FC3", "FCz", "FC4", "C5", "C1", "C2", "C6",
        "CP3", "CP4", "P5", "P1", "P2", "P6", "PO3", "PO4", "FT7", "FT8", "TP7",
        "TP8", "PO7", "PO8", "Oz", "CPz",
    ]
)
# fmt: on
raw.set_eeg_reference("average")
raw.set_montage("standard_1020")
epochs1 = make_fixed_length_epochs(raw.copy().crop(0, 80), id=1, preload=True)
epochs2 = make_fixed_length_epochs(raw.copy().crop(60, 140), id=2, preload=True)
evokeds = [epochs1.average(), epochs2.average()]

# create a layout based on 'pos' in .get_montage().plot(kind="topomap", sphere="eeglab")
pos = np.load(r"pos.npy")
fig, ax = plt.subplots(1, 1)
ax.scatter(pos[:, 0], pos[:, 1])
layout = generate_2d_layout(
    pos, w=0.11, h=0.08, ch_names=raw.ch_names, name="custom", pad=0.2
)

# create topographic plot
fig, ax = plt.subplots(1, 1, figsize=(10, 10), layout=None)
plot_evoked_topo(
    evoked=evokeds,
    layout=layout,
    color=["#3498DB", "gray"],
    title="test",
    vline=[0, 0.080],
    exclude=(),
    legend=False,
    axes=ax,
    show=False,
)

plt.show()

The file pos.npy was obtained by looking through the code of raw.get_montage().plot(kind="topomap", sphere="eeglab").

pos.zip

Scatter plot of the X/Y position used to generate the layout:

image

Topographic plot:

test2

I will try to have a look in the coming days, if anyone has hints or ideas, they will be more than welcome :)

larsoner commented 9 months ago

I can never remember what any of the stuff means and have to re-learn every time :disappointed: I would test by tweaking each one to see the effects and/or look at the code to see where each field actually gets used.