Converting Gradiometers to virtual magnetometers for Neuromag 122

Hi all,

I am working on a script that allows to create virtual magnetometers from planar gradiometers for Neuromag 122 system. Currently, I am using MNE’s Vectorview magnetometers properties i.e., coil definitions to create the virtual magnetometers.

Current implementation (testing phase):
https://github.com/dasdiptyajit/meeg_diprocessing/commit/4285d3fd5139e46dd58537dcf527c4783309b543

Some results:


Issue that I am facing now:
Interactive plot doesn’t work for combined evoked data (grad + mag). Its a layout issue. Reconstructed/virtual magnetometers are not present in the current channel layout, which is normal in this case. Is there a way to modify/update the channel layout??

I couldn’t manage to come up with an alternative solution, So it would be nice if someone can verify if the current implementation is legitimate.

PS: I am willing to share the Neuromag 122 data if you like to run the script for testing. Just let me know.

best,
Dip

@larsoner @mscheltienne @drammock I can use some help :slight_smile:

You’re talking about layouts so I assume you mean for plot_topo (?). For the layout you can always create your own. In this case you’d have to create it for the grads, for the (virtual) mags, and then combine them somehow.

Thanks for the reply. Yes, I am talking about the plot_topo.

I wrote a small script to create a custom layout for virtual magnetometers:

info = evk_recon_mags.info
chs = info["chs"]
pos = np.empty((len(chs), 2))
for ci, ch in enumerate(chs):
    pos[ci] = ch["loc"][:2]

picks = mne.pick_channels(ch_names=info["ch_names"], include=[]).tolist()
vmaglout = mne.channels.generate_2d_layout(xy=pos, ch_names=info['ch_names'], ch_indices=picks,
                                           name='Neuromag_122_mag', normalize=True)
# replace original sensor position in the layout 
vmaglout.pos = np.array(ori_lout)
vmaglout.plot()

# read virtual layout
fname_lout = join(subjects_dir, 'Neuromag_122_mag.lout')
vmag_lout = mne.channels.read_layout(fname_lout)
evk_recon_mags.plot_topo(layout=vmag_lout)


The new custom layout looks fine to me.

Is there any way to apply the custom layout to the data to replace the previous layout? I recon there is no implementation is currently available for MEG system.

I tried the plot_topo by passing the custom layout. But,
evk_recon_mags.plot_topo(layout=vmag_lout) doesn’t work. Somehow MNE is not able read the channels info from the custom layout.

This is the info directory for my virtual magnetometers:

 acq_pars: ACQch001 111001 ACQch002 111002 ACQch003 111003 ACQch004 111004 ...
 bads: []
 ch_names: MAG 001_v, MAG 003_v, MAG 005_v, MAG 007_v, MAG 009_v, MAG ...
 chs: 61 Magnetometers
 custom_ref_applied: False
 description: Anonymized using a time shift to preserve age at acquisition
 dev_head_t: MEG device -> head transform
 dig: 150 items (3 Cardinal, 4 HPI, 64 EEG, 79 Extra)
 events: 1 item (list)
 file_id: 4 items (dict)
 highpass: 0.5 Hz
 hpi_meas: 1 item (list)
 hpi_results: 1 item (list)
 lowpass: 30.0 Hz
 maxshield: False
 meas_date: 2000-01-01 00:00:00 UTC
 meas_id: 4 items (dict)
 nchan: 61
 proj_id: 1 item (ndarray)
 proj_name: mne_anonymize
 projs: []
 sfreq: 1000.0 Hz

Many thanks,
Dip

I have slightly modified the plot_topo API, there is no error now. But, apparently sensors are not visible.

I have a feeling, this can be a BUG. When passing a custom layout, plot_topo doesn’t work. Can we test this somehow?

I have found the issue. During creating the 2D custom layout, the API mne.channels.generate_2d_layout expects the channel names to be a continuous strings without a space. For example, if the channel name is MAG 000 instead of MAG000; plot_topo doesn’t work (creates an empty figure).

This is the test code snippets! This can be run with MNE example data.

"""
Test case: create custom layout and plot topoplot of evoked data
@author: diptyajit das <bmedasdiptyajit@gmail.com>
Created on june 13th, 2023
"""


# modules
from pathlib import Path
import os
import numpy as np
import mne

# Create some dummy magnetometers data
n_channels = 102
sampling_freq = 1000  # in Hertz
info = mne.create_info(n_channels, sfreq=sampling_freq)
ch_names = [f"MAG{n:03}" for n in range(n_channels)]
ch_types = ["mag"] * n_channels
info = mne.create_info(ch_names, ch_types=ch_types, sfreq=sampling_freq)
info["description"] = "custom data"
print('info directory', info)

# add some random data to Raw array
times = np.linspace(0, 1, sampling_freq, endpoint=False)
data = np.random.rand(n_channels, len(times)) * 1e-12  # mag channels scale
custom_raw = mne.io.RawArray(data, info)
custom_raw.plot(block=True)
lout_ori = mne.channels.find_layout(custom_raw.info)
print('default custom layout', lout_ori)

# create dummy evoked
epochs = mne.make_fixed_length_epochs(custom_raw, duration=.5, preload=True)
evoked = epochs.average()
evoked = evoked.filter(None, 40.)
evoked.plot()
print('channel locations', evoked.info['chs'])  # at the moment, no locations for MEG channel is present

# define meg sample data path
sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, "MEG", "sample", "sample_audvis_raw.fif")
raw = mne.io.read_raw_fif(sample_data_raw_file)
raw.load_data()
raw = raw.copy().pick_types(meg='mag')

# add channel locations to custom evoked data from MNE sample data
for itype, ch in enumerate(evoked.info['ch_names']):
    pos_mag = evoked.info['ch_names'].index(ch)
    evoked.info['chs'][pos_mag]['loc'] = raw.info['chs'][pos_mag]['loc']  # add location

print('new channel locations', evoked.info['chs'])
evoked.plot()

# create a dummy custom layout, we will first extract xy coordinates from evoked data
info = evoked.info
chs = info["chs"]
pos = np.empty((len(chs), 2))
for ci, ch in enumerate(chs):
    pos[ci] = ch["loc"][:2]

picks = mne.pick_channels(ch_names=info["ch_names"], include=[]).tolist()

# modify channel names for layout
mod_ch_names1 = [f"MAG {n:03}_v" for n in range(n_channels)]  # error occurs
mod_ch_names2 = [f"MAG {n:03}" for n in range(n_channels)]  # error occurs

# test cases
types = [ch_names, mod_ch_names1, mod_ch_names2]

for type in types:
    dmaglout = mne.channels.generate_2d_layout(xy=pos, ch_names=type, ch_indices=picks,
                                               name='custom_mag_layout', normalize=True)

    # replace channel position with original layout ~ 2D custom layout doesn't match nicely with original layout,
    # so we will be strict to original one for now.
    # read the original layout
    layout_dir = Path(mne.__file__).parent / "channels" / "data" / "layouts"
    layouts = sorted(path.name for path in layout_dir.iterdir())
    print("\n" "BUILT-IN LAYOUTS\n" "================")
    print("\n".join(layouts))

    Vectorview_layout = mne.channels.read_layout("Vectorview-mag")
    dmaglout.pos = Vectorview_layout.pos[:n_channels]
    print('modified custom layout', dmaglout)
    dmaglout.plot()

    # now we will try to plot topomap for evoked with custom layout with changed channel names
    evoked.plot_topo(layout=dmaglout)

@larsoner Is it possible to add a warning massage or change the doc strings to recommend a specific naming for the channels?

Can you try to get that code trimmed to a minimal example (i.e., trim to as little code as possible so you can just show the bug – the example above has a lot more than needed I think) then open a GitHub issue? Hopefully in principle we can get generate_2d_layout to work with channel names with spaces, in principle it seems like it should work. But if we can’t get that to work then indeed a warning at least (an error might be better?) would be good.

The sample dataset already has spaces in the names so if the bug is as you describe then running generate_2d_layout on e.g. just the MEG channels should already show the bug