Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add interpolate_to method #13044

Open
wants to merge 27 commits into
base: main
Choose a base branch
from

Conversation

antoinecollas
Copy link

@antoinecollas antoinecollas commented Dec 30, 2024

Reference issue (if any)

Implements #12486

What does this implement/fix?

Implements interpolate_to next to interpolate_bads to interpolate EEG data to a given montage

Additional information

Interpolating channels using this implementation has shown to be effective in

Mellot, A., Collas, A., Chevallier, S., Engemann, D. and Gramfort, A., 2024. Physics-informed and Unsupervised Riemannian Domain Adaptation for Machine Learning on Heterogeneous EEG Datasets. EUSIPCO 2024

Copy link

welcome bot commented Dec 30, 2024

Hello! 👋 Thanks for opening your first pull request here! ❤️ We will try to get back to you soon. 🚴

@antoinecollas antoinecollas marked this pull request as ready for review December 31, 2024 07:45
@antoinecollas antoinecollas changed the title WIP: add interpolate_to method ENH: add interpolate_to method Dec 31, 2024
mne/channels/channels.py Show resolved Hide resolved
mne/channels/channels.py Outdated Show resolved Hide resolved
mne/channels/channels.py Outdated Show resolved Hide resolved
mne/channels/channels.py Show resolved Hide resolved
Comment on lines 997 to 1023
# Create a new info structure
sfreq = self.info["sfreq"]
ch_types = ["eeg"] * len(target_ch_names)
new_info = create_info(ch_names=target_ch_names, sfreq=sfreq, ch_types=ch_types)
new_info.set_montage(montage)

# Create a simple old_info
sfreq = self.info["sfreq"]
ch_names = self.info["ch_names"]
ch_types = ["eeg"] * len(ch_names)
old_info = create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
old_info.set_montage(self.info.get_montage())

# Compute mapping from current montage to target montage
mapping = _map_meg_or_eeg_channels(
old_info, new_info, mode="accurate", origin="auto"
)

# Apply the interpolation mapping
data_interp = mapping.dot(data_orig)

# Update bad channels
new_info["bads"] = [ch for ch in self.info["bads"] if ch in target_ch_names]

# Update the instance's info and data
self.info = new_info
self._data = data_interp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the impression that this will drop any other channel that is not EEG. It should not. Typically you would want to keep ECG and EOG channels. In other words this should only modify the EEG channels and leave the remaining channels unchanged.

def test_interpolate_to_eeg(montage_name):
"""Test the interpolate_to method for EEG."""
# Load EEG data
raw, _ = _load_data("eeg")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also test that it works fine for epochs and evoked calling the container "inst" as above

@antoinecollas
Copy link
Author

I completely agree with your comment regarding preserving non-EEG channels (e.g., ECG, EOG) while only modifying the EEG channels. However, I’m struggling to find a clean way to achieve this without unintentionally dropping or altering the other channel types.

Currently, my implementation effectively replaces the Info and data of EEG channels but does not account for the non-EEG channels. As you pointed out, this would result in losing the other channels, which is undesirable.

To address this, I guess the solution likely involves:
1. Extracting and preserving the non-EEG channels and their data.
2. Interpolating only the EEG channels using the new montage.
3. Merging the non-EEG channels back with the interpolated EEG channels, ensuring the resulting object (e.g., Raw, Epochs, or Evoked) includes all channels in their correct order.

But, I’m not sure about the best way to merge the original non-EEG channels with the new EEG channels while preserving Info consistency.

@larsoner
Copy link
Member

larsoner commented Jan 6, 2025

Hmmm, over in #12486 (comment) I suggested to follow the proposal in #9609 (comment) which had the API:

def interpolate_to(self, sensors=None, dev_head_t=None):

where sensors can be (to start) only None, 'ctf planar grad', 'ctf', and 'neuromag' ... in theory we could eventually support remapping other channel types (e.g., EEG) someday. But for now the function should operate only on MEG data.

Since your use case is EEG, I guess we're skipping the MEG interpolation bit here and going straight to EEG. But I still think sensors is a more future-compatible option than montage. For example if you had M/EEG data we could eventually have sensors=["ctf", "standard_1020"] or similar. montage implies that you're going to pass a DigMontage-like instance, which isn't as compatible with the MEG use case (though we could support doing so at some point).

So I'd suggest to switch to the name from montage to sensors here.

However, I’m struggling to find a clean way to achieve this without unintentionally dropping or altering the other channel types... I’m not sure about the best way to merge the original non-EEG channels with the new EEG channels while preserving Info consistency.

The safest way is to stick with public functions like add_channels etc. It won't necessarily be as memory-efficient but it should be safer (private attributes should be updated properly). Something like the following pseudocode should work:

_validate_type(inst, (BaseRaw, BaseEpochs, Evoked), "inst", extra="when intepolating channels")
picks_good_eeg = pick_types(info, eeg=True, exclude="bads")  # use only good EEG channels to interp
picks_remove_eeg = pick_types(info, eeg=True, exclude=())  # remove all EEG channels when interpolating (including bad)
picks_other = np.setdiff1d(np.arange(len(info["chs"])), picks_remove_eeg)
info_interp = ...  # however you construct destination info from the desired montage
ch_interp = ...  # however you construct the interpolation matrix
assert ch_interp.shape == (len(info_interp["chs"]), len(picks_good))
data_interp = ch_interp @ inst.get_data(picks_good_eeg)
# now we have our new data and our new info, create a new instance
if isinstance(inst, BaseRaw):
    inst_interp = RawArray(info_interp, data_interp, first_samp=inst.first_samp)
elif isinstance(inst, BaseEpochs):  # need other branches for Epochs and Evoked
    inst_interp = EpochsArray(info_interp, data_interp[np.newaxis])
else:
    assert isinstance(inst, Evoked)  # guaranteed above
    inst_interp = EvokedArray(info_interp, data_interp)
# concatenate new channels to the end of the old non-EEG ones
inst_out = inst.copy().pick(picks_other).load_data().add_channels([inst_interp], force_update_info=True)
# now reorder channels so that EEG are wherever they used to be
eeg_start_idx = picks_remove_eeg[0]
new_order = np.insert(
    np.arange(len(picks_other)),
    eeg_start_idx,
    np.arange(len(picks_other)), len(inst_out.ch_names)),
)
inst_out.reorder_channels(new_order)
assert inst_out.ch_names[eeg_start_idx] == info_interp.ch_names[0]
return inst_out

elif method == "MNE":
info_eeg = pick_info(self.info, picks_from)
mapping = _map_meg_or_eeg_channels(
info_eeg, new_info, mode="accurate", origin="auto"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be:

Suggested change
info_eeg, new_info, mode="accurate", origin="auto"
info_eeg, new_info, mode="accurate", origin=origin

not interpolated.

reg : float
The regularization parameter for the interpolation method (if applicable).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The regularization parameter for the interpolation method (if applicable).
The regularization parameter for the interpolation method (only used when the method is 'spline').


.. warning::
Be careful, only EEG channels are interpolated. Other channel types are
not interpolated.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this warning applies to the entire function, not the method parameter specifically. Perhaps move it to the main text, so above the Parameters line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants