Skip to content

[ENH] Add support for ieeg interpolation #12336

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

Merged
merged 17 commits into from
Jan 10, 2024
Merged

Conversation

alexrockhill
Copy link
Contributor

Fixes #12333.

It also looked like there was a bug where epochs would have the channel indices of their epochs set to nan if nan was the method which was also fixed.

@alexrockhill
Copy link
Contributor Author

image

The seeg interpolation looks pretty good.

@@ -213,10 +239,6 @@ def _interpolate_bads_meeg(
# select the bad channels to be interpolated
picks_bad = pick_channels(inst.info["ch_names"], bads_type, exclude=[])

if method[ch_type] == "nan":
inst._data[picks_bad] = np.nan
Copy link
Contributor Author

@alexrockhill alexrockhill Jan 4, 2024

Choose a reason for hiding this comment

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

This is the line with the bug as far as I can tell, this should only work on raw and have some odd and unexpected indexing behavior on epochs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alexrockhill
Copy link
Contributor Author

alexrockhill commented Jan 4, 2024

Confirmed, this is incorrect on main.

import mne
raw = mne.io.read_raw(mne.datasets.sample.data_path() / 'MEG' / 'sample' / 'sample_audvis_raw.fif')
events = mne.find_events(raw)
epochs = mne.Epochs(raw, events)
epochs.info['bads'] = epochs.ch_names[:10]
epochs.load_data()
epochs.interpolate_bads(method=dict(meg='nan'), reset_bads=False)

epochs._data[20, 0]  # should be nan
epochs._data[0, 20]  # shouldn't be nan but is

@alexrockhill
Copy link
Contributor Author

Oh odd, if you try the above code snippet on this branch, the data is correct but when you call epochs.plot it'll all blank and you get

mne-python/mne/viz/_figure.py:372: RuntimeWarning: Mean of empty slice
  data -= np.nanmean(data, axis=1, keepdims=True)

Looks like an unrelated bug.

@alexrockhill
Copy link
Contributor Author

alexrockhill commented Jan 5, 2024

Looks like everything works as expected. This fixes both the ieeg quiet non-interpolations and the newly discovered nan bug. Once this merges, I will open an issue for the epochs browser bug but it would just confuse things to open it depending on main now because of the nan bug.

Copy link
Member

@drammock drammock left a comment

Choose a reason for hiding this comment

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

nice PR, thanks! A few questions/suggestions; I ran out of time and didn't thoroughly check the tests FWIW, so should get at least one other review (or another pass from me next week)

inst.info._check_consistency()
bads_idx[picks] = [inst.ch_names[ch] in inst.info["bads"] for ch in picks]

if len(picks) < 3 or bads_idx.sum() == 0:
Copy link
Member

Choose a reason for hiding this comment

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

[question] IDK how much of an edge case this is or if it's a realistic worry, but should we warn if len(picks) < 3? I can imagine maybe being surprised that the single bad channel wasn't interpolated because I (inadvertantly?) excluded all but one of the good channels (leaving just 2 channels in picks)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think maybe best just to kick it to the runtime error if there's a bad channel on a shaft with less than 3 contacts which is the edge case and I think an error is warranted there since that's not a good interpolation situation. So no need to silently return.

Copy link
Member

Choose a reason for hiding this comment

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

I think a warning is better. If you have 3 bad channels and this edge-case represents only one of them, the interpolation will fail and raise unless you mark the edge-case bad channel as good, interpolate, and mark it again as bad. I find this more error prone than warning, interpolating the 2 other channels and ignoring the edge-case one, and finally removing only the 2 interpolated channels from .info["bads"].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This won't raise an error if there are 3 bad channels on a shaft (electrode) only if there aren't three good ones. If there are only two good channels, I don't think there is a use case where you want to use those to interpolate any of the bad channels so I'd still be disposed to an error. Although if you had 3 contacts (usually 4 for DBS) but as in a DBS and you wanted to interpolate the middle one on some epochs that would make sense so maybe it should just be two required.

Copy link
Member

@mscheltienne mscheltienne Jan 6, 2024

Choose a reason for hiding this comment

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

I mixed up bad channels/good ones in my comment. I agree there is no use case for interpolating along a shaft if you lack the information to do so. But I would still warn, else if you have 2 shafts each with some bad channels, one with less than 3 good contacts remaining; the interpolation function will fail even for the shaft which has enough information to interpolate.

raw.ch_names = [f"Shaft1_{k} for k in range(5)] + [f"Shaft2_{k} for k in range(5)]
raw.info["bads"] = [f"Shaft1_{k} for k in range(4)] + ["Shaft2_0"]
raw.interpolate_bads(...)  # warning for the bad contacts on Shaft1, interpolation for Shaft2_0
assert raw.info["bads"] == [f"Shaft1_{k} for k in range(4)]

Or am I misunderstanding something and/or a multi-shaft recording is not realistic? (I lack experience with intra-cranial recordings)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The length of the shaft isn't checked if the channels aren't marked bad so I think if you do mark it and try and interpolate it you should get an error rather than unreliable data. You would then have to drop that channel without interpolation to continue without an error which I think is the safest way to go and wouldn't cause major issues in multi-subject pipelines.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the other consideration if it's a warning is what do you do to the data in that channel? If it was supposed to be interpolated and you just warned and the data was left as is, that seems potentially misleading. If it only requires two other contacts, I think it's pretty safe that this is an unlikely edge case and the failure will be warranted so that the user can be notified to fix their data by dropping all the contacts on that electrode shaft.

@alexrockhill
Copy link
Contributor Author

Thanks for the review, some nice cleanups

inst.info._check_consistency()
bads_idx[picks] = [inst.ch_names[ch] in inst.info["bads"] for ch in picks]

if len(picks) < 3 or bads_idx.sum() == 0:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The length of the shaft isn't checked if the channels aren't marked bad so I think if you do mark it and try and interpolate it you should get an error rather than unreliable data. You would then have to drop that channel without interpolation to continue without an error which I think is the safest way to go and wouldn't cause major issues in multi-subject pipelines.

@alexrockhill
Copy link
Contributor Author

Good to go by me, no rush though

np.fill_diagonal(dist, np.inf)

picks_bad = list(np.where(bads_idx_pos)[0])
while picks_bad:
Copy link
Member

Choose a reason for hiding this comment

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

can we avoid while loops and use a plain for? I find them usually unecessary and error prone.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The while loop was used because you don't know if two bad channels are on the same shaft until you find the shaft. I can't think of an easy way around that with a for loop.

Copy link
Member

Choose a reason for hiding this comment

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

I wonder if a helper function (_find_shafts or so) that returns a list-of-lists of channel indices would be generally useful? If so, it could be used here to pre-compute the channels into shaft groupings, then a for-loop over picks_bad would work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, we could do that. I was just finding them for the bad contacts to save the computation but it's a very small amount

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I wrote that but even if you know the shafts, you still have to iterate over picks_bad (list of bad indices) and you don't know whether two are on the same shaft or not so you have to use a while loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh never mind you can just loop over shafts

@alexrockhill
Copy link
Contributor Author

alexrockhill commented Jan 9, 2024

image

Here are comparisons from a middle contact (left) and and edge contact (right) interpolated (blue) vs original (orange). Looks pretty reasonable.

This is 10 s of real data.

@agramfort agramfort merged commit f70378a into mne-tools:main Jan 10, 2024
@alexrockhill alexrockhill deleted the interp2 branch January 10, 2024 17:57
snwnde pushed a commit to snwnde/mne-python that referenced this pull request Mar 20, 2024
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
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.

[BUG] Interpolate bads ignores ECoG and sEEG channels
4 participants